diff --git a/cypress.config.ts b/cypress.config.ts index 55a35132..225323d8 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -11,8 +11,16 @@ export default defineConfig({ videosFolder: `${CYPRESS_BASE_DIR}/videos`, e2e: { - baseUrl: `http://localhost:${ViteConfig.server.port}/`, + baseUrl: `http://localhost:${getApplicationPort()}/`, specPattern: `${CYPRESS_BASE_DIR}/**/*.cy.{js,jsx,ts,tsx}`, // Default: cypress/e2e/**/*.cy.{js,jsx,ts,tsx} supportFile: `${CYPRESS_BASE_DIR}/support/e2e.ts`, }, }); + +function getApplicationPort(): number { + const port = ViteConfig.server?.port; + if (port === undefined) { + throw new Error('Unknown application port'); + } + return port; +} diff --git a/docs/tests.md b/docs/tests.md index 715f6967..d6736080 100644 --- a/docs/tests.md +++ b/docs/tests.md @@ -68,13 +68,13 @@ These checks validate various qualities like runtime execution, building process - [`./src/`](./../src/): Contains the code subject to testing. - [`./tests/shared/`](./../tests/shared/): Contains code shared by different test categories. - [`bootstrap/setup.ts`](./../tests/shared/bootstrap/setup.ts): Initializes unit and integration tests. + - [`Assertions/`](./../tests/shared/Assertions/): Contains common assertion functions, prefixed with `expect`. - [`./tests/unit/`](./../tests/unit/) - Stores unit test code. - The directory structure mirrors [`./src/`](./../src). - E.g., tests for [`./src/application/ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts) reside in [`./tests/unit/application/ApplicationFactory.spec.ts`](./../tests/unit/application/ApplicationFactory.spec.ts). - [`shared/`](./../tests/unit/shared/) - Contains shared unit test functionalities. - - [`Assertions/`](./../tests/unit/shared/Assertions): Contains common assertion functions, prefixed with `expect`. - [`TestCases/`](./../tests/unit/shared/TestCases/) - Shared test cases. - Functions that calls `it()` from [Vitest](https://vitest.dev/) should have `it` prefix. diff --git a/src/application/ApplicationFactory.ts b/src/application/ApplicationFactory.ts index 6e80451e..e9b7a60d 100644 --- a/src/application/ApplicationFactory.ts +++ b/src/application/ApplicationFactory.ts @@ -12,9 +12,6 @@ export class ApplicationFactory implements IApplicationFactory { private readonly getter: AsyncLazy; protected constructor(costlyGetter: ApplicationGetterType) { - if (!costlyGetter) { - throw new Error('missing getter'); - } this.getter = new AsyncLazy(() => Promise.resolve(costlyGetter())); } diff --git a/src/application/Common/Array.ts b/src/application/Common/Array.ts index a3f95dfd..c48b831a 100644 --- a/src/application/Common/Array.ts +++ b/src/application/Common/Array.ts @@ -1,7 +1,5 @@ // Compares to Array objects for equality, ignoring order export function scrambledEqual(array1: readonly T[], array2: readonly T[]) { - if (!array1) { throw new Error('missing first array'); } - if (!array2) { throw new Error('missing second array'); } const sortedArray1 = sort(array1); const sortedArray2 = sort(array2); return sequenceEqual(sortedArray1, sortedArray2); @@ -12,8 +10,6 @@ export function scrambledEqual(array1: readonly T[], array2: readonly T[]) { // Compares to Array objects for equality in same order export function sequenceEqual(array1: readonly T[], array2: readonly T[]) { - if (!array1) { throw new Error('missing first array'); } - if (!array2) { throw new Error('missing second array'); } if (array1.length !== array2.length) { return false; } diff --git a/src/application/Common/CustomError.ts b/src/application/Common/CustomError.ts index f572cd06..b219a945 100644 --- a/src/application/Common/CustomError.ts +++ b/src/application/Common/CustomError.ts @@ -20,23 +20,30 @@ export abstract class CustomError extends Error { } } -export const Environment = { +interface ErrorPrototypeManipulation { + getSetPrototypeOf: () => (typeof Object.setPrototypeOf | undefined); + getCaptureStackTrace: () => (typeof Error.captureStackTrace | undefined); +} + +export const PlatformErrorPrototypeManipulation: ErrorPrototypeManipulation = { getSetPrototypeOf: () => Object.setPrototypeOf, getCaptureStackTrace: () => Error.captureStackTrace, }; function fixPrototype(target: Error, prototype: CustomError) { - // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget - const setPrototypeOf = Environment.getSetPrototypeOf(); - if (!functionExists(setPrototypeOf)) { + // This is recommended by TypeScript guidelines. + // Source: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget + // Snapshots: https://web.archive.org/web/20231111234849/https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget, https://archive.ph/tr7cX#support-for-newtarget + const setPrototypeOf = PlatformErrorPrototypeManipulation.getSetPrototypeOf(); + if (!isFunction(setPrototypeOf)) { return; } setPrototypeOf(target, prototype); } function ensureStackTrace(target: Error) { - const captureStackTrace = Environment.getCaptureStackTrace(); - if (!functionExists(captureStackTrace)) { + const captureStackTrace = PlatformErrorPrototypeManipulation.getCaptureStackTrace(); + if (!isFunction(captureStackTrace)) { // captureStackTrace is only available on V8, if it's not available // modern JS engines will usually generate a stack trace on error objects when they're thrown. return; @@ -44,7 +51,7 @@ function ensureStackTrace(target: Error) { captureStackTrace(target, target.constructor); } -function functionExists(func: unknown): boolean { - // Not doing truthy/falsy check i.e. if(func) as most values are truthy in JS for robustness +// eslint-disable-next-line @typescript-eslint/ban-types +function isFunction(func: unknown): func is Function { return typeof func === 'function'; } diff --git a/src/application/Common/Enum.ts b/src/application/Common/Enum.ts index 624a52ab..b7f37da7 100644 --- a/src/application/Common/Enum.ts +++ b/src/application/Common/Enum.ts @@ -54,9 +54,6 @@ export function assertInRange( value: TEnumValue, enumVariable: EnumVariable, ) { - if (value === undefined || value === null) { - throw new Error('absent enum value'); - } if (!(value in enumVariable)) { throw new RangeError(`enum value "${value}" is out of range`); } diff --git a/src/application/Common/ScriptingLanguage/ScriptingLanguageFactory.ts b/src/application/Common/ScriptingLanguage/ScriptingLanguageFactory.ts index 6ae46fdf..28415239 100644 --- a/src/application/Common/ScriptingLanguage/ScriptingLanguageFactory.ts +++ b/src/application/Common/ScriptingLanguage/ScriptingLanguageFactory.ts @@ -9,19 +9,16 @@ export abstract class ScriptingLanguageFactory implements IScriptingLanguageF public create(language: ScriptingLanguage): T { assertInRange(language, ScriptingLanguage); - if (!this.getters.has(language)) { + const getter = this.getters.get(language); + if (!getter) { throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`); } - const getter = this.getters.get(language); const instance = getter(); return instance; } protected registerGetter(language: ScriptingLanguage, getter: Getter) { assertInRange(language, ScriptingLanguage); - if (!getter) { - throw new Error('missing getter'); - } if (this.getters.has(language)) { throw new Error(`${ScriptingLanguage[language]} is already registered`); } diff --git a/src/application/Context/ApplicationContext.ts b/src/application/Context/ApplicationContext.ts index 2ed02b5f..284664f3 100644 --- a/src/application/Context/ApplicationContext.ts +++ b/src/application/Context/ApplicationContext.ts @@ -26,7 +26,6 @@ export class ApplicationContext implements IApplicationContext { public readonly app: IApplication, initialContext: OperatingSystem, ) { - validateApp(app); this.states = initializeStates(app); this.changeContext(initialContext); } @@ -36,10 +35,8 @@ export class ApplicationContext implements IApplicationContext { if (this.currentOs === os) { return; } - this.collection = this.app.getCollection(os); - if (!this.collection) { - throw new Error(`os "${OperatingSystem[os]}" is not defined in application`); - } + const collection = this.app.getCollection(os); + this.collection = collection; const event: IApplicationContextChangedEvent = { newState: this.states[os], oldState: this.states[this.currentOs], @@ -49,12 +46,6 @@ export class ApplicationContext implements IApplicationContext { } } -function validateApp(app: IApplication) { - if (!app) { - throw new Error('missing app'); - } -} - function initializeStates(app: IApplication): StateMachine { const machine = new Map(); for (const collection of app.collections) { diff --git a/src/application/Context/ApplicationContextFactory.ts b/src/application/Context/ApplicationContextFactory.ts index 44350ef5..bfde26d6 100644 --- a/src/application/Context/ApplicationContextFactory.ts +++ b/src/application/Context/ApplicationContextFactory.ts @@ -10,18 +10,23 @@ export async function buildContext( factory: IApplicationFactory = ApplicationFactory.Current, environment = RuntimeEnvironment.CurrentEnvironment, ): Promise { - if (!factory) { throw new Error('missing factory'); } - if (!environment) { throw new Error('missing environment'); } const app = await factory.getApp(); const os = getInitialOs(app, environment.os); return new ApplicationContext(app, os); } -function getInitialOs(app: IApplication, currentOs: OperatingSystem): OperatingSystem { +function getInitialOs( + app: IApplication, + currentOs: OperatingSystem | undefined, +): OperatingSystem { const supportedOsList = app.getSupportedOsList(); - if (supportedOsList.includes(currentOs)) { + if (currentOs !== undefined && supportedOsList.includes(currentOs)) { return currentOs; } + return getMostSupportedOs(supportedOsList, app); +} + +function getMostSupportedOs(supportedOsList: OperatingSystem[], app: IApplication) { supportedOsList.sort((os1, os2) => { const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts; return getPriority(os2) - getPriority(os1); diff --git a/src/application/Context/State/Code/ApplicationCode.ts b/src/application/Context/State/Code/ApplicationCode.ts index 165d809b..91cf795f 100644 --- a/src/application/Context/State/Code/ApplicationCode.ts +++ b/src/application/Context/State/Code/ApplicationCode.ts @@ -21,9 +21,6 @@ export class ApplicationCode implements IApplicationCode { private readonly scriptingDefinition: IScriptingDefinition, private readonly generator: IUserScriptGenerator = new UserScriptGenerator(), ) { - if (!userSelection) { throw new Error('missing userSelection'); } - if (!scriptingDefinition) { throw new Error('missing scriptingDefinition'); } - if (!generator) { throw new Error('missing generator'); } this.setCode(userSelection.selectedScripts); userSelection.changed.on((scripts) => { this.setCode(scripts); diff --git a/src/application/Context/State/Code/Event/CodeChangedEvent.ts b/src/application/Context/State/Code/Event/CodeChangedEvent.ts index 52cbb532..681df926 100644 --- a/src/application/Context/State/Code/Event/CodeChangedEvent.ts +++ b/src/application/Context/State/Code/Event/CodeChangedEvent.ts @@ -36,7 +36,11 @@ export class CodeChangedEvent implements ICodeChangedEvent { } public getScriptPositionInCode(script: IScript): ICodePosition { - return this.scripts.get(script); + const position = this.scripts.get(script); + if (!position) { + throw new Error('Unknown script: Position could not be found for the script'); + } + return position; } } diff --git a/src/application/Context/State/Code/Generation/CodeBuilder.ts b/src/application/Context/State/Code/Generation/CodeBuilder.ts index 22fa6ea1..4264c876 100644 --- a/src/application/Context/State/Code/Generation/CodeBuilder.ts +++ b/src/application/Context/State/Code/Generation/CodeBuilder.ts @@ -16,7 +16,9 @@ export abstract class CodeBuilder implements ICodeBuilder { return this; } const lines = code.match(/[^\r\n]+/g); - this.lines.push(...lines); + if (lines) { + this.lines.push(...lines); + } return this; } diff --git a/src/application/Context/State/Code/Generation/UserScriptGenerator.ts b/src/application/Context/State/Code/Generation/UserScriptGenerator.ts index 7743942b..e50b4a28 100644 --- a/src/application/Context/State/Code/Generation/UserScriptGenerator.ts +++ b/src/application/Context/State/Code/Generation/UserScriptGenerator.ts @@ -17,8 +17,6 @@ export class UserScriptGenerator implements IUserScriptGenerator { selectedScripts: ReadonlyArray, scriptingDefinition: IScriptingDefinition, ): IUserScript { - if (!selectedScripts) { throw new Error('missing scripts'); } - if (!scriptingDefinition) { throw new Error('missing definition'); } if (!selectedScripts.length) { return { code: '', scriptPositions: new Map() }; } @@ -68,8 +66,19 @@ function appendSelection( function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder { const { script } = selection; const name = selection.revert ? `${script.name} (revert)` : script.name; - const scriptCode = selection.revert ? script.code.revert : script.code.execute; + const scriptCode = getSelectedCode(selection); return builder .appendLine() .appendFunction(name, scriptCode); } + +function getSelectedCode(selection: SelectedScript): string { + const { code } = selection.script; + if (!selection.revert) { + return code.execute; + } + if (!code.revert) { + throw new Error('Reverted script lacks revert code.'); + } + return code.revert; +} diff --git a/src/application/Context/State/Filter/Event/FilterChange.ts b/src/application/Context/State/Filter/Event/FilterChange.ts index e90eb2a9..bed54dc8 100644 --- a/src/application/Context/State/Filter/Event/FilterChange.ts +++ b/src/application/Context/State/Filter/Event/FilterChange.ts @@ -1,37 +1,37 @@ import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult'; import { FilterActionType } from './FilterActionType'; -import { IFilterChangeDetails, IFilterChangeDetailsVisitor } from './IFilterChangeDetails'; +import { + IFilterChangeDetails, IFilterChangeDetailsVisitor, + ApplyFilterAction, ClearFilterAction, +} from './IFilterChangeDetails'; export class FilterChange implements IFilterChangeDetails { - public static forApply(filter: IFilterResult) { - if (!filter) { - throw new Error('missing filter'); - } - return new FilterChange(FilterActionType.Apply, filter); + public static forApply( + filter: IFilterResult, + ): IFilterChangeDetails { + return new FilterChange({ type: FilterActionType.Apply, filter }); } - public static forClear() { - return new FilterChange(FilterActionType.Clear); + public static forClear(): IFilterChangeDetails { + return new FilterChange({ type: FilterActionType.Clear }); } - private constructor( - public readonly actionType: FilterActionType, - public readonly filter?: IFilterResult, - ) { } + private constructor(public readonly action: ApplyFilterAction | ClearFilterAction) { } public visit(visitor: IFilterChangeDetailsVisitor): void { - if (!visitor) { - throw new Error('missing visitor'); - } - switch (this.actionType) { + switch (this.action.type) { case FilterActionType.Apply: - visitor.onApply(this.filter); + if (visitor.onApply) { + visitor.onApply(this.action.filter); + } break; case FilterActionType.Clear: - visitor.onClear(); + if (visitor.onClear) { + visitor.onClear(); + } break; default: - throw new Error(`Unknown action type: ${this.actionType}`); + throw new Error(`Unknown action: ${this.action}`); } } } diff --git a/src/application/Context/State/Filter/Event/IFilterChangeDetails.ts b/src/application/Context/State/Filter/Event/IFilterChangeDetails.ts index 2bb32bbe..1cf343e1 100644 --- a/src/application/Context/State/Filter/Event/IFilterChangeDetails.ts +++ b/src/application/Context/State/Filter/Event/IFilterChangeDetails.ts @@ -2,13 +2,22 @@ import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult' import { FilterActionType } from './FilterActionType'; export interface IFilterChangeDetails { - readonly actionType: FilterActionType; - readonly filter?: IFilterResult; - + readonly action: FilterAction; visit(visitor: IFilterChangeDetailsVisitor): void; } export interface IFilterChangeDetailsVisitor { - onClear(): void; - onApply(filter: IFilterResult): void; + readonly onClear?: () => void; + readonly onApply?: (filter: IFilterResult) => void; } + +export type ApplyFilterAction = { + readonly type: FilterActionType.Apply, + readonly filter: IFilterResult; +}; + +export type ClearFilterAction = { + readonly type: FilterActionType.Clear, +}; + +export type FilterAction = ApplyFilterAction | ClearFilterAction; diff --git a/src/application/Context/State/Filter/FilterResult.ts b/src/application/Context/State/Filter/FilterResult.ts index 2bf8e46d..5c949ebe 100644 --- a/src/application/Context/State/Filter/FilterResult.ts +++ b/src/application/Context/State/Filter/FilterResult.ts @@ -9,8 +9,6 @@ export class FilterResult implements IFilterResult { public readonly query: string, ) { if (!query) { throw new Error('Query is empty or undefined'); } - if (!scriptMatches) { throw new Error('Script matches is undefined'); } - if (!categoryMatches) { throw new Error('Category matches is undefined'); } } public hasAnyMatches(): boolean { diff --git a/src/application/Context/State/Selection/UserSelection.ts b/src/application/Context/State/Selection/UserSelection.ts index 1aee15f5..bba4d93d 100644 --- a/src/application/Context/State/Selection/UserSelection.ts +++ b/src/application/Context/State/Selection/UserSelection.ts @@ -43,7 +43,7 @@ export class UserSelection implements IUserSelection { } public removeAllInCategory(categoryId: number): void { - const category = this.collection.findCategory(categoryId); + const category = this.collection.getCategory(categoryId); const scriptsToRemove = category.getAllScriptsRecursively() .filter((script) => this.scripts.exists(script.id)); if (!scriptsToRemove.length) { @@ -57,7 +57,7 @@ export class UserSelection implements IUserSelection { public addOrUpdateAllInCategory(categoryId: number, revert = false): void { const scriptsToAddOrUpdate = this.collection - .findCategory(categoryId) + .getCategory(categoryId) .getAllScriptsRecursively() .filter( (script) => !this.scripts.exists(script.id) @@ -74,17 +74,14 @@ export class UserSelection implements IUserSelection { } public addSelectedScript(scriptId: string, revert: boolean): void { - const script = this.collection.findScript(scriptId); - if (!script) { - throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`); - } + const script = this.collection.getScript(scriptId); const selectedScript = new SelectedScript(script, revert); this.scripts.addItem(selectedScript); this.changed.notify(this.scripts.getItems()); } public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void { - const script = this.collection.findScript(scriptId); + const script = this.collection.getScript(scriptId); const selectedScript = new SelectedScript(script, revert); this.scripts.addOrUpdateItem(selectedScript); this.changed.notify(this.scripts.getItems()); @@ -130,7 +127,7 @@ export class UserSelection implements IUserSelection { } public selectOnly(scripts: readonly IScript[]): void { - if (!scripts || scripts.length === 0) { + if (!scripts.length) { throw new Error('Scripts are empty. Use deselectAll() if you want to deselect everything'); } let totalChanged = 0; diff --git a/src/application/Parser/ApplicationParser.ts b/src/application/Parser/ApplicationParser.ts index e5be8c57..1f6c24fa 100644 --- a/src/application/Parser/ApplicationParser.ts +++ b/src/application/Parser/ApplicationParser.ts @@ -32,10 +32,7 @@ const PreParsedCollections: readonly CollectionData [] = [ ]; function validateCollectionsData(collections: readonly CollectionData[]) { - if (!collections?.length) { + if (!collections.length) { throw new Error('missing collections'); } - if (collections.some((collection) => !collection)) { - throw new Error('missing collection provided'); - } } diff --git a/src/application/Parser/CategoryCollectionParser.ts b/src/application/Parser/CategoryCollectionParser.ts index 9bc86504..0752dd0a 100644 --- a/src/application/Parser/CategoryCollectionParser.ts +++ b/src/application/Parser/CategoryCollectionParser.ts @@ -28,10 +28,7 @@ export function parseCategoryCollection( } function validate(content: CollectionData): void { - if (!content) { - throw new Error('missing content'); - } - if (!content.actions || content.actions.length <= 0) { + if (!content.actions.length) { throw new Error('content does not define any action'); } } diff --git a/src/application/Parser/CategoryParser.ts b/src/application/Parser/CategoryParser.ts index 10f8d2ef..c424a5f0 100644 --- a/src/application/Parser/CategoryParser.ts +++ b/src/application/Parser/CategoryParser.ts @@ -1,5 +1,5 @@ import type { - CategoryData, ScriptData, CategoryOrScriptData, InstructionHolder, + CategoryData, ScriptData, CategoryOrScriptData, } from '@/application/collections/'; import { Script } from '@/domain/Script'; import { Category } from '@/domain/Category'; @@ -16,7 +16,6 @@ export function parseCategory( context: ICategoryCollectionParseContext, factory: CategoryFactoryType = CategoryFactory, ): Category { - if (!context) { throw new Error('missing context'); } return parseCategoryRecursively({ categoryData: category, context, @@ -30,8 +29,8 @@ interface ICategoryParseContext { readonly factory: CategoryFactoryType, readonly parentCategory?: CategoryData, } -// eslint-disable-next-line consistent-return -function parseCategoryRecursively(context: ICategoryParseContext): Category { + +function parseCategoryRecursively(context: ICategoryParseContext): Category | never { ensureValidCategory(context.categoryData, context.parentCategory); const children: ICategoryChildren = { subCategories: new Array(), @@ -55,7 +54,7 @@ function parseCategoryRecursively(context: ICategoryParseContext): Category { /* scripts: */ children.subScripts, ); } catch (err) { - new NodeValidator({ + return new NodeValidator({ type: NodeType.Category, selfNode: context.categoryData, parentNode: context.parentCategory, @@ -72,7 +71,7 @@ function ensureValidCategory(category: CategoryData, parentCategory?: CategoryDa .assertDefined(category) .assertValidName(category.category) .assert( - () => category.children && category.children.length > 0, + () => category.children.length > 0, `"${category.category}" has no children.`, ); } @@ -94,14 +93,14 @@ function parseNode(context: INodeParseContext) { validator.assertDefined(context.nodeData); if (isCategory(context.nodeData)) { const subCategory = parseCategoryRecursively({ - categoryData: context.nodeData as CategoryData, + categoryData: context.nodeData, context: context.context, factory: context.factory, parentCategory: context.parent, }); context.children.subCategories.push(subCategory); } else if (isScript(context.nodeData)) { - const script = parseScript(context.nodeData as ScriptData, context.context); + const script = parseScript(context.nodeData, context.context); context.children.subScripts.push(script); } else { validator.throw('Node is neither a category or a script.'); @@ -109,19 +108,18 @@ function parseNode(context: INodeParseContext) { } function isScript(data: CategoryOrScriptData): data is ScriptData { - const holder = (data as InstructionHolder); - return hasCode(holder) || hasCall(holder); + return hasCode(data) || hasCall(data); } function isCategory(data: CategoryOrScriptData): data is CategoryData { return hasProperty(data, 'category'); } -function hasCode(data: InstructionHolder): boolean { +function hasCode(data: unknown): boolean { return hasProperty(data, 'code'); } -function hasCall(data: InstructionHolder) { +function hasCall(data: unknown) { return hasProperty(data, 'call'); } diff --git a/src/application/Parser/DocumentationParser.ts b/src/application/Parser/DocumentationParser.ts index 0639dfad..9d1b0ca3 100644 --- a/src/application/Parser/DocumentationParser.ts +++ b/src/application/Parser/DocumentationParser.ts @@ -1,9 +1,6 @@ import type { DocumentableData, DocumentationData } from '@/application/collections/'; export function parseDocs(documentable: DocumentableData): readonly string[] { - if (!documentable) { - throw new Error('missing documentable'); - } const { docs } = documentable; if (!docs) { return []; diff --git a/src/application/Parser/NodeValidation/NodeValidator.ts b/src/application/Parser/NodeValidation/NodeValidator.ts index 0fce365c..4c154dbd 100644 --- a/src/application/Parser/NodeValidation/NodeValidator.ts +++ b/src/application/Parser/NodeValidation/NodeValidator.ts @@ -32,7 +32,7 @@ export class NodeValidator { return this; } - public throw(errorMessage: string) { + public throw(errorMessage: string): never { throw new NodeDataError(errorMessage, this.context); } } diff --git a/src/application/Parser/Script/CategoryCollectionParseContext.ts b/src/application/Parser/Script/CategoryCollectionParseContext.ts index fc02301f..68245e56 100644 --- a/src/application/Parser/Script/CategoryCollectionParseContext.ts +++ b/src/application/Parser/Script/CategoryCollectionParseContext.ts @@ -17,8 +17,7 @@ export class CategoryCollectionParseContext implements ICategoryCollectionParseC scripting: IScriptingDefinition, syntaxFactory: ISyntaxFactory = new SyntaxFactory(), ) { - if (!scripting) { throw new Error('missing scripting'); } this.syntax = syntaxFactory.create(scripting.language); - this.compiler = new ScriptCompiler(functionsData, this.syntax); + this.compiler = new ScriptCompiler(functionsData ?? [], this.syntax); } } diff --git a/src/application/Parser/Script/Compiler/Expressions/Expression/Expression.ts b/src/application/Parser/Script/Compiler/Expressions/Expression/Expression.ts index f44fa287..279f398d 100644 --- a/src/application/Parser/Script/Compiler/Expressions/Expression/Expression.ts +++ b/src/application/Parser/Script/Compiler/Expressions/Expression/Expression.ts @@ -15,19 +15,10 @@ export class Expression implements IExpression { public readonly evaluator: ExpressionEvaluator, parameters?: IReadOnlyFunctionParameterCollection, ) { - if (!position) { - throw new Error('missing position'); - } - if (!evaluator) { - throw new Error('missing evaluator'); - } this.parameters = parameters ?? new FunctionParameterCollection(); } public evaluate(context: IExpressionEvaluationContext): string { - if (!context) { - throw new Error('missing context'); - } validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args); const args = filterUnusedArguments(this.parameters, context.args); const filteredContext = new ExpressionEvaluationContext(args, context.pipelineCompiler); diff --git a/src/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext.ts b/src/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext.ts index 74362320..4685a49d 100644 --- a/src/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext.ts +++ b/src/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext.ts @@ -12,8 +12,5 @@ export class ExpressionEvaluationContext implements IExpressionEvaluationContext public readonly args: IReadOnlyFunctionCallArgumentCollection, public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(), ) { - if (!args) { - throw new Error('missing args, send empty collection instead.'); - } } } diff --git a/src/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPositionFactory.ts b/src/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPositionFactory.ts new file mode 100644 index 00000000..10eb73ae --- /dev/null +++ b/src/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPositionFactory.ts @@ -0,0 +1,16 @@ +import { ExpressionPosition } from './ExpressionPosition'; + +export function createPositionFromRegexFullMatch( + match: RegExpMatchArray, +): ExpressionPosition { + const startPos = match.index; + if (startPos === undefined) { + throw new Error(`Regex match did not yield any results: ${JSON.stringify(match)}`); + } + const fullMatch = match[0]; + if (!fullMatch.length) { + throw new Error(`Regex match is empty: ${JSON.stringify(match)}`); + } + const endPos = startPos + fullMatch.length; + return new ExpressionPosition(startPos, endPos); +} diff --git a/src/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler.ts b/src/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler.ts index ec81a581..d2d6d444 100644 --- a/src/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler.ts +++ b/src/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler.ts @@ -11,14 +11,11 @@ export class ExpressionsCompiler implements IExpressionsCompiler { ) { } public compileExpressions( - code: string | undefined, + code: string, args: IReadOnlyFunctionCallArgumentCollection, ): string { - if (!args) { - throw new Error('missing args, send empty collection instead.'); - } if (!code) { - return code; + return ''; } const context = new ExpressionEvaluationContext(args); const compiledCode = compileRecursively(code, context, this.extractor); @@ -145,7 +142,7 @@ function ensureParamsUsedInCodeHasArgsProvided( providedArgs: IReadOnlyFunctionCallArgumentCollection, ): void { const usedParameterNames = extractRequiredParameterNames(expressions); - if (!usedParameterNames?.length) { + if (!usedParameterNames.length) { return; } const notProvidedParameters = usedParameterNames diff --git a/src/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler.ts b/src/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler.ts index 87b4671d..0c4e82a3 100644 --- a/src/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler.ts +++ b/src/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler.ts @@ -2,6 +2,7 @@ import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argume export interface IExpressionsCompiler { compileExpressions( - code: string | undefined, - args: IReadOnlyFunctionCallArgumentCollection): string; + code: string, + args: IReadOnlyFunctionCallArgumentCollection, + ): string; } diff --git a/src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts b/src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts index aa75c480..d564f33d 100644 --- a/src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts +++ b/src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts @@ -10,12 +10,9 @@ const Parsers = [ export class CompositeExpressionParser implements IExpressionParser { public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) { - if (!leafs) { + if (!leafs.length) { throw new Error('missing leafs'); } - if (leafs.some((leaf) => !leaf)) { - throw new Error('missing leaf'); - } } public findExpressions(code: string): IExpression[] { diff --git a/src/application/Parser/Script/Compiler/Expressions/Parser/Regex/RegexParser.ts b/src/application/Parser/Script/Compiler/Expressions/Parser/Regex/RegexParser.ts index 7bc53d0c..8745c44c 100644 --- a/src/application/Parser/Script/Compiler/Expressions/Parser/Regex/RegexParser.ts +++ b/src/application/Parser/Script/Compiler/Expressions/Parser/Regex/RegexParser.ts @@ -1,9 +1,9 @@ import { IExpressionParser } from '../IExpressionParser'; -import { ExpressionPosition } from '../../Expression/ExpressionPosition'; import { IExpression } from '../../Expression/IExpression'; import { Expression, ExpressionEvaluator } from '../../Expression/Expression'; import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter'; import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection'; +import { createPositionFromRegexFullMatch } from '../../Expression/ExpressionPositionFactory'; export abstract class RegexParser implements IExpressionParser { protected abstract readonly regex: RegExp; @@ -21,7 +21,7 @@ export abstract class RegexParser implements IExpressionParser { const matches = code.matchAll(this.regex); for (const match of matches) { const primitiveExpression = this.buildExpression(match); - const position = this.doOrRethrow(() => createPosition(match), 'invalid script position', code); + const position = this.doOrRethrow(() => createPositionFromRegexFullMatch(match), 'invalid script position', code); const parameters = createParameters(primitiveExpression); const expression = new Expression(position, primitiveExpression.evaluator, parameters); yield expression; @@ -37,12 +37,6 @@ export abstract class RegexParser implements IExpressionParser { } } -function createPosition(match: RegExpMatchArray): ExpressionPosition { - const startPos = match.index; - const endPos = startPos + match[0].length; - return new ExpressionPosition(startPos, endPos); -} - function createParameters( expression: IPrimitiveExpression, ): FunctionParameterCollection { diff --git a/src/application/Parser/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShell.ts b/src/application/Parser/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShell.ts index 02273322..ebe28437 100644 --- a/src/application/Parser/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShell.ts +++ b/src/application/Parser/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShell.ts @@ -28,7 +28,7 @@ function hasLines(text: string) { */ function inlineComments(code: string): string { const makeInlineComment = (comment: string) => { - const value = comment?.trim(); + const value = comment.trim(); if (!value) { return '<##>'; } diff --git a/src/application/Parser/Script/Compiler/Expressions/Pipes/PipeFactory.ts b/src/application/Parser/Script/Compiler/Expressions/Pipes/PipeFactory.ts index 5b47a9db..a96e8cff 100644 --- a/src/application/Parser/Script/Compiler/Expressions/Pipes/PipeFactory.ts +++ b/src/application/Parser/Script/Compiler/Expressions/Pipes/PipeFactory.ts @@ -15,12 +15,6 @@ export class PipeFactory implements IPipeFactory { private readonly pipes = new Map(); constructor(pipes: readonly IPipe[] = RegisteredPipes) { - if (!pipes) { - throw new Error('missing pipes'); - } - if (pipes.some((pipe) => !pipe)) { - throw new Error('missing pipe in list'); - } for (const pipe of pipes) { this.registerPipe(pipe); } @@ -28,10 +22,11 @@ export class PipeFactory implements IPipeFactory { public get(pipeName: string): IPipe { validatePipeName(pipeName); - if (!this.pipes.has(pipeName)) { + const pipe = this.pipes.get(pipeName); + if (!pipe) { throw new Error(`Unknown pipe: "${pipeName}"`); } - return this.pipes.get(pipeName); + return pipe; } private registerPipe(pipe: IPipe): void { diff --git a/src/application/Parser/Script/Compiler/Expressions/SyntaxParsers/WithParser.ts b/src/application/Parser/Script/Compiler/Expressions/SyntaxParsers/WithParser.ts index 4c165828..e47151b9 100644 --- a/src/application/Parser/Script/Compiler/Expressions/SyntaxParsers/WithParser.ts +++ b/src/application/Parser/Script/Compiler/Expressions/SyntaxParsers/WithParser.ts @@ -5,6 +5,7 @@ import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function import { IExpression } from '../Expression/IExpression'; import { ExpressionPosition } from '../Expression/ExpressionPosition'; import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder'; +import { createPositionFromRegexFullMatch } from '../Expression/ExpressionPositionFactory'; export class WithParser implements IExpressionParser { public findExpressions(code: string): IExpression[] { @@ -42,31 +43,25 @@ function parseAllWithExpressions( expressions.push({ type: WithStatementType.Start, parameterName: match[1], - position: createPosition(match), + position: createPositionFromRegexFullMatch(match), }); } for (const match of input.matchAll(WithStatementEndRegEx)) { expressions.push({ type: WithStatementType.End, - position: createPosition(match), + position: createPositionFromRegexFullMatch(match), }); } for (const match of input.matchAll(ContextVariableWithPipelineRegEx)) { expressions.push({ type: WithStatementType.ContextVariable, - position: createPosition(match), + position: createPositionFromRegexFullMatch(match), pipeline: match[1], }); } return expressions; } -function createPosition(match: RegExpMatchArray): ExpressionPosition { - const startPos = match.index; - const endPos = startPos + match[0].length; - return new ExpressionPosition(startPos, endPos); -} - class WithStatementBuilder { private readonly contextVariables = new Array<{ readonly positionInScope: ExpressionPosition; @@ -125,7 +120,7 @@ class WithStatementBuilder { private substituteContextVariables( scope: string, - substituter: (pipeline: string) => string, + substituter: (pipeline?: string) => string, ): string { if (!this.contextVariables.length) { return scope; @@ -157,7 +152,7 @@ function parseWithExpressions(input: string): IExpression[] { .sort((a, b) => b.position.start - a.position.start); const expressions = new Array(); const builders = new Array(); - const throwWithContext = (message: string) => { + const throwWithContext = (message: string): never => { throw new Error(`${message}\n${buildErrorContext(input, allStatements)}}`); }; while (sortedStatements.length > 0) { @@ -178,12 +173,15 @@ function parseWithExpressions(input: string): IExpression[] { } builders[builders.length - 1].addContextVariable(statement.position, statement.pipeline); break; - case WithStatementType.End: - if (builders.length === 0) { + case WithStatementType.End: { + const builder = builders.pop(); + if (!builder) { throwWithContext('Redundant `end` statement, missing `with`?'); + break; } - expressions.push(builders.pop().buildExpression(statement.position, input)); + expressions.push(builder.buildExpression(statement.position, input)); break; + } } } if (builders.length > 0) { diff --git a/src/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection.ts b/src/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection.ts index 9775b6b5..47e4fd10 100644 --- a/src/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection.ts +++ b/src/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection.ts @@ -5,9 +5,6 @@ export class FunctionCallArgumentCollection implements IFunctionCallArgumentColl private readonly arguments = new Map(); public addArgument(argument: IFunctionCallArgument): void { - if (!argument) { - throw new Error('missing argument'); - } if (this.hasArgument(argument.parameterName)) { throw new Error(`argument value for parameter ${argument.parameterName} is already provided`); } diff --git a/src/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger.ts b/src/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger.ts index fab561ea..5f094978 100644 --- a/src/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger.ts +++ b/src/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger.ts @@ -3,18 +3,22 @@ import { CodeSegmentMerger } from './CodeSegmentMerger'; export class NewlineCodeSegmentMerger implements CodeSegmentMerger { public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode { - if (!codeSegments?.length) { + if (!codeSegments.length) { throw new Error('missing segments'); } return { code: joinCodeParts(codeSegments.map((f) => f.code)), - revertCode: joinCodeParts(codeSegments.map((f) => f.revertCode)), + revertCode: joinCodeParts( + codeSegments + .map((f) => f.revertCode) + .filter((code): code is string => Boolean(code)), + ), }; } } function joinCodeParts(codeSegments: readonly string[]): string { return codeSegments - .filter((segment) => segment?.length > 0) + .filter((segment) => segment.length > 0) .join('\n'); } diff --git a/src/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallSequenceCompiler.ts b/src/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallSequenceCompiler.ts index 85ea8b73..12d0a8ec 100644 --- a/src/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallSequenceCompiler.ts +++ b/src/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallSequenceCompiler.ts @@ -21,9 +21,7 @@ export class FunctionCallSequenceCompiler implements FunctionCallCompiler { calls: readonly FunctionCall[], functions: ISharedFunctionCollection, ): CompiledCode { - if (!functions) { throw new Error('missing functions'); } - if (!calls?.length) { throw new Error('missing calls'); } - if (calls.some((f) => !f)) { throw new Error('missing function call'); } + if (!calls.length) { throw new Error('missing calls'); } const context: FunctionCallCompilationContext = { allFunctions: functions, rootCallSequence: calls, diff --git a/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler.ts b/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler.ts index 7886723d..442833ca 100644 --- a/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler.ts +++ b/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler.ts @@ -1,6 +1,6 @@ import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler'; import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler'; -import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; +import { FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode'; import { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy'; @@ -12,19 +12,33 @@ export class InlineFunctionCallCompiler implements SingleCallCompilerStrategy { } public canCompile(func: ISharedFunction): boolean { - return func.body.code !== undefined; + return func.body.type === FunctionBodyType.Code; } public compileFunction( calledFunction: ISharedFunction, callToFunction: FunctionCall, ): CompiledCode[] { + if (calledFunction.body.type !== FunctionBodyType.Code) { + throw new Error([ + 'Unexpected function body type.', + `\tExpected: "${FunctionBodyType[FunctionBodyType.Code]}"`, + `\tActual: "${FunctionBodyType[calledFunction.body.type]}"`, + 'Function:', + `\t${JSON.stringify(callToFunction)}`, + ].join('\n')); + } const { code } = calledFunction.body; const { args } = callToFunction; return [ { code: this.expressionsCompiler.compileExpressions(code.execute, args), - revertCode: this.expressionsCompiler.compileExpressions(code.revert, args), + revertCode: (() => { + if (!code.revert) { + return undefined; + } + return this.expressionsCompiler.compileExpressions(code.revert, args); + })(), }, ]; } diff --git a/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/NestedFunctionCallCompiler.ts b/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/NestedFunctionCallCompiler.ts index baaea003..c1042235 100644 --- a/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/NestedFunctionCallCompiler.ts +++ b/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/NestedFunctionCallCompiler.ts @@ -1,4 +1,4 @@ -import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; +import { CallFunctionBody, FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext'; import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode'; @@ -13,7 +13,7 @@ export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy { } public canCompile(func: ISharedFunction): boolean { - return func.body.calls !== undefined; + return func.body.type === FunctionBodyType.Calls; } public compileFunction( @@ -21,7 +21,7 @@ export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy { callToFunction: FunctionCall, context: FunctionCallCompilationContext, ): CompiledCode[] { - const nestedCalls = calledFunction.body.calls; + const nestedCalls = (calledFunction.body as CallFunctionBody).calls; return nestedCalls.map((nestedCall) => { try { const compiledParentCall = this.argumentCompiler diff --git a/src/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.ts b/src/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.ts index bb1f696a..59c8382e 100644 --- a/src/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.ts +++ b/src/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.ts @@ -5,9 +5,6 @@ import { FunctionCallArgument } from './Argument/FunctionCallArgument'; import { ParsedFunctionCall } from './ParsedFunctionCall'; export function parseFunctionCalls(calls: FunctionCallsData): FunctionCall[] { - if (calls === undefined) { - throw new Error('missing call data'); - } const sequence = getCallSequence(calls); return sequence.map((call) => parseFunctionCall(call)); } @@ -19,22 +16,21 @@ function getCallSequence(calls: FunctionCallsData): FunctionCallData[] { if (calls instanceof Array) { return calls as FunctionCallData[]; } - return [calls as FunctionCallData]; + const singleCall = calls; + return [singleCall]; } function parseFunctionCall(call: FunctionCallData): FunctionCall { - if (!call) { - throw new Error('missing call data'); - } const callArgs = parseArgs(call.parameters); return new ParsedFunctionCall(call.function, callArgs); } function parseArgs( - parameters: FunctionCallParametersData, + parameters: FunctionCallParametersData | undefined, ): FunctionCallArgumentCollection { - return Object.keys(parameters || {}) - .map((parameterName) => new FunctionCallArgument(parameterName, parameters[parameterName])) + const parametersMap = parameters ?? {}; + return Object.keys(parametersMap) + .map((parameterName) => new FunctionCallArgument(parameterName, parametersMap[parameterName])) .reduce((args, arg) => { args.addArgument(arg); return args; diff --git a/src/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall.ts b/src/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall.ts index 71b41469..e4c7a423 100644 --- a/src/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall.ts +++ b/src/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall.ts @@ -9,8 +9,5 @@ export class ParsedFunctionCall implements FunctionCall { if (!functionName) { throw new Error('missing function name in function call'); } - if (!args) { - throw new Error('missing args'); - } } } diff --git a/src/application/Parser/Script/Compiler/Function/ISharedFunction.ts b/src/application/Parser/Script/Compiler/Function/ISharedFunction.ts index de0267f5..2d9d1536 100644 --- a/src/application/Parser/Script/Compiler/Function/ISharedFunction.ts +++ b/src/application/Parser/Script/Compiler/Function/ISharedFunction.ts @@ -4,15 +4,21 @@ import { FunctionCall } from './Call/FunctionCall'; export interface ISharedFunction { readonly name: string; readonly parameters: IReadOnlyFunctionParameterCollection; - readonly body: ISharedFunctionBody; + readonly body: SharedFunctionBody; } -export interface ISharedFunctionBody { - readonly type: FunctionBodyType; - readonly code: IFunctionCode | undefined; - readonly calls: readonly FunctionCall[] | undefined; +export interface CallFunctionBody { + readonly type: FunctionBodyType.Calls, + readonly calls: readonly FunctionCall[], } +export interface CodeFunctionBody { + readonly type: FunctionBodyType.Code; + readonly code: IFunctionCode, +} + +export type SharedFunctionBody = CallFunctionBody | CodeFunctionBody; + export enum FunctionBodyType { Code, Calls, diff --git a/src/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection.ts b/src/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection.ts index f680b9e4..d740d1af 100644 --- a/src/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection.ts +++ b/src/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection.ts @@ -18,9 +18,6 @@ export class FunctionParameterCollection implements IFunctionParameterCollection } private ensureValidParameter(parameter: IFunctionParameter) { - if (!parameter) { - throw new Error('missing parameter'); - } if (this.includesName(parameter.name)) { throw new Error(`duplicate parameter name: "${parameter.name}"`); } diff --git a/src/application/Parser/Script/Compiler/Function/SharedFunction.ts b/src/application/Parser/Script/Compiler/Function/SharedFunction.ts index 5108a510..753b5ab5 100644 --- a/src/application/Parser/Script/Compiler/Function/SharedFunction.ts +++ b/src/application/Parser/Script/Compiler/Function/SharedFunction.ts @@ -1,7 +1,7 @@ import { FunctionCall } from './Call/FunctionCall'; import { - FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody, + FunctionBodyType, IFunctionCode, ISharedFunction, SharedFunctionBody, } from './ISharedFunction'; import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection'; @@ -10,7 +10,7 @@ export function createCallerFunction( parameters: IReadOnlyFunctionParameterCollection, callSequence: readonly FunctionCall[], ): ISharedFunction { - if (!callSequence || !callSequence.length) { + if (!callSequence.length) { throw new Error(`missing call sequence in function "${name}"`); } return new SharedFunction(name, parameters, callSequence, FunctionBodyType.Calls); @@ -33,7 +33,7 @@ export function createFunctionWithInlineCode( } class SharedFunction implements ISharedFunction { - public readonly body: ISharedFunctionBody; + public readonly body: SharedFunctionBody; constructor( public readonly name: string, @@ -42,11 +42,22 @@ class SharedFunction implements ISharedFunction { bodyType: FunctionBodyType, ) { if (!name) { throw new Error('missing function name'); } - if (!parameters) { throw new Error('missing parameters'); } - this.body = { - type: bodyType, - code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined, - calls: bodyType === FunctionBodyType.Calls ? content as readonly FunctionCall[] : undefined, - }; + + switch (bodyType) { + case FunctionBodyType.Code: + this.body = { + type: FunctionBodyType.Code, + code: content as IFunctionCode, + }; + break; + case FunctionBodyType.Calls: + this.body = { + type: FunctionBodyType.Calls, + calls: content as readonly FunctionCall[], + }; + break; + default: + throw new Error(`unknown body type: ${FunctionBodyType[bodyType]}`); + } } } diff --git a/src/application/Parser/Script/Compiler/Function/SharedFunctionCollection.ts b/src/application/Parser/Script/Compiler/Function/SharedFunctionCollection.ts index 9cba7948..653e796e 100644 --- a/src/application/Parser/Script/Compiler/Function/SharedFunctionCollection.ts +++ b/src/application/Parser/Script/Compiler/Function/SharedFunctionCollection.ts @@ -5,7 +5,6 @@ export class SharedFunctionCollection implements ISharedFunctionCollection { private readonly functionsByName = new Map(); public addFunction(func: ISharedFunction): void { - if (!func) { throw new Error('missing function'); } if (this.has(func.name)) { throw new Error(`function with name ${func.name} already exists`); } diff --git a/src/application/Parser/Script/Compiler/Function/SharedFunctionsParser.ts b/src/application/Parser/Script/Compiler/Function/SharedFunctionsParser.ts index aa3f482a..5d978111 100644 --- a/src/application/Parser/Script/Compiler/Function/SharedFunctionsParser.ts +++ b/src/application/Parser/Script/Compiler/Function/SharedFunctionsParser.ts @@ -1,4 +1,6 @@ -import type { FunctionData, InstructionHolder } from '@/application/collections/'; +import type { + FunctionData, CodeInstruction, CodeFunctionData, CallFunctionData, CallInstruction, +} from '@/application/collections/'; import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax'; import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator'; import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines'; @@ -23,9 +25,8 @@ export class SharedFunctionsParser implements ISharedFunctionsParser { functions: readonly FunctionData[], syntax: ILanguageSyntax, ): ISharedFunctionCollection { - if (!syntax) { throw new Error('missing syntax'); } const collection = new SharedFunctionCollection(); - if (!functions || !functions.length) { + if (!functions.length) { return collection; } ensureValidFunctions(functions); @@ -55,16 +56,18 @@ function parseFunction( } function validateCode( - data: FunctionData, + data: CodeFunctionData, syntax: ILanguageSyntax, validator: ICodeValidator, ): void { - [data.code, data.revertCode].forEach( - (code) => validator.throwIfInvalid( - code, - [new NoEmptyLines(), new NoDuplicatedLines(syntax)], - ), - ); + [data.code, data.revertCode] + .filter((code): code is string => Boolean(code)) + .forEach( + (code) => validator.throwIfInvalid( + code, + [new NoEmptyLines(), new NoDuplicatedLines(syntax)], + ), + ); } function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection { @@ -85,19 +88,18 @@ function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollecti }, new FunctionParameterCollection()); } -function hasCode(data: FunctionData): boolean { - return Boolean(data.code); +function hasCode(data: FunctionData): data is CodeFunctionData { + return (data as CodeInstruction).code !== undefined; } -function hasCall(data: FunctionData): boolean { - return Boolean(data.call); +function hasCall(data: FunctionData): data is CallFunctionData { + return (data as CallInstruction).call !== undefined; } function ensureValidFunctions(functions: readonly FunctionData[]) { - ensureNoUndefinedItem(functions); ensureNoDuplicatesInFunctionNames(functions); - ensureNoDuplicateCode(functions); ensureEitherCallOrCodeIsDefined(functions); + ensureNoDuplicateCode(functions); ensureExpectedParametersType(functions); } @@ -105,7 +107,7 @@ function printList(list: readonly string[]): string { return `"${list.join('","')}"`; } -function ensureEitherCallOrCodeIsDefined(holders: readonly InstructionHolder[]) { +function ensureEitherCallOrCodeIsDefined(holders: readonly FunctionData[]) { // Ensure functions do not define both call and code const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder)); if (withBothCallAndCode.length) { @@ -132,7 +134,7 @@ function isArrayOfObjects(value: unknown): boolean { && value.every((item) => typeof item === 'object'); } -function printNames(holders: readonly InstructionHolder[]) { +function printNames(holders: readonly FunctionData[]) { return printList(holders.map((holder) => holder.name)); } @@ -144,22 +146,19 @@ function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) { } } -function ensureNoUndefinedItem(functions: readonly FunctionData[]) { - if (functions.some((func) => !func)) { - throw new Error('some functions are undefined'); - } -} - function ensureNoDuplicateCode(functions: readonly FunctionData[]) { - const duplicateCodes = getDuplicates(functions + const callFunctions = functions + .filter((func) => hasCode(func)) + .map((func) => func as CodeFunctionData); + const duplicateCodes = getDuplicates(callFunctions .map((func) => func.code) .filter((code) => code)); if (duplicateCodes.length > 0) { throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`); } - const duplicateRevertCodes = getDuplicates(functions - .filter((func) => func.revertCode) - .map((func) => func.revertCode)); + const duplicateRevertCodes = getDuplicates(callFunctions + .map((func) => func.revertCode) + .filter((code): code is string => Boolean(code))); if (duplicateRevertCodes.length > 0) { throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`); } diff --git a/src/application/Parser/Script/Compiler/ScriptCompiler.ts b/src/application/Parser/Script/Compiler/ScriptCompiler.ts index 38c07a9c..e333a72b 100644 --- a/src/application/Parser/Script/Compiler/ScriptCompiler.ts +++ b/src/application/Parser/Script/Compiler/ScriptCompiler.ts @@ -1,4 +1,4 @@ -import type { FunctionData, ScriptData } from '@/application/collections/'; +import type { FunctionData, ScriptData, CallInstruction } from '@/application/collections/'; import { IScriptCode } from '@/domain/IScriptCode'; import { ScriptCode } from '@/domain/ScriptCode'; import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax'; @@ -18,27 +18,24 @@ export class ScriptCompiler implements IScriptCompiler { private readonly functions: ISharedFunctionCollection; constructor( - functions: readonly FunctionData[] | undefined, + functions: readonly FunctionData[], syntax: ILanguageSyntax, sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance, private readonly callCompiler: FunctionCallCompiler = FunctionCallSequenceCompiler.instance, private readonly codeValidator: ICodeValidator = CodeValidator.instance, ) { - if (!syntax) { throw new Error('missing syntax'); } this.functions = sharedFunctionsParser.parseFunctions(functions, syntax); } public canCompile(script: ScriptData): boolean { - if (!script) { throw new Error('missing script'); } - if (!script.call) { - return false; - } - return true; + return hasCall(script); } public compile(script: ScriptData): IScriptCode { - if (!script) { throw new Error('missing script'); } try { + if (!hasCall(script)) { + throw new Error('Script does include any calls.'); + } const calls = parseFunctionCalls(script.call); const compiledCode = this.callCompiler.compileFunctionCalls(calls, this.functions); validateCompiledCode(compiledCode, this.codeValidator); @@ -53,7 +50,17 @@ export class ScriptCompiler implements IScriptCompiler { } function validateCompiledCode(compiledCode: CompiledCode, validator: ICodeValidator): void { - [compiledCode.code, compiledCode.revertCode].forEach( - (code) => validator.throwIfInvalid(code, [new NoEmptyLines()]), - ); + [compiledCode.code, compiledCode.revertCode] + .filter((code): code is string => Boolean(code)) + .map((code) => code as string) + .forEach( + (code) => validator.throwIfInvalid( + code, + [new NoEmptyLines()], + ), + ); +} + +function hasCall(data: ScriptData): data is ScriptData & CallInstruction { + return (data as CallInstruction).call !== undefined; } diff --git a/src/application/Parser/Script/ScriptParser.ts b/src/application/Parser/Script/ScriptParser.ts index 5e894602..adb3d20f 100644 --- a/src/application/Parser/Script/ScriptParser.ts +++ b/src/application/Parser/Script/ScriptParser.ts @@ -1,4 +1,4 @@ -import type { ScriptData } from '@/application/collections/'; +import type { ScriptData, CodeScriptData, CallScriptData } from '@/application/collections/'; import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines'; import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax'; import { Script } from '@/domain/Script'; @@ -14,7 +14,6 @@ import { ICategoryCollectionParseContext } from './ICategoryCollectionParseConte import { CodeValidator } from './Validation/CodeValidator'; import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines'; -// eslint-disable-next-line consistent-return export function parseScript( data: ScriptData, context: ICategoryCollectionParseContext, @@ -24,7 +23,6 @@ export function parseScript( ): Script { const validator = new NodeValidator({ type: NodeType.Script, selfNode: data }); validateScript(data, validator); - if (!context) { throw new Error('missing context'); } try { const script = scriptFactory( /* name: */ data.name, @@ -34,12 +32,12 @@ export function parseScript( ); return script; } catch (err) { - validator.throw(err.message); + return validator.throw(err.message); } } function parseLevel( - level: string, + level: string | undefined, parser: IEnumParser, ): RecommendationLevel | undefined { if (!level) { @@ -56,39 +54,45 @@ function parseCode( if (context.compiler.canCompile(script)) { return context.compiler.compile(script); } - const code = new ScriptCode(script.code, script.revertCode); + const codeScript = script as CodeScriptData; // Must be inline code if it cannot be compiled + const code = new ScriptCode(codeScript.code, codeScript.revertCode); validateHardcodedCodeWithoutCalls(code, codeValidator, context.syntax); return code; } function validateHardcodedCodeWithoutCalls( scriptCode: ScriptCode, - codeValidator: ICodeValidator, + validator: ICodeValidator, syntax: ILanguageSyntax, ) { - [scriptCode.execute, scriptCode.revert].forEach( - (code) => codeValidator.throwIfInvalid( - code, - [new NoEmptyLines(), new NoDuplicatedLines(syntax)], - ), - ); + [scriptCode.execute, scriptCode.revert] + .filter((code): code is string => Boolean(code)) + .forEach( + (code) => validator.throwIfInvalid( + code, + [new NoEmptyLines(), new NoDuplicatedLines(syntax)], + ), + ); } -function validateScript(script: ScriptData, validator: NodeValidator) { +function validateScript( + script: ScriptData, + validator: NodeValidator, +): asserts script is NonNullable { validator .assertDefined(script) .assertValidName(script.name) .assert( - () => Boolean(script.code || script.call), - 'Must define either "call" or "code".', + () => Boolean((script as CodeScriptData).code || (script as CallScriptData).call), + 'Neither "call" or "code" is defined.', ) .assert( - () => !(script.code && script.call), - 'Cannot define both "call" and "code".', + () => !((script as CodeScriptData).code && (script as CallScriptData).call), + 'Both "call" and "code" are defined.', ) .assert( - () => !(script.revertCode && script.call), - 'Cannot define "revertCode" if "call" is defined.', + () => !((script as CodeScriptData).revertCode && (script as CallScriptData).call), + 'Both "call" and "revertCode" are defined.', ); } diff --git a/src/application/Parser/Script/Validation/CodeValidator.ts b/src/application/Parser/Script/Validation/CodeValidator.ts index 130a93e7..42f3e22d 100644 --- a/src/application/Parser/Script/Validation/CodeValidator.ts +++ b/src/application/Parser/Script/Validation/CodeValidator.ts @@ -9,7 +9,7 @@ export class CodeValidator implements ICodeValidator { code: string, rules: readonly ICodeValidationRule[], ): void { - if (!rules || rules.length === 0) { throw new Error('missing rules'); } + if (rules.length === 0) { throw new Error('missing rules'); } if (!code) { return; } diff --git a/src/application/Parser/Script/Validation/Rules/NoDuplicatedLines.ts b/src/application/Parser/Script/Validation/Rules/NoDuplicatedLines.ts index ebea7dc6..0d0ce03d 100644 --- a/src/application/Parser/Script/Validation/Rules/NoDuplicatedLines.ts +++ b/src/application/Parser/Script/Validation/Rules/NoDuplicatedLines.ts @@ -3,9 +3,7 @@ import { ICodeLine } from '../ICodeLine'; import { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule'; export class NoDuplicatedLines implements ICodeValidationRule { - constructor(private readonly syntax: ILanguageSyntax) { - if (!syntax) { throw new Error('missing syntax'); } - } + constructor(private readonly syntax: ILanguageSyntax) { } public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] { return lines diff --git a/src/application/Parser/ScriptingDefinition/CodeSubstituter.ts b/src/application/Parser/ScriptingDefinition/CodeSubstituter.ts index 00af3ee6..fefadd22 100644 --- a/src/application/Parser/ScriptingDefinition/CodeSubstituter.ts +++ b/src/application/Parser/ScriptingDefinition/CodeSubstituter.ts @@ -17,7 +17,6 @@ export class CodeSubstituter implements ICodeSubstituter { public substitute(code: string, info: IProjectInformation): string { if (!code) { throw new Error('missing code'); } - if (!info) { throw new Error('missing info'); } const args = new FunctionCallArgumentCollection(); const substitute = (name: string, value: string) => args .addArgument(new FunctionCallArgument(name, value)); diff --git a/src/application/Parser/ScriptingDefinition/ScriptingDefinitionParser.ts b/src/application/Parser/ScriptingDefinition/ScriptingDefinitionParser.ts index 6fa20e90..2c25d6db 100644 --- a/src/application/Parser/ScriptingDefinition/ScriptingDefinitionParser.ts +++ b/src/application/Parser/ScriptingDefinition/ScriptingDefinitionParser.ts @@ -18,8 +18,6 @@ export class ScriptingDefinitionParser { definition: ScriptingDefinitionData, info: IProjectInformation, ): IScriptingDefinition { - if (!info) { throw new Error('missing info'); } - if (!definition) { throw new Error('missing definition'); } const language = this.languageParser.parseEnum(definition.language, 'language'); const startCode = this.codeSubstituter.substitute(definition.startCode, info); const endCode = this.codeSubstituter.substitute(definition.endCode, info); diff --git a/src/application/collections/collection.yaml.d.ts b/src/application/collections/collection.yaml.d.ts index 6e7f0bce..b429420b 100644 --- a/src/application/collections/collection.yaml.d.ts +++ b/src/application/collections/collection.yaml.d.ts @@ -12,29 +12,38 @@ declare module '@/application/collections/*' { } export type CategoryOrScriptData = CategoryData | ScriptData; - export type DocumentationData = ReadonlyArray | string; + export type DocumentationData = ReadonlyArray | string | undefined; export interface DocumentableData { readonly docs?: DocumentationData; } - export interface InstructionHolder { - readonly name: string; - - readonly code?: string; + export interface CodeInstruction { + readonly code: string; readonly revertCode?: string; + } - readonly call?: FunctionCallsData; + export interface CallInstruction { + readonly call: FunctionCallsData; } + export type InstructionHolder = CodeInstruction | CallInstruction; + export interface ParameterDefinitionData { readonly name: string; readonly optional?: boolean; } - export interface FunctionData extends InstructionHolder { + export type FunctionDefinition = { + readonly name: string; readonly parameters?: readonly ParameterDefinitionData[]; - } + }; + + export type CodeFunctionData = FunctionDefinition & CodeInstruction; + + export type CallFunctionData = FunctionDefinition & CallInstruction; + + export type FunctionData = CodeFunctionData | CallFunctionData; export interface FunctionCallParametersData { readonly [index: string]: string; @@ -47,10 +56,16 @@ declare module '@/application/collections/*' { export type FunctionCallsData = readonly FunctionCallData[] | FunctionCallData | undefined; - export interface ScriptData extends InstructionHolder, DocumentableData { + export type ScriptDefinition = DocumentableData & { readonly name: string; readonly recommend?: string; - } + }; + + export type CodeScriptData = ScriptDefinition & CodeInstruction; + + export type CallScriptData = ScriptDefinition & CallInstruction; + + export type ScriptData = CodeScriptData | CallScriptData; export interface ScriptingDefinitionData { readonly language: string; diff --git a/src/domain/Application.ts b/src/domain/Application.ts index 633db872..afbb710c 100644 --- a/src/domain/Application.ts +++ b/src/domain/Application.ts @@ -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) { diff --git a/src/domain/Category.ts b/src/domain/Category.ts index 9e9e35e6..2ce6c9d7 100644 --- a/src/domain/Category.ts +++ b/src/domain/Category.ts @@ -3,14 +3,14 @@ import { IScript } from './IScript'; import { ICategory } from './ICategory'; export class Category extends BaseEntity implements ICategory { - private allSubScripts: ReadonlyArray = undefined; + private allSubScripts?: ReadonlyArray = undefined; constructor( id: number, public readonly name: string, public readonly docs: ReadonlyArray, - public readonly subCategories?: ReadonlyArray, - public readonly scripts?: ReadonlyArray, + public readonly subCategories: ReadonlyArray, + public readonly scripts: ReadonlyArray, ) { 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'); } } diff --git a/src/domain/CategoryCollection.ts b/src/domain/CategoryCollection.ts index 0a627dd2..83f3b362 100644 --- a/src/domain/CategoryCollection.ts +++ b/src/domain/CategoryCollection.ts @@ -19,9 +19,6 @@ export class CategoryCollection implements ICategoryCollection { public readonly actions: ReadonlyArray, 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) diff --git a/src/domain/IApplication.ts b/src/domain/IApplication.ts index 61bc3de2..4ff92fae 100644 --- a/src/domain/IApplication.ts +++ b/src/domain/IApplication.ts @@ -7,5 +7,5 @@ export interface IApplication { readonly collections: readonly ICategoryCollection[]; getSupportedOsList(): OperatingSystem[]; - getCollection(operatingSystem: OperatingSystem): ICategoryCollection | undefined; + getCollection(operatingSystem: OperatingSystem): ICategoryCollection; } diff --git a/src/domain/ICategory.ts b/src/domain/ICategory.ts index 8f44a424..2413cd09 100644 --- a/src/domain/ICategory.ts +++ b/src/domain/ICategory.ts @@ -5,8 +5,8 @@ import { IDocumentable } from './IDocumentable'; export interface ICategory extends IEntity, IDocumentable { readonly id: number; readonly name: string; - readonly subCategories?: ReadonlyArray; - readonly scripts?: ReadonlyArray; + readonly subCategories: ReadonlyArray; + readonly scripts: ReadonlyArray; includes(script: IScript): boolean; getAllScriptsRecursively(): ReadonlyArray; } diff --git a/src/domain/ICategoryCollection.ts b/src/domain/ICategoryCollection.ts index 95e6e4f0..913d8b00 100644 --- a/src/domain/ICategoryCollection.ts +++ b/src/domain/ICategoryCollection.ts @@ -12,8 +12,8 @@ export interface ICategoryCollection { readonly actions: ReadonlyArray; getScriptsByLevel(level: RecommendationLevel): ReadonlyArray; - findCategory(categoryId: number): ICategory | undefined; - findScript(scriptId: string): IScript | undefined; + getCategory(categoryId: number): ICategory; + getScript(scriptId: string): IScript; getAllScripts(): ReadonlyArray; getAllCategories(): ReadonlyArray; } diff --git a/src/domain/IScriptCode.ts b/src/domain/IScriptCode.ts index 06af8165..c2c8900c 100644 --- a/src/domain/IScriptCode.ts +++ b/src/domain/IScriptCode.ts @@ -1,4 +1,4 @@ export interface IScriptCode { readonly execute: string; - readonly revert: string; + readonly revert?: string; } diff --git a/src/domain/ProjectInformation.ts b/src/domain/ProjectInformation.ts index a353875c..380e8840 100644 --- a/src/domain/ProjectInformation.ts +++ b/src/domain/ProjectInformation.ts @@ -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'); } diff --git a/src/domain/Script.ts b/src/domain/Script.ts index 6f8d5ebc..a852effc 100644 --- a/src/domain/Script.ts +++ b/src/domain/Script.ts @@ -11,9 +11,6 @@ export class Script extends BaseEntity implements IScript { public readonly level?: RecommendationLevel, ) { super(name); - if (!code) { - throw new Error('missing code'); - } validateLevel(level); } diff --git a/src/domain/ScriptCode.ts b/src/domain/ScriptCode.ts index 89791ac8..51eb56bd 100644 --- a/src/domain/ScriptCode.ts +++ b/src/domain/ScriptCode.ts @@ -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'); } } diff --git a/src/infrastructure/CodeRunner.ts b/src/infrastructure/CodeRunner.ts index 30ae771c..c6135c7d 100644 --- a/src/infrastructure/CodeRunner.ts +++ b/src/infrastructure/CodeRunner.ts @@ -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 { const { os } = this.environment; + if (os === undefined) { + throw new Error('Unidentified operating system'); + } const dir = this.system.location.combinePaths( this.system.operatingSystem.getTempDirectory(), folderName, diff --git a/src/infrastructure/EnvironmentVariables/EnvironmentVariablesValidator.ts b/src/infrastructure/EnvironmentVariables/EnvironmentVariablesValidator.ts index 70676ce4..2f01af12 100644 --- a/src/infrastructure/EnvironmentVariables/EnvironmentVariablesValidator.ts +++ b/src/infrastructure/EnvironmentVariables/EnvironmentVariablesValidator.ts @@ -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[] * 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 { +function capturePropertyValues(instance: object): Record { const obj: Record = {}; const descriptors = Object.getOwnPropertyDescriptors(instance.constructor.prototype); diff --git a/src/infrastructure/Events/EventSubscriptionCollection.ts b/src/infrastructure/Events/EventSubscriptionCollection.ts index df06163e..0d299f08 100644 --- a/src/infrastructure/Events/EventSubscriptionCollection.ts +++ b/src/infrastructure/Events/EventSubscriptionCollection.ts @@ -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); } diff --git a/src/infrastructure/Log/ConsoleLogger.ts b/src/infrastructure/Log/ConsoleLogger.ts index 4a1bc6bd..cb5f23ec 100644 --- a/src/infrastructure/Log/ConsoleLogger.ts +++ b/src/infrastructure/Log/ConsoleLogger.ts @@ -1,13 +1,17 @@ import { ILogger } from './ILogger'; export class ConsoleLogger implements ILogger { - constructor(private readonly globalConsole: Partial = console) { - if (!globalConsole) { + constructor(private readonly consoleProxy: Partial = 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); } } diff --git a/src/infrastructure/Log/ElectronLogger.ts b/src/infrastructure/Log/ElectronLogger.ts index 5a1dc1ae..3c44ca80 100644 --- a/src/infrastructure/Log/ElectronLogger.ts +++ b/src/infrastructure/Log/ElectronLogger.ts @@ -7,6 +7,11 @@ export function createElectronLogger(logger: Partial): 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); + }, }; } diff --git a/src/infrastructure/Log/WindowInjectedLogger.ts b/src/infrastructure/Log/WindowInjectedLogger.ts index c3642dbe..0be0b8ef 100644 --- a/src/infrastructure/Log/WindowInjectedLogger.ts +++ b/src/infrastructure/Log/WindowInjectedLogger.ts @@ -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) { diff --git a/src/infrastructure/Repository/IRepository.ts b/src/infrastructure/Repository/IRepository.ts index a69280ef..e7c14e0b 100644 --- a/src/infrastructure/Repository/IRepository.ts +++ b/src/infrastructure/Repository/IRepository.ts @@ -3,7 +3,7 @@ import { IEntity } from '../Entity/IEntity'; export interface IRepository> { 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; diff --git a/src/infrastructure/Repository/InMemoryRepository.ts b/src/infrastructure/Repository/InMemoryRepository.ts index 1096e9d2..af0da495 100644 --- a/src/infrastructure/Repository/InMemoryRepository.ts +++ b/src/infrastructure/Repository/InMemoryRepository.ts @@ -6,7 +6,7 @@ implements IRepository { private readonly items: TEntity[]; constructor(items?: TEntity[]) { - this.items = items || new Array(); + this.items = items ?? new Array(); } public get length(): number { @@ -17,18 +17,15 @@ implements IRepository { 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 { } public addOrUpdateItem(item: TEntity): void { - if (!item) { - throw new Error('missing item'); - } if (this.exists(item.id)) { this.removeItem(item.id); } diff --git a/src/infrastructure/RuntimeEnvironment/BrowserOs/DetectorBuilder.ts b/src/infrastructure/RuntimeEnvironment/BrowserOs/DetectorBuilder.ts index 9e8382ca..3d81edb3 100644 --- a/src/infrastructure/RuntimeEnvironment/BrowserOs/DetectorBuilder.ts +++ b/src/infrastructure/RuntimeEnvironment/BrowserOs/DetectorBuilder.ts @@ -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; diff --git a/src/infrastructure/RuntimeEnvironment/RuntimeEnvironment.ts b/src/infrastructure/RuntimeEnvironment/RuntimeEnvironment.ts index fa088a15..87c4d021 100644 --- a/src/infrastructure/RuntimeEnvironment/RuntimeEnvironment.ts +++ b/src/infrastructure/RuntimeEnvironment/RuntimeEnvironment.ts @@ -37,7 +37,7 @@ export class RuntimeEnvironment implements IRuntimeEnvironment { } } -function getUserAgent(window: Partial): string { +function getUserAgent(window: Partial): string | undefined { return window?.navigator?.userAgent; } diff --git a/src/infrastructure/RuntimeSanity/Common/FactoryValidator.ts b/src/infrastructure/RuntimeSanity/Common/FactoryValidator.ts index 46f0030a..1372e81a 100644 --- a/src/infrastructure/RuntimeSanity/Common/FactoryValidator.ts +++ b/src/infrastructure/RuntimeSanity/Common/FactoryValidator.ts @@ -7,9 +7,6 @@ export abstract class FactoryValidator implements ISanityValidator { private readonly factory: FactoryFunction; protected constructor(factory: FactoryFunction) { - if (!factory) { - throw new Error('missing factory'); - } this.factory = factory; } diff --git a/src/infrastructure/RuntimeSanity/SanityChecks.ts b/src/infrastructure/RuntimeSanity/SanityChecks.ts index 36f44bf9..9019ae15 100644 --- a/src/infrastructure/RuntimeSanity/SanityChecks.ts +++ b/src/infrastructure/RuntimeSanity/SanityChecks.ts @@ -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) { diff --git a/src/infrastructure/SaveFileDialog.ts b/src/infrastructure/SaveFileDialog.ts index 29678df6..c00bf599 100644 --- a/src/infrastructure/SaveFileDialog.ts +++ b/src/infrastructure/SaveFileDialog.ts @@ -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([ + private static readonly mimeTypes: Record = { // 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 { diff --git a/src/infrastructure/SystemOperations/ISystemOperations.ts b/src/infrastructure/SystemOperations/ISystemOperations.ts index 217da587..8915d0f5 100644 --- a/src/infrastructure/SystemOperations/ISystemOperations.ts +++ b/src/infrastructure/SystemOperations/ISystemOperations.ts @@ -19,6 +19,6 @@ export interface ICommandOps { export interface IFileSystemOps { setFilePermissions(filePath: string, mode: string | number): Promise; - createDirectory(directoryPath: string, isRecursive?: boolean): Promise; + createDirectory(directoryPath: string, isRecursive?: boolean): Promise; writeToFile(filePath: string, data: string): Promise; } diff --git a/src/infrastructure/SystemOperations/NodeSystemOperations.ts b/src/infrastructure/SystemOperations/NodeSystemOperations.ts index 3377fb3b..6979ec61 100644 --- a/src/infrastructure/SystemOperations/NodeSystemOperations.ts +++ b/src/infrastructure/SystemOperations/NodeSystemOperations.ts @@ -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, diff --git a/src/infrastructure/Threading/AsyncLazy.ts b/src/infrastructure/Threading/AsyncLazy.ts index b024169b..0912ecf2 100644 --- a/src/infrastructure/Threading/AsyncLazy.ts +++ b/src/infrastructure/Threading/AsyncLazy.ts @@ -1,13 +1,9 @@ import { EventSource } from '../Events/EventSource'; export class AsyncLazy { - private valueCreated = new EventSource(); + private valueCreated = new EventSource(); - private isValueCreated = false; - - private isCreatingValue = false; - - private value: T | undefined; + private state: ValueState = { status: ValueStatus.NotRequested }; constructor(private valueFactory: () => Promise) {} @@ -15,23 +11,44 @@ export class AsyncLazy { this.valueFactory = valueFactory; } - public async getValue(): Promise { - // If value is already created, return the value directly - if (this.isValueCreated) { - return Promise.resolve(this.value); + public getValue(): Promise { + 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((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 = + | { + readonly status: ValueStatus.NotRequested; + } + | { + readonly status: ValueStatus.BeingCreated; + readonly value: Promise; + } + | { + readonly status: ValueStatus.Created; + readonly value: T + }; diff --git a/src/infrastructure/WindowVariables/WindowVariables.ts b/src/infrastructure/WindowVariables/WindowVariables.ts index 06485ce6..a5a6ed24 100644 --- a/src/infrastructure/WindowVariables/WindowVariables.ts +++ b/src/infrastructure/WindowVariables/WindowVariables.ts @@ -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; } diff --git a/src/infrastructure/WindowVariables/WindowVariablesValidator.ts b/src/infrastructure/WindowVariables/WindowVariablesValidator.ts index 66d764e9..8d2a98b7 100644 --- a/src/infrastructure/WindowVariables/WindowVariablesValidator.ts +++ b/src/infrastructure/WindowVariables/WindowVariablesValidator.ts @@ -17,7 +17,7 @@ export function validateWindowVariables(variables: Partial) { function* testEveryProperty(variables: Partial): Iterable { const tests: { - [K in PropertyKeys]: boolean; + [K in PropertyKeys>]: boolean; } = { os: testOperatingSystem(variables.os), isDesktop: testIsDesktop(variables.isDesktop), diff --git a/src/presentation/components/App.vue b/src/presentation/components/App.vue index b7ca298d..d40f48cd 100644 --- a/src/presentation/components/App.vue +++ b/src/presentation/components/App.vue @@ -7,22 +7,18 @@ - +