Refactor to enforce strictNullChecks

This commit applies `strictNullChecks` to the entire codebase to improve
maintainability and type safety. Key changes include:

- Remove some explicit null-checks where unnecessary.
- Add necessary null-checks.
- Refactor static factory functions for a more functional approach.
- Improve some test names and contexts for better debugging.
- Add unit tests for any additional logic introduced.
- Refactor `createPositionFromRegexFullMatch` to its own function as the
  logic is reused.
- Prefer `find` prefix on functions that may return `undefined` and
  `get` prefix for those that always return a value.
This commit is contained in:
undergroundwires
2023-11-12 22:54:00 +01:00
parent 7ab16ecccb
commit 949fac1a7c
294 changed files with 2477 additions and 2738 deletions

View File

@@ -11,8 +11,16 @@ export default defineConfig({
videosFolder: `${CYPRESS_BASE_DIR}/videos`, videosFolder: `${CYPRESS_BASE_DIR}/videos`,
e2e: { 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} specPattern: `${CYPRESS_BASE_DIR}/**/*.cy.{js,jsx,ts,tsx}`, // Default: cypress/e2e/**/*.cy.{js,jsx,ts,tsx}
supportFile: `${CYPRESS_BASE_DIR}/support/e2e.ts`, 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;
}

View File

@@ -68,13 +68,13 @@ These checks validate various qualities like runtime execution, building process
- [`./src/`](./../src/): Contains the code subject to testing. - [`./src/`](./../src/): Contains the code subject to testing.
- [`./tests/shared/`](./../tests/shared/): Contains code shared by different test categories. - [`./tests/shared/`](./../tests/shared/): Contains code shared by different test categories.
- [`bootstrap/setup.ts`](./../tests/shared/bootstrap/setup.ts): Initializes unit and integration tests. - [`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/) - [`./tests/unit/`](./../tests/unit/)
- Stores unit test code. - Stores unit test code.
- The directory structure mirrors [`./src/`](./../src). - 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). - 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/) - [`shared/`](./../tests/unit/shared/)
- Contains shared unit test functionalities. - Contains shared unit test functionalities.
- [`Assertions/`](./../tests/unit/shared/Assertions): Contains common assertion functions, prefixed with `expect`.
- [`TestCases/`](./../tests/unit/shared/TestCases/) - [`TestCases/`](./../tests/unit/shared/TestCases/)
- Shared test cases. - Shared test cases.
- Functions that calls `it()` from [Vitest](https://vitest.dev/) should have `it` prefix. - Functions that calls `it()` from [Vitest](https://vitest.dev/) should have `it` prefix.

View File

@@ -12,9 +12,6 @@ export class ApplicationFactory implements IApplicationFactory {
private readonly getter: AsyncLazy<IApplication>; private readonly getter: AsyncLazy<IApplication>;
protected constructor(costlyGetter: ApplicationGetterType) { protected constructor(costlyGetter: ApplicationGetterType) {
if (!costlyGetter) {
throw new Error('missing getter');
}
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter())); this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
} }

View File

@@ -1,7 +1,5 @@
// Compares to Array<T> objects for equality, ignoring order // Compares to Array<T> objects for equality, ignoring order
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) { export function scrambledEqual<T>(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 sortedArray1 = sort(array1);
const sortedArray2 = sort(array2); const sortedArray2 = sort(array2);
return sequenceEqual(sortedArray1, sortedArray2); return sequenceEqual(sortedArray1, sortedArray2);
@@ -12,8 +10,6 @@ export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
// Compares to Array<T> objects for equality in same order // Compares to Array<T> objects for equality in same order
export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) { export function sequenceEqual<T>(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) { if (array1.length !== array2.length) {
return false; return false;
} }

View File

@@ -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, getSetPrototypeOf: () => Object.setPrototypeOf,
getCaptureStackTrace: () => Error.captureStackTrace, getCaptureStackTrace: () => Error.captureStackTrace,
}; };
function fixPrototype(target: Error, prototype: CustomError) { function fixPrototype(target: Error, prototype: CustomError) {
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget // This is recommended by TypeScript guidelines.
const setPrototypeOf = Environment.getSetPrototypeOf(); // Source: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
if (!functionExists(setPrototypeOf)) { // 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; return;
} }
setPrototypeOf(target, prototype); setPrototypeOf(target, prototype);
} }
function ensureStackTrace(target: Error) { function ensureStackTrace(target: Error) {
const captureStackTrace = Environment.getCaptureStackTrace(); const captureStackTrace = PlatformErrorPrototypeManipulation.getCaptureStackTrace();
if (!functionExists(captureStackTrace)) { if (!isFunction(captureStackTrace)) {
// captureStackTrace is only available on V8, if it's not available // 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. // modern JS engines will usually generate a stack trace on error objects when they're thrown.
return; return;
@@ -44,7 +51,7 @@ function ensureStackTrace(target: Error) {
captureStackTrace(target, target.constructor); captureStackTrace(target, target.constructor);
} }
function functionExists(func: unknown): boolean { // eslint-disable-next-line @typescript-eslint/ban-types
// Not doing truthy/falsy check i.e. if(func) as most values are truthy in JS for robustness function isFunction(func: unknown): func is Function {
return typeof func === 'function'; return typeof func === 'function';
} }

View File

@@ -54,9 +54,6 @@ export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
value: TEnumValue, value: TEnumValue,
enumVariable: EnumVariable<T, TEnumValue>, enumVariable: EnumVariable<T, TEnumValue>,
) { ) {
if (value === undefined || value === null) {
throw new Error('absent enum value');
}
if (!(value in enumVariable)) { if (!(value in enumVariable)) {
throw new RangeError(`enum value "${value}" is out of range`); throw new RangeError(`enum value "${value}" is out of range`);
} }

View File

@@ -9,19 +9,16 @@ export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageF
public create(language: ScriptingLanguage): T { public create(language: ScriptingLanguage): T {
assertInRange(language, ScriptingLanguage); assertInRange(language, ScriptingLanguage);
if (!this.getters.has(language)) { const getter = this.getters.get(language);
if (!getter) {
throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`); throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
} }
const getter = this.getters.get(language);
const instance = getter(); const instance = getter();
return instance; return instance;
} }
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) { protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
assertInRange(language, ScriptingLanguage); assertInRange(language, ScriptingLanguage);
if (!getter) {
throw new Error('missing getter');
}
if (this.getters.has(language)) { if (this.getters.has(language)) {
throw new Error(`${ScriptingLanguage[language]} is already registered`); throw new Error(`${ScriptingLanguage[language]} is already registered`);
} }

View File

@@ -26,7 +26,6 @@ export class ApplicationContext implements IApplicationContext {
public readonly app: IApplication, public readonly app: IApplication,
initialContext: OperatingSystem, initialContext: OperatingSystem,
) { ) {
validateApp(app);
this.states = initializeStates(app); this.states = initializeStates(app);
this.changeContext(initialContext); this.changeContext(initialContext);
} }
@@ -36,10 +35,8 @@ export class ApplicationContext implements IApplicationContext {
if (this.currentOs === os) { if (this.currentOs === os) {
return; return;
} }
this.collection = this.app.getCollection(os); const collection = this.app.getCollection(os);
if (!this.collection) { this.collection = collection;
throw new Error(`os "${OperatingSystem[os]}" is not defined in application`);
}
const event: IApplicationContextChangedEvent = { const event: IApplicationContextChangedEvent = {
newState: this.states[os], newState: this.states[os],
oldState: this.states[this.currentOs], 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 { function initializeStates(app: IApplication): StateMachine {
const machine = new Map<OperatingSystem, ICategoryCollectionState>(); const machine = new Map<OperatingSystem, ICategoryCollectionState>();
for (const collection of app.collections) { for (const collection of app.collections) {

View File

@@ -10,18 +10,23 @@ export async function buildContext(
factory: IApplicationFactory = ApplicationFactory.Current, factory: IApplicationFactory = ApplicationFactory.Current,
environment = RuntimeEnvironment.CurrentEnvironment, environment = RuntimeEnvironment.CurrentEnvironment,
): Promise<IApplicationContext> { ): Promise<IApplicationContext> {
if (!factory) { throw new Error('missing factory'); }
if (!environment) { throw new Error('missing environment'); }
const app = await factory.getApp(); const app = await factory.getApp();
const os = getInitialOs(app, environment.os); const os = getInitialOs(app, environment.os);
return new ApplicationContext(app, 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(); const supportedOsList = app.getSupportedOsList();
if (supportedOsList.includes(currentOs)) { if (currentOs !== undefined && supportedOsList.includes(currentOs)) {
return currentOs; return currentOs;
} }
return getMostSupportedOs(supportedOsList, app);
}
function getMostSupportedOs(supportedOsList: OperatingSystem[], app: IApplication) {
supportedOsList.sort((os1, os2) => { supportedOsList.sort((os1, os2) => {
const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts; const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
return getPriority(os2) - getPriority(os1); return getPriority(os2) - getPriority(os1);

View File

@@ -21,9 +21,6 @@ export class ApplicationCode implements IApplicationCode {
private readonly scriptingDefinition: IScriptingDefinition, private readonly scriptingDefinition: IScriptingDefinition,
private readonly generator: IUserScriptGenerator = new UserScriptGenerator(), 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); this.setCode(userSelection.selectedScripts);
userSelection.changed.on((scripts) => { userSelection.changed.on((scripts) => {
this.setCode(scripts); this.setCode(scripts);

View File

@@ -36,7 +36,11 @@ export class CodeChangedEvent implements ICodeChangedEvent {
} }
public getScriptPositionInCode(script: IScript): ICodePosition { 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;
} }
} }

View File

@@ -16,7 +16,9 @@ export abstract class CodeBuilder implements ICodeBuilder {
return this; return this;
} }
const lines = code.match(/[^\r\n]+/g); const lines = code.match(/[^\r\n]+/g);
if (lines) {
this.lines.push(...lines); this.lines.push(...lines);
}
return this; return this;
} }

View File

@@ -17,8 +17,6 @@ export class UserScriptGenerator implements IUserScriptGenerator {
selectedScripts: ReadonlyArray<SelectedScript>, selectedScripts: ReadonlyArray<SelectedScript>,
scriptingDefinition: IScriptingDefinition, scriptingDefinition: IScriptingDefinition,
): IUserScript { ): IUserScript {
if (!selectedScripts) { throw new Error('missing scripts'); }
if (!scriptingDefinition) { throw new Error('missing definition'); }
if (!selectedScripts.length) { if (!selectedScripts.length) {
return { code: '', scriptPositions: new Map<SelectedScript, ICodePosition>() }; return { code: '', scriptPositions: new Map<SelectedScript, ICodePosition>() };
} }
@@ -68,8 +66,19 @@ function appendSelection(
function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder { function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
const { script } = selection; const { script } = selection;
const name = selection.revert ? `${script.name} (revert)` : script.name; 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 return builder
.appendLine() .appendLine()
.appendFunction(name, scriptCode); .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;
}

View File

@@ -1,37 +1,37 @@
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult'; import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
import { FilterActionType } from './FilterActionType'; import { FilterActionType } from './FilterActionType';
import { IFilterChangeDetails, IFilterChangeDetailsVisitor } from './IFilterChangeDetails'; import {
IFilterChangeDetails, IFilterChangeDetailsVisitor,
ApplyFilterAction, ClearFilterAction,
} from './IFilterChangeDetails';
export class FilterChange implements IFilterChangeDetails { export class FilterChange implements IFilterChangeDetails {
public static forApply(filter: IFilterResult) { public static forApply(
if (!filter) { filter: IFilterResult,
throw new Error('missing filter'); ): IFilterChangeDetails {
} return new FilterChange({ type: FilterActionType.Apply, filter });
return new FilterChange(FilterActionType.Apply, filter);
} }
public static forClear() { public static forClear(): IFilterChangeDetails {
return new FilterChange(FilterActionType.Clear); return new FilterChange({ type: FilterActionType.Clear });
} }
private constructor( private constructor(public readonly action: ApplyFilterAction | ClearFilterAction) { }
public readonly actionType: FilterActionType,
public readonly filter?: IFilterResult,
) { }
public visit(visitor: IFilterChangeDetailsVisitor): void { public visit(visitor: IFilterChangeDetailsVisitor): void {
if (!visitor) { switch (this.action.type) {
throw new Error('missing visitor');
}
switch (this.actionType) {
case FilterActionType.Apply: case FilterActionType.Apply:
visitor.onApply(this.filter); if (visitor.onApply) {
visitor.onApply(this.action.filter);
}
break; break;
case FilterActionType.Clear: case FilterActionType.Clear:
if (visitor.onClear) {
visitor.onClear(); visitor.onClear();
}
break; break;
default: default:
throw new Error(`Unknown action type: ${this.actionType}`); throw new Error(`Unknown action: ${this.action}`);
} }
} }
} }

View File

@@ -2,13 +2,22 @@ import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult'
import { FilterActionType } from './FilterActionType'; import { FilterActionType } from './FilterActionType';
export interface IFilterChangeDetails { export interface IFilterChangeDetails {
readonly actionType: FilterActionType; readonly action: FilterAction;
readonly filter?: IFilterResult;
visit(visitor: IFilterChangeDetailsVisitor): void; visit(visitor: IFilterChangeDetailsVisitor): void;
} }
export interface IFilterChangeDetailsVisitor { export interface IFilterChangeDetailsVisitor {
onClear(): void; readonly onClear?: () => void;
onApply(filter: IFilterResult): 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;

View File

@@ -9,8 +9,6 @@ export class FilterResult implements IFilterResult {
public readonly query: string, public readonly query: string,
) { ) {
if (!query) { throw new Error('Query is empty or undefined'); } 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 { public hasAnyMatches(): boolean {

View File

@@ -43,7 +43,7 @@ export class UserSelection implements IUserSelection {
} }
public removeAllInCategory(categoryId: number): void { public removeAllInCategory(categoryId: number): void {
const category = this.collection.findCategory(categoryId); const category = this.collection.getCategory(categoryId);
const scriptsToRemove = category.getAllScriptsRecursively() const scriptsToRemove = category.getAllScriptsRecursively()
.filter((script) => this.scripts.exists(script.id)); .filter((script) => this.scripts.exists(script.id));
if (!scriptsToRemove.length) { if (!scriptsToRemove.length) {
@@ -57,7 +57,7 @@ export class UserSelection implements IUserSelection {
public addOrUpdateAllInCategory(categoryId: number, revert = false): void { public addOrUpdateAllInCategory(categoryId: number, revert = false): void {
const scriptsToAddOrUpdate = this.collection const scriptsToAddOrUpdate = this.collection
.findCategory(categoryId) .getCategory(categoryId)
.getAllScriptsRecursively() .getAllScriptsRecursively()
.filter( .filter(
(script) => !this.scripts.exists(script.id) (script) => !this.scripts.exists(script.id)
@@ -74,17 +74,14 @@ export class UserSelection implements IUserSelection {
} }
public addSelectedScript(scriptId: string, revert: boolean): void { public addSelectedScript(scriptId: string, revert: boolean): void {
const script = this.collection.findScript(scriptId); const script = this.collection.getScript(scriptId);
if (!script) {
throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
}
const selectedScript = new SelectedScript(script, revert); const selectedScript = new SelectedScript(script, revert);
this.scripts.addItem(selectedScript); this.scripts.addItem(selectedScript);
this.changed.notify(this.scripts.getItems()); this.changed.notify(this.scripts.getItems());
} }
public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void { 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); const selectedScript = new SelectedScript(script, revert);
this.scripts.addOrUpdateItem(selectedScript); this.scripts.addOrUpdateItem(selectedScript);
this.changed.notify(this.scripts.getItems()); this.changed.notify(this.scripts.getItems());
@@ -130,7 +127,7 @@ export class UserSelection implements IUserSelection {
} }
public selectOnly(scripts: readonly IScript[]): void { 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'); throw new Error('Scripts are empty. Use deselectAll() if you want to deselect everything');
} }
let totalChanged = 0; let totalChanged = 0;

View File

@@ -32,10 +32,7 @@ const PreParsedCollections: readonly CollectionData [] = [
]; ];
function validateCollectionsData(collections: readonly CollectionData[]) { function validateCollectionsData(collections: readonly CollectionData[]) {
if (!collections?.length) { if (!collections.length) {
throw new Error('missing collections'); throw new Error('missing collections');
} }
if (collections.some((collection) => !collection)) {
throw new Error('missing collection provided');
}
} }

View File

@@ -28,10 +28,7 @@ export function parseCategoryCollection(
} }
function validate(content: CollectionData): void { function validate(content: CollectionData): void {
if (!content) { if (!content.actions.length) {
throw new Error('missing content');
}
if (!content.actions || content.actions.length <= 0) {
throw new Error('content does not define any action'); throw new Error('content does not define any action');
} }
} }

View File

@@ -1,5 +1,5 @@
import type { import type {
CategoryData, ScriptData, CategoryOrScriptData, InstructionHolder, CategoryData, ScriptData, CategoryOrScriptData,
} from '@/application/collections/'; } from '@/application/collections/';
import { Script } from '@/domain/Script'; import { Script } from '@/domain/Script';
import { Category } from '@/domain/Category'; import { Category } from '@/domain/Category';
@@ -16,7 +16,6 @@ export function parseCategory(
context: ICategoryCollectionParseContext, context: ICategoryCollectionParseContext,
factory: CategoryFactoryType = CategoryFactory, factory: CategoryFactoryType = CategoryFactory,
): Category { ): Category {
if (!context) { throw new Error('missing context'); }
return parseCategoryRecursively({ return parseCategoryRecursively({
categoryData: category, categoryData: category,
context, context,
@@ -30,8 +29,8 @@ interface ICategoryParseContext {
readonly factory: CategoryFactoryType, readonly factory: CategoryFactoryType,
readonly parentCategory?: CategoryData, 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); ensureValidCategory(context.categoryData, context.parentCategory);
const children: ICategoryChildren = { const children: ICategoryChildren = {
subCategories: new Array<Category>(), subCategories: new Array<Category>(),
@@ -55,7 +54,7 @@ function parseCategoryRecursively(context: ICategoryParseContext): Category {
/* scripts: */ children.subScripts, /* scripts: */ children.subScripts,
); );
} catch (err) { } catch (err) {
new NodeValidator({ return new NodeValidator({
type: NodeType.Category, type: NodeType.Category,
selfNode: context.categoryData, selfNode: context.categoryData,
parentNode: context.parentCategory, parentNode: context.parentCategory,
@@ -72,7 +71,7 @@ function ensureValidCategory(category: CategoryData, parentCategory?: CategoryDa
.assertDefined(category) .assertDefined(category)
.assertValidName(category.category) .assertValidName(category.category)
.assert( .assert(
() => category.children && category.children.length > 0, () => category.children.length > 0,
`"${category.category}" has no children.`, `"${category.category}" has no children.`,
); );
} }
@@ -94,14 +93,14 @@ function parseNode(context: INodeParseContext) {
validator.assertDefined(context.nodeData); validator.assertDefined(context.nodeData);
if (isCategory(context.nodeData)) { if (isCategory(context.nodeData)) {
const subCategory = parseCategoryRecursively({ const subCategory = parseCategoryRecursively({
categoryData: context.nodeData as CategoryData, categoryData: context.nodeData,
context: context.context, context: context.context,
factory: context.factory, factory: context.factory,
parentCategory: context.parent, parentCategory: context.parent,
}); });
context.children.subCategories.push(subCategory); context.children.subCategories.push(subCategory);
} else if (isScript(context.nodeData)) { } 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); context.children.subScripts.push(script);
} else { } else {
validator.throw('Node is neither a category or a script.'); 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 { function isScript(data: CategoryOrScriptData): data is ScriptData {
const holder = (data as InstructionHolder); return hasCode(data) || hasCall(data);
return hasCode(holder) || hasCall(holder);
} }
function isCategory(data: CategoryOrScriptData): data is CategoryData { function isCategory(data: CategoryOrScriptData): data is CategoryData {
return hasProperty(data, 'category'); return hasProperty(data, 'category');
} }
function hasCode(data: InstructionHolder): boolean { function hasCode(data: unknown): boolean {
return hasProperty(data, 'code'); return hasProperty(data, 'code');
} }
function hasCall(data: InstructionHolder) { function hasCall(data: unknown) {
return hasProperty(data, 'call'); return hasProperty(data, 'call');
} }

View File

@@ -1,9 +1,6 @@
import type { DocumentableData, DocumentationData } from '@/application/collections/'; import type { DocumentableData, DocumentationData } from '@/application/collections/';
export function parseDocs(documentable: DocumentableData): readonly string[] { export function parseDocs(documentable: DocumentableData): readonly string[] {
if (!documentable) {
throw new Error('missing documentable');
}
const { docs } = documentable; const { docs } = documentable;
if (!docs) { if (!docs) {
return []; return [];

View File

@@ -32,7 +32,7 @@ export class NodeValidator {
return this; return this;
} }
public throw(errorMessage: string) { public throw(errorMessage: string): never {
throw new NodeDataError(errorMessage, this.context); throw new NodeDataError(errorMessage, this.context);
} }
} }

View File

@@ -17,8 +17,7 @@ export class CategoryCollectionParseContext implements ICategoryCollectionParseC
scripting: IScriptingDefinition, scripting: IScriptingDefinition,
syntaxFactory: ISyntaxFactory = new SyntaxFactory(), syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
) { ) {
if (!scripting) { throw new Error('missing scripting'); }
this.syntax = syntaxFactory.create(scripting.language); this.syntax = syntaxFactory.create(scripting.language);
this.compiler = new ScriptCompiler(functionsData, this.syntax); this.compiler = new ScriptCompiler(functionsData ?? [], this.syntax);
} }
} }

View File

@@ -15,19 +15,10 @@ export class Expression implements IExpression {
public readonly evaluator: ExpressionEvaluator, public readonly evaluator: ExpressionEvaluator,
parameters?: IReadOnlyFunctionParameterCollection, parameters?: IReadOnlyFunctionParameterCollection,
) { ) {
if (!position) {
throw new Error('missing position');
}
if (!evaluator) {
throw new Error('missing evaluator');
}
this.parameters = parameters ?? new FunctionParameterCollection(); this.parameters = parameters ?? new FunctionParameterCollection();
} }
public evaluate(context: IExpressionEvaluationContext): string { public evaluate(context: IExpressionEvaluationContext): string {
if (!context) {
throw new Error('missing context');
}
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args); validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
const args = filterUnusedArguments(this.parameters, context.args); const args = filterUnusedArguments(this.parameters, context.args);
const filteredContext = new ExpressionEvaluationContext(args, context.pipelineCompiler); const filteredContext = new ExpressionEvaluationContext(args, context.pipelineCompiler);

View File

@@ -12,8 +12,5 @@ export class ExpressionEvaluationContext implements IExpressionEvaluationContext
public readonly args: IReadOnlyFunctionCallArgumentCollection, public readonly args: IReadOnlyFunctionCallArgumentCollection,
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(), public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(),
) { ) {
if (!args) {
throw new Error('missing args, send empty collection instead.');
}
} }
} }

View File

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

View File

@@ -11,14 +11,11 @@ export class ExpressionsCompiler implements IExpressionsCompiler {
) { } ) { }
public compileExpressions( public compileExpressions(
code: string | undefined, code: string,
args: IReadOnlyFunctionCallArgumentCollection, args: IReadOnlyFunctionCallArgumentCollection,
): string { ): string {
if (!args) {
throw new Error('missing args, send empty collection instead.');
}
if (!code) { if (!code) {
return code; return '';
} }
const context = new ExpressionEvaluationContext(args); const context = new ExpressionEvaluationContext(args);
const compiledCode = compileRecursively(code, context, this.extractor); const compiledCode = compileRecursively(code, context, this.extractor);
@@ -145,7 +142,7 @@ function ensureParamsUsedInCodeHasArgsProvided(
providedArgs: IReadOnlyFunctionCallArgumentCollection, providedArgs: IReadOnlyFunctionCallArgumentCollection,
): void { ): void {
const usedParameterNames = extractRequiredParameterNames(expressions); const usedParameterNames = extractRequiredParameterNames(expressions);
if (!usedParameterNames?.length) { if (!usedParameterNames.length) {
return; return;
} }
const notProvidedParameters = usedParameterNames const notProvidedParameters = usedParameterNames

View File

@@ -2,6 +2,7 @@ import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argume
export interface IExpressionsCompiler { export interface IExpressionsCompiler {
compileExpressions( compileExpressions(
code: string | undefined, code: string,
args: IReadOnlyFunctionCallArgumentCollection): string; args: IReadOnlyFunctionCallArgumentCollection,
): string;
} }

View File

@@ -10,12 +10,9 @@ const Parsers = [
export class CompositeExpressionParser implements IExpressionParser { export class CompositeExpressionParser implements IExpressionParser {
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) { public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
if (!leafs) { if (!leafs.length) {
throw new Error('missing leafs'); throw new Error('missing leafs');
} }
if (leafs.some((leaf) => !leaf)) {
throw new Error('missing leaf');
}
} }
public findExpressions(code: string): IExpression[] { public findExpressions(code: string): IExpression[] {

View File

@@ -1,9 +1,9 @@
import { IExpressionParser } from '../IExpressionParser'; import { IExpressionParser } from '../IExpressionParser';
import { ExpressionPosition } from '../../Expression/ExpressionPosition';
import { IExpression } from '../../Expression/IExpression'; import { IExpression } from '../../Expression/IExpression';
import { Expression, ExpressionEvaluator } from '../../Expression/Expression'; import { Expression, ExpressionEvaluator } from '../../Expression/Expression';
import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter'; import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter';
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection'; import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
import { createPositionFromRegexFullMatch } from '../../Expression/ExpressionPositionFactory';
export abstract class RegexParser implements IExpressionParser { export abstract class RegexParser implements IExpressionParser {
protected abstract readonly regex: RegExp; protected abstract readonly regex: RegExp;
@@ -21,7 +21,7 @@ export abstract class RegexParser implements IExpressionParser {
const matches = code.matchAll(this.regex); const matches = code.matchAll(this.regex);
for (const match of matches) { for (const match of matches) {
const primitiveExpression = this.buildExpression(match); 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 parameters = createParameters(primitiveExpression);
const expression = new Expression(position, primitiveExpression.evaluator, parameters); const expression = new Expression(position, primitiveExpression.evaluator, parameters);
yield expression; 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( function createParameters(
expression: IPrimitiveExpression, expression: IPrimitiveExpression,
): FunctionParameterCollection { ): FunctionParameterCollection {

View File

@@ -28,7 +28,7 @@ function hasLines(text: string) {
*/ */
function inlineComments(code: string): string { function inlineComments(code: string): string {
const makeInlineComment = (comment: string) => { const makeInlineComment = (comment: string) => {
const value = comment?.trim(); const value = comment.trim();
if (!value) { if (!value) {
return '<##>'; return '<##>';
} }

View File

@@ -15,12 +15,6 @@ export class PipeFactory implements IPipeFactory {
private readonly pipes = new Map<string, IPipe>(); private readonly pipes = new Map<string, IPipe>();
constructor(pipes: readonly IPipe[] = RegisteredPipes) { 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) { for (const pipe of pipes) {
this.registerPipe(pipe); this.registerPipe(pipe);
} }
@@ -28,10 +22,11 @@ export class PipeFactory implements IPipeFactory {
public get(pipeName: string): IPipe { public get(pipeName: string): IPipe {
validatePipeName(pipeName); validatePipeName(pipeName);
if (!this.pipes.has(pipeName)) { const pipe = this.pipes.get(pipeName);
if (!pipe) {
throw new Error(`Unknown pipe: "${pipeName}"`); throw new Error(`Unknown pipe: "${pipeName}"`);
} }
return this.pipes.get(pipeName); return pipe;
} }
private registerPipe(pipe: IPipe): void { private registerPipe(pipe: IPipe): void {

View File

@@ -5,6 +5,7 @@ import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function
import { IExpression } from '../Expression/IExpression'; import { IExpression } from '../Expression/IExpression';
import { ExpressionPosition } from '../Expression/ExpressionPosition'; import { ExpressionPosition } from '../Expression/ExpressionPosition';
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder'; import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
import { createPositionFromRegexFullMatch } from '../Expression/ExpressionPositionFactory';
export class WithParser implements IExpressionParser { export class WithParser implements IExpressionParser {
public findExpressions(code: string): IExpression[] { public findExpressions(code: string): IExpression[] {
@@ -42,31 +43,25 @@ function parseAllWithExpressions(
expressions.push({ expressions.push({
type: WithStatementType.Start, type: WithStatementType.Start,
parameterName: match[1], parameterName: match[1],
position: createPosition(match), position: createPositionFromRegexFullMatch(match),
}); });
} }
for (const match of input.matchAll(WithStatementEndRegEx)) { for (const match of input.matchAll(WithStatementEndRegEx)) {
expressions.push({ expressions.push({
type: WithStatementType.End, type: WithStatementType.End,
position: createPosition(match), position: createPositionFromRegexFullMatch(match),
}); });
} }
for (const match of input.matchAll(ContextVariableWithPipelineRegEx)) { for (const match of input.matchAll(ContextVariableWithPipelineRegEx)) {
expressions.push({ expressions.push({
type: WithStatementType.ContextVariable, type: WithStatementType.ContextVariable,
position: createPosition(match), position: createPositionFromRegexFullMatch(match),
pipeline: match[1], pipeline: match[1],
}); });
} }
return expressions; return expressions;
} }
function createPosition(match: RegExpMatchArray): ExpressionPosition {
const startPos = match.index;
const endPos = startPos + match[0].length;
return new ExpressionPosition(startPos, endPos);
}
class WithStatementBuilder { class WithStatementBuilder {
private readonly contextVariables = new Array<{ private readonly contextVariables = new Array<{
readonly positionInScope: ExpressionPosition; readonly positionInScope: ExpressionPosition;
@@ -125,7 +120,7 @@ class WithStatementBuilder {
private substituteContextVariables( private substituteContextVariables(
scope: string, scope: string,
substituter: (pipeline: string) => string, substituter: (pipeline?: string) => string,
): string { ): string {
if (!this.contextVariables.length) { if (!this.contextVariables.length) {
return scope; return scope;
@@ -157,7 +152,7 @@ function parseWithExpressions(input: string): IExpression[] {
.sort((a, b) => b.position.start - a.position.start); .sort((a, b) => b.position.start - a.position.start);
const expressions = new Array<IExpression>(); const expressions = new Array<IExpression>();
const builders = new Array<WithStatementBuilder>(); const builders = new Array<WithStatementBuilder>();
const throwWithContext = (message: string) => { const throwWithContext = (message: string): never => {
throw new Error(`${message}\n${buildErrorContext(input, allStatements)}}`); throw new Error(`${message}\n${buildErrorContext(input, allStatements)}}`);
}; };
while (sortedStatements.length > 0) { while (sortedStatements.length > 0) {
@@ -178,13 +173,16 @@ function parseWithExpressions(input: string): IExpression[] {
} }
builders[builders.length - 1].addContextVariable(statement.position, statement.pipeline); builders[builders.length - 1].addContextVariable(statement.position, statement.pipeline);
break; break;
case WithStatementType.End: case WithStatementType.End: {
if (builders.length === 0) { const builder = builders.pop();
if (!builder) {
throwWithContext('Redundant `end` statement, missing `with`?'); throwWithContext('Redundant `end` statement, missing `with`?');
}
expressions.push(builders.pop().buildExpression(statement.position, input));
break; break;
} }
expressions.push(builder.buildExpression(statement.position, input));
break;
}
}
} }
if (builders.length > 0) { if (builders.length > 0) {
throwWithContext('Missing `end` statement, forgot `{{ end }}?'); throwWithContext('Missing `end` statement, forgot `{{ end }}?');

View File

@@ -5,9 +5,6 @@ export class FunctionCallArgumentCollection implements IFunctionCallArgumentColl
private readonly arguments = new Map<string, IFunctionCallArgument>(); private readonly arguments = new Map<string, IFunctionCallArgument>();
public addArgument(argument: IFunctionCallArgument): void { public addArgument(argument: IFunctionCallArgument): void {
if (!argument) {
throw new Error('missing argument');
}
if (this.hasArgument(argument.parameterName)) { if (this.hasArgument(argument.parameterName)) {
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`); throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
} }

View File

@@ -3,18 +3,22 @@ import { CodeSegmentMerger } from './CodeSegmentMerger';
export class NewlineCodeSegmentMerger implements CodeSegmentMerger { export class NewlineCodeSegmentMerger implements CodeSegmentMerger {
public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode { public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode {
if (!codeSegments?.length) { if (!codeSegments.length) {
throw new Error('missing segments'); throw new Error('missing segments');
} }
return { return {
code: joinCodeParts(codeSegments.map((f) => f.code)), 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 { function joinCodeParts(codeSegments: readonly string[]): string {
return codeSegments return codeSegments
.filter((segment) => segment?.length > 0) .filter((segment) => segment.length > 0)
.join('\n'); .join('\n');
} }

View File

@@ -21,9 +21,7 @@ export class FunctionCallSequenceCompiler implements FunctionCallCompiler {
calls: readonly FunctionCall[], calls: readonly FunctionCall[],
functions: ISharedFunctionCollection, functions: ISharedFunctionCollection,
): CompiledCode { ): CompiledCode {
if (!functions) { throw new Error('missing functions'); } if (!calls.length) { throw new Error('missing calls'); }
if (!calls?.length) { throw new Error('missing calls'); }
if (calls.some((f) => !f)) { throw new Error('missing function call'); }
const context: FunctionCallCompilationContext = { const context: FunctionCallCompilationContext = {
allFunctions: functions, allFunctions: functions,
rootCallSequence: calls, rootCallSequence: calls,

View File

@@ -1,6 +1,6 @@
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler'; import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler'; 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 { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode'; import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy'; import { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
@@ -12,19 +12,33 @@ export class InlineFunctionCallCompiler implements SingleCallCompilerStrategy {
} }
public canCompile(func: ISharedFunction): boolean { public canCompile(func: ISharedFunction): boolean {
return func.body.code !== undefined; return func.body.type === FunctionBodyType.Code;
} }
public compileFunction( public compileFunction(
calledFunction: ISharedFunction, calledFunction: ISharedFunction,
callToFunction: FunctionCall, callToFunction: FunctionCall,
): CompiledCode[] { ): 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 { code } = calledFunction.body;
const { args } = callToFunction; const { args } = callToFunction;
return [ return [
{ {
code: this.expressionsCompiler.compileExpressions(code.execute, args), 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);
})(),
}, },
]; ];
} }

View File

@@ -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 { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext'; import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode'; 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 { public canCompile(func: ISharedFunction): boolean {
return func.body.calls !== undefined; return func.body.type === FunctionBodyType.Calls;
} }
public compileFunction( public compileFunction(
@@ -21,7 +21,7 @@ export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
callToFunction: FunctionCall, callToFunction: FunctionCall,
context: FunctionCallCompilationContext, context: FunctionCallCompilationContext,
): CompiledCode[] { ): CompiledCode[] {
const nestedCalls = calledFunction.body.calls; const nestedCalls = (calledFunction.body as CallFunctionBody).calls;
return nestedCalls.map((nestedCall) => { return nestedCalls.map((nestedCall) => {
try { try {
const compiledParentCall = this.argumentCompiler const compiledParentCall = this.argumentCompiler

View File

@@ -5,9 +5,6 @@ import { FunctionCallArgument } from './Argument/FunctionCallArgument';
import { ParsedFunctionCall } from './ParsedFunctionCall'; import { ParsedFunctionCall } from './ParsedFunctionCall';
export function parseFunctionCalls(calls: FunctionCallsData): FunctionCall[] { export function parseFunctionCalls(calls: FunctionCallsData): FunctionCall[] {
if (calls === undefined) {
throw new Error('missing call data');
}
const sequence = getCallSequence(calls); const sequence = getCallSequence(calls);
return sequence.map((call) => parseFunctionCall(call)); return sequence.map((call) => parseFunctionCall(call));
} }
@@ -19,22 +16,21 @@ function getCallSequence(calls: FunctionCallsData): FunctionCallData[] {
if (calls instanceof Array) { if (calls instanceof Array) {
return calls as FunctionCallData[]; return calls as FunctionCallData[];
} }
return [calls as FunctionCallData]; const singleCall = calls;
return [singleCall];
} }
function parseFunctionCall(call: FunctionCallData): FunctionCall { function parseFunctionCall(call: FunctionCallData): FunctionCall {
if (!call) {
throw new Error('missing call data');
}
const callArgs = parseArgs(call.parameters); const callArgs = parseArgs(call.parameters);
return new ParsedFunctionCall(call.function, callArgs); return new ParsedFunctionCall(call.function, callArgs);
} }
function parseArgs( function parseArgs(
parameters: FunctionCallParametersData, parameters: FunctionCallParametersData | undefined,
): FunctionCallArgumentCollection { ): FunctionCallArgumentCollection {
return Object.keys(parameters || {}) const parametersMap = parameters ?? {};
.map((parameterName) => new FunctionCallArgument(parameterName, parameters[parameterName])) return Object.keys(parametersMap)
.map((parameterName) => new FunctionCallArgument(parameterName, parametersMap[parameterName]))
.reduce((args, arg) => { .reduce((args, arg) => {
args.addArgument(arg); args.addArgument(arg);
return args; return args;

View File

@@ -9,8 +9,5 @@ export class ParsedFunctionCall implements FunctionCall {
if (!functionName) { if (!functionName) {
throw new Error('missing function name in function call'); throw new Error('missing function name in function call');
} }
if (!args) {
throw new Error('missing args');
}
} }
} }

View File

@@ -4,15 +4,21 @@ import { FunctionCall } from './Call/FunctionCall';
export interface ISharedFunction { export interface ISharedFunction {
readonly name: string; readonly name: string;
readonly parameters: IReadOnlyFunctionParameterCollection; readonly parameters: IReadOnlyFunctionParameterCollection;
readonly body: ISharedFunctionBody; readonly body: SharedFunctionBody;
} }
export interface ISharedFunctionBody { export interface CallFunctionBody {
readonly type: FunctionBodyType; readonly type: FunctionBodyType.Calls,
readonly code: IFunctionCode | undefined; readonly calls: readonly FunctionCall[],
readonly calls: readonly FunctionCall[] | undefined;
} }
export interface CodeFunctionBody {
readonly type: FunctionBodyType.Code;
readonly code: IFunctionCode,
}
export type SharedFunctionBody = CallFunctionBody | CodeFunctionBody;
export enum FunctionBodyType { export enum FunctionBodyType {
Code, Code,
Calls, Calls,

View File

@@ -18,9 +18,6 @@ export class FunctionParameterCollection implements IFunctionParameterCollection
} }
private ensureValidParameter(parameter: IFunctionParameter) { private ensureValidParameter(parameter: IFunctionParameter) {
if (!parameter) {
throw new Error('missing parameter');
}
if (this.includesName(parameter.name)) { if (this.includesName(parameter.name)) {
throw new Error(`duplicate parameter name: "${parameter.name}"`); throw new Error(`duplicate parameter name: "${parameter.name}"`);
} }

View File

@@ -1,7 +1,7 @@
import { FunctionCall } from './Call/FunctionCall'; import { FunctionCall } from './Call/FunctionCall';
import { import {
FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody, FunctionBodyType, IFunctionCode, ISharedFunction, SharedFunctionBody,
} from './ISharedFunction'; } from './ISharedFunction';
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection'; import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
@@ -10,7 +10,7 @@ export function createCallerFunction(
parameters: IReadOnlyFunctionParameterCollection, parameters: IReadOnlyFunctionParameterCollection,
callSequence: readonly FunctionCall[], callSequence: readonly FunctionCall[],
): ISharedFunction { ): ISharedFunction {
if (!callSequence || !callSequence.length) { if (!callSequence.length) {
throw new Error(`missing call sequence in function "${name}"`); throw new Error(`missing call sequence in function "${name}"`);
} }
return new SharedFunction(name, parameters, callSequence, FunctionBodyType.Calls); return new SharedFunction(name, parameters, callSequence, FunctionBodyType.Calls);
@@ -33,7 +33,7 @@ export function createFunctionWithInlineCode(
} }
class SharedFunction implements ISharedFunction { class SharedFunction implements ISharedFunction {
public readonly body: ISharedFunctionBody; public readonly body: SharedFunctionBody;
constructor( constructor(
public readonly name: string, public readonly name: string,
@@ -42,11 +42,22 @@ class SharedFunction implements ISharedFunction {
bodyType: FunctionBodyType, bodyType: FunctionBodyType,
) { ) {
if (!name) { throw new Error('missing function name'); } if (!name) { throw new Error('missing function name'); }
if (!parameters) { throw new Error('missing parameters'); }
switch (bodyType) {
case FunctionBodyType.Code:
this.body = { this.body = {
type: bodyType, type: FunctionBodyType.Code,
code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined, code: content as IFunctionCode,
calls: bodyType === FunctionBodyType.Calls ? content as readonly FunctionCall[] : undefined,
}; };
break;
case FunctionBodyType.Calls:
this.body = {
type: FunctionBodyType.Calls,
calls: content as readonly FunctionCall[],
};
break;
default:
throw new Error(`unknown body type: ${FunctionBodyType[bodyType]}`);
}
} }
} }

View File

@@ -5,7 +5,6 @@ export class SharedFunctionCollection implements ISharedFunctionCollection {
private readonly functionsByName = new Map<string, ISharedFunction>(); private readonly functionsByName = new Map<string, ISharedFunction>();
public addFunction(func: ISharedFunction): void { public addFunction(func: ISharedFunction): void {
if (!func) { throw new Error('missing function'); }
if (this.has(func.name)) { if (this.has(func.name)) {
throw new Error(`function with name ${func.name} already exists`); throw new Error(`function with name ${func.name} already exists`);
} }

View File

@@ -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 { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator'; import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines'; import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
@@ -23,9 +25,8 @@ export class SharedFunctionsParser implements ISharedFunctionsParser {
functions: readonly FunctionData[], functions: readonly FunctionData[],
syntax: ILanguageSyntax, syntax: ILanguageSyntax,
): ISharedFunctionCollection { ): ISharedFunctionCollection {
if (!syntax) { throw new Error('missing syntax'); }
const collection = new SharedFunctionCollection(); const collection = new SharedFunctionCollection();
if (!functions || !functions.length) { if (!functions.length) {
return collection; return collection;
} }
ensureValidFunctions(functions); ensureValidFunctions(functions);
@@ -55,11 +56,13 @@ function parseFunction(
} }
function validateCode( function validateCode(
data: FunctionData, data: CodeFunctionData,
syntax: ILanguageSyntax, syntax: ILanguageSyntax,
validator: ICodeValidator, validator: ICodeValidator,
): void { ): void {
[data.code, data.revertCode].forEach( [data.code, data.revertCode]
.filter((code): code is string => Boolean(code))
.forEach(
(code) => validator.throwIfInvalid( (code) => validator.throwIfInvalid(
code, code,
[new NoEmptyLines(), new NoDuplicatedLines(syntax)], [new NoEmptyLines(), new NoDuplicatedLines(syntax)],
@@ -85,19 +88,18 @@ function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollecti
}, new FunctionParameterCollection()); }, new FunctionParameterCollection());
} }
function hasCode(data: FunctionData): boolean { function hasCode(data: FunctionData): data is CodeFunctionData {
return Boolean(data.code); return (data as CodeInstruction).code !== undefined;
} }
function hasCall(data: FunctionData): boolean { function hasCall(data: FunctionData): data is CallFunctionData {
return Boolean(data.call); return (data as CallInstruction).call !== undefined;
} }
function ensureValidFunctions(functions: readonly FunctionData[]) { function ensureValidFunctions(functions: readonly FunctionData[]) {
ensureNoUndefinedItem(functions);
ensureNoDuplicatesInFunctionNames(functions); ensureNoDuplicatesInFunctionNames(functions);
ensureNoDuplicateCode(functions);
ensureEitherCallOrCodeIsDefined(functions); ensureEitherCallOrCodeIsDefined(functions);
ensureNoDuplicateCode(functions);
ensureExpectedParametersType(functions); ensureExpectedParametersType(functions);
} }
@@ -105,7 +107,7 @@ function printList(list: readonly string[]): string {
return `"${list.join('","')}"`; return `"${list.join('","')}"`;
} }
function ensureEitherCallOrCodeIsDefined(holders: readonly InstructionHolder[]) { function ensureEitherCallOrCodeIsDefined(holders: readonly FunctionData[]) {
// Ensure functions do not define both call and code // Ensure functions do not define both call and code
const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder)); const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder));
if (withBothCallAndCode.length) { if (withBothCallAndCode.length) {
@@ -132,7 +134,7 @@ function isArrayOfObjects(value: unknown): boolean {
&& value.every((item) => typeof item === 'object'); && value.every((item) => typeof item === 'object');
} }
function printNames(holders: readonly InstructionHolder[]) { function printNames(holders: readonly FunctionData[]) {
return printList(holders.map((holder) => holder.name)); 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[]) { 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) .map((func) => func.code)
.filter((code) => code)); .filter((code) => code));
if (duplicateCodes.length > 0) { if (duplicateCodes.length > 0) {
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`); throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
} }
const duplicateRevertCodes = getDuplicates(functions const duplicateRevertCodes = getDuplicates(callFunctions
.filter((func) => func.revertCode) .map((func) => func.revertCode)
.map((func) => func.revertCode)); .filter((code): code is string => Boolean(code)));
if (duplicateRevertCodes.length > 0) { if (duplicateRevertCodes.length > 0) {
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`); throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
} }

View File

@@ -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 { IScriptCode } from '@/domain/IScriptCode';
import { ScriptCode } from '@/domain/ScriptCode'; import { ScriptCode } from '@/domain/ScriptCode';
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax'; import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
@@ -18,27 +18,24 @@ export class ScriptCompiler implements IScriptCompiler {
private readonly functions: ISharedFunctionCollection; private readonly functions: ISharedFunctionCollection;
constructor( constructor(
functions: readonly FunctionData[] | undefined, functions: readonly FunctionData[],
syntax: ILanguageSyntax, syntax: ILanguageSyntax,
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance, sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
private readonly callCompiler: FunctionCallCompiler = FunctionCallSequenceCompiler.instance, private readonly callCompiler: FunctionCallCompiler = FunctionCallSequenceCompiler.instance,
private readonly codeValidator: ICodeValidator = CodeValidator.instance, private readonly codeValidator: ICodeValidator = CodeValidator.instance,
) { ) {
if (!syntax) { throw new Error('missing syntax'); }
this.functions = sharedFunctionsParser.parseFunctions(functions, syntax); this.functions = sharedFunctionsParser.parseFunctions(functions, syntax);
} }
public canCompile(script: ScriptData): boolean { public canCompile(script: ScriptData): boolean {
if (!script) { throw new Error('missing script'); } return hasCall(script);
if (!script.call) {
return false;
}
return true;
} }
public compile(script: ScriptData): IScriptCode { public compile(script: ScriptData): IScriptCode {
if (!script) { throw new Error('missing script'); }
try { try {
if (!hasCall(script)) {
throw new Error('Script does include any calls.');
}
const calls = parseFunctionCalls(script.call); const calls = parseFunctionCalls(script.call);
const compiledCode = this.callCompiler.compileFunctionCalls(calls, this.functions); const compiledCode = this.callCompiler.compileFunctionCalls(calls, this.functions);
validateCompiledCode(compiledCode, this.codeValidator); validateCompiledCode(compiledCode, this.codeValidator);
@@ -53,7 +50,17 @@ export class ScriptCompiler implements IScriptCompiler {
} }
function validateCompiledCode(compiledCode: CompiledCode, validator: ICodeValidator): void { function validateCompiledCode(compiledCode: CompiledCode, validator: ICodeValidator): void {
[compiledCode.code, compiledCode.revertCode].forEach( [compiledCode.code, compiledCode.revertCode]
(code) => validator.throwIfInvalid(code, [new NoEmptyLines()]), .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;
}

View File

@@ -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 { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax'; import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
import { Script } from '@/domain/Script'; import { Script } from '@/domain/Script';
@@ -14,7 +14,6 @@ import { ICategoryCollectionParseContext } from './ICategoryCollectionParseConte
import { CodeValidator } from './Validation/CodeValidator'; import { CodeValidator } from './Validation/CodeValidator';
import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines'; import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines';
// eslint-disable-next-line consistent-return
export function parseScript( export function parseScript(
data: ScriptData, data: ScriptData,
context: ICategoryCollectionParseContext, context: ICategoryCollectionParseContext,
@@ -24,7 +23,6 @@ export function parseScript(
): Script { ): Script {
const validator = new NodeValidator({ type: NodeType.Script, selfNode: data }); const validator = new NodeValidator({ type: NodeType.Script, selfNode: data });
validateScript(data, validator); validateScript(data, validator);
if (!context) { throw new Error('missing context'); }
try { try {
const script = scriptFactory( const script = scriptFactory(
/* name: */ data.name, /* name: */ data.name,
@@ -34,12 +32,12 @@ export function parseScript(
); );
return script; return script;
} catch (err) { } catch (err) {
validator.throw(err.message); return validator.throw(err.message);
} }
} }
function parseLevel( function parseLevel(
level: string, level: string | undefined,
parser: IEnumParser<RecommendationLevel>, parser: IEnumParser<RecommendationLevel>,
): RecommendationLevel | undefined { ): RecommendationLevel | undefined {
if (!level) { if (!level) {
@@ -56,39 +54,45 @@ function parseCode(
if (context.compiler.canCompile(script)) { if (context.compiler.canCompile(script)) {
return context.compiler.compile(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); validateHardcodedCodeWithoutCalls(code, codeValidator, context.syntax);
return code; return code;
} }
function validateHardcodedCodeWithoutCalls( function validateHardcodedCodeWithoutCalls(
scriptCode: ScriptCode, scriptCode: ScriptCode,
codeValidator: ICodeValidator, validator: ICodeValidator,
syntax: ILanguageSyntax, syntax: ILanguageSyntax,
) { ) {
[scriptCode.execute, scriptCode.revert].forEach( [scriptCode.execute, scriptCode.revert]
(code) => codeValidator.throwIfInvalid( .filter((code): code is string => Boolean(code))
.forEach(
(code) => validator.throwIfInvalid(
code, code,
[new NoEmptyLines(), new NoDuplicatedLines(syntax)], [new NoEmptyLines(), new NoDuplicatedLines(syntax)],
), ),
); );
} }
function validateScript(script: ScriptData, validator: NodeValidator) { function validateScript(
script: ScriptData,
validator: NodeValidator,
): asserts script is NonNullable<ScriptData> {
validator validator
.assertDefined(script) .assertDefined(script)
.assertValidName(script.name) .assertValidName(script.name)
.assert( .assert(
() => Boolean(script.code || script.call), () => Boolean((script as CodeScriptData).code || (script as CallScriptData).call),
'Must define either "call" or "code".', 'Neither "call" or "code" is defined.',
) )
.assert( .assert(
() => !(script.code && script.call), () => !((script as CodeScriptData).code && (script as CallScriptData).call),
'Cannot define both "call" and "code".', 'Both "call" and "code" are defined.',
) )
.assert( .assert(
() => !(script.revertCode && script.call), () => !((script as CodeScriptData).revertCode && (script as CallScriptData).call),
'Cannot define "revertCode" if "call" is defined.', 'Both "call" and "revertCode" are defined.',
); );
} }

View File

@@ -9,7 +9,7 @@ export class CodeValidator implements ICodeValidator {
code: string, code: string,
rules: readonly ICodeValidationRule[], rules: readonly ICodeValidationRule[],
): void { ): void {
if (!rules || rules.length === 0) { throw new Error('missing rules'); } if (rules.length === 0) { throw new Error('missing rules'); }
if (!code) { if (!code) {
return; return;
} }

View File

@@ -3,9 +3,7 @@ import { ICodeLine } from '../ICodeLine';
import { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule'; import { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule';
export class NoDuplicatedLines implements ICodeValidationRule { export class NoDuplicatedLines implements ICodeValidationRule {
constructor(private readonly syntax: ILanguageSyntax) { constructor(private readonly syntax: ILanguageSyntax) { }
if (!syntax) { throw new Error('missing syntax'); }
}
public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] { public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] {
return lines return lines

View File

@@ -17,7 +17,6 @@ export class CodeSubstituter implements ICodeSubstituter {
public substitute(code: string, info: IProjectInformation): string { public substitute(code: string, info: IProjectInformation): string {
if (!code) { throw new Error('missing code'); } if (!code) { throw new Error('missing code'); }
if (!info) { throw new Error('missing info'); }
const args = new FunctionCallArgumentCollection(); const args = new FunctionCallArgumentCollection();
const substitute = (name: string, value: string) => args const substitute = (name: string, value: string) => args
.addArgument(new FunctionCallArgument(name, value)); .addArgument(new FunctionCallArgument(name, value));

View File

@@ -18,8 +18,6 @@ export class ScriptingDefinitionParser {
definition: ScriptingDefinitionData, definition: ScriptingDefinitionData,
info: IProjectInformation, info: IProjectInformation,
): IScriptingDefinition { ): IScriptingDefinition {
if (!info) { throw new Error('missing info'); }
if (!definition) { throw new Error('missing definition'); }
const language = this.languageParser.parseEnum(definition.language, 'language'); const language = this.languageParser.parseEnum(definition.language, 'language');
const startCode = this.codeSubstituter.substitute(definition.startCode, info); const startCode = this.codeSubstituter.substitute(definition.startCode, info);
const endCode = this.codeSubstituter.substitute(definition.endCode, info); const endCode = this.codeSubstituter.substitute(definition.endCode, info);

View File

@@ -12,29 +12,38 @@ declare module '@/application/collections/*' {
} }
export type CategoryOrScriptData = CategoryData | ScriptData; export type CategoryOrScriptData = CategoryData | ScriptData;
export type DocumentationData = ReadonlyArray<string> | string; export type DocumentationData = ReadonlyArray<string> | string | undefined;
export interface DocumentableData { export interface DocumentableData {
readonly docs?: DocumentationData; readonly docs?: DocumentationData;
} }
export interface InstructionHolder { export interface CodeInstruction {
readonly name: string; readonly code: string;
readonly code?: string;
readonly revertCode?: string; readonly revertCode?: string;
readonly call?: FunctionCallsData;
} }
export interface CallInstruction {
readonly call: FunctionCallsData;
}
export type InstructionHolder = CodeInstruction | CallInstruction;
export interface ParameterDefinitionData { export interface ParameterDefinitionData {
readonly name: string; readonly name: string;
readonly optional?: boolean; readonly optional?: boolean;
} }
export interface FunctionData extends InstructionHolder { export type FunctionDefinition = {
readonly name: string;
readonly parameters?: readonly ParameterDefinitionData[]; readonly parameters?: readonly ParameterDefinitionData[];
} };
export type CodeFunctionData = FunctionDefinition & CodeInstruction;
export type CallFunctionData = FunctionDefinition & CallInstruction;
export type FunctionData = CodeFunctionData | CallFunctionData;
export interface FunctionCallParametersData { export interface FunctionCallParametersData {
readonly [index: string]: string; readonly [index: string]: string;
@@ -47,10 +56,16 @@ declare module '@/application/collections/*' {
export type FunctionCallsData = readonly FunctionCallData[] | FunctionCallData | undefined; export type FunctionCallsData = readonly FunctionCallData[] | FunctionCallData | undefined;
export interface ScriptData extends InstructionHolder, DocumentableData { export type ScriptDefinition = DocumentableData & {
readonly name: string; readonly name: string;
readonly recommend?: string; readonly recommend?: string;
} };
export type CodeScriptData = ScriptDefinition & CodeInstruction;
export type CallScriptData = ScriptDefinition & CallInstruction;
export type ScriptData = CodeScriptData | CallScriptData;
export interface ScriptingDefinitionData { export interface ScriptingDefinitionData {
readonly language: string; readonly language: string;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,10 +17,16 @@ export function createNodeSystemOperations(): ISystemOperations {
filePath: string, filePath: string,
mode: string | number, mode: string | number,
) => chmod(filePath, mode), ) => chmod(filePath, mode),
createDirectory: ( createDirectory: async (
directoryPath: string, directoryPath: string,
isRecursive?: boolean, 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: ( writeToFile: (
filePath: string, filePath: string,
data: string, data: string,

View File

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

View File

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

View File

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

View File

@@ -7,22 +7,18 @@
<TheCodeButtons class="app__row app__code-buttons" /> <TheCodeButtons class="app__row app__code-buttons" />
<TheFooter /> <TheFooter />
</div> </div>
<OptionalDevToolkit /> <component v-if="devToolkitComponent" :is="devToolkitComponent" />
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue'; import { defineAsyncComponent, defineComponent, Component } from 'vue';
import TheHeader from '@/presentation/components/TheHeader.vue'; import TheHeader from '@/presentation/components/TheHeader.vue';
import TheFooter from '@/presentation/components/TheFooter/TheFooter.vue'; import TheFooter from '@/presentation/components/TheFooter/TheFooter.vue';
import TheCodeButtons from '@/presentation/components/Code/CodeButtons/TheCodeButtons.vue'; import TheCodeButtons from '@/presentation/components/Code/CodeButtons/TheCodeButtons.vue';
import TheScriptArea from '@/presentation/components/Scripts/TheScriptArea.vue'; import TheScriptArea from '@/presentation/components/Scripts/TheScriptArea.vue';
import TheSearchBar from '@/presentation/components/TheSearchBar.vue'; import TheSearchBar from '@/presentation/components/TheSearchBar.vue';
const OptionalDevToolkit = process.env.NODE_ENV !== 'production'
? defineAsyncComponent(() => import('@/presentation/components/DevToolkit/DevToolkit.vue'))
: null;
export default defineComponent({ export default defineComponent({
components: { components: {
TheHeader, TheHeader,
@@ -30,10 +26,23 @@ export default defineComponent({
TheScriptArea, TheScriptArea,
TheSearchBar, TheSearchBar,
TheFooter, TheFooter,
OptionalDevToolkit,
}, },
setup() { }, setup() {
const devToolkitComponent = getOptionalDevToolkitComponent();
return {
devToolkitComponent,
};
},
}); });
function getOptionalDevToolkitComponent(): Component | undefined {
const isDevelopment = process.env.NODE_ENV !== 'production';
if (!isDevelopment) {
return undefined;
}
return defineAsyncComponent(() => import('@/presentation/components/DevToolkit/DevToolkit.vue'));
}
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -40,7 +40,7 @@ export default defineComponent({
function getCanRunState( function getCanRunState(
selectedOs: OperatingSystem, selectedOs: OperatingSystem,
isDesktopVersion: boolean, isDesktopVersion: boolean,
hostOs: OperatingSystem, hostOs: OperatingSystem | undefined,
): boolean { ): boolean {
const isRunningOnSelectedOs = selectedOs === hostOs; const isRunningOnSelectedOs = selectedOs === hostOs;
return isDesktopVersion && isRunningOnSelectedOs; return isDesktopVersion && isRunningOnSelectedOs;

View File

@@ -21,11 +21,10 @@ import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue'
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition'; import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { OperatingSystem } from '@/domain/OperatingSystem';
import IconButton from '../IconButton.vue'; import IconButton from '../IconButton.vue';
import InstructionList from './Instructions/InstructionList.vue'; import InstructionList from './Instructions/InstructionList.vue';
import { IInstructionListData } from './Instructions/InstructionListData'; import { IInstructionListData } from './Instructions/InstructionListData';
import { getInstructions, hasInstructions } from './Instructions/InstructionListDataFactory'; import { getInstructions } from './Instructions/InstructionListDataFactory';
export default defineComponent({ export default defineComponent({
components: { components: {
@@ -39,7 +38,7 @@ export default defineComponent({
const areInstructionsVisible = ref(false); const areInstructionsVisible = ref(false);
const fileName = computed<string>(() => buildFileName(currentState.value.collection.scripting)); const fileName = computed<string>(() => buildFileName(currentState.value.collection.scripting));
const instructions = computed<IInstructionListData | undefined>(() => getDownloadInstructions( const instructions = computed<IInstructionListData | undefined>(() => getInstructions(
currentState.value.collection.os, currentState.value.collection.os,
fileName.value, fileName.value,
)); ));
@@ -59,16 +58,6 @@ export default defineComponent({
}, },
}); });
function getDownloadInstructions(
os: OperatingSystem,
fileName: string,
): IInstructionListData | undefined {
if (!hasInstructions(os)) {
return undefined;
}
return getInstructions(os, fileName);
}
function saveCodeToDisk(fileName: string, state: IReadOnlyCategoryCollectionState) { function saveCodeToDisk(fileName: string, state: IReadOnlyCategoryCollectionState) {
const content = state.code.current; const content = state.code.current;
const type = getType(state.collection.scripting.language); const type = getType(state.collection.scripting.language);

View File

@@ -15,13 +15,11 @@ export class InstructionsBuilder {
} }
public withStep(stepBuilder: InstructionStepBuilderType) { public withStep(stepBuilder: InstructionStepBuilderType) {
if (!stepBuilder) { throw new Error('missing stepBuilder'); }
this.stepBuilders.push(stepBuilder); this.stepBuilders.push(stepBuilder);
return this; return this;
} }
public build(data: IInstructionsBuilderData): IInstructionListData { public build(data: IInstructionsBuilderData): IInstructionListData {
if (!data) { throw new Error('missing data'); }
return { return {
operatingSystem: this.os, operatingSystem: this.os,
steps: this.stepBuilders.map((stepBuilder) => stepBuilder(data)), steps: this.stepBuilders.map((stepBuilder) => stepBuilder(data)),

View File

@@ -86,12 +86,9 @@ export default defineComponent({
() => info.getDownloadUrl(OperatingSystem.macOS), () => info.getDownloadUrl(OperatingSystem.macOS),
); );
const osName = computed<string>(() => { const osName = computed<string>(
if (!props.data) { () => renderOsName(props.data.operatingSystem),
throw new Error('missing data'); );
}
return renderOsName(props.data.operatingSystem);
});
return { return {
appName, appName,

View File

@@ -9,15 +9,11 @@ const builders = new Map<OperatingSystem, InstructionsBuilder>([
[OperatingSystem.Linux, new LinuxInstructionsBuilder()], [OperatingSystem.Linux, new LinuxInstructionsBuilder()],
]); ]);
export function hasInstructions(os: OperatingSystem) {
return builders.has(os);
}
export function getInstructions( export function getInstructions(
os: OperatingSystem, os: OperatingSystem,
fileName: string, fileName: string,
): IInstructionListData { ): IInstructionListData | undefined {
return builders return builders
.get(os) .get(os)
.build({ fileName }); ?.build({ fileName });
} }

View File

@@ -70,7 +70,7 @@ export default defineComponent({
function updateCode(code: string, language: ScriptingLanguage) { function updateCode(code: string, language: ScriptingLanguage) {
const innerCode = code || getDefaultCode(language); const innerCode = code || getDefaultCode(language);
editor.setValue(innerCode, 1); editor?.setValue(innerCode, 1);
} }
function handleCodeChange(event: ICodeChangedEvent) { function handleCodeChange(event: ICodeChangedEvent) {
@@ -96,7 +96,7 @@ export default defineComponent({
if (!currentMarkerId) { if (!currentMarkerId) {
return; return;
} }
editor.session.removeMarker(currentMarkerId); editor?.session.removeMarker(currentMarkerId);
currentMarkerId = undefined; currentMarkerId = undefined;
} }
@@ -115,7 +115,7 @@ export default defineComponent({
function highlight(startRow: number, endRow: number) { function highlight(startRow: number, endRow: number) {
const AceRange = ace.require('ace/range').Range; const AceRange = ace.require('ace/range').Range;
currentMarkerId = editor.session.addMarker( currentMarkerId = editor?.session.addMarker(
new AceRange(startRow, 0, endRow, 0), new AceRange(startRow, 0, endRow, 0),
'code-area__highlight', 'code-area__highlight',
'fullLine', 'fullLine',
@@ -123,8 +123,11 @@ export default defineComponent({
} }
function scrollToLine(row: number) { function scrollToLine(row: number) {
const column = editor.session.getLine(row).length; const column = editor?.session.getLine(row).length;
editor.gotoLine(row, column, true); if (column === undefined) {
return;
}
editor?.gotoLine(row, column, true);
} }
return { return {

View File

@@ -15,9 +15,12 @@ export enum SelectionType {
export function setCurrentSelectionType(type: SelectionType, context: SelectionMutationContext) { export function setCurrentSelectionType(type: SelectionType, context: SelectionMutationContext) {
if (type === SelectionType.Custom) { if (type === SelectionType.Custom) {
throw new Error('cannot select custom type'); throw new Error('Cannot select custom type.');
} }
const selector = selectors.get(type); const selector = selectors.get(type);
if (!selector) {
throw new Error(`Cannot handle the type: ${SelectionType[type]}`);
}
selector.select(context); selector.select(context);
} }

View File

@@ -31,14 +31,14 @@ export default defineComponent({
const { modifyCurrentContext, currentState } = injectKey((keys) => keys.useCollectionState); const { modifyCurrentContext, currentState } = injectKey((keys) => keys.useCollectionState);
const { application } = injectKey((keys) => keys.useApplication); const { application } = injectKey((keys) => keys.useApplication);
const allOses = computed<ReadonlyArray<IOsViewModel>>(() => ( const allOses = computed<ReadonlyArray<IOsViewModel>>(
application.getSupportedOsList() ?? []) () => application
.map((os) : IOsViewModel => ( .getSupportedOsList()
{ .map((os) : IOsViewModel => ({
os, os,
name: renderOsName(os), name: renderOsName(os),
} })),
))); );
const currentOs = computed<OperatingSystem>(() => { const currentOs = computed<OperatingSystem>(() => {
return currentState.value.os; return currentState.value.os;

View File

@@ -48,8 +48,12 @@ export default defineComponent({
const firstElement = shallowRef<HTMLElement>(); const firstElement = shallowRef<HTMLElement>();
function onResize(displacementX: number): void { function onResize(displacementX: number): void {
const leftWidth = firstElement.value.offsetWidth + displacementX; const element = firstElement.value;
firstElement.value.style.width = `${leftWidth}px`; if (!element) {
throw new Error('The element reference ref is not correctly assigned to a DOM element.');
}
const leftWidth = element.offsetWidth + displacementX;
element.style.width = `${leftWidth}px`;
} }
return { return {

View File

@@ -29,7 +29,10 @@ export default defineComponent({
const cursorCssValue = 'ew-resize'; const cursorCssValue = 'ew-resize';
let initialX: number | undefined; let initialX: number | undefined;
const resize = (event) => { const resize = (event: MouseEvent) => {
if (initialX === undefined) {
throw new Error('Resize action started without an initial X coordinate.');
}
const displacementX = event.clientX - initialX; const displacementX = event.clientX - initialX;
emit('resized', displacementX); emit('resized', displacementX);
initialX = event.clientX; initialX = event.clientX;

View File

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

View File

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

View File

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

View File

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

View File

@@ -42,7 +42,7 @@ function renderText(docs: readonly string[] | undefined): string {
} }
function renderAsMarkdownListItem(content: string): string { function renderAsMarkdownListItem(content: string): string {
if (!content || content.length === 0) { if (content.length === 0) {
throw new Error('missing content'); throw new Error('missing content');
} }
const lines = content.split(/\r\n|\r|\n/); const lines = content.split(/\r\n|\r|\n/);

View File

@@ -85,7 +85,8 @@ function removeTrailingExtension(value: string): string {
if (parts.length === 1) { if (parts.length === 1) {
return value; return value;
} }
if (parts.at(-1).length > 9) { const extension = parts[parts.length - 1];
if (extension.length > 9) {
return value; // Heuristically web file extension is no longer than 9 chars (e.g. "html") return value; // Heuristically web file extension is no longer than 9 chars (e.g. "html")
} }
return parts.slice(0, -1).join('.'); return parts.slice(0, -1).join('.');
@@ -115,9 +116,8 @@ function selectMostDescriptiveName(parts: readonly string[]): string | undefined
} }
function isGoodPathPart(part: string): boolean { function isGoodPathPart(part: string): boolean {
return part return part.length > 2 // This is often non-human categories like T5 etc.
&& !isDigit(part) // E.g. article numbers, issue numbers && !isDigit(part) // E.g. article numbers, issue numbers
&& part.length > 2 // This is often non-human categories like T5 etc.
&& !/^index(?:\.\S{0,10}$|$)/.test(part) // E.g. index.html && !/^index(?:\.\S{0,10}$|$)/.test(part) // E.g. index.html
&& !/^[A-Za-z]{2,4}([_-][A-Za-z]{4})?([_-]([A-Za-z]{2}|[0-9]{3}))$/.test(part) // Locale string e.g. fr-FR && !/^[A-Za-z]{2,4}([_-][A-Za-z]{4})?([_-]([A-Za-z]{2}|[0-9]{3}))$/.test(part) // Locale string e.g. fr-FR
&& !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(part) // GUID && !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(part) // GUID

View File

@@ -8,6 +8,6 @@ export interface NodeMetadata {
readonly text: string; readonly text: string;
readonly isReversible: boolean; readonly isReversible: boolean;
readonly docs: ReadonlyArray<string>; readonly docs: ReadonlyArray<string>;
readonly children?: ReadonlyArray<NodeMetadata>; readonly children: ReadonlyArray<NodeMetadata>;
readonly type: NodeType; readonly type: NodeType;
} }

View File

@@ -25,10 +25,7 @@ export class CategoryReverter implements IReverter {
} }
function getAllSubScriptReverters(categoryId: number, collection: ICategoryCollection) { function getAllSubScriptReverters(categoryId: number, collection: ICategoryCollection) {
const category = collection.findCategory(categoryId); const category = collection.getCategory(categoryId);
if (!category) {
throw new Error(`Category with id "${categoryId}" does not exist`);
}
const scripts = category.getAllScriptsRecursively(); const scripts = category.getAllScriptsRecursively();
return scripts.map((script) => new ScriptReverter(script.id)); return scripts.map((script) => new ScriptReverter(script.id));
} }

View File

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

Some files were not shown because too many files have changed in this diff Show More