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

@@ -6,14 +6,13 @@ export class CodeRunner {
constructor(
private readonly system = getWindowInjectedSystemOperations(),
private readonly environment = RuntimeEnvironment.CurrentEnvironment,
) {
if (!system) {
throw new Error('missing system operations');
}
}
) { }
public async runCode(code: string, folderName: string, fileExtension: string): Promise<void> {
const { os } = this.environment;
if (os === undefined) {
throw new Error('Unidentified operating system');
}
const dir = this.system.location.combinePaths(
this.system.operatingSystem.getTempDirectory(),
folderName,

View File

@@ -2,9 +2,6 @@ import { IEnvironmentVariables } from './IEnvironmentVariables';
/* Validation is externalized to keep the environment objects simple */
export function validateEnvironmentVariables(environment: IEnvironmentVariables): void {
if (!environment) {
throw new Error('missing environment');
}
const keyValues = capturePropertyValues(environment);
if (!Object.keys(keyValues).length) {
throw new Error('Unable to capture key/value pairs');
@@ -30,7 +27,7 @@ function getKeysMissingValues(keyValuePairs: Record<string, unknown>): string[]
* Necessary because code transformations can make class getters non-enumerable during bundling.
* This ensures that even if getters are non-enumerable, their values are still captured and used.
*/
function capturePropertyValues(instance: unknown): Record<string, unknown> {
function capturePropertyValues(instance: object): Record<string, unknown> {
const obj: Record<string, unknown> = {};
const descriptors = Object.getOwnPropertyDescriptors(instance.constructor.prototype);

View File

@@ -9,12 +9,9 @@ export class EventSubscriptionCollection implements IEventSubscriptionCollection
}
public register(subscriptions: IEventSubscription[]) {
if (!subscriptions || subscriptions.length === 0) {
if (subscriptions.length === 0) {
throw new Error('missing subscriptions');
}
if (subscriptions.some((subscription) => !subscription)) {
throw new Error('missing subscription in list');
}
this.subscriptions.push(...subscriptions);
}

View File

@@ -1,13 +1,17 @@
import { ILogger } from './ILogger';
export class ConsoleLogger implements ILogger {
constructor(private readonly globalConsole: Partial<Console> = console) {
if (!globalConsole) {
constructor(private readonly consoleProxy: Partial<Console> = console) {
if (!consoleProxy) { // do not trust strictNullChecks for global objects
throw new Error('missing console');
}
}
public info(...params: unknown[]): void {
this.globalConsole.info(...params);
const logFunction = this.consoleProxy?.info;
if (!logFunction) {
throw new Error('missing "info" function');
}
logFunction.call(this.consoleProxy, ...params);
}
}

View File

@@ -7,6 +7,11 @@ export function createElectronLogger(logger: Partial<ElectronLog>): ILogger {
throw new Error('missing logger');
}
return {
info: (...params) => logger.info(...params),
info: (...params) => {
if (!logger.info) {
throw new Error('missing "info" function');
}
logger.info(...params);
},
};
}

View File

@@ -4,8 +4,8 @@ import { ILogger } from './ILogger';
export class WindowInjectedLogger implements ILogger {
private readonly logger: ILogger;
constructor(windowVariables: WindowVariables = window) {
if (!windowVariables) {
constructor(windowVariables: WindowVariables | undefined | null = window) {
if (!windowVariables) { // do not trust strict null checks for global objects
throw new Error('missing window');
}
if (!windowVariables.log) {

View File

@@ -3,7 +3,7 @@ import { IEntity } from '../Entity/IEntity';
export interface IRepository<TKey, TEntity extends IEntity<TKey>> {
readonly length: number;
getItems(predicate?: (entity: TEntity) => boolean): TEntity[];
getById(id: TKey): TEntity | undefined;
getById(id: TKey): TEntity;
addItem(item: TEntity): void;
addOrUpdateItem(item: TEntity): void;
removeItem(id: TKey): void;

View File

@@ -6,7 +6,7 @@ implements IRepository<TKey, TEntity> {
private readonly items: TEntity[];
constructor(items?: TEntity[]) {
this.items = items || new Array<TEntity>();
this.items = items ?? new Array<TEntity>();
}
public get length(): number {
@@ -17,18 +17,15 @@ implements IRepository<TKey, TEntity> {
return predicate ? this.items.filter(predicate) : this.items;
}
public getById(id: TKey): TEntity | undefined {
public getById(id: TKey): TEntity {
const items = this.getItems((entity) => entity.id === id);
if (!items.length) {
return undefined;
throw new Error(`missing item: ${id}`);
}
return items[0];
}
public addItem(item: TEntity): void {
if (!item) {
throw new Error('missing item');
}
if (this.exists(item.id)) {
throw new Error(`Cannot add (id: ${item.id}) as it is already exists`);
}
@@ -36,9 +33,6 @@ implements IRepository<TKey, TEntity> {
}
public addOrUpdateItem(item: TEntity): void {
if (!item) {
throw new Error('missing item');
}
if (this.exists(item.id)) {
this.removeItem(item.id);
}

View File

@@ -25,9 +25,9 @@ export class DetectorBuilder {
};
}
private detect(userAgent: string): OperatingSystem {
private detect(userAgent: string): OperatingSystem | undefined {
if (!userAgent) {
throw new Error('missing userAgent');
return undefined;
}
if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) {
return undefined;

View File

@@ -37,7 +37,7 @@ export class RuntimeEnvironment implements IRuntimeEnvironment {
}
}
function getUserAgent(window: Partial<Window>): string {
function getUserAgent(window: Partial<Window>): string | undefined {
return window?.navigator?.userAgent;
}

View File

@@ -7,9 +7,6 @@ export abstract class FactoryValidator<T> implements ISanityValidator {
private readonly factory: FactoryFunction<T>;
protected constructor(factory: FactoryFunction<T>) {
if (!factory) {
throw new Error('missing factory');
}
this.factory = factory;
}

View File

@@ -11,7 +11,9 @@ export function validateRuntimeSanity(
options: ISanityCheckOptions,
validators: readonly ISanityValidator[] = DefaultSanityValidators,
): void {
validateContext(options, validators);
if (!validators.length) {
throw new Error('missing validators');
}
const errorMessages = validators.reduce((errors, validator) => {
if (validator.shouldValidate(options)) {
const errorMessage = getErrorMessage(validator);
@@ -26,21 +28,6 @@ export function validateRuntimeSanity(
}
}
function validateContext(
options: ISanityCheckOptions,
validators: readonly ISanityValidator[],
) {
if (!options) {
throw new Error('missing options');
}
if (!validators?.length) {
throw new Error('missing validators');
}
if (validators.some((validator) => !validator)) {
throw new Error('missing validator in validators');
}
}
function getErrorMessage(validator: ISanityValidator): string | undefined {
const errorMessages = [...validator.collectErrors()];
if (!errorMessages.length) {

View File

@@ -6,17 +6,21 @@ export enum FileType {
}
export class SaveFileDialog {
public static saveFile(text: string, fileName: string, type: FileType): void {
const mimeType = this.mimeTypes.get(type);
public static saveFile(
text: string,
fileName: string,
type: FileType,
): void {
const mimeType = this.mimeTypes[type];
this.saveBlob(text, mimeType, fileName);
}
private static readonly mimeTypes = new Map<FileType, string>([
private static readonly mimeTypes: Record<FileType, string> = {
// Some browsers (including firefox + IE) require right mime type
// otherwise they ignore extension and save the file as text.
[FileType.BatchFile, 'application/bat'], // https://en.wikipedia.org/wiki/Batch_file
[FileType.ShellScript, 'text/x-shellscript'], // https://de.wikipedia.org/wiki/Shellskript#MIME-Typ
]);
[FileType.BatchFile]: 'application/bat', // https://en.wikipedia.org/wiki/Batch_file
[FileType.ShellScript]: 'text/x-shellscript', // https://de.wikipedia.org/wiki/Shellskript#MIME-Typ
};
private static saveBlob(file: BlobPart, fileType: string, fileName: string): void {
try {

View File

@@ -19,6 +19,6 @@ export interface ICommandOps {
export interface IFileSystemOps {
setFilePermissions(filePath: string, mode: string | number): Promise<void>;
createDirectory(directoryPath: string, isRecursive?: boolean): Promise<string>;
createDirectory(directoryPath: string, isRecursive?: boolean): Promise<void>;
writeToFile(filePath: string, data: string): Promise<void>;
}

View File

@@ -17,10 +17,16 @@ export function createNodeSystemOperations(): ISystemOperations {
filePath: string,
mode: string | number,
) => chmod(filePath, mode),
createDirectory: (
createDirectory: async (
directoryPath: string,
isRecursive?: boolean,
) => mkdir(directoryPath, { recursive: isRecursive }),
) => {
await mkdir(directoryPath, { recursive: isRecursive });
// Ignoring the return value from `mkdir`, which is the first directory created
// when `recursive` is true. The function contract is to not return any value,
// and we avoid handling this inconsistent behavior.
// See https://github.com/nodejs/node/pull/31530
},
writeToFile: (
filePath: string,
data: string,

View File

@@ -1,13 +1,9 @@
import { EventSource } from '../Events/EventSource';
export class AsyncLazy<T> {
private valueCreated = new EventSource();
private valueCreated = new EventSource<T>();
private isValueCreated = false;
private isCreatingValue = false;
private value: T | undefined;
private state: ValueState<T> = { status: ValueStatus.NotRequested };
constructor(private valueFactory: () => Promise<T>) {}
@@ -15,23 +11,44 @@ export class AsyncLazy<T> {
this.valueFactory = valueFactory;
}
public async getValue(): Promise<T> {
// If value is already created, return the value directly
if (this.isValueCreated) {
return Promise.resolve(this.value);
public getValue(): Promise<T> {
if (this.state.status === ValueStatus.Created) {
return Promise.resolve(this.state.value);
}
// If value is being created, wait until the value is created and then return it.
if (this.isCreatingValue) {
return new Promise<T>((resolve) => {
// Return/result when valueCreated event is triggered.
this.valueCreated.on(() => resolve(this.value));
});
if (this.state.status === ValueStatus.BeingCreated) {
return this.state.value;
}
this.isCreatingValue = true;
this.value = await this.valueFactory();
this.isCreatingValue = false;
this.isValueCreated = true;
this.valueCreated.notify(null);
return Promise.resolve(this.value);
const valuePromise = this.valueFactory();
this.state = {
status: ValueStatus.BeingCreated,
value: valuePromise,
};
valuePromise.then((value) => {
this.state = {
status: ValueStatus.Created,
value,
};
this.valueCreated.notify(value);
});
return valuePromise;
}
}
enum ValueStatus {
NotRequested,
BeingCreated,
Created,
}
type ValueState<T> =
| {
readonly status: ValueStatus.NotRequested;
}
| {
readonly status: ValueStatus.BeingCreated;
readonly value: Promise<T>;
}
| {
readonly status: ValueStatus.Created;
readonly value: T
};

View File

@@ -4,8 +4,8 @@ import { ILogger } from '@/infrastructure/Log/ILogger';
/* Primary entry point for platform-specific injections */
export interface WindowVariables {
readonly system: ISystemOperations;
readonly isDesktop: boolean;
readonly os: OperatingSystem;
readonly log: ILogger;
readonly isDesktop?: boolean;
readonly system?: ISystemOperations;
readonly os?: OperatingSystem;
readonly log?: ILogger;
}

View File

@@ -17,7 +17,7 @@ export function validateWindowVariables(variables: Partial<WindowVariables>) {
function* testEveryProperty(variables: Partial<WindowVariables>): Iterable<string> {
const tests: {
[K in PropertyKeys<WindowVariables>]: boolean;
[K in PropertyKeys<Required<WindowVariables>>]: boolean;
} = {
os: testOperatingSystem(variables.os),
isDesktop: testIsDesktop(variables.isDesktop),