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:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user