Refactor to enforce strictNullChecks
This commit applies `strictNullChecks` to the entire codebase to improve maintainability and type safety. Key changes include: - Remove some explicit null-checks where unnecessary. - Add necessary null-checks. - Refactor static factory functions for a more functional approach. - Improve some test names and contexts for better debugging. - Add unit tests for any additional logic introduced. - Refactor `createPositionFromRegexFullMatch` to its own function as the logic is reused. - Prefer `find` prefix on functions that may return `undefined` and `get` prefix for those that always return a value.
This commit is contained in:
@@ -42,7 +42,7 @@ function renderText(docs: readonly string[] | undefined): string {
|
||||
}
|
||||
|
||||
function renderAsMarkdownListItem(content: string): string {
|
||||
if (!content || content.length === 0) {
|
||||
if (content.length === 0) {
|
||||
throw new Error('missing content');
|
||||
}
|
||||
const lines = content.split(/\r\n|\r|\n/);
|
||||
|
||||
@@ -85,7 +85,8 @@ function removeTrailingExtension(value: string): string {
|
||||
if (parts.length === 1) {
|
||||
return value;
|
||||
}
|
||||
if (parts.at(-1).length > 9) {
|
||||
const extension = parts[parts.length - 1];
|
||||
if (extension.length > 9) {
|
||||
return value; // Heuristically web file extension is no longer than 9 chars (e.g. "html")
|
||||
}
|
||||
return parts.slice(0, -1).join('.');
|
||||
@@ -115,9 +116,8 @@ function selectMostDescriptiveName(parts: readonly string[]): string | undefined
|
||||
}
|
||||
|
||||
function isGoodPathPart(part: string): boolean {
|
||||
return part
|
||||
return part.length > 2 // This is often non-human categories like T5 etc.
|
||||
&& !isDigit(part) // E.g. article numbers, issue numbers
|
||||
&& part.length > 2 // This is often non-human categories like T5 etc.
|
||||
&& !/^index(?:\.\S{0,10}$|$)/.test(part) // E.g. index.html
|
||||
&& !/^[A-Za-z]{2,4}([_-][A-Za-z]{4})?([_-]([A-Za-z]{2}|[0-9]{3}))$/.test(part) // Locale string e.g. fr-FR
|
||||
&& !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(part) // GUID
|
||||
|
||||
@@ -8,6 +8,6 @@ export interface NodeMetadata {
|
||||
readonly text: string;
|
||||
readonly isReversible: boolean;
|
||||
readonly docs: ReadonlyArray<string>;
|
||||
readonly children?: ReadonlyArray<NodeMetadata>;
|
||||
readonly children: ReadonlyArray<NodeMetadata>;
|
||||
readonly type: NodeType;
|
||||
}
|
||||
|
||||
@@ -25,10 +25,7 @@ export class CategoryReverter implements IReverter {
|
||||
}
|
||||
|
||||
function getAllSubScriptReverters(categoryId: number, collection: ICategoryCollection) {
|
||||
const category = collection.findCategory(categoryId);
|
||||
if (!category) {
|
||||
throw new Error(`Category with id "${categoryId}" does not exist`);
|
||||
}
|
||||
const category = collection.getCategory(categoryId);
|
||||
const scripts = category.getAllScriptsRecursively();
|
||||
return scripts.map((script) => new ScriptReverter(script.id));
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ import { useSelectedScriptNodeIds } from './TreeViewAdapter/UseSelectedScriptNod
|
||||
export default defineComponent({
|
||||
props: {
|
||||
categoryId: {
|
||||
type: [Number, undefined],
|
||||
type: [Number],
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import type { ReadOnlyTreeNode } from '../Node/TreeNode';
|
||||
|
||||
export interface TreeViewFilterEvent {
|
||||
readonly action: TreeViewFilterAction;
|
||||
readonly predicate?: TreeViewFilterPredicate;
|
||||
}
|
||||
type TreeViewFilterTriggeredEvent = {
|
||||
readonly action: TreeViewFilterAction.Triggered;
|
||||
readonly predicate: TreeViewFilterPredicate;
|
||||
};
|
||||
|
||||
type TreeViewFilterRemovedEvent = {
|
||||
readonly action: TreeViewFilterAction.Removed;
|
||||
};
|
||||
|
||||
export type TreeViewFilterEvent = TreeViewFilterTriggeredEvent | TreeViewFilterRemovedEvent;
|
||||
|
||||
export enum TreeViewFilterAction {
|
||||
Triggered,
|
||||
@@ -14,14 +20,14 @@ export type TreeViewFilterPredicate = (node: ReadOnlyTreeNode) => boolean;
|
||||
|
||||
export function createFilterTriggeredEvent(
|
||||
predicate: TreeViewFilterPredicate,
|
||||
): TreeViewFilterEvent {
|
||||
): TreeViewFilterTriggeredEvent {
|
||||
return {
|
||||
action: TreeViewFilterAction.Triggered,
|
||||
predicate,
|
||||
};
|
||||
}
|
||||
|
||||
export function createFilterRemovedEvent(): TreeViewFilterEvent {
|
||||
export function createFilterRemovedEvent(): TreeViewFilterRemovedEvent {
|
||||
return {
|
||||
action: TreeViewFilterAction.Removed,
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ export class TreeNodeHierarchy implements HierarchyAccess {
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
public setParent(parent: TreeNode): void {
|
||||
public setParent(parent: TreeNode | undefined): void {
|
||||
this.parent = parent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,6 @@ import { TreeNodeManager } from '../../Node/TreeNodeManager';
|
||||
export function parseTreeInput(
|
||||
input: readonly TreeInputNodeData[],
|
||||
): TreeNode[] {
|
||||
if (!input) {
|
||||
throw new Error('missing input');
|
||||
}
|
||||
if (!Array.isArray(input)) {
|
||||
throw new Error('input data must be an array');
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export class TreeNodeInitializerAndUpdater implements TreeNodeCollection {
|
||||
public nodesUpdated = new EventSource<QueryableNodes>();
|
||||
|
||||
public updateRootNodes(rootNodesData: readonly TreeInputNodeData[]): void {
|
||||
if (!rootNodesData?.length) {
|
||||
if (!rootNodesData.length) {
|
||||
throw new Error('missing data');
|
||||
}
|
||||
const rootNodes = this.treeNodeParser(rootNodesData);
|
||||
|
||||
@@ -71,8 +71,12 @@ TreeNavigationKeyCodes,
|
||||
};
|
||||
|
||||
function focusPreviousVisibleNode(context: TreeNavigationContext): void {
|
||||
const focusedNode = context.focus.currentSingleFocusedNode;
|
||||
if (!focusedNode) {
|
||||
return;
|
||||
}
|
||||
const previousVisibleNode = findPreviousVisibleNode(
|
||||
context.focus.currentSingleFocusedNode,
|
||||
focusedNode,
|
||||
context.nodes,
|
||||
);
|
||||
if (!previousVisibleNode) {
|
||||
@@ -83,6 +87,9 @@ function focusPreviousVisibleNode(context: TreeNavigationContext): void {
|
||||
|
||||
function focusNextVisibleNode(context: TreeNavigationContext): void {
|
||||
const focusedNode = context.focus.currentSingleFocusedNode;
|
||||
if (!focusedNode) {
|
||||
return;
|
||||
}
|
||||
const nextVisibleNode = findNextVisibleNode(focusedNode, context.nodes);
|
||||
if (!nextVisibleNode) {
|
||||
return;
|
||||
@@ -92,6 +99,9 @@ function focusNextVisibleNode(context: TreeNavigationContext): void {
|
||||
|
||||
function toggleTreeNodeCheckStatus(context: TreeNavigationContext): void {
|
||||
const focusedNode = context.focus.currentSingleFocusedNode;
|
||||
if (!focusedNode) {
|
||||
return;
|
||||
}
|
||||
const nodeState = focusedNode.state;
|
||||
let transaction = nodeState.beginTransaction();
|
||||
if (nodeState.current.checkState === TreeNodeCheckState.Checked) {
|
||||
@@ -104,19 +114,28 @@ function toggleTreeNodeCheckStatus(context: TreeNavigationContext): void {
|
||||
|
||||
function collapseNodeOrFocusParent(context: TreeNavigationContext): void {
|
||||
const focusedNode = context.focus.currentSingleFocusedNode;
|
||||
if (!focusedNode) {
|
||||
return;
|
||||
}
|
||||
const nodeState = focusedNode.state;
|
||||
const parentNode = focusedNode.hierarchy.parent;
|
||||
if (focusedNode.hierarchy.isBranchNode && nodeState.current.isExpanded) {
|
||||
nodeState.commitTransaction(
|
||||
nodeState.beginTransaction().withExpansionState(false),
|
||||
);
|
||||
} else {
|
||||
const parentNode = focusedNode.hierarchy.parent;
|
||||
if (!parentNode) {
|
||||
return;
|
||||
}
|
||||
context.focus.setSingleFocus(parentNode);
|
||||
}
|
||||
}
|
||||
|
||||
function expandNodeOrFocusFirstChild(context: TreeNavigationContext): void {
|
||||
const focusedNode = context.focus.currentSingleFocusedNode;
|
||||
if (!focusedNode) {
|
||||
return;
|
||||
}
|
||||
const nodeState = focusedNode.state;
|
||||
if (focusedNode.hierarchy.isBranchNode && !nodeState.current.isExpanded) {
|
||||
nodeState.commitTransaction(
|
||||
@@ -151,7 +170,10 @@ function findNextNode(node: TreeNode, nodes: QueryableNodes): TreeNode | undefin
|
||||
return nodes.flattenedNodes[index + 1] || undefined;
|
||||
}
|
||||
|
||||
function findPreviousVisibleNode(node: TreeNode, nodes: QueryableNodes): TreeNode | undefined {
|
||||
function findPreviousVisibleNode(
|
||||
node: TreeNode,
|
||||
nodes: QueryableNodes,
|
||||
): TreeNode | undefined {
|
||||
const previousNode = findPreviousNode(node, nodes);
|
||||
if (!previousNode) {
|
||||
return node.hierarchy.parent;
|
||||
|
||||
@@ -14,6 +14,7 @@ export function useTreeQueryFilter(
|
||||
const { nodes } = useCurrentTreeNodes(treeRootRef);
|
||||
|
||||
let isFiltering = false;
|
||||
|
||||
const statesBeforeFiltering = new NodeStateRestorer();
|
||||
statesBeforeFiltering.saveStateBeforeFilter(nodes.value);
|
||||
|
||||
@@ -35,7 +36,7 @@ export function useTreeQueryFilter(
|
||||
currentNodes.flattenedNodes.forEach((node: TreeNode) => {
|
||||
let transaction = node.state.beginTransaction()
|
||||
.withMatchState(false);
|
||||
transaction = statesBeforeFiltering.applyOriginalState(node, transaction);
|
||||
transaction = statesBeforeFiltering.applyStateBeforeFilter(node, transaction);
|
||||
node.state.commitTransaction(transaction);
|
||||
});
|
||||
statesBeforeFiltering.clear();
|
||||
@@ -150,18 +151,20 @@ class NodeStateRestorer {
|
||||
});
|
||||
}
|
||||
|
||||
public applyOriginalState(
|
||||
public applyStateBeforeFilter(
|
||||
node: TreeNode,
|
||||
transaction: TreeNodeStateTransaction,
|
||||
): TreeNodeStateTransaction {
|
||||
if (!this.originalStates.has(node)) {
|
||||
const originalState = this.originalStates.get(node);
|
||||
if (!originalState) {
|
||||
return transaction;
|
||||
}
|
||||
const originalState = this.originalStates.get(node);
|
||||
if (originalState.isExpanded !== undefined) {
|
||||
transaction = transaction.withExpansionState(originalState.isExpanded);
|
||||
}
|
||||
transaction = transaction.withVisibilityState(originalState.isVisible);
|
||||
if (originalState.isVisible !== undefined) {
|
||||
transaction = transaction.withVisibilityState(originalState.isVisible);
|
||||
}
|
||||
return transaction;
|
||||
}
|
||||
|
||||
@@ -185,18 +188,13 @@ function setupWatchers(options: {
|
||||
options.nodesRef,
|
||||
],
|
||||
([filterEvent, nodes]) => {
|
||||
if (!filterEvent) {
|
||||
if (filterEvent === undefined) {
|
||||
return;
|
||||
}
|
||||
switch (filterEvent.action) {
|
||||
case TreeViewFilterAction.Triggered:
|
||||
options.onFilterTrigger(filterEvent.predicate, nodes);
|
||||
break;
|
||||
case TreeViewFilterAction.Removed:
|
||||
options.onFilterReset();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown action: ${TreeViewFilterAction[filterEvent.action]}`);
|
||||
if (filterEvent.action === TreeViewFilterAction.Triggered) {
|
||||
options.onFilterTrigger(filterEvent.predicate, nodes);
|
||||
} else {
|
||||
options.onFilterReset();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
|
||||
@@ -2,18 +2,15 @@ import { ICategory, IScript } from '@/domain/ICategory';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { NodeMetadata, NodeType } from '../NodeContent/NodeMetadata';
|
||||
|
||||
export function parseAllCategories(collection: ICategoryCollection): NodeMetadata[] | undefined {
|
||||
export function parseAllCategories(collection: ICategoryCollection): NodeMetadata[] {
|
||||
return createCategoryNodes(collection.actions);
|
||||
}
|
||||
|
||||
export function parseSingleCategory(
|
||||
categoryId: number,
|
||||
collection: ICategoryCollection,
|
||||
): NodeMetadata[] | undefined {
|
||||
const category = collection.findCategory(categoryId);
|
||||
if (!category) {
|
||||
throw new Error(`Category with id ${categoryId} does not exist`);
|
||||
}
|
||||
): NodeMetadata[] {
|
||||
const category = collection.getCategory(categoryId);
|
||||
const tree = parseCategoryRecursively(category);
|
||||
return tree;
|
||||
}
|
||||
@@ -73,7 +70,7 @@ function convertScriptToNode(script: IScript): NodeMetadata {
|
||||
id: getScriptNodeId(script),
|
||||
type: NodeType.Script,
|
||||
text: script.name,
|
||||
children: undefined,
|
||||
children: [],
|
||||
docs: script.docs,
|
||||
isReversible: script.canRevert(),
|
||||
};
|
||||
|
||||
@@ -5,7 +5,6 @@ import { TreeInputNodeData } from '../TreeView/Bindings/TreeInputNodeData';
|
||||
export function getNodeMetadata(
|
||||
treeNode: ReadOnlyTreeNode,
|
||||
): NodeMetadata {
|
||||
if (!treeNode) { throw new Error('missing tree node'); }
|
||||
const data = treeNode.metadata as NodeMetadata;
|
||||
if (!data) {
|
||||
throw new Error('Provided node does not contain the expected metadata.');
|
||||
@@ -14,7 +13,6 @@ export function getNodeMetadata(
|
||||
}
|
||||
|
||||
export function convertToNodeInput(metadata: NodeMetadata): TreeInputNodeData {
|
||||
if (!metadata) { throw new Error('missing metadata'); }
|
||||
return {
|
||||
id: metadata.id,
|
||||
children: convertChildren(metadata.children, convertToNodeInput),
|
||||
@@ -23,7 +21,7 @@ export function convertToNodeInput(metadata: NodeMetadata): TreeInputNodeData {
|
||||
}
|
||||
|
||||
function convertChildren<TOldNode, TNewNode>(
|
||||
oldChildren: readonly TOldNode[],
|
||||
oldChildren: readonly TOldNode[] | undefined,
|
||||
callback: (value: TOldNode) => TNewNode,
|
||||
): TNewNode[] {
|
||||
if (!oldChildren || oldChildren.length === 0) {
|
||||
|
||||
@@ -42,7 +42,7 @@ export function useTreeViewFilterEvent() {
|
||||
|
||||
function subscribeToFilterChanges(
|
||||
filter: IReadOnlyUserFilter,
|
||||
latestFilterEvent: Ref<TreeViewFilterEvent>,
|
||||
latestFilterEvent: Ref<TreeViewFilterEvent | undefined>,
|
||||
filterPredicate: TreeNodeFilterResultPredicate,
|
||||
) {
|
||||
return filter.filterChanged.on((event) => {
|
||||
|
||||
Reference in New Issue
Block a user