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.
This commit is contained in:
undergroundwires
2022-01-04 21:45:22 +01:00
parent bd23faa28f
commit 31f70913a2
35 changed files with 342 additions and 343 deletions

View File

@@ -42,13 +42,12 @@ export class CodeChangedEvent implements ICodeChangedEvent {
function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) { function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
const totalLines = script.split(/\r\n|\r|\n/).length; const totalLines = script.split(/\r\n|\r|\n/).length;
for (const position of positions) { const missingPositions = positions.filter((position) => position.endLine > totalLines);
if (position.endLine > totalLines) { if (missingPositions.length > 0) {
throw new Error( throw new Error(
`script end line (${position.endLine}) is out of range.` `Out of range script end line: "${missingPositions.map((pos) => pos.endLine).join('", "')}"`
+ `(total code lines: ${totalLines}`, + `(total code lines: ${totalLines}).`,
); );
}
} }
} }

View File

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

View File

@@ -19,15 +19,14 @@ export class UserScriptGenerator implements IUserScriptGenerator {
): IUserScript { ): IUserScript {
if (!selectedScripts) { throw new Error('undefined scripts'); } if (!selectedScripts) { throw new Error('undefined scripts'); }
if (!scriptingDefinition) { throw new Error('undefined definition'); } if (!scriptingDefinition) { throw new Error('undefined definition'); }
let scriptPositions = new Map<SelectedScript, ICodePosition>();
if (!selectedScripts.length) { if (!selectedScripts.length) {
return { code: '', scriptPositions }; return { code: '', scriptPositions: new Map<SelectedScript, ICodePosition>() };
} }
let builder = this.codeBuilderFactory.create(scriptingDefinition.language); let builder = this.codeBuilderFactory.create(scriptingDefinition.language);
builder = initializeCode(scriptingDefinition.startCode, builder); builder = initializeCode(scriptingDefinition.startCode, builder);
for (const selection of selectedScripts) { const scriptPositions = selectedScripts.reduce((result, selection) => {
scriptPositions = appendSelection(selection, scriptPositions, builder); return appendSelection(selection, result, builder);
} }, new Map<SelectedScript, ICodePosition>());
const code = finalizeCode(builder, scriptingDefinition.endCode); const code = finalizeCode(builder, scriptingDefinition.endCode);
return { code, scriptPositions }; return { code, scriptPositions };
} }

View File

@@ -17,10 +17,8 @@ export class UserSelection implements IUserSelection {
selectedScripts: ReadonlyArray<SelectedScript>, selectedScripts: ReadonlyArray<SelectedScript>,
) { ) {
this.scripts = new InMemoryRepository<string, SelectedScript>(); this.scripts = new InMemoryRepository<string, SelectedScript>();
if (selectedScripts && selectedScripts.length > 0) { for (const script of selectedScripts) {
for (const script of selectedScripts) { this.scripts.addItem(script);
this.scripts.addItem(script);
}
} }
} }
@@ -58,18 +56,19 @@ export class UserSelection implements IUserSelection {
} }
public addOrUpdateAllInCategory(categoryId: number, revert = false): void { public addOrUpdateAllInCategory(categoryId: number, revert = false): void {
const category = this.collection.findCategory(categoryId); const scriptsToAddOrUpdate = this.collection
const scriptsToAddOrUpdate = category.getAllScriptsRecursively() .findCategory(categoryId)
.getAllScriptsRecursively()
.filter( .filter(
(script) => !this.scripts.exists(script.id) (script) => !this.scripts.exists(script.id)
|| this.scripts.getById(script.id).revert !== revert, || this.scripts.getById(script.id).revert !== revert,
); )
.map((script) => new SelectedScript(script, revert));
if (!scriptsToAddOrUpdate.length) { if (!scriptsToAddOrUpdate.length) {
return; return;
} }
for (const script of scriptsToAddOrUpdate) { for (const script of scriptsToAddOrUpdate) {
const selectedScript = new SelectedScript(script, revert); this.scripts.addOrUpdateItem(script);
this.scripts.addOrUpdateItem(selectedScript);
} }
this.changed.notify(this.scripts.getItems()); this.changed.notify(this.scripts.getItems());
} }
@@ -106,11 +105,12 @@ export class UserSelection implements IUserSelection {
} }
public selectAll(): void { public selectAll(): void {
for (const script of this.collection.getAllScripts()) { const scriptsToSelect = this.collection
if (!this.scripts.exists(script.id)) { .getAllScripts()
const selection = new SelectedScript(script, false); .filter((script) => !this.scripts.exists(script.id))
this.scripts.addItem(selection); .map((script) => new SelectedScript(script, false));
} for (const script of scriptsToSelect) {
this.scripts.addItem(script);
} }
this.changed.notify(this.scripts.getItems()); this.changed.notify(this.scripts.getItems());
} }
@@ -135,10 +135,11 @@ export class UserSelection implements IUserSelection {
.forEach((scriptId) => this.scripts.removeItem(scriptId)); .forEach((scriptId) => this.scripts.removeItem(scriptId));
} }
// Select from unselected scripts // 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) { for (const toSelect of unselectedScripts) {
const selection = new SelectedScript(toSelect, false); this.scripts.addItem(toSelect);
this.scripts.addItem(selection);
} }
this.changed.notify(this.scripts.getItems()); this.changed.notify(this.scripts.getItems());
} }

