From 31f70913a2f30baf5a9d6690f192e6a63da50114 Mon Sep 17 00:00:00 2001 From: undergroundwires Date: Tue, 4 Jan 2022 21:45:22 +0100 Subject: [PATCH] Refactor to improve iterations - Use function abstractions (such as map, reduce, filter etc.) over for-of loops to gain benefits of having less side effects and easier readability. - Enable `downLevelIterations` for writing modern code with lazy evaluation. - Refactor for of loops to named abstractions to clearly express their intentions without needing to analyse the loop itself. - Add missing cases for changes that had no tests. --- .../State/Code/Event/CodeChangedEvent.ts | 13 +-- .../State/Code/Generation/CodeBuilder.ts | 4 +- .../Code/Generation/UserScriptGenerator.ts | 9 +- .../Context/State/Selection/UserSelection.ts | 35 +++--- .../Parser/CategoryCollectionParser.ts | 7 +- src/application/Parser/CategoryParser.ts | 10 +- .../Expressions/ExpressionsCompiler.ts | 9 +- .../Parser/CompositeExpressionParser.ts | 11 +- .../Expressions/Parser/Regex/RegexParser.ts | 47 ++++---- .../Expressions/Pipes/PipelineCompiler.ts | 8 +- .../Call/Compiler/FunctionCallCompiler.ts | 26 ++--- .../Function/Call/FunctionCallParser.ts | 21 ++-- .../Function/SharedFunctionsParser.ts | 38 ++++--- src/domain/CategoryCollection.ts | 103 ++++++++---------- src/infrastructure/Events/EventSource.ts | 2 +- .../Menu/Selector/SelectionTypeHandler.ts | 2 +- .../View/ScriptsTree/ScriptNodeParser.ts | 39 ++----- .../ScriptingLanguageFactoryTestRunner.ts | 2 +- .../State/Code/ApplicationCode.spec.ts | 2 +- .../State/Code/Event/CodeChangedEvent.spec.ts | 19 +++- .../Expressions/ExpressionsCompiler.spec.ts | 8 ++ .../Parser/Regex/RegexParser.spec.ts | 25 +++++ .../Compiler/FunctionCallCompiler.spec.ts | 10 +- .../collections/NoUnintentedInlining.spec.ts | 14 +-- tests/unit/domain/CategoryCollection.spec.ts | 57 +++++++--- tests/unit/domain/ScriptCode.spec.ts | 76 ++++++------- tests/unit/domain/ScriptingDefinition.spec.ts | 4 +- .../Threading/AsyncSleep.spec.ts | 12 +- .../View/Tree/ScriptNodeParser.spec.ts | 1 - tests/unit/stubs/CategoryCollectionStub.ts | 45 +++----- tests/unit/stubs/CategoryStub.ts | 7 +- tests/unit/stubs/ExpressionsCompilerStub.ts | 9 +- .../FunctionCallArgumentCollectionStub.ts | 5 +- tests/unit/stubs/ScriptStub.ts | 4 +- tsconfig.json | 1 + 35 files changed, 342 insertions(+), 343 deletions(-) diff --git a/src/application/Context/State/Code/Event/CodeChangedEvent.ts b/src/application/Context/State/Code/Event/CodeChangedEvent.ts index d91238fc..52cbb532 100644 --- a/src/application/Context/State/Code/Event/CodeChangedEvent.ts +++ b/src/application/Context/State/Code/Event/CodeChangedEvent.ts @@ -42,13 +42,12 @@ export class CodeChangedEvent implements ICodeChangedEvent { function ensureAllPositionsExist(script: string, positions: ReadonlyArray) { const totalLines = script.split(/\r\n|\r|\n/).length; - for (const position of positions) { - if (position.endLine > totalLines) { - throw new Error( - `script end line (${position.endLine}) is out of range.` - + `(total code lines: ${totalLines}`, - ); - } + const missingPositions = positions.filter((position) => position.endLine > totalLines); + if (missingPositions.length > 0) { + throw new Error( + `Out of range script end line: "${missingPositions.map((pos) => pos.endLine).join('", "')}"` + + `(total code lines: ${totalLines}).`, + ); } } diff --git a/src/application/Context/State/Code/Generation/CodeBuilder.ts b/src/application/Context/State/Code/Generation/CodeBuilder.ts index bf352d3f..dae192dd 100644 --- a/src/application/Context/State/Code/Generation/CodeBuilder.ts +++ b/src/application/Context/State/Code/Generation/CodeBuilder.ts @@ -17,9 +17,7 @@ export abstract class CodeBuilder implements ICodeBuilder { return this; } const lines = code.match(/[^\r\n]+/g); - for (const line of lines) { - this.lines.push(line); - } + this.lines.push(...lines); return this; } diff --git a/src/application/Context/State/Code/Generation/UserScriptGenerator.ts b/src/application/Context/State/Code/Generation/UserScriptGenerator.ts index e3a4945b..5a37ab11 100644 --- a/src/application/Context/State/Code/Generation/UserScriptGenerator.ts +++ b/src/application/Context/State/Code/Generation/UserScriptGenerator.ts @@ -19,15 +19,14 @@ export class UserScriptGenerator implements IUserScriptGenerator { ): IUserScript { if (!selectedScripts) { throw new Error('undefined scripts'); } if (!scriptingDefinition) { throw new Error('undefined definition'); } - let scriptPositions = new Map(); if (!selectedScripts.length) { - return { code: '', scriptPositions }; + return { code: '', scriptPositions: new Map() }; } let builder = this.codeBuilderFactory.create(scriptingDefinition.language); builder = initializeCode(scriptingDefinition.startCode, builder); - for (const selection of selectedScripts) { - scriptPositions = appendSelection(selection, scriptPositions, builder); - } + const scriptPositions = selectedScripts.reduce((result, selection) => { + return appendSelection(selection, result, builder); + }, new Map()); const code = finalizeCode(builder, scriptingDefinition.endCode); return { code, scriptPositions }; } diff --git a/src/application/Context/State/Selection/UserSelection.ts b/src/application/Context/State/Selection/UserSelection.ts index d87f152a..0ff7b1a3 100644 --- a/src/application/Context/State/Selection/UserSelection.ts +++ b/src/application/Context/State/Selection/UserSelection.ts @@ -17,10 +17,8 @@ export class UserSelection implements IUserSelection { selectedScripts: ReadonlyArray, ) { this.scripts = new InMemoryRepository(); - if (selectedScripts && selectedScripts.length > 0) { - for (const script of selectedScripts) { - this.scripts.addItem(script); - } + for (const script of selectedScripts) { + this.scripts.addItem(script); } } @@ -58,18 +56,19 @@ export class UserSelection implements IUserSelection { } public addOrUpdateAllInCategory(categoryId: number, revert = false): void { - const category = this.collection.findCategory(categoryId); - const scriptsToAddOrUpdate = category.getAllScriptsRecursively() + const scriptsToAddOrUpdate = this.collection + .findCategory(categoryId) + .getAllScriptsRecursively() .filter( (script) => !this.scripts.exists(script.id) || this.scripts.getById(script.id).revert !== revert, - ); + ) + .map((script) => new SelectedScript(script, revert)); if (!scriptsToAddOrUpdate.length) { return; } for (const script of scriptsToAddOrUpdate) { - const selectedScript = new SelectedScript(script, revert); - this.scripts.addOrUpdateItem(selectedScript); + this.scripts.addOrUpdateItem(script); } this.changed.notify(this.scripts.getItems()); } @@ -106,11 +105,12 @@ export class UserSelection implements IUserSelection { } public selectAll(): void { - for (const script of this.collection.getAllScripts()) { - if (!this.scripts.exists(script.id)) { - const selection = new SelectedScript(script, false); - this.scripts.addItem(selection); - } + const scriptsToSelect = this.collection + .getAllScripts() + .filter((script) => !this.scripts.exists(script.id)) + .map((script) => new SelectedScript(script, false)); + for (const script of scriptsToSelect) { + this.scripts.addItem(script); } this.changed.notify(this.scripts.getItems()); } @@ -135,10 +135,11 @@ export class UserSelection implements IUserSelection { .forEach((scriptId) => this.scripts.removeItem(scriptId)); } // Select from unselected scripts - const unselectedScripts = scripts.filter((script) => !this.scripts.exists(script.id)); + const unselectedScripts = scripts + .filter((script) => !this.scripts.exists(script.id)) + .map((script) => new SelectedScript(script, false)); for (const toSelect of unselectedScripts) { - const selection = new SelectedScript(toSelect, false); - this.scripts.addItem(selection); + this.scripts.addItem(toSelect); } this.changed.notify(this.scripts.getItems()); } diff --git a/src/application/Parser/CategoryCollectionParser.ts b/src/application/Parser/CategoryCollectionParser.ts index 7c0e6ceb..e9c27ce1 100644 --- a/src/application/Parser/CategoryCollectionParser.ts +++ b/src/application/Parser/CategoryCollectionParser.ts @@ -1,5 +1,4 @@ import { CollectionData } from 'js-yaml-loader!@/*'; -import { Category } from '@/domain/Category'; import { OperatingSystem } from '@/domain/OperatingSystem'; import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { CategoryCollection } from '@/domain/CategoryCollection'; @@ -18,11 +17,7 @@ export function parseCategoryCollection( const scripting = new ScriptingDefinitionParser() .parse(content.scripting, info); const context = new CategoryCollectionParseContext(content.functions, scripting); - const categories = new Array(); - for (const action of content.actions) { - const category = parseCategory(action, context); - categories.push(category); - } + const categories = content.actions.map((action) => parseCategory(action, context)); const os = osParser.parseEnum(content.os, 'os'); const collection = new CategoryCollection( os, diff --git a/src/application/Parser/CategoryParser.ts b/src/application/Parser/CategoryParser.ts index 30939f9e..eeae57b0 100644 --- a/src/application/Parser/CategoryParser.ts +++ b/src/application/Parser/CategoryParser.ts @@ -9,11 +9,6 @@ import { parseScript } from './Script/ScriptParser'; let categoryIdCounter = 0; -interface ICategoryChildren { - subCategories: Category[]; - subScripts: Script[]; -} - export function parseCategory( category: CategoryData, context: ICategoryCollectionParseContext, @@ -48,6 +43,11 @@ function ensureValid(category: CategoryData) { } } +interface ICategoryChildren { + subCategories: Category[]; + subScripts: Script[]; +} + function parseCategoryChild( data: CategoryOrScriptData, children: ICategoryChildren, diff --git a/src/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler.ts b/src/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler.ts index 05f495cd..a6a726e6 100644 --- a/src/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler.ts +++ b/src/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler.ts @@ -56,14 +56,13 @@ function compileExpressions( function extractRequiredParameterNames( expressions: readonly IExpression[], ): string[] { - const usedParameterNames = expressions + return expressions .map((e) => e.parameters.all .filter((p) => !p.isOptional) .map((p) => p.name)) - .filter((p) => p) - .flat(); - const uniqueParameterNames = Array.from(new Set(usedParameterNames)); - return uniqueParameterNames; + .filter(Boolean) // Remove empty or undefined + .flat() + .filter((name, index, array) => array.indexOf(name) === index); // Remove duplicates } function ensureParamsUsedInCodeHasArgsProvided( diff --git a/src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts b/src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts index 8d1de55f..6e43abe5 100644 --- a/src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts +++ b/src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts @@ -16,13 +16,8 @@ export class CompositeExpressionParser implements IExpressionParser { } public findExpressions(code: string): IExpression[] { - const expressions = new Array(); - for (const parser of this.leafs) { - const newExpressions = parser.findExpressions(code); - if (newExpressions && newExpressions.length) { - expressions.push(...newExpressions); - } - } - return expressions; + return this.leafs.flatMap( + (parser) => parser.findExpressions(code) || [], + ); } } diff --git a/src/application/Parser/Script/Compiler/Expressions/Parser/Regex/RegexParser.ts b/src/application/Parser/Script/Compiler/Expressions/Parser/Regex/RegexParser.ts index f9de24e2..89ec583a 100644 --- a/src/application/Parser/Script/Compiler/Expressions/Parser/Regex/RegexParser.ts +++ b/src/application/Parser/Script/Compiler/Expressions/Parser/Regex/RegexParser.ts @@ -18,35 +18,42 @@ export abstract class RegexParser implements IExpressionParser { if (!code) { throw new Error('undefined code'); } - const matches = Array.from(code.matchAll(this.regex)); + const matches = code.matchAll(this.regex); for (const match of matches) { - const startPos = match.index; - const endPos = startPos + match[0].length; - let position: ExpressionPosition; - try { - position = new ExpressionPosition(startPos, endPos); - } catch (error) { - throw new Error(`[${this.constructor.name}] invalid script position: ${error.message}\nRegex ${this.regex}\nCode: ${code}`); - } const primitiveExpression = this.buildExpression(match); - const parameters = getParameters(primitiveExpression); + const position = this.doOrRethrow(() => createPosition(match), 'invalid script position', code); + const parameters = createParameters(primitiveExpression); const expression = new Expression(position, primitiveExpression.evaluator, parameters); yield expression; } } + + private doOrRethrow(action: () => T, errorText: string, code: string): T { + try { + return action(); + } catch (error) { + throw new Error(`[${this.constructor.name}] ${errorText}: ${error.message}\nRegex: ${this.regex}\nCode: ${code}`); + } + } +} + +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 { + return (expression.parameters || []) + .reduce((parameters, parameter) => { + parameters.addParameter(parameter); + return parameters; + }, new FunctionParameterCollection()); } export interface IPrimitiveExpression { evaluator: ExpressionEvaluator; parameters?: readonly IFunctionParameter[]; } - -function getParameters( - expression: IPrimitiveExpression, -): FunctionParameterCollection { - const parameters = new FunctionParameterCollection(); - for (const parameter of expression.parameters || []) { - parameters.addParameter(parameter); - } - return parameters; -} diff --git a/src/application/Parser/Script/Compiler/Expressions/Pipes/PipelineCompiler.ts b/src/application/Parser/Script/Compiler/Expressions/Pipes/PipelineCompiler.ts index f1d8f46d..344ed1fe 100644 --- a/src/application/Parser/Script/Compiler/Expressions/Pipes/PipelineCompiler.ts +++ b/src/application/Parser/Script/Compiler/Expressions/Pipes/PipelineCompiler.ts @@ -8,11 +8,9 @@ export class PipelineCompiler implements IPipelineCompiler { ensureValidArguments(value, pipeline); const pipeNames = extractPipeNames(pipeline); const pipes = pipeNames.map((pipeName) => this.factory.get(pipeName)); - let valueInCompilation = value; - for (const pipe of pipes) { - valueInCompilation = pipe.apply(valueInCompilation); - } - return valueInCompilation; + return pipes.reduce((previousValue, pipe) => { + return pipe.apply(previousValue); + }, value); } } diff --git a/src/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler.ts b/src/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler.ts index eb294343..db8e5ac5 100644 --- a/src/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler.ts +++ b/src/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler.ts @@ -47,11 +47,8 @@ interface ICompiledFunctionCall { } function compileCallSequence(context: ICompilationContext): ICompiledFunctionCall { - const compiledFunctions = new Array(); - for (const call of context.callSequence) { - const compiledCode = compileSingleCall(call, context); - compiledFunctions.push(...compiledCode); - } + const compiledFunctions = context.callSequence + .flatMap((call) => compileSingleCall(call, context)); return { code: merge(compiledFunctions.map((f) => f.code)), revertCode: merge(compiledFunctions.map((f) => f.revertCode)), @@ -94,14 +91,17 @@ function compileArgs( args: IReadOnlyFunctionCallArgumentCollection, compiler: IExpressionsCompiler, ): IReadOnlyFunctionCallArgumentCollection { - const compiledArgs = new FunctionCallArgumentCollection(); - for (const parameterName of argsToCompile.getAllParameterNames()) { - const { argumentValue } = argsToCompile.getArgument(parameterName); - const compiledValue = compiler.compileExpressions(argumentValue, args); - const newArgument = new FunctionCallArgument(parameterName, compiledValue); - compiledArgs.addArgument(newArgument); - } - return compiledArgs; + return argsToCompile + .getAllParameterNames() + .map((parameterName) => { + const { argumentValue } = argsToCompile.getArgument(parameterName); + const compiledValue = compiler.compileExpressions(argumentValue, args); + return new FunctionCallArgument(parameterName, compiledValue); + }) + .reduce((compiledArgs, arg) => { + compiledArgs.addArgument(arg); + return compiledArgs; + }, new FunctionCallArgumentCollection()); } function merge(codeParts: readonly string[]): string { diff --git a/src/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.ts b/src/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.ts index 5540b588..ccb6f776 100644 --- a/src/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.ts +++ b/src/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.ts @@ -1,4 +1,4 @@ -import { FunctionCallData, FunctionCallsData } from 'js-yaml-loader!@/*'; +import { FunctionCallData, FunctionCallsData, FunctionCallParametersData } from 'js-yaml-loader!@/*'; import { IFunctionCall } from './IFunctionCall'; import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection'; import { FunctionCallArgument } from './Argument/FunctionCallArgument'; @@ -26,10 +26,17 @@ function parseFunctionCall(call: FunctionCallData): IFunctionCall { if (!call) { throw new Error('undefined function call'); } - const args = new FunctionCallArgumentCollection(); - for (const parameterName of Object.keys(call.parameters || {})) { - const arg = new FunctionCallArgument(parameterName, call.parameters[parameterName]); - args.addArgument(arg); - } - return new FunctionCall(call.function, args); + const callArgs = parseArgs(call.parameters); + return new FunctionCall(call.function, callArgs); +} + +function parseArgs( + parameters: FunctionCallParametersData, +): FunctionCallArgumentCollection { + return Object.keys(parameters || {}) + .map((parameterName) => new FunctionCallArgument(parameterName, parameters[parameterName])) + .reduce((args, arg) => { + args.addArgument(arg); + return args; + }, new FunctionCallArgumentCollection()); } diff --git a/src/application/Parser/Script/Compiler/Function/SharedFunctionsParser.ts b/src/application/Parser/Script/Compiler/Function/SharedFunctionsParser.ts index aba8b46f..b20b4968 100644 --- a/src/application/Parser/Script/Compiler/Function/SharedFunctionsParser.ts +++ b/src/application/Parser/Script/Compiler/Function/SharedFunctionsParser.ts @@ -20,11 +20,12 @@ export class SharedFunctionsParser implements ISharedFunctionsParser { return collection; } ensureValidFunctions(functions); - for (const func of functions) { - const sharedFunction = parseFunction(func); - collection.addFunction(sharedFunction); - } - return collection; + return functions + .map((func) => parseFunction(func)) + .reduce((acc, func) => { + acc.addFunction(func); + return acc; + }, collection); } } @@ -40,20 +41,21 @@ function parseFunction(data: FunctionData): ISharedFunction { } function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection { - const parameters = new FunctionParameterCollection(); - if (!data.parameters) { - return parameters; - } - for (const parameterData of data.parameters) { - const isOptional = parameterData.optional || false; - try { - const parameter = new FunctionParameter(parameterData.name, isOptional); + return (data.parameters || []) + .map((parameter) => { + try { + return new FunctionParameter( + parameter.name, + parameter.optional || false, + ); + } catch (err) { + throw new Error(`"${data.name}": ${err.message}`); + } + }) + .reduce((parameters, parameter) => { parameters.addParameter(parameter); - } catch (err) { - throw new Error(`"${data.name}": ${err.message}`); - } - } - return parameters; + return parameters; + }, new FunctionParameterCollection()); } function hasCode(data: FunctionData): boolean { diff --git a/src/domain/CategoryCollection.ts b/src/domain/CategoryCollection.ts index aa49d868..85400349 100644 --- a/src/domain/CategoryCollection.ts +++ b/src/domain/CategoryCollection.ts @@ -1,4 +1,4 @@ -import { getEnumNames, getEnumValues, assertInRange } from '@/application/Common/Enum'; +import { getEnumValues, assertInRange } from '@/application/Common/Enum'; import { IEntity } from '../infrastructure/Entity/IEntity'; import { ICategory } from './ICategory'; import { IScript } from './IScript'; @@ -57,16 +57,12 @@ export class CategoryCollection implements ICategoryCollection { } function ensureNoDuplicates(entities: ReadonlyArray>) { - const totalOccurrencesById = new Map(); - for (const entity of entities) { - totalOccurrencesById.set(entity.id, (totalOccurrencesById.get(entity.id) || 0) + 1); - } - const duplicatedIds = new Array(); - totalOccurrencesById.forEach((index, id) => { - if (index > 1) { - duplicatedIds.push(id); - } - }); + const isUniqueInArray = (id: TKey, index: number, array: readonly TKey[]) => array + .findIndex((otherId) => otherId === id) !== index; + const duplicatedIds = entities + .map((entity) => entity.id) + .filter((id, index, array) => !isUniqueInArray(id, index, array)) + .filter(isUniqueInArray); if (duplicatedIds.length > 0) { const duplicatedIdsText = duplicatedIds.map((id) => `"${id}"`).join(','); throw new Error( @@ -96,48 +92,37 @@ function ensureValidScripts(allScripts: readonly IScript[]) { if (!allScripts || allScripts.length === 0) { throw new Error('must consist of at least one script'); } - for (const level of getEnumValues(RecommendationLevel)) { - if (allScripts.every((script) => script.level !== level)) { - throw new Error(`none of the scripts are recommended as ${RecommendationLevel[level]}`); - } + const missingRecommendationLevels = getEnumValues(RecommendationLevel) + .filter((level) => allScripts.every((script) => script.level !== level)); + if (missingRecommendationLevels.length > 0) { + throw new Error('none of the scripts are recommended as' + + ` "${missingRecommendationLevels.map((level) => RecommendationLevel[level]).join(', "')}".`); } } -function flattenApplication(categories: ReadonlyArray): [ICategory[], IScript[]] { - const allCategories = new Array(); - const allScripts = new Array(); - flattenCategories(categories, allCategories, allScripts); - return [ - allCategories, - allScripts, - ]; -} - -function flattenCategories( +function flattenApplication( categories: ReadonlyArray, - allCategories: ICategory[], - allScripts: IScript[], -): IQueryableCollection { - if (!categories || categories.length === 0) { - return; - } - for (const category of categories) { - allCategories.push(category); - flattenScripts(category.scripts, allScripts); - flattenCategories(category.subCategories, allCategories, allScripts); - } -} - -function flattenScripts( - scripts: ReadonlyArray, - allScripts: IScript[], -): IScript[] { - if (!scripts) { - return; - } - for (const script of scripts) { - allScripts.push(script); - } +): [ICategory[], IScript[]] { + const [subCategories, subScripts] = (categories || []) + // Parse children + .map((category) => flattenApplication(category.subCategories)) + // Flatten results + .reduce(([previousCategories, previousScripts], [currentCategories, currentScripts]) => { + return [ + [...previousCategories, ...currentCategories], + [...previousScripts, ...currentScripts], + ]; + }, [new Array(), new Array()]); + return [ + [ + ...(categories || []), + ...subCategories, + ], + [ + ...(categories || []).flatMap((category) => category.scripts || []), + ...subScripts, + ], + ]; } function makeQueryable( @@ -154,13 +139,15 @@ function makeQueryable( function groupByLevel( allScripts: readonly IScript[], ): Map { - const map = new Map(); - for (const levelName of getEnumNames(RecommendationLevel)) { - const level = RecommendationLevel[levelName]; - const scripts = allScripts.filter( - (script) => script.level !== undefined && script.level <= level, - ); - map.set(level, scripts); - } - return map; + return getEnumValues(RecommendationLevel) + .map((level) => ({ + level, + scripts: allScripts.filter( + (script) => script.level !== undefined && script.level <= level, + ), + })) + .reduce((map, group) => { + map.set(group.level, group.scripts); + return map; + }, new Map()); } diff --git a/src/infrastructure/Events/EventSource.ts b/src/infrastructure/Events/EventSource.ts index 9f0e7227..b302d784 100644 --- a/src/infrastructure/Events/EventSource.ts +++ b/src/infrastructure/Events/EventSource.ts @@ -12,7 +12,7 @@ export class EventSource implements IEventSource { } public notify(data: T) { - for (const handler of Array.from(this.handlers.values())) { + for (const handler of this.handlers.values()) { handler(data); } } diff --git a/src/presentation/components/Scripts/Menu/Selector/SelectionTypeHandler.ts b/src/presentation/components/Scripts/Menu/Selector/SelectionTypeHandler.ts index 90d76f57..7340337b 100644 --- a/src/presentation/components/Scripts/Menu/Selector/SelectionTypeHandler.ts +++ b/src/presentation/components/Scripts/Menu/Selector/SelectionTypeHandler.ts @@ -26,7 +26,7 @@ export class SelectionTypeHandler { } public getCurrentSelectionType(): SelectionType { - for (const [type, selector] of Array.from(selectors.entries())) { + for (const [type, selector] of selectors.entries()) { if (selector.isSelected(this.state)) { return type; } diff --git a/src/presentation/components/Scripts/View/ScriptsTree/ScriptNodeParser.ts b/src/presentation/components/Scripts/View/ScriptsTree/ScriptNodeParser.ts index 1cc6a22b..d99a543b 100644 --- a/src/presentation/components/Scripts/View/ScriptsTree/ScriptNodeParser.ts +++ b/src/presentation/components/Scripts/View/ScriptsTree/ScriptNodeParser.ts @@ -3,12 +3,7 @@ import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { INode, NodeType } from './SelectableTree/Node/INode'; export function parseAllCategories(collection: ICategoryCollection): INode[] | undefined { - const nodes = new Array(); - for (const category of collection.actions) { - const children = parseCategoryRecursively(category); - nodes.push(convertCategoryToNode(category, children)); - } - return nodes; + return createCategoryNodes(collection.actions); } export function parseSingleCategory( @@ -43,31 +38,21 @@ function parseCategoryRecursively( if (!parentCategory) { throw new Error('parentCategory is undefined'); } - let nodes = new Array(); - nodes = addCategories(parentCategory.subCategories, nodes); - nodes = addScripts(parentCategory.scripts, nodes); - return nodes; + return [ + ...createCategoryNodes(parentCategory.subCategories), + ...createScriptNodes(parentCategory.scripts), + ]; } -function addScripts(scripts: ReadonlyArray, nodes: INode[]): INode[] { - if (!scripts || scripts.length === 0) { - return nodes; - } - for (const script of scripts) { - nodes.push(convertScriptToNode(script)); - } - return nodes; +function createScriptNodes(scripts: ReadonlyArray): INode[] { + return (scripts || []) + .map((script) => convertScriptToNode(script)); } -function addCategories(categories: ReadonlyArray, nodes: INode[]): INode[] { - if (!categories || categories.length === 0) { - return nodes; - } - for (const category of categories) { - const subCategoryNodes = parseCategoryRecursively(category); - nodes.push(convertCategoryToNode(category, subCategoryNodes)); - } - return nodes; +function createCategoryNodes(categories: ReadonlyArray): INode[] { + return (categories || []) + .map((category) => ({ category, children: parseCategoryRecursively(category) })) + .map((data) => convertCategoryToNode(data.category, data.children)); } function convertCategoryToNode( diff --git a/tests/unit/application/Common/ScriptingLanguage/ScriptingLanguageFactoryTestRunner.ts b/tests/unit/application/Common/ScriptingLanguage/ScriptingLanguageFactoryTestRunner.ts index d7d050cd..a35f1c68 100644 --- a/tests/unit/application/Common/ScriptingLanguage/ScriptingLanguageFactoryTestRunner.ts +++ b/tests/unit/application/Common/ScriptingLanguage/ScriptingLanguageFactoryTestRunner.ts @@ -25,7 +25,7 @@ function testExpectedInstanceTypes( ) { describe('create returns expected instances', () => { // arrange - for (const language of Array.from(expectedTypes.keys())) { + for (const language of expectedTypes.keys()) { it(ScriptingLanguage[language], () => { // act const expected = expectedTypes.get(language); diff --git a/tests/unit/application/Context/State/Code/ApplicationCode.spec.ts b/tests/unit/application/Context/State/Code/ApplicationCode.spec.ts index f2035328..19d0ccd3 100644 --- a/tests/unit/application/Context/State/Code/ApplicationCode.spec.ts +++ b/tests/unit/application/Context/State/Code/ApplicationCode.spec.ts @@ -197,7 +197,7 @@ class UserScriptGeneratorMock implements IUserScriptGenerator { selectedScripts: readonly SelectedScript[], scriptingDefinition: IScriptingDefinition, ): IUserScript { - for (const [parameters, result] of Array.from(this.prePlanned)) { + for (const [parameters, result] of this.prePlanned) { if (selectedScripts === parameters.scripts && scriptingDefinition === parameters.definition) { return result; diff --git a/tests/unit/application/Context/State/Code/Event/CodeChangedEvent.spec.ts b/tests/unit/application/Context/State/Code/Event/CodeChangedEvent.spec.ts index d115bc97..658342cf 100644 --- a/tests/unit/application/Context/State/Code/Event/CodeChangedEvent.spec.ts +++ b/tests/unit/application/Context/State/Code/Event/CodeChangedEvent.spec.ts @@ -13,16 +13,23 @@ describe('CodeChangedEvent', () => { it('throws when code position is out of range', () => { // arrange const code = 'singleline code'; + const nonExistingLine1 = 2; + const nonExistingLine2 = 31; const newScripts = new Map([ - [new SelectedScriptStub('1'), new CodePosition(0, 2 /* nonexisting line */)], + [new SelectedScriptStub('1'), new CodePosition(0, nonExistingLine1)], + [new SelectedScriptStub('2'), new CodePosition(0, nonExistingLine2)], ]); // act - const act = () => new CodeChangedEventBuilder() - .withCode(code) - .withNewScripts(newScripts) - .build(); + let errorText = ''; + try { + new CodeChangedEventBuilder() + .withCode(code) + .withNewScripts(newScripts) + .build(); + } catch (error) { errorText = error.message; } // assert - expect(act).to.throw(); + expect(errorText).to.include(nonExistingLine1); + expect(errorText).to.include(nonExistingLine2); }); describe('does not throw with valid code position', () => { // arrange diff --git a/tests/unit/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler.spec.ts b/tests/unit/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler.spec.ts index 0caa2876..447eb783 100644 --- a/tests/unit/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler.spec.ts +++ b/tests/unit/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler.spec.ts @@ -151,6 +151,14 @@ describe('ExpressionsCompiler', () => { .withArgument('parameter2', 'value'), expectedError: 'parameter value(s) not provided for: "parameter1", "parameter3" but used in code', }, + { + name: 'parameter names are not repeated in error message', + expressions: [ + new ExpressionStub().withParameterNames(['parameter1', 'parameter1', 'parameter2', 'parameter2'], false), + ], + args: new FunctionCallArgumentCollectionStub(), + expectedError: 'parameter value(s) not provided for: "parameter1", "parameter2" but used in code', + }, ]; for (const testCase of testCases) { it(testCase.name, () => { diff --git a/tests/unit/application/Parser/Script/Compiler/Expressions/Parser/Regex/RegexParser.spec.ts b/tests/unit/application/Parser/Script/Compiler/Expressions/Parser/Regex/RegexParser.spec.ts index b12a2306..edd0e70c 100644 --- a/tests/unit/application/Parser/Script/Compiler/Expressions/Parser/Regex/RegexParser.spec.ts +++ b/tests/unit/application/Parser/Script/Compiler/Expressions/Parser/Regex/RegexParser.spec.ts @@ -31,6 +31,31 @@ describe('RegexParser', () => { }); } }); + it('throws when position is invalid', () => { + // arrange + const regexMatchingEmpty = /^/gm; /* expressions cannot be empty */ + const code = 'unimportant'; + const expectedErrorParts = [ + `[${RegexParserConcrete.constructor.name}]`, + 'invalid script position', + `Regex: ${regexMatchingEmpty}`, + `Code: ${code}`, + ]; + const sut = new RegexParserConcrete(regexMatchingEmpty); + // act + let error: string; + try { + sut.findExpressions(code); + } catch (err) { + error = err.message; + } + // assert + expect( + expectedErrorParts.every((part) => error.includes(part)), + `Expected parts: ${expectedErrorParts.join(', ')}` + + `Actual error: ${error}`, + ); + }); describe('matches regex as expected', () => { // arrange const testCases = [ diff --git a/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler.spec.ts b/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler.spec.ts index 9f19c273..5e156776 100644 --- a/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler.spec.ts +++ b/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler.spec.ts @@ -70,7 +70,7 @@ describe('FunctionCallCompiler', () => { }, { name: 'provided: an unexpected parameter, when: none required', - functionParameters: undefined, + functionParameters: [], callParameters: ['unexpected-call-parameter'], expectedError: `Function "${functionName}" has unexpected parameter(s) provided: "unexpected-call-parameter"` @@ -90,10 +90,10 @@ describe('FunctionCallCompiler', () => { const func = new SharedFunctionStub(FunctionBodyType.Code) .withName('test-function-name') .withParameterNames(...testCase.functionParameters); - let params: FunctionCallParametersData = {}; - for (const parameter of testCase.callParameters) { - params = { ...params, [parameter]: 'defined-parameter-value ' }; - } + const params = testCase.callParameters + .reduce((result, parameter) => { + return { ...result, [parameter]: 'defined-parameter-value ' }; + }, {} as FunctionCallParametersData); const call = new FunctionCallStub() .withFunctionName(func.name) .withArguments(params); diff --git a/tests/unit/application/collections/NoUnintentedInlining.spec.ts b/tests/unit/application/collections/NoUnintentedInlining.spec.ts index 94cc0b8b..206c6610 100644 --- a/tests/unit/application/collections/NoUnintentedInlining.spec.ts +++ b/tests/unit/application/collections/NoUnintentedInlining.spec.ts @@ -49,15 +49,11 @@ async function findBadLineNumbers(fileContent: string): Promise { function findLineNumbersEndingWith(content: string, ending: string): number[] { sanityCheck(content, ending); - const lines = content.split(/\r\n|\r|\n/); - const results = new Array(); - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (line.trim().endsWith(ending)) { - results.push((i + 1 /* first line is 1 not 0 */)); - } - } - return results; + return content + .split(/\r\n|\r|\n/) + .map((line, index) => ({ text: line, index })) + .filter((line) => line.text.trim().endsWith(ending)) + .map((line) => line.index + 1 /* first line is 1, not 0 */); } function sanityCheck(content: string, ending: string): void { diff --git a/tests/unit/domain/CategoryCollection.spec.ts b/tests/unit/domain/CategoryCollection.spec.ts index cb5348d9..c2e8b159 100644 --- a/tests/unit/domain/CategoryCollection.spec.ts +++ b/tests/unit/domain/CategoryCollection.spec.ts @@ -121,24 +121,45 @@ describe('CategoryCollection', () => { expect(construct).to.throw('must consist of at least one script'); }); describe('cannot construct without any recommended scripts', () => { - // arrange - const recommendationLevels = getEnumValues(RecommendationLevel); - for (const missingLevel of recommendationLevels) { - it(`when "${RecommendationLevel[missingLevel]}" is missing`, () => { - const expectedError = `none of the scripts are recommended as ${RecommendationLevel[missingLevel]}`; - const otherLevels = recommendationLevels.filter((level) => level !== missingLevel); - const categories = otherLevels.map( - (level, index) => new CategoryStub(index) - .withScript(new ScriptStub(`Script${index}`).withLevel(level)), - ); - // act - const construct = () => new CategoryCollectionBuilder() - .withActions(categories) - .construct(); - // assert - expect(construct).to.throw(expectedError); - }); - } + describe('single missing', () => { + // arrange + const recommendationLevels = getEnumValues(RecommendationLevel); + for (const missingLevel of recommendationLevels) { + it(`when "${RecommendationLevel[missingLevel]}" is missing`, () => { + const expectedError = `none of the scripts are recommended as "${RecommendationLevel[missingLevel]}".`; + const otherLevels = recommendationLevels.filter((level) => level !== missingLevel); + const categories = otherLevels.map( + (level, index) => new CategoryStub(index) + .withScript( + new ScriptStub(`Script${index}`).withLevel(level), + ), + ); + // act + const construct = () => new CategoryCollectionBuilder() + .withActions(categories) + .construct(); + // assert + expect(construct).to.throw(expectedError); + }); + } + }); + it('multiple are missing', () => { + // arrange + const expectedError = 'none of the scripts are recommended as ' + + `"${RecommendationLevel[RecommendationLevel.Standard]}, "${RecommendationLevel[RecommendationLevel.Strict]}".`; + const categories = [ + new CategoryStub(0) + .withScript( + new ScriptStub(`Script${0}`).withLevel(undefined), + ), + ]; + // act + const construct = () => new CategoryCollectionBuilder() + .withActions(categories) + .construct(); + // assert + expect(construct).to.throw(expectedError); + }); }); }); describe('totalScripts', () => { diff --git a/tests/unit/domain/ScriptCode.spec.ts b/tests/unit/domain/ScriptCode.spec.ts index f386bfef..67766827 100644 --- a/tests/unit/domain/ScriptCode.spec.ts +++ b/tests/unit/domain/ScriptCode.spec.ts @@ -61,25 +61,24 @@ describe('ScriptCode', () => { }, ]; // act - const actions = []; - for (const testCase of testCases) { - actions.push(...[ - { - act: () => new ScriptCodeBuilder() - .withExecute(testCase.code) - .build(), - testName: `execute: ${testCase.testName}`, - expectedMessage: testCase.expectedMessage, - }, - { - act: () => new ScriptCodeBuilder() - .withRevert(testCase.code) - .build(), - testName: `revert: ${testCase.testName}`, - expectedMessage: `(revert): ${testCase.expectedMessage}`, - }, - ]); - } + const actions = testCases.flatMap((testCase) => ([ + { + act: () => new ScriptCodeBuilder() + .withExecute(testCase.code) + .build(), + testName: `execute: ${testCase.testName}`, + expectedMessage: testCase.expectedMessage, + code: testCase.code, + }, + { + act: () => new ScriptCodeBuilder() + .withRevert(testCase.code) + .build(), + testName: `revert: ${testCase.testName}`, + expectedMessage: `(revert): ${testCase.expectedMessage}`, + code: testCase.code, + }, + ])); // assert for (const action of actions) { it(action.testName, () => { @@ -115,27 +114,24 @@ describe('ScriptCode', () => { }, ]; // act - const actions = []; - for (const testCase of testCases) { - actions.push(...[ - { - testName: `execute: ${testCase.testName}`, - act: () => new ScriptCodeBuilder() - .withSyntax(syntax) - .withExecute(testCase.code) - .build(), - expect: (sut: IScriptCode) => sut.execute === testCase.code, - }, - { - testName: `revert: ${testCase.testName}`, - act: () => new ScriptCodeBuilder() - .withSyntax(syntax) - .withRevert(testCase.code) - .build(), - expect: (sut: IScriptCode) => sut.revert === testCase.code, - }, - ]); - } + const actions = testCases.flatMap((testCase) => ([ + { + testName: `execute: ${testCase.testName}`, + act: () => new ScriptCodeBuilder() + .withSyntax(syntax) + .withExecute(testCase.code) + .build(), + expect: (sut: IScriptCode) => sut.execute === testCase.code, + }, + { + testName: `revert: ${testCase.testName}`, + act: () => new ScriptCodeBuilder() + .withSyntax(syntax) + .withRevert(testCase.code) + .build(), + expect: (sut: IScriptCode) => sut.revert === testCase.code, + }, + ])); // assert for (const action of actions) { it(action.testName, () => { diff --git a/tests/unit/domain/ScriptingDefinition.spec.ts b/tests/unit/domain/ScriptingDefinition.spec.ts index 73ad2f8d..4b03cf55 100644 --- a/tests/unit/domain/ScriptingDefinition.spec.ts +++ b/tests/unit/domain/ScriptingDefinition.spec.ts @@ -39,7 +39,7 @@ describe('ScriptingDefinition', () => { [ScriptingLanguage.batchfile, 'bat'], [ScriptingLanguage.shellscript, 'sh'], ]); - Array.from(testCases.entries()).forEach((test) => { + for (const test of testCases.entries()) { const language = test[0]; const expectedExtension = test[1]; it(`${ScriptingLanguage[language]} has ${expectedExtension}`, () => { @@ -50,7 +50,7 @@ describe('ScriptingDefinition', () => { // assert expect(sut.fileExtension, expectedExtension); }); - }); + } }); }); describe('startCode', () => { diff --git a/tests/unit/infrastructure/Threading/AsyncSleep.spec.ts b/tests/unit/infrastructure/Threading/AsyncSleep.spec.ts index 64f2e0ce..09e9d931 100644 --- a/tests/unit/infrastructure/Threading/AsyncSleep.spec.ts +++ b/tests/unit/infrastructure/Threading/AsyncSleep.spec.ts @@ -52,14 +52,12 @@ class SchedulerMock { public tickNext(ms: number) { const newTime = this.currentTime + ms; - let newActions = this.scheduledActions; - for (const action of this.scheduledActions) { - if (newTime >= action.time) { - newActions = newActions.filter((a) => a !== action); - action.action(); - } + const dueActions = this.scheduledActions + .filter((action) => newTime >= action.time); + for (const action of dueActions) { + action.action(); } - this.scheduledActions = newActions; + this.scheduledActions = this.scheduledActions.filter((action) => !dueActions.includes(action)); } } diff --git a/tests/unit/presentation/components/Scripts/View/Tree/ScriptNodeParser.spec.ts b/tests/unit/presentation/components/Scripts/View/Tree/ScriptNodeParser.spec.ts index bfd443a3..139f393d 100644 --- a/tests/unit/presentation/components/Scripts/View/Tree/ScriptNodeParser.spec.ts +++ b/tests/unit/presentation/components/Scripts/View/Tree/ScriptNodeParser.spec.ts @@ -63,7 +63,6 @@ describe('ScriptNodeParser', () => { expectSameScript(nodes[2], scripts[2]); }); }); - it('parseAllCategories parses as expected', () => { // arrange const collection = new CategoryCollectionStub() diff --git a/tests/unit/stubs/CategoryCollectionStub.ts b/tests/unit/stubs/CategoryCollectionStub.ts index ac83bfb9..3ace2475 100644 --- a/tests/unit/stubs/CategoryCollectionStub.ts +++ b/tests/unit/stubs/CategoryCollectionStub.ts @@ -61,46 +61,27 @@ export class CategoryCollectionStub implements ICategoryCollection { } public getAllScripts(): ReadonlyArray { - const scripts = []; - for (const category of this.actions) { - const categoryScripts = getScriptsRecursively(category); - scripts.push(...categoryScripts); - } - return scripts; + return this.actions.flatMap((category) => getScriptsRecursively(category)); } public getAllCategories(): ReadonlyArray { - const categories = []; - categories.push(...this.actions); - for (const category of this.actions) { - const subCategories = getSubCategoriesRecursively(category); - categories.push(...subCategories); - } - return categories; + return this.actions.flatMap( + (category) => [category, ...getSubCategoriesRecursively(category)], + ); } } function getSubCategoriesRecursively(category: ICategory): ReadonlyArray { - const subCategories = []; - if (category.subCategories) { - for (const subCategory of category.subCategories) { - subCategories.push(subCategory); - subCategories.push(...getSubCategoriesRecursively(subCategory)); - } - } - return subCategories; + return (category.subCategories || []).flatMap( + (subCategory) => [subCategory, ...getSubCategoriesRecursively(subCategory)], + ); } function getScriptsRecursively(category: ICategory): ReadonlyArray { - const categoryScripts = []; - if (category.scripts) { - categoryScripts.push(...category.scripts); - } - if (category.subCategories) { - for (const subCategory of category.subCategories) { - const subCategoryScripts = getScriptsRecursively(subCategory); - categoryScripts.push(...subCategoryScripts); - } - } - return categoryScripts; + return [ + ...(category.scripts || []), + ...(category.subCategories || []).flatMap( + (subCategory) => getScriptsRecursively(subCategory), + ), + ]; } diff --git a/tests/unit/stubs/CategoryStub.ts b/tests/unit/stubs/CategoryStub.ts index 74fee7f5..7ef88fd1 100644 --- a/tests/unit/stubs/CategoryStub.ts +++ b/tests/unit/stubs/CategoryStub.ts @@ -27,10 +27,9 @@ export class CategoryStub extends BaseEntity implements ICategory { } public withScriptIds(...scriptIds: string[]): CategoryStub { - for (const scriptId of scriptIds) { - this.withScript(new ScriptStub(scriptId)); - } - return this; + return this.withScripts( + ...scriptIds.map((id) => new ScriptStub(id)), + ); } public withScripts(...scripts: IScript[]): CategoryStub { diff --git a/tests/unit/stubs/ExpressionsCompilerStub.ts b/tests/unit/stubs/ExpressionsCompilerStub.ts index be5ca4c4..a91fb7f4 100644 --- a/tests/unit/stubs/ExpressionsCompilerStub.ts +++ b/tests/unit/stubs/ExpressionsCompilerStub.ts @@ -58,12 +58,9 @@ function deepEqual( if (!scrambledEqual(expectedParameterNames, actualParameterNames)) { return false; } - for (const parameterName of expectedParameterNames) { + return expectedParameterNames.every((parameterName) => { const expectedValue = expected.getArgument(parameterName).argumentValue; const actualValue = actual.getArgument(parameterName).argumentValue; - if (expectedValue !== actualValue) { - return false; - } - } - return true; + return expectedValue === actualValue; + }); } diff --git a/tests/unit/stubs/FunctionCallArgumentCollectionStub.ts b/tests/unit/stubs/FunctionCallArgumentCollectionStub.ts index 3c822083..8a8b69f1 100644 --- a/tests/unit/stubs/FunctionCallArgumentCollectionStub.ts +++ b/tests/unit/stubs/FunctionCallArgumentCollectionStub.ts @@ -14,9 +14,8 @@ export class FunctionCallArgumentCollectionStub implements IFunctionCallArgument } public withArguments(args: { readonly [index: string]: string }) { - for (const parameterName of Object.keys(args)) { - const parameterValue = args[parameterName]; - this.withArgument(parameterName, parameterValue); + for (const [name, value] of Object.entries(args)) { + this.withArgument(name, value); } return this; } diff --git a/tests/unit/stubs/ScriptStub.ts b/tests/unit/stubs/ScriptStub.ts index 8f319bbb..e2cb8c0f 100644 --- a/tests/unit/stubs/ScriptStub.ts +++ b/tests/unit/stubs/ScriptStub.ts @@ -13,7 +13,7 @@ export class ScriptStub extends BaseEntity implements IScript { public readonly documentationUrls = new Array(); - public level = RecommendationLevel.Standard; + public level? = RecommendationLevel.Standard; constructor(public readonly id: string) { super(id); @@ -23,7 +23,7 @@ export class ScriptStub extends BaseEntity implements IScript { return Boolean(this.code.revert); } - public withLevel(value: RecommendationLevel): ScriptStub { + public withLevel(value?: RecommendationLevel): ScriptStub { this.level = value; return this; } diff --git a/tsconfig.json b/tsconfig.json index 87be2566..91f49a5b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,7 @@ "module": "esnext", "jsx": "preserve", "importHelpers": true, + "downlevelIteration": true, "moduleResolution": "node", "experimentalDecorators": true, "esModuleInterop": true,