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

@@ -8,7 +8,6 @@ export class Application implements IApplication {
public info: IProjectInformation,
public collections: readonly ICategoryCollection[],
) {
validateInformation(info);
validateCollections(collections);
}
@@ -16,19 +15,17 @@ export class Application implements IApplication {
return this.collections.map((collection) => collection.os);
}
public getCollection(operatingSystem: OperatingSystem): ICategoryCollection | undefined {
return this.collections.find((collection) => collection.os === operatingSystem);
}
}
function validateInformation(info: IProjectInformation) {
if (!info) {
throw new Error('missing project information');
public getCollection(operatingSystem: OperatingSystem): ICategoryCollection {
const collection = this.collections.find((c) => c.os === operatingSystem);
if (!collection) {
throw new Error(`Operating system "${OperatingSystem[operatingSystem]}" is not defined in application`);
}
return collection;
}
}
function validateCollections(collections: readonly ICategoryCollection[]) {
if (!collections || !collections.length) {
if (!collections.length) {
throw new Error('missing collections');
}
if (collections.filter((c) => !c).length > 0) {

View File

@@ -3,14 +3,14 @@ import { IScript } from './IScript';
import { ICategory } from './ICategory';
export class Category extends BaseEntity<number> implements ICategory {
private allSubScripts: ReadonlyArray<IScript> = undefined;
private allSubScripts?: ReadonlyArray<IScript> = undefined;
constructor(
id: number,
public readonly name: string,
public readonly docs: ReadonlyArray<string>,
public readonly subCategories?: ReadonlyArray<ICategory>,
public readonly scripts?: ReadonlyArray<IScript>,
public readonly subCategories: ReadonlyArray<ICategory>,
public readonly scripts: ReadonlyArray<IScript>,
) {
super(id);
validateCategory(this);
@@ -39,10 +39,7 @@ function validateCategory(category: ICategory) {
if (!category.name) {
throw new Error('missing name');
}
if (
(!category.subCategories || category.subCategories.length === 0)
&& (!category.scripts || category.scripts.length === 0)
) {
if (category.subCategories.length === 0 && category.scripts.length === 0) {
throw new Error('A category must have at least one sub-category or script');
}
}

View File

@@ -19,9 +19,6 @@ export class CategoryCollection implements ICategoryCollection {
public readonly actions: ReadonlyArray<ICategory>,
public readonly scripting: IScriptingDefinition,
) {
if (!scripting) {
throw new Error('missing scripting definition');
}
this.queryable = makeQueryable(actions);
assertInRange(os, OperatingSystem);
ensureValid(this.queryable);
@@ -29,17 +26,26 @@ export class CategoryCollection implements ICategoryCollection {
ensureNoDuplicates(this.queryable.allScripts);
}
public findCategory(categoryId: number): ICategory | undefined {
return this.queryable.allCategories.find((category) => category.id === categoryId);
public getCategory(categoryId: number): ICategory {
const category = this.queryable.allCategories.find((c) => c.id === categoryId);
if (!category) {
throw new Error(`Missing category with ID: "${categoryId}"`);
}
return category;
}
public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
assertInRange(level, RecommendationLevel);
return this.queryable.scriptsByLevel.get(level);
const scripts = this.queryable.scriptsByLevel.get(level);
return scripts ?? [];
}
public findScript(scriptId: string): IScript | undefined {
return this.queryable.allScripts.find((script) => script.id === scriptId);
public getScript(scriptId: string): IScript {
const script = this.queryable.allScripts.find((s) => s.id === scriptId);
if (!script) {
throw new Error(`missing script: ${scriptId}`);
}
return script;
}
public getAllScripts(): IScript[] {
@@ -78,13 +84,13 @@ function ensureValid(application: IQueryableCollection) {
}
function ensureValidCategories(allCategories: readonly ICategory[]) {
if (!allCategories || allCategories.length === 0) {
if (!allCategories.length) {
throw new Error('must consist of at least one category');
}
}
function ensureValidScripts(allScripts: readonly IScript[]) {
if (!allScripts || allScripts.length === 0) {
if (!allScripts.length) {
throw new Error('must consist of at least one script');
}
const missingRecommendationLevels = getEnumValues(RecommendationLevel)

View File

@@ -7,5 +7,5 @@ export interface IApplication {
readonly collections: readonly ICategoryCollection[];
getSupportedOsList(): OperatingSystem[];
getCollection(operatingSystem: OperatingSystem): ICategoryCollection | undefined;
getCollection(operatingSystem: OperatingSystem): ICategoryCollection;
}

View File

@@ -5,8 +5,8 @@ import { IDocumentable } from './IDocumentable';
export interface ICategory extends IEntity<number>, IDocumentable {
readonly id: number;
readonly name: string;
readonly subCategories?: ReadonlyArray<ICategory>;
readonly scripts?: ReadonlyArray<IScript>;
readonly subCategories: ReadonlyArray<ICategory>;
readonly scripts: ReadonlyArray<IScript>;
includes(script: IScript): boolean;
getAllScriptsRecursively(): ReadonlyArray<IScript>;
}

View File

@@ -12,8 +12,8 @@ export interface ICategoryCollection {
readonly actions: ReadonlyArray<ICategory>;
getScriptsByLevel(level: RecommendationLevel): ReadonlyArray<IScript>;
findCategory(categoryId: number): ICategory | undefined;
findScript(scriptId: string): IScript | undefined;
getCategory(categoryId: number): ICategory;
getScript(scriptId: string): IScript;
getAllScripts(): ReadonlyArray<IScript>;
getAllCategories(): ReadonlyArray<ICategory>;
}

View File

@@ -1,4 +1,4 @@
export interface IScriptCode {
readonly execute: string;
readonly revert: string;
readonly revert?: string;
}

View File

@@ -16,9 +16,6 @@ export class ProjectInformation implements IProjectInformation {
if (!name) {
throw new Error('name is undefined');
}
if (!version) {
throw new Error('undefined version');
}
if (!slogan) {
throw new Error('undefined slogan');
}

View File

@@ -11,9 +11,6 @@ export class Script extends BaseEntity<string> implements IScript {
public readonly level?: RecommendationLevel,
) {
super(name);
if (!code) {
throw new Error('missing code');
}
validateLevel(level);
}

View File

@@ -3,14 +3,14 @@ import { IScriptCode } from './IScriptCode';
export class ScriptCode implements IScriptCode {
constructor(
public readonly execute: string,
public readonly revert: string,
public readonly revert: string | undefined,
) {
validateCode(execute);
validateRevertCode(revert, execute);
}
}
function validateRevertCode(revertCode: string, execute: string) {
function validateRevertCode(revertCode: string | undefined, execute: string) {
if (!revertCode) {
return;
}
@@ -25,7 +25,7 @@ function validateRevertCode(revertCode: string, execute: string) {
}
function validateCode(code: string): void {
if (!code || code.length === 0) {
if (code.length === 0) {
throw new Error('missing code');
}
}