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:
undergroundwires
2023-11-12 22:54:00 +01:00
parent 7ab16ecccb
commit 949fac1a7c
294 changed files with 2477 additions and 2738 deletions

View File

@@ -10,7 +10,7 @@
</div>
-->
<div
v-if="categoryIds != null && categoryIds.length > 0"
v-if="categoryIds.length > 0"
class="cards"
>
<CardListItem
@@ -50,8 +50,9 @@ export default defineComponent({
const { currentState, onStateChange } = injectKey((keys) => keys.useCollectionState);
const width = ref<number>(0);
const categoryIds = computed<ReadonlyArray<number>>(() => currentState
.value.collection.actions.map((category) => category.id));
const categoryIds = computed<readonly number[]>(
() => currentState.value.collection.actions.map((category) => category.id),
);
const activeCategoryId = ref<number | undefined>(undefined);
function onSelected(categoryId: number, isExpanded: boolean) {

View File

@@ -89,12 +89,9 @@ export default defineComponent({
const cardElement = shallowRef<HTMLElement>();
const cardTitle = computed<string | undefined>(() => {
if (!props.categoryId || !currentState.value) {
return undefined;
}
const category = currentState.value.collection.findCategory(props.categoryId);
return category?.name;
const cardTitle = computed<string>(() => {
const category = currentState.value.collection.getCategory(props.categoryId);
return category.name;
});
function collapse() {
@@ -102,8 +99,12 @@ export default defineComponent({
}
async function scrollToCard() {
const card = cardElement.value;
if (!card) {
throw new Error('Card is not found');
}
await sleep(400); // wait a bit to allow GUI to render the expanded card
cardElement.value.scrollIntoView({ behavior: 'smooth' });
card.scrollIntoView({ behavior: 'smooth' });
}
return {

View File

@@ -34,7 +34,7 @@ export default defineComponent({
const currentCollection = computed<ICategoryCollection>(() => currentState.value.collection);
const currentCategory = computed<ICategory>(
() => currentCollection.value.findCategory(props.categoryId),
() => currentCollection.value.getCategory(props.categoryId),
);
const isAnyChildSelected = computed<boolean>(

View File

@@ -71,6 +71,9 @@ export default defineComponent({
const searchHasMatches = ref(false);
const trimmedSearchQuery = computed(() => {
const query = searchQuery.value;
if (!query) {
return '';
}
const threshold = 30;
if (query.length <= threshold - 3) {
return query;
@@ -94,7 +97,7 @@ export default defineComponent({
function updateFromInitialFilter(filter?: IFilterResult) {
searchQuery.value = filter?.query;
searchHasMatches.value = filter?.hasAnyMatches();
searchHasMatches.value = filter?.hasAnyMatches() ?? false;
}
function subscribeToFilterChanges(filter: IReadOnlyUserFilter) {

View File

@@ -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/);

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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));
}

View File

@@ -30,7 +30,7 @@ import { useSelectedScriptNodeIds } from './TreeViewAdapter/UseSelectedScriptNod
export default defineComponent({
props: {
categoryId: {
type: [Number, undefined],
type: [Number],
default: undefined,
},
},

View File

@@ -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,
};

View File

@@ -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;
}
}

View File

@@ -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');
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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 },

View File

@@ -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(),
};

View File

@@ -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) {

View File

@@ -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) => {