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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,9 +21,6 @@ export class ApplicationCode implements IApplicationCode {
private readonly scriptingDefinition: IScriptingDefinition,
private readonly generator: IUserScriptGenerator = new UserScriptGenerator(),
) {
if (!userSelection) { throw new Error('missing userSelection'); }
if (!scriptingDefinition) { throw new Error('missing scriptingDefinition'); }
if (!generator) { throw new Error('missing generator'); }
this.setCode(userSelection.selectedScripts);
userSelection.changed.on((scripts) => {
this.setCode(scripts);

View File

@@ -36,7 +36,11 @@ export class CodeChangedEvent implements ICodeChangedEvent {
}
public getScriptPositionInCode(script: IScript): ICodePosition {
return this.scripts.get(script);
const position = this.scripts.get(script);
if (!position) {
throw new Error('Unknown script: Position could not be found for the script');
}
return position;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,8 +9,6 @@ export class FilterResult implements IFilterResult {
public readonly query: string,
) {
if (!query) { throw new Error('Query is empty or undefined'); }
if (!scriptMatches) { throw new Error('Script matches is undefined'); }
if (!categoryMatches) { throw new Error('Category matches is undefined'); }
}
public hasAnyMatches(): boolean {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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(
code: string | undefined,
code: string,
args: IReadOnlyFunctionCallArgumentCollection,
): string {
if (!args) {
throw new Error('missing args, send empty collection instead.');
}
if (!code) {
return code;
return '';
}
const context = new ExpressionEvaluationContext(args);
const compiledCode = compileRecursively(code, context, this.extractor);
@@ -145,7 +142,7 @@ function ensureParamsUsedInCodeHasArgsProvided(
providedArgs: IReadOnlyFunctionCallArgumentCollection,
): void {
const usedParameterNames = extractRequiredParameterNames(expressions);
if (!usedParameterNames?.length) {
if (!usedParameterNames.length) {
return;
}
const notProvidedParameters = usedParameterNames

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
import { IExpressionParser } from '../IExpressionParser';
import { ExpressionPosition } from '../../Expression/ExpressionPosition';
import { IExpression } from '../../Expression/IExpression';
import { Expression, ExpressionEvaluator } from '../../Expression/Expression';
import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter';
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
import { createPositionFromRegexFullMatch } from '../../Expression/ExpressionPositionFactory';
export abstract class RegexParser implements IExpressionParser {
protected abstract readonly regex: RegExp;
@@ -21,7 +21,7 @@ export abstract class RegexParser implements IExpressionParser {
const matches = code.matchAll(this.regex);
for (const match of matches) {
const primitiveExpression = this.buildExpression(match);
const position = this.doOrRethrow(() => createPosition(match), 'invalid script position', code);
const position = this.doOrRethrow(() => createPositionFromRegexFullMatch(match), 'invalid script position', code);
const parameters = createParameters(primitiveExpression);
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
yield expression;
@@ -37,12 +37,6 @@ export abstract class RegexParser implements IExpressionParser {
}
}
function createPosition(match: RegExpMatchArray): ExpressionPosition {
const startPos = match.index;
const endPos = startPos + match[0].length;
return new ExpressionPosition(startPos, endPos);
}
function createParameters(
expression: IPrimitiveExpression,
): FunctionParameterCollection {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
@@ -12,19 +12,33 @@ export class InlineFunctionCallCompiler implements SingleCallCompilerStrategy {
}
public canCompile(func: ISharedFunction): boolean {
return func.body.code !== undefined;
return func.body.type === FunctionBodyType.Code;
}
public compileFunction(
calledFunction: ISharedFunction,
callToFunction: FunctionCall,
): CompiledCode[] {
if (calledFunction.body.type !== FunctionBodyType.Code) {
throw new Error([
'Unexpected function body type.',
`\tExpected: "${FunctionBodyType[FunctionBodyType.Code]}"`,
`\tActual: "${FunctionBodyType[calledFunction.body.type]}"`,
'Function:',
`\t${JSON.stringify(callToFunction)}`,
].join('\n'));
}
const { code } = calledFunction.body;
const { args } = callToFunction;
return [
{
code: this.expressionsCompiler.compileExpressions(code.execute, args),
revertCode: this.expressionsCompiler.compileExpressions(code.revert, args),
revertCode: (() => {
if (!code.revert) {
return undefined;
}
return this.expressionsCompiler.compileExpressions(code.revert, args);
})(),
},
];
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,22 +7,18 @@
<TheCodeButtons class="app__row app__code-buttons" />
<TheFooter />
</div>
<OptionalDevToolkit />
<component v-if="devToolkitComponent" :is="devToolkitComponent" />
</div>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
import { defineAsyncComponent, defineComponent, Component } from 'vue';
import TheHeader from '@/presentation/components/TheHeader.vue';
import TheFooter from '@/presentation/components/TheFooter/TheFooter.vue';
import TheCodeButtons from '@/presentation/components/Code/CodeButtons/TheCodeButtons.vue';
import TheScriptArea from '@/presentation/components/Scripts/TheScriptArea.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({
components: {
TheHeader,
@@ -30,10 +26,23 @@ export default defineComponent({
TheScriptArea,
TheSearchBar,
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>
<style lang="scss">

View File

@@ -40,7 +40,7 @@ export default defineComponent({
function getCanRunState(
selectedOs: OperatingSystem,
isDesktopVersion: boolean,
hostOs: OperatingSystem,
hostOs: OperatingSystem | undefined,
): boolean {
const isRunningOnSelectedOs = selectedOs === hostOs;
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 { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { OperatingSystem } from '@/domain/OperatingSystem';
import IconButton from '../IconButton.vue';
import InstructionList from './Instructions/InstructionList.vue';
import { IInstructionListData } from './Instructions/InstructionListData';
import { getInstructions, hasInstructions } from './Instructions/InstructionListDataFactory';
import { getInstructions } from './Instructions/InstructionListDataFactory';
export default defineComponent({
components: {
@@ -39,7 +38,7 @@ export default defineComponent({
const areInstructionsVisible = ref(false);
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,
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) {
const content = state.code.current;
const type = getType(state.collection.scripting.language);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -48,8 +48,12 @@ export default defineComponent({
const firstElement = shallowRef<HTMLElement>();
function onResize(displacementX: number): void {
const leftWidth = firstElement.value.offsetWidth + displacementX;
firstElement.value.style.width = `${leftWidth}px`;
const element = firstElement.value;
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 {

View File

@@ -29,7 +29,10 @@ export default defineComponent({
const cursorCssValue = 'ew-resize';
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;
emit('resized', displacementX);
initialX = event.clientX;

View File

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

View File

@@ -89,12 +89,9 @@ export default defineComponent({
const cardElement = shallowRef<HTMLElement>();
const cardTitle = computed<string | undefined>(() => {
if (!props.categoryId || !currentState.value) {
return undefined;
}
const category = currentState.value.collection.findCategory(props.categoryId);
return category?.name;
const cardTitle = computed<string>(() => {
const category = currentState.value.collection.getCategory(props.categoryId);
return category.name;
});
function collapse() {
@@ -102,8 +99,12 @@ export default defineComponent({
}
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
cardElement.value.scrollIntoView({ behavior: 'smooth' });
card.scrollIntoView({ behavior: 'smooth' });
}
return {

View File

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

View File

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

View File

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

View File

@@ -85,7 +85,8 @@ function removeTrailingExtension(value: string): string {
if (parts.length === 1) {
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 parts.slice(0, -1).join('.');
@@ -115,9 +116,8 @@ function selectMostDescriptiveName(parts: readonly string[]): string | undefined
}
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
&& part.length > 2 // This is often non-human categories like T5 etc.
&& !/^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
&& !/^[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 isReversible: boolean;
readonly docs: ReadonlyArray<string>;
readonly children?: ReadonlyArray<NodeMetadata>;
readonly children: ReadonlyArray<NodeMetadata>;
readonly type: NodeType;
}

View File

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

View File

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

View File

@@ -1,9 +1,15 @@
import type { ReadOnlyTreeNode } from '../Node/TreeNode';
export interface TreeViewFilterEvent {
readonly action: TreeViewFilterAction;
readonly predicate?: TreeViewFilterPredicate;
}
type TreeViewFilterTriggeredEvent = {
readonly action: TreeViewFilterAction.Triggered;
readonly predicate: TreeViewFilterPredicate;
};
type TreeViewFilterRemovedEvent = {
readonly action: TreeViewFilterAction.Removed;
};
export type TreeViewFilterEvent = TreeViewFilterTriggeredEvent | TreeViewFilterRemovedEvent;
export enum TreeViewFilterAction {
Triggered,
@@ -14,14 +20,14 @@ export type TreeViewFilterPredicate = (node: ReadOnlyTreeNode) => boolean;
export function createFilterTriggeredEvent(
predicate: TreeViewFilterPredicate,
): TreeViewFilterEvent {
): TreeViewFilterTriggeredEvent {
return {
action: TreeViewFilterAction.Triggered,
predicate,
};
}
export function createFilterRemovedEvent(): TreeViewFilterEvent {
export function createFilterRemovedEvent(): TreeViewFilterRemovedEvent {
return {
action: TreeViewFilterAction.Removed,
};

View File

@@ -25,7 +25,7 @@ export class TreeNodeHierarchy implements HierarchyAccess {
this.children = children;
}
public setParent(parent: TreeNode): void {
public setParent(parent: TreeNode | undefined): void {
this.parent = parent;
}
}

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