View File

@@ -1,5 +1,4 @@
import { CollectionData } from 'js-yaml-loader!@/*'; import { CollectionData } from 'js-yaml-loader!@/*';
import { Category } from '@/domain/Category';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { CategoryCollection } from '@/domain/CategoryCollection'; import { CategoryCollection } from '@/domain/CategoryCollection';
@@ -18,11 +17,7 @@ export function parseCategoryCollection(
const scripting = new ScriptingDefinitionParser() const scripting = new ScriptingDefinitionParser()
.parse(content.scripting, info); .parse(content.scripting, info);
const context = new CategoryCollectionParseContext(content.functions, scripting); const context = new CategoryCollectionParseContext(content.functions, scripting);
const categories = new Array<Category>(); const categories = content.actions.map((action) => parseCategory(action, context));
for (const action of content.actions) {
const category = parseCategory(action, context);
categories.push(category);
}
const os = osParser.parseEnum(content.os, 'os'); const os = osParser.parseEnum(content.os, 'os');
const collection = new CategoryCollection( const collection = new CategoryCollection(
os, os,

View File

@@ -9,11 +9,6 @@ import { parseScript } from './Script/ScriptParser';
let categoryIdCounter = 0; let categoryIdCounter = 0;
interface ICategoryChildren {
subCategories: Category[];
subScripts: Script[];
}
export function parseCategory( export function parseCategory(
category: CategoryData, category: CategoryData,
context: ICategoryCollectionParseContext, context: ICategoryCollectionParseContext,
@@ -48,6 +43,11 @@ function ensureValid(category: CategoryData) {
} }
} }
interface ICategoryChildren {
subCategories: Category[];
subScripts: Script[];
}
function parseCategoryChild( function parseCategoryChild(
data: CategoryOrScriptData, data: CategoryOrScriptData,
children: ICategoryChildren, children: ICategoryChildren,

View File

@@ -56,14 +56,13 @@ function compileExpressions(
function extractRequiredParameterNames( function extractRequiredParameterNames(
expressions: readonly IExpression[], expressions: readonly IExpression[],
): string[] { ): string[] {
const usedParameterNames = expressions return expressions
.map((e) => e.parameters.all .map((e) => e.parameters.all
.filter((p) => !p.isOptional) .filter((p) => !p.isOptional)
.map((p) => p.name)) .map((p) => p.name))
.filter((p) => p) .filter(Boolean) // Remove empty or undefined
.flat(); .flat()
const uniqueParameterNames = Array.from(new Set(usedParameterNames)); .filter((name, index, array) => array.indexOf(name) === index); // Remove duplicates
return uniqueParameterNames;
} }
function ensureParamsUsedInCodeHasArgsProvided( function ensureParamsUsedInCodeHasArgsProvided(

View File

@@ -16,13 +16,8 @@ export class CompositeExpressionParser implements IExpressionParser {
} }
public findExpressions(code: string): IExpression[] { public findExpressions(code: string): IExpression[] {
const expressions = new Array<IExpression>(); return this.leafs.flatMap(
for (const parser of this.leafs) { (parser) => parser.findExpressions(code) || [],
const newExpressions = parser.findExpressions(code); );
if (newExpressions && newExpressions.length) {
expressions.push(...newExpressions);
}
}
return expressions;
} }
} }

View File

@@ -18,35 +18,42 @@ export abstract class RegexParser implements IExpressionParser {
if (!code) { if (!code) {
throw new Error('undefined 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) { 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 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); const expression = new Expression(position, primitiveExpression.evaluator, parameters);
yield expression; yield expression;
} }
} }
private doOrRethrow<T>(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 { export interface IPrimitiveExpression {
evaluator: ExpressionEvaluator; evaluator: ExpressionEvaluator;
parameters?: readonly IFunctionParameter[]; parameters?: readonly IFunctionParameter[];
} }
function getParameters(
expression: IPrimitiveExpression,
): FunctionParameterCollection {
const parameters = new FunctionParameterCollection();
for (const parameter of expression.parameters || []) {
parameters.addParameter(parameter);
}
return parameters;
}

View File

@@ -8,11 +8,9 @@ export class PipelineCompiler implements IPipelineCompiler {
ensureValidArguments(value, pipeline); ensureValidArguments(value, pipeline);
const pipeNames = extractPipeNames(pipeline); const pipeNames = extractPipeNames(pipeline);
const pipes = pipeNames.map((pipeName) => this.factory.get(pipeName)); const pipes = pipeNames.map((pipeName) => this.factory.get(pipeName));
let valueInCompilation = value; return pipes.reduce((previousValue, pipe) => {
for (const pipe of pipes) { return pipe.apply(previousValue);
valueInCompilation = pipe.apply(valueInCompilation); }, value);
}
return valueInCompilation;
} }
} }

View File

@@ -47,11 +47,8 @@ interface ICompiledFunctionCall {
} }
function compileCallSequence(context: ICompilationContext): ICompiledFunctionCall { function compileCallSequence(context: ICompilationContext): ICompiledFunctionCall {
const compiledFunctions = new Array<ICompiledFunctionCall>(); const compiledFunctions = context.callSequence
for (const call of context.callSequence) { .flatMap((call) => compileSingleCall(call, context));
const compiledCode = compileSingleCall(call, context);
compiledFunctions.push(...compiledCode);
}
return { return {
code: merge(compiledFunctions.map((f) => f.code)), code: merge(compiledFunctions.map((f) => f.code)),
revertCode: merge(compiledFunctions.map((f) => f.revertCode)), revertCode: merge(compiledFunctions.map((f) => f.revertCode)),
@@ -94,14 +91,17 @@ function compileArgs(
args: IReadOnlyFunctionCallArgumentCollection, args: IReadOnlyFunctionCallArgumentCollection,
compiler: IExpressionsCompiler, compiler: IExpressionsCompiler,
): IReadOnlyFunctionCallArgumentCollection { ): IReadOnlyFunctionCallArgumentCollection {
const compiledArgs = new FunctionCallArgumentCollection(); return argsToCompile
for (const parameterName of argsToCompile.getAllParameterNames()) { .getAllParameterNames()
const { argumentValue } = argsToCompile.getArgument(parameterName); .map((parameterName) => {
const compiledValue = compiler.compileExpressions(argumentValue, args); const { argumentValue } = argsToCompile.getArgument(parameterName);
const newArgument = new FunctionCallArgument(parameterName, compiledValue); const compiledValue = compiler.compileExpressions(argumentValue, args);
compiledArgs.addArgument(newArgument); return new FunctionCallArgument(parameterName, compiledValue);
} })
return compiledArgs; .reduce((compiledArgs, arg) => {
compiledArgs.addArgument(arg);
return compiledArgs;
}, new FunctionCallArgumentCollection());
} }
function merge(codeParts: readonly string[]): string { function merge(codeParts: readonly string[]): string {

View File

@@ -1,4 +1,4 @@
import { FunctionCallData, FunctionCallsData } from 'js-yaml-loader!@/*'; import { FunctionCallData, FunctionCallsData, FunctionCallParametersData } from 'js-yaml-loader!@/*';
import { IFunctionCall } from './IFunctionCall'; import { IFunctionCall } from './IFunctionCall';
import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection'; import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection';
import { FunctionCallArgument } from './Argument/FunctionCallArgument'; import { FunctionCallArgument } from './Argument/FunctionCallArgument';
@@ -26,10 +26,17 @@ function parseFunctionCall(call: FunctionCallData): IFunctionCall {
if (!call) { if (!call) {
throw new Error('undefined function call'); throw new Error('undefined function call');
} }
const args = new FunctionCallArgumentCollection(); const callArgs = parseArgs(call.parameters);
for (const parameterName of Object.keys(call.parameters || {})) { return new FunctionCall(call.function, callArgs);
const arg = new FunctionCallArgument(parameterName, call.parameters[parameterName]); }
args.addArgument(arg);
} function parseArgs(
return new FunctionCall(call.function, args); parameters: FunctionCallParametersData,
): FunctionCallArgumentCollection {
return Object.keys(parameters || {})
.map((parameterName) => new FunctionCallArgument(parameterName, parameters[parameterName]))
.reduce((args, arg) => {
args.addArgument(arg);
return args;
}, new FunctionCallArgumentCollection());
} }

View File

@@ -20,11 +20,12 @@ export class SharedFunctionsParser implements ISharedFunctionsParser {
return collection; return collection;
} }
ensureValidFunctions(functions); ensureValidFunctions(functions);
for (const func of functions) { return functions
const sharedFunction = parseFunction(func); .map((func) => parseFunction(func))
collection.addFunction(sharedFunction); .reduce((acc, func) => {
} acc.addFunction(func);
return collection; return acc;
}, collection);
} }
} }
@@ -40,20 +41,21 @@ function parseFunction(data: FunctionData): ISharedFunction {
} }
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection { function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
const parameters = new FunctionParameterCollection(); return (data.parameters || [])
if (!data.parameters) { .map((parameter) => {
return parameters; try {
} return new FunctionParameter(
for (const parameterData of data.parameters) { parameter.name,
const isOptional = parameterData.optional || false; parameter.optional || false,
try { );
const parameter = new FunctionParameter(parameterData.name, isOptional); } catch (err) {
throw new Error(`"${data.name}": ${err.message}`);
}
})
.reduce((parameters, parameter) => {
parameters.addParameter(parameter); parameters.addParameter(parameter);
} catch (err) { return parameters;
throw new Error(`"${data.name}": ${err.message}`); }, new FunctionParameterCollection());
}
}
return parameters;
} }
function hasCode(data: FunctionData): boolean { function hasCode(data: FunctionData): boolean {

View File

@@ -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 { IEntity } from '../infrastructure/Entity/IEntity';
import { ICategory } from './ICategory'; import { ICategory } from './ICategory';
import { IScript } from './IScript'; import { IScript } from './IScript';
@@ -57,16 +57,12 @@ export class CategoryCollection implements ICategoryCollection {
} }
function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) { function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
const totalOccurrencesById = new Map<TKey, number>(); const isUniqueInArray = (id: TKey, index: number, array: readonly TKey[]) => array
for (const entity of entities) { .findIndex((otherId) => otherId === id) !== index;
totalOccurrencesById.set(entity.id, (totalOccurrencesById.get(entity.id) || 0) + 1); const duplicatedIds = entities
} .map((entity) => entity.id)
const duplicatedIds = new Array<TKey>(); .filter((id, index, array) => !isUniqueInArray(id, index, array))
totalOccurrencesById.forEach((index, id) => { .filter(isUniqueInArray);
if (index > 1) {
duplicatedIds.push(id);
}
});
if (duplicatedIds.length > 0) { if (duplicatedIds.length > 0) {
const duplicatedIdsText = duplicatedIds.map((id) => `"${id}"`).join(','); const duplicatedIdsText = duplicatedIds.map((id) => `"${id}"`).join(',');
throw new Error( throw new Error(
@@ -96,48 +92,37 @@ function ensureValidScripts(allScripts: readonly IScript[]) {
if (!allScripts || allScripts.length === 0) { if (!allScripts || allScripts.length === 0) {
throw new Error('must consist of at least one script'); throw new Error('must consist of at least one script');
} }
for (const level of getEnumValues(RecommendationLevel)) { const missingRecommendationLevels = getEnumValues(RecommendationLevel)
if (allScripts.every((script) => script.level !== level)) { .filter((level) => allScripts.every((script) => script.level !== level));
throw new Error(`none of the scripts are recommended as ${RecommendationLevel[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>): [ICategory[], IScript[]] { function flattenApplication(
const allCategories = new Array<ICategory>();
const allScripts = new Array<IScript>();
flattenCategories(categories, allCategories, allScripts);
return [
allCategories,
allScripts,
];
}
function flattenCategories(
categories: ReadonlyArray<ICategory>, categories: ReadonlyArray<ICategory>,
allCategories: ICategory[], ): [ICategory[], IScript[]] {
allScripts: IScript[], const [subCategories, subScripts] = (categories || [])
): IQueryableCollection { // Parse children
if (!categories || categories.length === 0) { .map((category) => flattenApplication(category.subCategories))
return; // Flatten results
} .reduce(([previousCategories, previousScripts], [currentCategories, currentScripts]) => {
for (const category of categories) { return [
allCategories.push(category); [...previousCategories, ...currentCategories],
flattenScripts(category.scripts, allScripts); [...previousScripts, ...currentScripts],
flattenCategories(category.subCategories, allCategories, allScripts); ];
} }, [new Array<ICategory>(), new Array<IScript>()]);
} return [
[
function flattenScripts( ...(categories || []),
scripts: ReadonlyArray<IScript>, ...subCategories,
allScripts: IScript[], ],
): IScript[] { [
if (!scripts) { ...(categories || []).flatMap((category) => category.scripts || []),
return; ...subScripts,
} ],
for (const script of scripts) { ];
allScripts.push(script);
}
} }
function makeQueryable( function makeQueryable(
@@ -154,13 +139,15 @@ function makeQueryable(
function groupByLevel( function groupByLevel(
allScripts: readonly IScript[], allScripts: readonly IScript[],
): Map<RecommendationLevel, readonly IScript[]> { ): Map<RecommendationLevel, readonly IScript[]> {
const map = new Map<RecommendationLevel, readonly IScript[]>(); return getEnumValues(RecommendationLevel)
for (const levelName of getEnumNames(RecommendationLevel)) { .map((level) => ({
const level = RecommendationLevel[levelName]; level,
const scripts = allScripts.filter( scripts: allScripts.filter(
(script) => script.level !== undefined && script.level <= level, (script) => script.level !== undefined && script.level <= level,
); ),
map.set(level, scripts); }))
} .reduce((map, group) => {
return map; map.set(group.level, group.scripts);
return map;
}, new Map<RecommendationLevel, readonly IScript[]>());
} }

View File

@@ -12,7 +12,7 @@ export class EventSource<T> implements IEventSource<T> {
} }
public notify(data: T) { public notify(data: T) {
for (const handler of Array.from(this.handlers.values())) { for (const handler of this.handlers.values()) {
handler(data); handler(data);
} }
} }

View File

@@ -26,7 +26,7 @@ export class SelectionTypeHandler {
} }
public getCurrentSelectionType(): SelectionType { public getCurrentSelectionType(): SelectionType {
for (const [type, selector] of Array.from(selectors.entries())) { for (const [type, selector] of selectors.entries()) {
if (selector.isSelected(this.state)) { if (selector.isSelected(this.state)) {
return type; return type;
} }

View File

@@ -3,12 +3,7 @@ import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { INode, NodeType } from './SelectableTree/Node/INode'; import { INode, NodeType } from './SelectableTree/Node/INode';
export function parseAllCategories(collection: ICategoryCollection): INode[] | undefined { export function parseAllCategories(collection: ICategoryCollection): INode[] | undefined {
const nodes = new Array<INode>(); return createCategoryNodes(collection.actions);
for (const category of collection.actions) {
const children = parseCategoryRecursively(category);
nodes.push(convertCategoryToNode(category, children));
}
return nodes;
} }
export function parseSingleCategory( export function parseSingleCategory(
@@ -43,31 +38,21 @@ function parseCategoryRecursively(
if (!parentCategory) { if (!parentCategory) {
throw new Error('parentCategory is undefined'); throw new Error('parentCategory is undefined');
} }
let nodes = new Array<INode>(); return [
nodes = addCategories(parentCategory.subCategories, nodes); ...createCategoryNodes(parentCategory.subCategories),
nodes = addScripts(parentCategory.scripts, nodes); ...createScriptNodes(parentCategory.scripts),
return nodes; ];
} }
function addScripts(scripts: ReadonlyArray<IScript>, nodes: INode[]): INode[] { function createScriptNodes(scripts: ReadonlyArray<IScript>): INode[] {
if (!scripts || scripts.length === 0) { return (scripts || [])
return nodes; .map((script) => convertScriptToNode(script));
}
for (const script of scripts) {
nodes.push(convertScriptToNode(script));
}
return nodes;
} }
function addCategories(categories: ReadonlyArray<ICategory>, nodes: INode[]): INode[] { function createCategoryNodes(categories: ReadonlyArray<ICategory>): INode[] {
if (!categories || categories.length === 0) { return (categories || [])
return nodes; .map((category) => ({ category, children: parseCategoryRecursively(category) }))
} .map((data) => convertCategoryToNode(data.category, data.children));
for (const category of categories) {
const subCategoryNodes = parseCategoryRecursively(category);
nodes.push(convertCategoryToNode(category, subCategoryNodes));
}
return nodes;
} }
function convertCategoryToNode( function convertCategoryToNode(

View File

@@ -25,7 +25,7 @@ function testExpectedInstanceTypes<T>(
) { ) {
describe('create returns expected instances', () => { describe('create returns expected instances', () => {
// arrange // arrange
for (const language of Array.from(expectedTypes.keys())) { for (const language of expectedTypes.keys()) {
it(ScriptingLanguage[language], () => { it(ScriptingLanguage[language], () => {
// act // act
const expected = expectedTypes.get(language); const expected = expectedTypes.get(language);

View File

@@ -197,7 +197,7 @@ class UserScriptGeneratorMock implements IUserScriptGenerator {
selectedScripts: readonly SelectedScript[], selectedScripts: readonly SelectedScript[],
scriptingDefinition: IScriptingDefinition, scriptingDefinition: IScriptingDefinition,
): IUserScript { ): IUserScript {
for (const [parameters, result] of Array.from(this.prePlanned)) { for (const [parameters, result] of this.prePlanned) {
if (selectedScripts === parameters.scripts if (selectedScripts === parameters.scripts
&& scriptingDefinition === parameters.definition) { && scriptingDefinition === parameters.definition) {
return result; return result;

View File

@@ -13,16 +13,23 @@ describe('CodeChangedEvent', () => {
it('throws when code position is out of range', () => { it('throws when code position is out of range', () => {
// arrange // arrange
const code = 'singleline code'; const code = 'singleline code';
const nonExistingLine1 = 2;
const nonExistingLine2 = 31;
const newScripts = new Map<SelectedScript, ICodePosition>([ const newScripts = new Map<SelectedScript, ICodePosition>([
[new SelectedScriptStub('1'), new CodePosition(0, 2 /* nonexisting line */)], [new SelectedScriptStub('1'), new CodePosition(0, nonExistingLine1)],
[new SelectedScriptStub('2'), new CodePosition(0, nonExistingLine2)],
]); ]);
// act // act
const act = () => new CodeChangedEventBuilder() let errorText = '';
.withCode(code) try {
.withNewScripts(newScripts) new CodeChangedEventBuilder()
.build(); .withCode(code)
.withNewScripts(newScripts)
.build();
} catch (error) { errorText = error.message; }
// assert // assert
expect(act).to.throw(); expect(errorText).to.include(nonExistingLine1);
expect(errorText).to.include(nonExistingLine2);
}); });
describe('does not throw with valid code position', () => { describe('does not throw with valid code position', () => {
// arrange // arrange

View File

@@ -151,6 +151,14 @@ describe('ExpressionsCompiler', () => {
.withArgument('parameter2', 'value'), .withArgument('parameter2', 'value'),
expectedError: 'parameter value(s) not provided for: "parameter1", "parameter3" but used in code', 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) { for (const testCase of testCases) {
it(testCase.name, () => { it(testCase.name, () => {

View File

@@ -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', () => { describe('matches regex as expected', () => {
// arrange // arrange
const testCases = [ const testCases = [

View File

@@ -70,7 +70,7 @@ describe('FunctionCallCompiler', () => {
}, },
{ {
name: 'provided: an unexpected parameter, when: none required', name: 'provided: an unexpected parameter, when: none required',
functionParameters: undefined, functionParameters: [],
callParameters: ['unexpected-call-parameter'], callParameters: ['unexpected-call-parameter'],
expectedError: expectedError:
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-call-parameter"` `Function "${functionName}" has unexpected parameter(s) provided: "unexpected-call-parameter"`
@@ -90,10 +90,10 @@ describe('FunctionCallCompiler', () => {
const func = new SharedFunctionStub(FunctionBodyType.Code) const func = new SharedFunctionStub(FunctionBodyType.Code)
.withName('test-function-name') .withName('test-function-name')
.withParameterNames(...testCase.functionParameters); .withParameterNames(...testCase.functionParameters);
let params: FunctionCallParametersData = {}; const params = testCase.callParameters
for (const parameter of testCase.callParameters) { .reduce((result, parameter) => {
params = { ...params, [parameter]: 'defined-parameter-value ' }; return { ...result, [parameter]: 'defined-parameter-value ' };
} }, {} as FunctionCallParametersData);
const call = new FunctionCallStub() const call = new FunctionCallStub()
.withFunctionName(func.name) .withFunctionName(func.name)
.withArguments(params); .withArguments(params);

View File

@@ -49,15 +49,11 @@ async function findBadLineNumbers(fileContent: string): Promise<number[]> {
function findLineNumbersEndingWith(content: string, ending: string): number[] { function findLineNumbersEndingWith(content: string, ending: string): number[] {
sanityCheck(content, ending); sanityCheck(content, ending);
const lines = content.split(/\r\n|\r|\n/); return content
const results = new Array<number>(); .split(/\r\n|\r|\n/)
for (let i = 0; i < lines.length; i++) { .map((line, index) => ({ text: line, index }))
const line = lines[i]; .filter((line) => line.text.trim().endsWith(ending))
if (line.trim().endsWith(ending)) { .map((line) => line.index + 1 /* first line is 1, not 0 */);
results.push((i + 1 /* first line is 1 not 0 */));
}
}
return results;
} }
function sanityCheck(content: string, ending: string): void { function sanityCheck(content: string, ending: string): void {

View File

@@ -121,24 +121,45 @@ describe('CategoryCollection', () => {
expect(construct).to.throw('must consist of at least one script'); expect(construct).to.throw('must consist of at least one script');
}); });
describe('cannot construct without any recommended scripts', () => { describe('cannot construct without any recommended scripts', () => {
// arrange describe('single missing', () => {
const recommendationLevels = getEnumValues(RecommendationLevel); // arrange
for (const missingLevel of recommendationLevels) { const recommendationLevels = getEnumValues(RecommendationLevel);
it(`when "${RecommendationLevel[missingLevel]}" is missing`, () => { for (const missingLevel of recommendationLevels) {
const expectedError = `none of the scripts are recommended as ${RecommendationLevel[missingLevel]}`; it(`when "${RecommendationLevel[missingLevel]}" is missing`, () => {
const otherLevels = recommendationLevels.filter((level) => level !== missingLevel); const expectedError = `none of the scripts are recommended as "${RecommendationLevel[missingLevel]}".`;
const categories = otherLevels.map( const otherLevels = recommendationLevels.filter((level) => level !== missingLevel);
(level, index) => new CategoryStub(index) const categories = otherLevels.map(
.withScript(new ScriptStub(`Script${index}`).withLevel(level)), (level, index) => new CategoryStub(index)
); .withScript(
// act new ScriptStub(`Script${index}`).withLevel(level),
const construct = () => new CategoryCollectionBuilder() ),
.withActions(categories) );
.construct(); // act
// assert const construct = () => new CategoryCollectionBuilder()
expect(construct).to.throw(expectedError); .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', () => { describe('totalScripts', () => {

View File

@@ -61,25 +61,24 @@ describe('ScriptCode', () => {
}, },
]; ];
// act // act
const actions = []; const actions = testCases.flatMap((testCase) => ([
for (const testCase of testCases) { {
actions.push(...[ act: () => new ScriptCodeBuilder()
{ .withExecute(testCase.code)
act: () => new ScriptCodeBuilder() .build(),
.withExecute(testCase.code) testName: `execute: ${testCase.testName}`,
.build(), expectedMessage: testCase.expectedMessage,
testName: `execute: ${testCase.testName}`, code: testCase.code,
expectedMessage: testCase.expectedMessage, },
}, {
{ act: () => new ScriptCodeBuilder()
act: () => new ScriptCodeBuilder() .withRevert(testCase.code)
.withRevert(testCase.code) .build(),
.build(), testName: `revert: ${testCase.testName}`,
testName: `revert: ${testCase.testName}`, expectedMessage: `(revert): ${testCase.expectedMessage}`,
expectedMessage: `(revert): ${testCase.expectedMessage}`, code: testCase.code,
}, },
]); ]));
}
// assert // assert
for (const action of actions) { for (const action of actions) {
it(action.testName, () => { it(action.testName, () => {
@@ -115,27 +114,24 @@ describe('ScriptCode', () => {
}, },
]; ];
// act // act
const actions = []; const actions = testCases.flatMap((testCase) => ([
for (const testCase of testCases) { {
actions.push(...[ testName: `execute: ${testCase.testName}`,
{ act: () => new ScriptCodeBuilder()
testName: `execute: ${testCase.testName}`, .withSyntax(syntax)
act: () => new ScriptCodeBuilder() .withExecute(testCase.code)
.withSyntax(syntax) .build(),
.withExecute(testCase.code) expect: (sut: IScriptCode) => sut.execute === testCase.code,
.build(), },
expect: (sut: IScriptCode) => sut.execute === testCase.code, {
}, testName: `revert: ${testCase.testName}`,
{ act: () => new ScriptCodeBuilder()
testName: `revert: ${testCase.testName}`, .withSyntax(syntax)
act: () => new ScriptCodeBuilder() .withRevert(testCase.code)
.withSyntax(syntax) .build(),
.withRevert(testCase.code) expect: (sut: IScriptCode) => sut.revert === testCase.code,
.build(), },
expect: (sut: IScriptCode) => sut.revert === testCase.code, ]));
},
]);
}
// assert // assert
for (const action of actions) { for (const action of actions) {
it(action.testName, () => { it(action.testName, () => {

View File

@@ -39,7 +39,7 @@ describe('ScriptingDefinition', () => {
[ScriptingLanguage.batchfile, 'bat'], [ScriptingLanguage.batchfile, 'bat'],
[ScriptingLanguage.shellscript, 'sh'], [ScriptingLanguage.shellscript, 'sh'],
]); ]);
Array.from(testCases.entries()).forEach((test) => { for (const test of testCases.entries()) {
const language = test[0]; const language = test[0];
const expectedExtension = test[1]; const expectedExtension = test[1];
it(`${ScriptingLanguage[language]} has ${expectedExtension}`, () => { it(`${ScriptingLanguage[language]} has ${expectedExtension}`, () => {
@@ -50,7 +50,7 @@ describe('ScriptingDefinition', () => {
// assert // assert
expect(sut.fileExtension, expectedExtension); expect(sut.fileExtension, expectedExtension);
}); });
}); }
}); });
}); });
describe('startCode', () => { describe('startCode', () => {

View File

@@ -52,14 +52,12 @@ class SchedulerMock {
public tickNext(ms: number) { public tickNext(ms: number) {
const newTime = this.currentTime + ms; const newTime = this.currentTime + ms;
let newActions = this.scheduledActions; const dueActions = this.scheduledActions
for (const action of this.scheduledActions) { .filter((action) => newTime >= action.time);
if (newTime >= action.time) { for (const action of dueActions) {
newActions = newActions.filter((a) => a !== action); action.action();
action.action();
}
} }
this.scheduledActions = newActions; this.scheduledActions = this.scheduledActions.filter((action) => !dueActions.includes(action));
} }
} }

View File

@@ -63,7 +63,6 @@ describe('ScriptNodeParser', () => {
expectSameScript(nodes[2], scripts[2]); expectSameScript(nodes[2], scripts[2]);
}); });
}); });
it('parseAllCategories parses as expected', () => { it('parseAllCategories parses as expected', () => {
// arrange // arrange
const collection = new CategoryCollectionStub() const collection = new CategoryCollectionStub()

View File

@@ -61,46 +61,27 @@ export class CategoryCollectionStub implements ICategoryCollection {
} }
public getAllScripts(): ReadonlyArray<IScript> { public getAllScripts(): ReadonlyArray<IScript> {
const scripts = []; return this.actions.flatMap((category) => getScriptsRecursively(category));
for (const category of this.actions) {
const categoryScripts = getScriptsRecursively(category);
scripts.push(...categoryScripts);
}
return scripts;
} }
public getAllCategories(): ReadonlyArray<ICategory> { public getAllCategories(): ReadonlyArray<ICategory> {
const categories = []; return this.actions.flatMap(
categories.push(...this.actions); (category) => [category, ...getSubCategoriesRecursively(category)],
for (const category of this.actions) { );
const subCategories = getSubCategoriesRecursively(category);
categories.push(...subCategories);
}
return categories;
} }
} }
function getSubCategoriesRecursively(category: ICategory): ReadonlyArray<ICategory> { function getSubCategoriesRecursively(category: ICategory): ReadonlyArray<ICategory> {
const subCategories = []; return (category.subCategories || []).flatMap(
if (category.subCategories) { (subCategory) => [subCategory, ...getSubCategoriesRecursively(subCategory)],
for (const subCategory of category.subCategories) { );
subCategories.push(subCategory);
subCategories.push(...getSubCategoriesRecursively(subCategory));
}
}
return subCategories;
} }
function getScriptsRecursively(category: ICategory): ReadonlyArray<IScript> { function getScriptsRecursively(category: ICategory): ReadonlyArray<IScript> {
const categoryScripts = []; return [
if (category.scripts) { ...(category.scripts || []),
categoryScripts.push(...category.scripts); ...(category.subCategories || []).flatMap(
} (subCategory) => getScriptsRecursively(subCategory),
if (category.subCategories) { ),
for (const subCategory of category.subCategories) { ];
const subCategoryScripts = getScriptsRecursively(subCategory);
categoryScripts.push(...subCategoryScripts);
}
}
return categoryScripts;
} }

View File

@@ -27,10 +27,9 @@ export class CategoryStub extends BaseEntity<number> implements ICategory {
} }
public withScriptIds(...scriptIds: string[]): CategoryStub { public withScriptIds(...scriptIds: string[]): CategoryStub {
for (const scriptId of scriptIds) { return this.withScripts(
this.withScript(new ScriptStub(scriptId)); ...scriptIds.map((id) => new ScriptStub(id)),
} );
return this;
} }
public withScripts(...scripts: IScript[]): CategoryStub { public withScripts(...scripts: IScript[]): CategoryStub {

View File

@@ -58,12 +58,9 @@ function deepEqual(
if (!scrambledEqual(expectedParameterNames, actualParameterNames)) { if (!scrambledEqual(expectedParameterNames, actualParameterNames)) {
return false; return false;
} }
for (const parameterName of expectedParameterNames) { return expectedParameterNames.every((parameterName) => {
const expectedValue = expected.getArgument(parameterName).argumentValue; const expectedValue = expected.getArgument(parameterName).argumentValue;
const actualValue = actual.getArgument(parameterName).argumentValue; const actualValue = actual.getArgument(parameterName).argumentValue;
if (expectedValue !== actualValue) { return expectedValue === actualValue;
return false; });
}
}
return true;
} }

View File

@@ -14,9 +14,8 @@ export class FunctionCallArgumentCollectionStub implements IFunctionCallArgument
} }
public withArguments(args: { readonly [index: string]: string }) { public withArguments(args: { readonly [index: string]: string }) {
for (const parameterName of Object.keys(args)) { for (const [name, value] of Object.entries(args)) {
const parameterValue = args[parameterName]; this.withArgument(name, value);
this.withArgument(parameterName, parameterValue);
} }
return this; return this;
} }

View File

@@ -13,7 +13,7 @@ export class ScriptStub extends BaseEntity<string> implements IScript {
public readonly documentationUrls = new Array<string>(); public readonly documentationUrls = new Array<string>();
public level = RecommendationLevel.Standard; public level? = RecommendationLevel.Standard;
constructor(public readonly id: string) { constructor(public readonly id: string) {
super(id); super(id);
@@ -23,7 +23,7 @@ export class ScriptStub extends BaseEntity<string> implements IScript {
return Boolean(this.code.revert); return Boolean(this.code.revert);
} }
public withLevel(value: RecommendationLevel): ScriptStub { public withLevel(value?: RecommendationLevel): ScriptStub {
this.level = value; this.level = value;
return this; return this;
} }

View File

@@ -10,6 +10,7 @@
"module": "esnext", "module": "esnext",
"jsx": "preserve", "jsx": "preserve",
"importHelpers": true, "importHelpers": true,
"downlevelIteration": true,
"moduleResolution": "node", "moduleResolution": "node",
"experimentalDecorators": true, "experimentalDecorators": true,
"esModuleInterop": true, "esModuleInterop": true,