diff --git a/src/application/Parser/Common/TypeValidator.ts b/src/application/Parser/Common/TypeValidator.ts index 05d13efa..78f79cf5 100644 --- a/src/application/Parser/Common/TypeValidator.ts +++ b/src/application/Parser/Common/TypeValidator.ts @@ -1,9 +1,12 @@ import type { PropertyKeys } from '@/TypeHelpers'; -import { isNullOrUndefined, isArray, isPlainObject } from '@/TypeHelpers'; +import { + isNullOrUndefined, isArray, isPlainObject, isString, +} from '@/TypeHelpers'; export interface TypeValidator { assertObject(assertion: ObjectAssertion): void; assertNonEmptyCollection(assertion: NonEmptyCollectionAssertion): void; + assertNonEmptyString(assertion: NonEmptyStringAssertion): void; } export interface NonEmptyCollectionAssertion { @@ -11,6 +14,17 @@ export interface NonEmptyCollectionAssertion { readonly valueName: string; } +export interface RegexValidationRule { + readonly expectedMatch: RegExp; + readonly errorMessage: string; +} + +export interface NonEmptyStringAssertion { + readonly value: unknown; + readonly valueName: string; + readonly rule?: RegexValidationRule; +} + export interface ObjectAssertion { readonly value: T | unknown; readonly valueName: string; @@ -33,6 +47,18 @@ export function createTypeValidator(): TypeValidator { assertArray(assertion.value, assertion.valueName); assertNonEmpty(assertion.value, assertion.valueName); }, + assertNonEmptyString: (assertion) => { + assertDefined(assertion.value, assertion.valueName); + assertString(assertion.value, assertion.valueName); + if (assertion.value.length === 0) { + throw new Error(`'${assertion.valueName}' is missing.`); + } + if (assertion.rule) { + if (!assertion.value.match(assertion.rule.expectedMatch)) { + throw new Error(assertion.rule.errorMessage); + } + } + }, }; } @@ -86,6 +112,15 @@ function assertArray( } } +function assertString( + value: unknown, + valueName: string, +): asserts value is string { + if (!isString(value)) { + throw new Error(`'${valueName}' should be of type 'string', but is of type '${typeof value}'.`); + } +} + function assertNonEmpty( value: Array, valueName: string, diff --git a/src/application/Parser/Executable/Script/Compiler/Expressions/Parser/Regex/RegexParser.ts b/src/application/Parser/Executable/Script/Compiler/Expressions/Parser/Regex/RegexParser.ts index aeaa53c0..f3055bb2 100644 --- a/src/application/Parser/Executable/Script/Compiler/Expressions/Parser/Regex/RegexParser.ts +++ b/src/application/Parser/Executable/Script/Compiler/Expressions/Parser/Regex/RegexParser.ts @@ -4,7 +4,7 @@ import { createPositionFromRegexFullMatch, type ExpressionPositionFactory } from import { createFunctionParameterCollection, type FunctionParameterCollectionFactory } from '../../../Function/Parameter/FunctionParameterCollectionFactory'; import type { IExpressionParser } from '../IExpressionParser'; import type { IExpression } from '../../Expression/IExpression'; -import type { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter'; +import type { FunctionParameter } from '../../../Function/Parameter/FunctionParameter'; import type { IFunctionParameterCollection, IReadOnlyFunctionParameterCollection } from '../../../Function/Parameter/IFunctionParameterCollection'; export interface RegexParserUtilities { @@ -110,7 +110,7 @@ function createParameters( export interface PrimitiveExpression { readonly evaluator: ExpressionEvaluator; - readonly parameters?: readonly IFunctionParameter[]; + readonly parameters?: readonly FunctionParameter[]; } export interface ExpressionFactory { diff --git a/src/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers/ParameterSubstitutionParser.ts b/src/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers/ParameterSubstitutionParser.ts index c9900ca6..dd9da3af 100644 --- a/src/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers/ParameterSubstitutionParser.ts +++ b/src/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers/ParameterSubstitutionParser.ts @@ -1,4 +1,3 @@ -import { FunctionParameter } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameter'; import { RegexParser, type PrimitiveExpression } from '../Parser/Regex/RegexParser'; import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder'; @@ -16,7 +15,10 @@ export class ParameterSubstitutionParser extends RegexParser { const parameterName = match[1]; const pipeline = match[2]; return { - parameters: [new FunctionParameter(parameterName, false)], + parameters: [{ + name: parameterName, + isOptional: false, + }], evaluator: (context) => { const { argumentValue } = context.args.getArgument(parameterName); if (!pipeline) { diff --git a/src/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers/WithParser.ts b/src/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers/WithParser.ts index d92244c2..19f251e3 100644 --- a/src/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers/WithParser.ts +++ b/src/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers/WithParser.ts @@ -1,7 +1,6 @@ // eslint-disable-next-line max-classes-per-file import type { IExpressionParser } from '@/application/Parser/Executable/Script/Compiler/Expressions/Parser/IExpressionParser'; import { FunctionParameterCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollection'; -import { FunctionParameter } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameter'; import { ExpressionPosition } from '../Expression/ExpressionPosition'; import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder'; import { createPositionFromRegexFullMatch } from '../Expression/ExpressionPositionFactory'; @@ -84,7 +83,10 @@ class WithStatementBuilder { public buildExpression(endExpressionPosition: ExpressionPosition, input: string): IExpression { const parameters = new FunctionParameterCollection(); - parameters.addParameter(new FunctionParameter(this.parameterName, true)); + parameters.addParameter({ + name: this.parameterName, + isOptional: true, + }); const position = new ExpressionPosition( this.startExpressionPosition.start, endExpressionPosition.end, diff --git a/src/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument.ts b/src/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument.ts index 59553003..4483f51c 100644 --- a/src/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument.ts +++ b/src/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument.ts @@ -1,14 +1,41 @@ -import { ensureValidParameterName } from '../../Shared/ParameterNameValidator'; -import type { IFunctionCallArgument } from './IFunctionCallArgument'; +import { createTypeValidator, type TypeValidator } from '@/application/Parser/Common/TypeValidator'; +import { validateParameterName, type ParameterNameValidator } from '@/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator'; -export class FunctionCallArgument implements IFunctionCallArgument { - constructor( - public readonly parameterName: string, - public readonly argumentValue: string, - ) { - ensureValidParameterName(parameterName); - if (!argumentValue) { - throw new Error(`Missing argument value for the parameter "${parameterName}".`); - } - } +export interface FunctionCallArgument { + readonly parameterName: string; + readonly argumentValue: string; } + +export interface FunctionCallArgumentFactory { + ( + parameterName: string, + argumentValue: string, + utilities?: FunctionCallArgumentFactoryUtilities, + ): FunctionCallArgument; +} + +export const createFunctionCallArgument: FunctionCallArgumentFactory = ( + parameterName: string, + argumentValue: string, + utilities: FunctionCallArgumentFactoryUtilities = DefaultUtilities, +): FunctionCallArgument => { + utilities.validateParameterName(parameterName); + utilities.typeValidator.assertNonEmptyString({ + value: argumentValue, + valueName: `Missing argument value for the parameter "${parameterName}".`, + }); + return { + parameterName, + argumentValue, + }; +}; + +interface FunctionCallArgumentFactoryUtilities { + readonly typeValidator: TypeValidator; + readonly validateParameterName: ParameterNameValidator; +} + +const DefaultUtilities: FunctionCallArgumentFactoryUtilities = { + typeValidator: createTypeValidator(), + validateParameterName, +}; diff --git a/src/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection.ts b/src/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection.ts index e9fffd5a..e11fe90e 100644 --- a/src/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection.ts +++ b/src/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection.ts @@ -1,10 +1,10 @@ -import type { IFunctionCallArgument } from './IFunctionCallArgument'; +import type { FunctionCallArgument } from './FunctionCallArgument'; import type { IFunctionCallArgumentCollection } from './IFunctionCallArgumentCollection'; export class FunctionCallArgumentCollection implements IFunctionCallArgumentCollection { - private readonly arguments = new Map(); + private readonly arguments = new Map(); - public addArgument(argument: IFunctionCallArgument): void { + public addArgument(argument: FunctionCallArgument): void { if (this.hasArgument(argument.parameterName)) { throw new Error(`argument value for parameter ${argument.parameterName} is already provided`); } @@ -22,7 +22,7 @@ export class FunctionCallArgumentCollection implements IFunctionCallArgumentColl return this.arguments.has(parameterName); } - public getArgument(parameterName: string): IFunctionCallArgument { + public getArgument(parameterName: string): FunctionCallArgument { if (!parameterName) { throw new Error('missing parameter name'); } diff --git a/src/application/Parser/Executable/Script/Compiler/Function/Call/Argument/IFunctionCallArgument.ts b/src/application/Parser/Executable/Script/Compiler/Function/Call/Argument/IFunctionCallArgument.ts deleted file mode 100644 index 99190d01..00000000 --- a/src/application/Parser/Executable/Script/Compiler/Function/Call/Argument/IFunctionCallArgument.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface IFunctionCallArgument { - readonly parameterName: string; - readonly argumentValue: string; -} diff --git a/src/application/Parser/Executable/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection.ts b/src/application/Parser/Executable/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection.ts index f3f76197..8ae2c938 100644 --- a/src/application/Parser/Executable/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection.ts +++ b/src/application/Parser/Executable/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection.ts @@ -1,11 +1,11 @@ -import type { IFunctionCallArgument } from './IFunctionCallArgument'; +import type { FunctionCallArgument } from './FunctionCallArgument'; export interface IReadOnlyFunctionCallArgumentCollection { - getArgument(parameterName: string): IFunctionCallArgument; + getArgument(parameterName: string): FunctionCallArgument; getAllParameterNames(): string[]; hasArgument(parameterName: string): boolean; } export interface IFunctionCallArgumentCollection extends IReadOnlyFunctionCallArgumentCollection { - addArgument(argument: IFunctionCallArgument): void; + addArgument(argument: FunctionCallArgument): void; } diff --git a/src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler.ts b/src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler.ts index f1ba6c04..20c77fbb 100644 --- a/src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler.ts +++ b/src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler.ts @@ -1,5 +1,4 @@ import type { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection'; -import { FunctionCallArgument } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; import { FunctionCallArgumentCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection'; import { ExpressionsCompiler } from '@/application/Parser/Executable/Script/Compiler/Expressions/ExpressionsCompiler'; import type { IExpressionsCompiler } from '@/application/Parser/Executable/Script/Compiler/Expressions/IExpressionsCompiler'; @@ -7,14 +6,11 @@ import type { FunctionCall } from '@/application/Parser/Executable/Script/Compil import type { FunctionCallCompilationContext } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext'; import { ParsedFunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/ParsedFunctionCall'; import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError'; +import { createFunctionCallArgument, type FunctionCallArgument, type FunctionCallArgumentFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; import type { ArgumentCompiler } from './ArgumentCompiler'; export class NestedFunctionArgumentCompiler implements ArgumentCompiler { - constructor( - private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(), - private readonly wrapError: ErrorWithContextWrapper - = wrapErrorWithAdditionalContext, - ) { } + constructor(private readonly utilities: ArgumentCompilationUtilities = DefaultUtilities) { } public createCompiledNestedCall( nestedFunction: FunctionCall, @@ -25,10 +21,7 @@ export class NestedFunctionArgumentCompiler implements ArgumentCompiler { nestedFunction, parentFunction.args, context, - { - expressionsCompiler: this.expressionsCompiler, - wrapError: this.wrapError, - }, + this.utilities, ); const compiledCall = new ParsedFunctionCall(nestedFunction.functionName, compiledArgs); return compiledCall; @@ -38,6 +31,7 @@ export class NestedFunctionArgumentCompiler implements ArgumentCompiler { interface ArgumentCompilationUtilities { readonly expressionsCompiler: IExpressionsCompiler, readonly wrapError: ErrorWithContextWrapper; + readonly createCallArgument: FunctionCallArgumentFactory; } function compileNestedFunctionArguments( @@ -78,7 +72,7 @@ function compileNestedFunctionArguments( .map(({ parameterName, compiledArgumentValue, - }) => new FunctionCallArgument(parameterName, compiledArgumentValue)); + }) => utilities.createCallArgument(parameterName, compiledArgumentValue)); return buildArgumentCollectionFromArguments(compiledArguments); } @@ -118,3 +112,9 @@ function buildArgumentCollectionFromArguments( return compiledArgs; }, new FunctionCallArgumentCollection()); } + +const DefaultUtilities: ArgumentCompilationUtilities = { + expressionsCompiler: new ExpressionsCompiler(), + wrapError: wrapErrorWithAdditionalContext, + createCallArgument: createFunctionCallArgument, +}; diff --git a/src/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser.ts b/src/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser.ts index 3c8c54c4..d7dba7e3 100644 --- a/src/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser.ts +++ b/src/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser.ts @@ -1,24 +1,38 @@ -import type { FunctionCallData, FunctionCallsData, FunctionCallParametersData } from '@/application/collections/'; +import type { + FunctionCallData, + FunctionCallsData, + FunctionCallParametersData, +} from '@/application/collections/'; import { isArray, isPlainObject } from '@/TypeHelpers'; import { createTypeValidator, type TypeValidator } from '@/application/Parser/Common/TypeValidator'; import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection'; -import { FunctionCallArgument } from './Argument/FunctionCallArgument'; import { ParsedFunctionCall } from './ParsedFunctionCall'; +import { createFunctionCallArgument, type FunctionCallArgumentFactory } from './Argument/FunctionCallArgument'; import type { FunctionCall } from './FunctionCall'; export interface FunctionCallsParser { ( calls: FunctionCallsData, - validator?: TypeValidator, + utilities?: FunctionCallParsingUtilities, ): FunctionCall[]; } +interface FunctionCallParsingUtilities { + readonly typeValidator: TypeValidator; + readonly createCallArgument: FunctionCallArgumentFactory; +} + +const DefaultUtilities: FunctionCallParsingUtilities = { + typeValidator: createTypeValidator(), + createCallArgument: createFunctionCallArgument, +}; + export const parseFunctionCalls: FunctionCallsParser = ( calls, - validator = createTypeValidator(), + utilities = DefaultUtilities, ) => { - const sequence = getCallSequence(calls, validator); - return sequence.map((call) => parseFunctionCall(call, validator)); + const sequence = getCallSequence(calls, utilities.typeValidator); + return sequence.map((call) => parseFunctionCall(call, utilities)); }; function getCallSequence(calls: FunctionCallsData, validator: TypeValidator): FunctionCallData[] { @@ -38,23 +52,27 @@ function getCallSequence(calls: FunctionCallsData, validator: TypeValidator): Fu function parseFunctionCall( call: FunctionCallData, - validator: TypeValidator, + utilities: FunctionCallParsingUtilities, ): FunctionCall { - validator.assertObject({ + utilities.typeValidator.assertObject({ value: call, valueName: 'function call', allowedProperties: ['function', 'parameters'], }); - const callArgs = parseArgs(call.parameters); + const callArgs = parseArgs(call.parameters, utilities.createCallArgument); return new ParsedFunctionCall(call.function, callArgs); } function parseArgs( parameters: FunctionCallParametersData | undefined, + createArgument: FunctionCallArgumentFactory, ): FunctionCallArgumentCollection { const parametersMap = parameters ?? {}; return Object.keys(parametersMap) - .map((parameterName) => new FunctionCallArgument(parameterName, parametersMap[parameterName])) + .map((parameterName) => { + const argumentValue = parametersMap[parameterName]; + return createArgument(parameterName, argumentValue); + }) .reduce((args, arg) => { args.addArgument(arg); return args; diff --git a/src/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameter.ts b/src/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameter.ts index a2775c81..0529766a 100644 --- a/src/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameter.ts +++ b/src/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameter.ts @@ -1,11 +1,4 @@ -import { ensureValidParameterName } from '../Shared/ParameterNameValidator'; -import type { IFunctionParameter } from './IFunctionParameter'; - -export class FunctionParameter implements IFunctionParameter { - constructor( - public readonly name: string, - public readonly isOptional: boolean, - ) { - ensureValidParameterName(name); - } +export interface FunctionParameter { + readonly name: string; + readonly isOptional: boolean; } diff --git a/src/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollection.ts b/src/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollection.ts index ce3b9e5c..bae48614 100644 --- a/src/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollection.ts +++ b/src/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollection.ts @@ -1,14 +1,14 @@ import type { IFunctionParameterCollection } from './IFunctionParameterCollection'; -import type { IFunctionParameter } from './IFunctionParameter'; +import type { FunctionParameter } from './FunctionParameter'; export class FunctionParameterCollection implements IFunctionParameterCollection { - private parameters = new Array(); + private parameters = new Array(); - public get all(): readonly IFunctionParameter[] { + public get all(): readonly FunctionParameter[] { return this.parameters; } - public addParameter(parameter: IFunctionParameter) { + public addParameter(parameter: FunctionParameter) { this.ensureValidParameter(parameter); this.parameters.push(parameter); } @@ -17,7 +17,7 @@ export class FunctionParameterCollection implements IFunctionParameterCollection return this.parameters.find((existingParameter) => existingParameter.name === name); } - private ensureValidParameter(parameter: IFunctionParameter) { + private ensureValidParameter(parameter: FunctionParameter) { if (this.includesName(parameter.name)) { throw new Error(`duplicate parameter name: "${parameter.name}"`); } diff --git a/src/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterParser.ts b/src/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterParser.ts new file mode 100644 index 00000000..0453a13a --- /dev/null +++ b/src/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterParser.ts @@ -0,0 +1,21 @@ +import type { ParameterDefinitionData } from '@/application/collections/'; +import { validateParameterName, type ParameterNameValidator } from '../Shared/ParameterNameValidator'; +import type { FunctionParameter } from './FunctionParameter'; + +export interface FunctionParameterParser { + ( + data: ParameterDefinitionData, + validator?: ParameterNameValidator, + ): FunctionParameter; +} + +export const parseFunctionParameter: FunctionParameterParser = ( + data, + validator = validateParameterName, +) => { + validator(data.name); + return { + name: data.name, + isOptional: data.optional || false, + }; +}; diff --git a/src/application/Parser/Executable/Script/Compiler/Function/Parameter/IFunctionParameter.ts b/src/application/Parser/Executable/Script/Compiler/Function/Parameter/IFunctionParameter.ts deleted file mode 100644 index b012abed..00000000 --- a/src/application/Parser/Executable/Script/Compiler/Function/Parameter/IFunctionParameter.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface IFunctionParameter { - readonly name: string; - readonly isOptional: boolean; -} diff --git a/src/application/Parser/Executable/Script/Compiler/Function/Parameter/IFunctionParameterCollection.ts b/src/application/Parser/Executable/Script/Compiler/Function/Parameter/IFunctionParameterCollection.ts index c024d0ed..ecbe06b4 100644 --- a/src/application/Parser/Executable/Script/Compiler/Function/Parameter/IFunctionParameterCollection.ts +++ b/src/application/Parser/Executable/Script/Compiler/Function/Parameter/IFunctionParameterCollection.ts @@ -1,9 +1,9 @@ -import type { IFunctionParameter } from './IFunctionParameter'; +import type { FunctionParameter } from './FunctionParameter'; export interface IReadOnlyFunctionParameterCollection { - readonly all: readonly IFunctionParameter[]; + readonly all: readonly FunctionParameter[]; } export interface IFunctionParameterCollection extends IReadOnlyFunctionParameterCollection { - addParameter(parameter: IFunctionParameter): void; + addParameter(parameter: FunctionParameter): void; } diff --git a/src/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator.ts b/src/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator.ts index a5490bb3..48a3a2f3 100644 --- a/src/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator.ts +++ b/src/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator.ts @@ -1,8 +1,22 @@ -export function ensureValidParameterName(parameterName: string) { - if (!parameterName) { - throw new Error('missing parameter name'); - } - if (!parameterName.match(/^[0-9a-zA-Z]+$/)) { - throw new Error(`parameter name must be alphanumeric but it was "${parameterName}"`); - } +import { createTypeValidator, type TypeValidator } from '@/application/Parser/Common/TypeValidator'; + +export interface ParameterNameValidator { + ( + parameterName: string, + typeValidator?: TypeValidator, + ): void; } + +export const validateParameterName = ( + parameterName: string, + typeValidator = createTypeValidator(), +) => { + typeValidator.assertNonEmptyString({ + value: parameterName, + valueName: 'parameter name', + rule: { + expectedMatch: /^[0-9a-zA-Z]+$/, + errorMessage: `parameter name must be alphanumeric but it was "${parameterName}".`, + }, + }); +}; diff --git a/src/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser.ts b/src/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser.ts index 60f9dde0..934b4cdf 100644 --- a/src/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser.ts +++ b/src/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser.ts @@ -11,9 +11,10 @@ import { isArray, isNullOrUndefined, isPlainObject } from '@/TypeHelpers'; import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError'; import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction'; import { SharedFunctionCollection } from './SharedFunctionCollection'; -import { FunctionParameter } from './Parameter/FunctionParameter'; import { parseFunctionCalls, type FunctionCallsParser } from './Call/FunctionCallsParser'; import { createFunctionParameterCollection, type FunctionParameterCollectionFactory } from './Parameter/FunctionParameterCollectionFactory'; +import { parseFunctionParameter, type FunctionParameterParser } from './Parameter/FunctionParameterParser'; +import type { FunctionParameter } from './Parameter/FunctionParameter'; import type { ISharedFunctionCollection } from './ISharedFunctionCollection'; import type { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection'; import type { ISharedFunction } from './ISharedFunction'; @@ -46,7 +47,7 @@ export const parseSharedFunctions: SharedFunctionsParser = ( const DefaultUtilities: SharedFunctionsParsingUtilities = { wrapError: wrapErrorWithAdditionalContext, - createParameter: (...args) => new FunctionParameter(...args), + parseParameter: parseFunctionParameter, codeValidator: CodeValidator.instance, createParameterCollection: createFunctionParameterCollection, parseFunctionCalls, @@ -54,16 +55,12 @@ const DefaultUtilities: SharedFunctionsParsingUtilities = { interface SharedFunctionsParsingUtilities { readonly wrapError: ErrorWithContextWrapper; - readonly createParameter: FunctionParameterFactory; + readonly parseParameter: FunctionParameterParser; readonly codeValidator: ICodeValidator; readonly createParameterCollection: FunctionParameterCollectionFactory; readonly parseFunctionCalls: FunctionCallsParser; } -export type FunctionParameterFactory = ( - ...args: ConstructorParameters -) => FunctionParameter; - function parseFunction( data: FunctionData, syntax: ILanguageSyntax, @@ -100,7 +97,7 @@ function parseParameters( utilities: SharedFunctionsParsingUtilities, ): IReadOnlyFunctionParameterCollection { return (data.parameters || []) - .map((parameter) => createFunctionParameter( + .map((parameter) => parseParameterWithContextualError( data.name, parameter, utilities, @@ -111,16 +108,13 @@ function parseParameters( }, utilities.createParameterCollection()); } -function createFunctionParameter( +function parseParameterWithContextualError( functionName: string, parameterData: ParameterDefinitionData, utilities: SharedFunctionsParsingUtilities, ): FunctionParameter { try { - return utilities.createParameter( - parameterData.name, - parameterData.optional || false, - ); + return utilities.parseParameter(parameterData); } catch (err) { throw utilities.wrapError( err, diff --git a/src/application/Parser/ScriptingDefinition/CodeSubstituter.ts b/src/application/Parser/ScriptingDefinition/CodeSubstituter.ts index 52b3257b..20f52ec9 100644 --- a/src/application/Parser/ScriptingDefinition/CodeSubstituter.ts +++ b/src/application/Parser/ScriptingDefinition/CodeSubstituter.ts @@ -4,31 +4,33 @@ import { CompositeExpressionParser } from '@/application/Parser/Executable/Scrip import { ExpressionsCompiler } from '@/application/Parser/Executable/Script/Compiler/Expressions/ExpressionsCompiler'; import type { ProjectDetails } from '@/domain/Project/ProjectDetails'; import { FunctionCallArgumentCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection'; -import { FunctionCallArgument } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; import type { IExpressionParser } from '@/application/Parser/Executable/Script/Compiler/Expressions/Parser/IExpressionParser'; -import type { ICodeSubstituter } from './ICodeSubstituter'; +import { createFunctionCallArgument, type FunctionCallArgumentFactory } from '../Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; -export class CodeSubstituter implements ICodeSubstituter { - constructor( - private readonly compiler: IExpressionsCompiler = createSubstituteCompiler(), - private readonly date = new Date(), - ) { - - } - - public substitute(code: string, projectDetails: ProjectDetails): string { - if (!code) { throw new Error('missing code'); } - const args = new FunctionCallArgumentCollection(); - const substitute = (name: string, value: string) => args - .addArgument(new FunctionCallArgument(name, value)); - substitute('homepage', projectDetails.homepage); - substitute('version', projectDetails.version.toString()); - substitute('date', this.date.toUTCString()); - const compiledCode = this.compiler.compileExpressions(code, args); - return compiledCode; - } +export interface CodeSubstituter { + ( + code: string, + projectDetails: ProjectDetails, + utilities?: CodeSubstitutionUtilities, + ): string; } +export const substituteCode: CodeSubstituter = ( + code, + projectDetails, + utilities = DefaultUtilities, +) => { + if (!code) { throw new Error('missing code'); } + const args = new FunctionCallArgumentCollection(); + const substitute = (name: string, value: string) => args + .addArgument(utilities.createCallArgument(name, value)); + substitute('homepage', projectDetails.homepage); + substitute('version', projectDetails.version.toString()); + substitute('date', utilities.provideDate().toUTCString()); + const compiledCode = utilities.compiler.compileExpressions(code, args); + return compiledCode; +}; + function createSubstituteCompiler(): IExpressionsCompiler { const parsers: readonly IExpressionParser[] = [ new ParameterSubstitutionParser(), @@ -37,3 +39,15 @@ function createSubstituteCompiler(): IExpressionsCompiler { const expressionCompiler = new ExpressionsCompiler(parser); return expressionCompiler; } + +interface CodeSubstitutionUtilities { + readonly compiler: IExpressionsCompiler; + readonly provideDate: () => Date; + readonly createCallArgument: FunctionCallArgumentFactory; +} + +const DefaultUtilities: CodeSubstitutionUtilities = { + compiler: createSubstituteCompiler(), + provideDate: () => new Date(), + createCallArgument: createFunctionCallArgument, +}; diff --git a/src/application/Parser/ScriptingDefinition/ICodeSubstituter.ts b/src/application/Parser/ScriptingDefinition/ICodeSubstituter.ts deleted file mode 100644 index b3d624e9..00000000 --- a/src/application/Parser/ScriptingDefinition/ICodeSubstituter.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { ProjectDetails } from '@/domain/Project/ProjectDetails'; - -export interface ICodeSubstituter { - substitute(code: string, projectDetails: ProjectDetails): string; -} diff --git a/src/application/Parser/ScriptingDefinition/ScriptingDefinitionParser.ts b/src/application/Parser/ScriptingDefinition/ScriptingDefinitionParser.ts index 49ccf9f6..baf065c1 100644 --- a/src/application/Parser/ScriptingDefinition/ScriptingDefinitionParser.ts +++ b/src/application/Parser/ScriptingDefinition/ScriptingDefinitionParser.ts @@ -5,8 +5,7 @@ import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import type { ProjectDetails } from '@/domain/Project/ProjectDetails'; import { createEnumParser, type EnumParser } from '../../Common/Enum'; import { createTypeValidator, type TypeValidator } from '../Common/TypeValidator'; -import { CodeSubstituter } from './CodeSubstituter'; -import type { ICodeSubstituter } from './ICodeSubstituter'; +import { type CodeSubstituter, substituteCode } from './CodeSubstituter'; export const parseScriptingDefinition: ScriptingDefinitionParser = ( definition, @@ -15,8 +14,8 @@ export const parseScriptingDefinition: ScriptingDefinitionParser = ( ) => { validateData(definition, utilities.validator); const language = utilities.languageParser.parseEnum(definition.language, 'language'); - const startCode = utilities.codeSubstituter.substitute(definition.startCode, projectDetails); - const endCode = utilities.codeSubstituter.substitute(definition.endCode, projectDetails); + const startCode = utilities.codeSubstituter(definition.startCode, projectDetails); + const endCode = utilities.codeSubstituter(definition.endCode, projectDetails); return new ScriptingDefinition( language, startCode, @@ -45,12 +44,12 @@ function validateData( interface ScriptingDefinitionParserUtilities { readonly languageParser: EnumParser; - readonly codeSubstituter: ICodeSubstituter; + readonly codeSubstituter: CodeSubstituter; readonly validator: TypeValidator; } const DefaultUtilities: ScriptingDefinitionParserUtilities = { languageParser: createEnumParser(ScriptingLanguage), - codeSubstituter: new CodeSubstituter(), + codeSubstituter: substituteCode, validator: createTypeValidator(), }; diff --git a/src/application/collections/windows.yaml b/src/application/collections/windows.yaml index fe11ebbd..57213eae 100644 --- a/src/application/collections/windows.yaml +++ b/src/application/collections/windows.yaml @@ -552,7 +552,7 @@ actions: function: ClearDirectoryContents parameters: directoryGlob: '%USERPROFILE%\Local Settings\Temporary Internet Files' - grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 📂 Unprotected on Windows 11 since 22H2 + grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 📂 Unprotected on Windows 11 since 22H2 - function: ClearDirectoryContents parameters: @@ -564,7 +564,7 @@ actions: # - C:\Users\undergroundwires\AppData\Local\Microsoft\Windows\Temporary Internet Files\Virtualized # Since Windows 10 22H2 and Windows 11 22H2, data files are observed in this subdirectories but not on the parent. # Especially in `IE` folder includes many files. These folders are protected and hidden by default. - grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 + grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 - function: ClearDirectoryContents parameters: @@ -573,7 +573,7 @@ actions: function: ClearDirectoryContents parameters: directoryGlob: '%LOCALAPPDATA%\Temporary Internet Files' - grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 + grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 - name: Clear Internet Explorer feeds cache recommend: standard @@ -1217,7 +1217,7 @@ actions: function: StopService parameters: serviceName: wuauserv - waitUntilStopped: true + waitUntilStopped: 'true' serviceRestartStateFile: '%APPDATA%\privacy.sexy-wuauserv' # Marked: refactor-with-variables (app dir should be unified, not using %TEMP% as it can be cleaned during operation) - function: ClearDirectoryContents @@ -1303,18 +1303,18 @@ actions: function: StopService parameters: serviceName: DiagTrack - waitUntilStopped: true + waitUntilStopped: 'true' serviceRestartStateFile: '%APPDATA%\privacy.sexy-DiagTrack' # Marked: refactor-with-variables (app dir should be unified, not using %TEMP% as it can be cleaned during operation) - function: DeleteFiles parameters: fileGlob: '%PROGRAMDATA%\Microsoft\Diagnosis\ETLLogs\AutoLogger\AutoLogger-Diagtrack-Listener.etl' - grantPermissions: true + grantPermissions: 'true' - function: DeleteFiles parameters: fileGlob: '%PROGRAMDATA%\Microsoft\Diagnosis\ETLLogs\ShutdownLogger\AutoLogger-Diagtrack-Listener.etl' - grantPermissions: true + grantPermissions: 'true' - function: StartService parameters: @@ -1371,7 +1371,7 @@ actions: function: ClearDirectoryContents # Otherwise it cannot access/delete files under `Scans\History`, see https://github.com/undergroundwires/privacy.sexy/issues/246 parameters: directoryGlob: '%ProgramData%\Microsoft\Windows Defender\Scans\History' - grantPermissions: true # Running as TrustedInstaller is not needed, and causes Defender to alarm https://github.com/undergroundwires/privacy.sexy/issues/264 + grantPermissions: 'true' # Running as TrustedInstaller is not needed, and causes Defender to alarm https://github.com/undergroundwires/privacy.sexy/issues/264 - name: Clear credentials in Windows Credential Manager call: @@ -1508,13 +1508,13 @@ actions: function: StopService parameters: serviceName: DPS - waitUntilStopped: true + waitUntilStopped: 'true' serviceRestartStateFile: '%APPDATA%\privacy.sexy-DPS' # Marked: refactor-with-variables (app dir should be unified, not using %TEMP% as it can be cleaned during operation) - function: DeleteFiles parameters: fileGlob: '%WINDIR%\System32\sru\SRUDB.dat' - grantPermissions: true + grantPermissions: 'true' - function: StartService parameters: @@ -1526,7 +1526,7 @@ actions: function: DeleteDirectory parameters: directoryGlob: '%SYSTEMDRIVE%\Windows.old' - grantPermissions: true + grantPermissions: 'true' - category: Disable OS data collection children: @@ -2813,7 +2813,7 @@ actions: function: SoftDeleteFiles parameters: fileGlob: '%WINDIR%\System32\CompatTelRunner.exe' - grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 23H2 + grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 23H2 - category: Disable background application compatibility checks (Application Experience scheduled tasks) docs: |- @@ -3037,7 +3037,7 @@ actions: # Check: Get-ScheduledTask -TaskPath '\Microsoft\Windows\Application Experience\' -TaskName 'SdbinstMergeDbTask' taskPathPattern: \Microsoft\Windows\Application Experience\ taskNamePattern: SdbinstMergeDbTask - grantPermissions: true # 🔒 No permissions, tested since [≥ Windows 11 23H2] + grantPermissions: 'true' # 🔒 No permissions, tested since [≥ Windows 11 23H2] - name: Disable application backup data gathering (`MareBackup`) recommend: strict @@ -5985,12 +5985,12 @@ actions: function: SoftDeleteFiles parameters: fileGlob: '%PROGRAMFILES(X86)%\NVIDIA Corporation\NvTelemetry\*' - recurse: true + recurse: 'true' - function: SoftDeleteFiles parameters: fileGlob: '%PROGRAMFILES%\NVIDIA Corporation\NvTelemetry\*' - recurse: true + recurse: 'true' - name: Disable Nvidia telemetry drivers recommend: standard @@ -5998,7 +5998,7 @@ actions: function: SoftDeleteFiles parameters: fileGlob: '%SYSTEMROOT%\System32\DriverStore\FileRepository\NvTelemetry*.dll' - recurse: true + recurse: 'true' - name: Disable participation in Nvidia telemetry recommend: standard @@ -7047,7 +7047,7 @@ actions: parameters: serviceName: adobeupdateservice # Check: (Get-Service -Name adobeupdateservice).StartType defaultStartupMode: Automatic # Allowed values: Automatic | Manual - ignoreMissingOnRevert: true + ignoreMissingOnRevert: 'true' - name: Disable "Adobe Acrobat Update Task" scheduled task recommend: standard @@ -7091,7 +7091,7 @@ actions: parameters: serviceName: Razer Game Scanner Service # Check: (Get-Service -Name 'Razer Game Scanner Service').StartType defaultStartupMode: Manual # Allowed values: Automatic | Manual - ignoreMissingOnRevert: true + ignoreMissingOnRevert: 'true' - name: Disable "Logitech Gaming Registry Service" recommend: standard @@ -7415,7 +7415,7 @@ actions: function: RequireTLSMinimumKeySize parameters: algorithmName: Diffie-Hellman - keySizeInBits: 2048 + keySizeInBits: '2048' - name: Enable strong RSA key requirement (breaks Hyper-V VMs) recommend: strict # Microsoft deprecated it and will end support; but breaks Hyper-V VMs, see #363 @@ -7467,8 +7467,8 @@ actions: function: RequireTLSMinimumKeySize parameters: algorithmName: PKCS - keySizeInBits: 2048 - ignoreServerSide: true # Controlled by the specified server certificate + keySizeInBits: '2048' + ignoreServerSide: 'true' # Controlled by the specified server certificate - category: Disable insecure connections docs: |- # refactor-with-variables: Same • Caution @@ -8140,23 +8140,23 @@ actions: function: DisableWindowsFeature parameters: featureName: SMB1Protocol # Get-WindowsOptionalFeature -FeatureName 'SMB1Protocol' -Online - disabledByDefault: true + disabledByDefault: 'true' - function: DisableWindowsFeature parameters: featureName: SMB1Protocol-Client # Get-WindowsOptionalFeature -FeatureName 'SMB1Protocol-Client' -Online - disabledByDefault: true + disabledByDefault: 'true' - function: DisableWindowsFeature parameters: featureName: SMB1Protocol-Server # Get-WindowsOptionalFeature -FeatureName 'SMB1Protocol-Server' -Online - disabledByDefault: true + disabledByDefault: 'true' - function: DisableService parameters: serviceName: mrxsmb10 # Check: (Get-Service -Name 'mrxsmb10').StartType defaultStartupMode: Automatic # Allowed values: Boot | System | Automatic | Manual - ignoreMissingOnRevert: true # This service is only available when SMB1 feature is installed + ignoreMissingOnRevert: 'true' # This service is only available when SMB1 feature is installed - function: RunInlineCode # This ensures that `lanmanworkstation` does not depend on `mrxsmb10` to avoid potential system issues. @@ -8809,7 +8809,7 @@ actions: function: DisableWindowsFeature parameters: featureName: TelnetClient # Get-WindowsOptionalFeature -FeatureName 'TelnetClient' -Online - disabledByDefault: true + disabledByDefault: 'true' - name: Remove "RAS Connection Manager Administration Kit (CMAK)" capability docs: |- # refactor-with-variables: Same • Caution @@ -9005,7 +9005,7 @@ actions: function: DisableWindowsFeature parameters: featureName: TFTP # Get-WindowsOptionalFeature -FeatureName 'TFTP' -Online - disabledByDefault: true + disabledByDefault: 'true' - name: Remove "RIP Listener" capability docs: |- # refactor-with-variables: *Caution** @@ -10456,7 +10456,7 @@ actions: function: SoftDeleteFiles parameters: fileGlob: '%SYSTEMROOT%\System32\drivers\mpsdrv.sys' - grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 + grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 - function: ShowComputerRestartSuggestion - @@ -10527,7 +10527,7 @@ actions: function: SoftDeleteFiles parameters: fileGlob: '%WINDIR%\System32\mpssvc.dll' - grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 + grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 - function: ShowComputerRestartSuggestion - @@ -10783,7 +10783,7 @@ actions: property: SubmitSamplesConsent # Status: Get-MpPreference | Select-Object -Property SubmitSamplesConsent value: "'2'" # Set: Set-MpPreference -Force -SubmitSamplesConsent 2 default: "'1'" # Default: 1 (Send safe samples automatically) | Remove-MpPreference -Force -SubmitSamplesConsent | Set-MpPreference -Force -SubmitSamplesConsent 1 - setDefaultOnWindows11: true # `Remove-MpPreference` sets it to 0 instead 1 (OS default) in Windows 11 + setDefaultOnWindows11: 'true' # `Remove-MpPreference` sets it to 0 instead 1 (OS default) in Windows 11 - function: SetRegistryValue parameters: @@ -11276,7 +11276,7 @@ actions: property: QuarantinePurgeItemsAfterDelay # Status: Get-MpPreference | Select-Object -Property QuarantinePurgeItemsAfterDelay value: "'1'" # Set: Set-MpPreference -Force -QuarantinePurgeItemsAfterDelay 1 default: "'90'" # Default: 90 | Remove-MpPreference -Force -QuarantinePurgeItemsAfterDelay | Set-MpPreference -Force -QuarantinePurgeItemsAfterDelay 90 - setDefaultOnWindows11: true # `Remove-MpPreference` sets it to 0 instead 90 (OS default) in Windows 11 + setDefaultOnWindows11: 'true' # `Remove-MpPreference` sets it to 0 instead 90 (OS default) in Windows 11 - function: SetRegistryValue parameters: @@ -11447,7 +11447,7 @@ actions: property: DisableAutoExclusions # Status: Get-MpPreference | Select-Object -Property DisableAutoExclusions value: $True # Set: Set-MpPreference -Force -DisableAutoExclusions $True default: $False # Default: False | Remove-MpPreference -Force -DisableAutoExclusions | Set-MpPreference -Force -DisableAutoExclusions $False - setDefaultOnWindows11: true # `Remove-MpPreference` has no affect (does not change the value) in Windows 11 + setDefaultOnWindows11: 'true' # `Remove-MpPreference` has no affect (does not change the value) in Windows 11 - function: SetRegistryValue parameters: @@ -11935,10 +11935,10 @@ actions: - function: SetMpPreference parameters: - property: ScanParameters # Status: Get-MpPreference | Select-Object -Property ScanParameters - value: "'1'" # Set: Set-MpPreference -Force -ScanParameters '1' - default: "'1'" # Default: 1 | Remove-MpPreference -Force -ScanParameters | Set-MpPreference -Force -ScanParameters '1' - setDefaultOnWindows11: true # ❌ Remove-MpPreference with -ScanParameters fails due to a buggy behavior where it tries to set it to True on Windows 11 + property: ScanParameters # Status: Get-MpPreference | Select-Object -Property ScanParameters + value: "'1'" # Set: Set-MpPreference -Force -ScanParameters '1' + default: "'1'" # Default: 1 | Remove-MpPreference -Force -ScanParameters | Set-MpPreference -Force -ScanParameters '1' + setDefaultOnWindows11: 'true' # ❌ Remove-MpPreference with -ScanParameters fails due to a buggy behavior where it tries to set it to True on Windows 11 - name: Minimize daily quick scan frequency docs: https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefender::Scan_QuickScanInterval @@ -12819,7 +12819,7 @@ actions: # function: SoftDeleteFiles # parameters: # fileGlob: '%PROGRAMFILES%\Windows Defender\MsMpEng.exe' # Found also in C:\ProgramData\Microsoft\Windows Defender\Platform\4.18.2107.4-0 and \4.18.2103.7-0 ... - # grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 + # grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 - category: Disable Defender kernel-level drivers children: @@ -12840,7 +12840,7 @@ actions: function: SoftDeleteFiles parameters: fileGlob: '%SYSTEMROOT%\System32\drivers\WdNisDrv.sys' - grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 + grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 - name: Disable "Microsoft Defender Antivirus Mini-Filter Driver" service docs: @@ -12858,7 +12858,7 @@ actions: function: SoftDeleteFiles parameters: fileGlob: '%SYSTEMROOT%\System32\drivers\WdFilter.sys' - grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 + grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 - name: Disable "Microsoft Defender Antivirus Boot Driver" service docs: https://web.archive.org/web/20240314062057/https://batcmd.com/windows/10/services/wdboot/ @@ -12874,7 +12874,7 @@ actions: function: SoftDeleteFiles parameters: fileGlob: '%SYSTEMROOT%\System32\drivers\WdBoot.sys' - grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 + grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 - name: Disable "Microsoft Defender Antivirus Network Inspection" service docs: @@ -12890,7 +12890,7 @@ actions: # function: SoftDeleteFiles # parameters: # fileGlob: '%PROGRAMFILES%\Windows Defender\NisSrv.exe' # Found also in C:\ProgramData\Microsoft\Windows Defender\Platform\4.18.2107.4-0 and \4.18.2103.7-0 ... - # grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 + # grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 - name: Disable "Windows Defender Advanced Threat Protection Service" service docs: https://web.archive.org/web/20240314091443/https://batcmd.com/windows/10/services/sense/ @@ -12904,7 +12904,7 @@ actions: function: SoftDeleteFiles parameters: fileGlob: '%PROGRAMFILES%\Windows Defender Advanced Threat Protection\MsSense.exe' - grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 + grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 - name: Disable "Windows Security Service" service docs: |- @@ -12939,7 +12939,7 @@ actions: function: SoftDeleteFiles parameters: fileGlob: '%WINDIR%\System32\SecurityHealthService.exe' - grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 + grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 - category: Disable SmartScreen docs: @@ -13411,7 +13411,7 @@ actions: function: SoftDeleteFiles parameters: fileGlob: '%SYSTEMROOT%\System32\WaaSMedicSvc.dll' - grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 23H2 + grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 23H2 - function: TerminateAndBlockExecution parameters: @@ -13420,38 +13420,38 @@ actions: function: SoftDeleteFiles parameters: fileGlob: '%SYSTEMROOT%\System32\WaaSMedicAgent.exe' - grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔍 Missing on Windows 11 since 23H2 + grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔍 Missing on Windows 11 since 23H2 - function: SoftDeleteFiles parameters: fileGlob: '%SYSTEMROOT%\System32\WaaSMedicCapsule.dll' - grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔍 Missing on Windows 11 since 23H2 + grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔍 Missing on Windows 11 since 23H2 - function: SoftDeleteFiles parameters: fileGlob: '%SYSTEMROOT%\System32\WaaSMedicPS.dll' - grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 23H2 + grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 23H2 - function: SoftDeleteFiles parameters: fileGlob: '%SYSTEMROOT%\System32\WaaSAssessment.dll' - grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 23H2 + grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 23H2 - function: SoftDeleteFiles parameters: fileGlob: '%SYSTEMROOT%\System32\Windows.Internal.WaaSMedicDocked.dll' - grantPermissions: true # 🔍 Missing on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 23H2 + grantPermissions: 'true' # 🔍 Missing on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 23H2 - function: SoftDeleteFiles parameters: fileGlob: '%WINDIR%\UUS\amd64\WaaSMedicSvcImpl.dll' - grantPermissions: true # 🔍 Missing on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 23H2 + grantPermissions: 'true' # 🔍 Missing on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 23H2 - function: SoftDeleteFiles parameters: fileGlob: '%WINDIR%\WaaS\*' # Includes `services` and `tasks` folders that defines the desired state configuration on remediation. - grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 23H2 - recurse: true + grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 23H2 + recurse: 'true' - name: Disable automatically enabling Windows Update Medic Service recommend: strict @@ -13491,7 +13491,7 @@ actions: function: SoftDeleteFiles parameters: fileGlob: '%SYSTEMROOT%\System32\upfc.exe' - grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 23H2 + grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 23H2 beforeIteration: |- # Skip Windows versions older than Windows 10 22H2 (build number 19045) to avoid reported blue screen issues. $osVersion = [System.Environment]::OSVersion.Version function Test-IsBeforeWin10Version22H2 { ($osVersion.Major -lt 10) -or (($osVersion.Major -eq 10) -and ($osVersion.Build -lt 19045)) } @@ -13658,7 +13658,7 @@ actions: # Check: Get-ScheduledTask -TaskPath '\Microsoft\Windows\InstallService\' -TaskName 'WakeUpAndContinueUpdates' taskPathPattern: \Microsoft\Windows\InstallService\ taskNamePattern: WakeUpAndContinueUpdates - disableOnRevert: true + disableOnRevert: 'true' - name: Disable "WakeUpAndScanForUpdates" task docs: |- @@ -13681,7 +13681,7 @@ actions: # Check: Get-ScheduledTask -TaskPath '\Microsoft\Windows\InstallService\' -TaskName 'WakeUpAndScanForUpdates' taskPathPattern: \Microsoft\Windows\InstallService\ taskNamePattern: WakeUpAndScanForUpdates - disableOnRevert: true + disableOnRevert: 'true' - name: Disable "Scheduled Start" task docs: |- @@ -13734,7 +13734,7 @@ actions: # Check: Get-ScheduledTask -TaskPath '\Microsoft\Windows\UpdateOrchestrator\' -TaskName 'Report policies' taskPathPattern: \Microsoft\Windows\UpdateOrchestrator\ taskNamePattern: Report policies - grantPermissions: true # 🔒 No permissions, tested since [≥ Windows 10 22H2] [≥ Windows 11 22H2] + grantPermissions: 'true' # 🔒 No permissions, tested since [≥ Windows 10 22H2] [≥ Windows 11 22H2] - name: Disable "Schedule Maintenance Work" task docs: |- @@ -13760,8 +13760,8 @@ actions: # Check: Get-ScheduledTask -TaskPath '\Microsoft\Windows\UpdateOrchestrator\' -TaskName 'Schedule Maintenance Work' taskPathPattern: \Microsoft\Windows\UpdateOrchestrator\ taskNamePattern: Schedule Maintenance Work - disableOnRevert: true - grantPermissions: true # 🔒 No permissions, tested since [≥ Windows 10 22H2] [≥ Windows 11 22H2] + disableOnRevert: 'true' + grantPermissions: 'true' # 🔒 No permissions, tested since [≥ Windows 10 22H2] [≥ Windows 11 22H2] - name: Disable "Schedule Scan" task docs: |- @@ -13786,7 +13786,7 @@ actions: # Check: Get-ScheduledTask -TaskPath '\Microsoft\Windows\UpdateOrchestrator\' -TaskName 'Schedule Scan' taskPathPattern: \Microsoft\Windows\UpdateOrchestrator\ taskNamePattern: Schedule Scan - grantPermissions: true # 🔒 No permissions, tested since [≥ Windows 10 22H2] [≥ Windows 11 22H2] + grantPermissions: 'true' # 🔒 No permissions, tested since [≥ Windows 10 22H2] [≥ Windows 11 22H2] - name: Disable "Schedule Scan Static Task" task docs: |- @@ -13812,7 +13812,7 @@ actions: # Check: Get-ScheduledTask -TaskPath '\Microsoft\Windows\UpdateOrchestrator\' -TaskName 'Schedule Scan Static Task' taskPathPattern: \Microsoft\Windows\UpdateOrchestrator\ taskNamePattern: Schedule Scan Static Task - grantPermissions: true # 🔒 No permissions, tested since [≥ Windows 10 22H2] [≥ Windows 11 22H2] + grantPermissions: 'true' # 🔒 No permissions, tested since [≥ Windows 10 22H2] [≥ Windows 11 22H2] - name: Disable "Schedule Wake To Work" task docs: |- @@ -13837,8 +13837,8 @@ actions: # Check: Get-ScheduledTask -TaskPath '\Microsoft\Windows\UpdateOrchestrator\' -TaskName 'Schedule Wake To Work' taskPathPattern: \Microsoft\Windows\UpdateOrchestrator\ taskNamePattern: Schedule Wake To Work - disableOnRevert: true - grantPermissions: true # 🔒 No permissions, tested since [≥ Windows 10 22H2] [≥ Windows 11 22H2] + disableOnRevert: 'true' + grantPermissions: 'true' # 🔒 No permissions, tested since [≥ Windows 10 22H2] [≥ Windows 11 22H2] - name: Disable "Schedule Work" task docs: |- @@ -13863,8 +13863,8 @@ actions: # Check: Get-ScheduledTask -TaskPath '\Microsoft\Windows\UpdateOrchestrator\' -TaskName 'Schedule Work' taskPathPattern: \Microsoft\Windows\UpdateOrchestrator\ taskNamePattern: Schedule Work - disableOnRevert: true - grantPermissions: true # 🔒 No permissions, tested since [≥ Windows 10 22H2] [≥ Windows 11 22H2] + disableOnRevert: 'true' + grantPermissions: 'true' # 🔒 No permissions, tested since [≥ Windows 10 22H2] [≥ Windows 11 22H2] - name: Disable "UpdateModelTask" task docs: |- @@ -13894,7 +13894,7 @@ actions: # Check: Get-ScheduledTask -TaskPath '\Microsoft\Windows\UpdateOrchestrator\' -TaskName 'UpdateModelTask' taskPathPattern: \Microsoft\Windows\UpdateOrchestrator\ taskNamePattern: UpdateModelTask - grantPermissions: true # 🔒 No permissions, tested since [≥ Windows 10 22H2] + grantPermissions: 'true' # 🔒 No permissions, tested since [≥ Windows 10 22H2] - name: Disable "Start Oobe Expedite Work" task docs: |- @@ -13921,7 +13921,7 @@ actions: # Check: Get-ScheduledTask -TaskPath '\Microsoft\Windows\UpdateOrchestrator\' -TaskName 'Start Oobe Expedite Work' taskPathPattern: \Microsoft\Windows\UpdateOrchestrator\ taskNamePattern: Start Oobe Expedite Work - grantPermissions: true # 🔒 No permissions, Tested since [≥ Windows 11 22H2] + grantPermissions: 'true' # 🔒 No permissions, Tested since [≥ Windows 11 22H2] - name: Disable "StartOobeAppsScan_LicenseAccepted" task docs: |- @@ -13948,7 +13948,7 @@ actions: # Check: Get-ScheduledTask -TaskPath '\Microsoft\Windows\UpdateOrchestrator\' -TaskName 'StartOobeAppsScan_LicenseAccepted' taskPathPattern: \Microsoft\Windows\UpdateOrchestrator\ taskNamePattern: StartOobeAppsScan_LicenseAccepted - grantPermissions: true # 🔒 No permissions, tested since [≥ Windows 11 22H2] + grantPermissions: 'true' # 🔒 No permissions, tested since [≥ Windows 11 22H2] - name: Disable "StartOobeAppsScan_OobeAppReady" task docs: |- @@ -13975,7 +13975,7 @@ actions: # Check: Get-ScheduledTask -TaskPath '\Microsoft\Windows\UpdateOrchestrator\' -TaskName 'StartOobeAppsScan_OobeAppReady' taskPathPattern: \Microsoft\Windows\UpdateOrchestrator\ taskNamePattern: StartOobeAppsScan_OobeAppReady - grantPermissions: true # 🔒 No permissions, tested since [≥ Windows 11 22H2] + grantPermissions: 'true' # 🔒 No permissions, tested since [≥ Windows 11 22H2] - name: Disable "StartOobeAppsScanAfterUpdate" task docs: |- @@ -14002,7 +14002,7 @@ actions: # Check: Get-ScheduledTask -TaskPath '\Microsoft\Windows\UpdateOrchestrator\' -TaskName 'StartOobeAppsScanAfterUpdate' taskPathPattern: \Microsoft\Windows\UpdateOrchestrator\ taskNamePattern: StartOobeAppsScanAfterUpdate - grantPermissions: true # 🔒 No permissions, tested since [≥ Windows 11 22H2] + grantPermissions: 'true' # 🔒 No permissions, tested since [≥ Windows 11 22H2] - name: Disable "USO_UxBroker" task docs: |- @@ -14033,7 +14033,7 @@ actions: # Check: Get-ScheduledTask -TaskPath '\Microsoft\Windows\UpdateOrchestrator\' -TaskName 'USO_UxBroker' taskPathPattern: \Microsoft\Windows\UpdateOrchestrator\ taskNamePattern: USO_UxBroker - grantPermissions: true # 🔒 No permissions, tested since [≥ Windows 10 22H2] [≥ Windows 11 22H2] + grantPermissions: 'true' # 🔒 No permissions, tested since [≥ Windows 10 22H2] [≥ Windows 11 22H2] - name: Disable "UUS Failover Task" task docs: |- @@ -14060,7 +14060,7 @@ actions: # Check: Get-ScheduledTask -TaskPath '\Microsoft\Windows\UpdateOrchestrator\' -TaskName 'UUS Failover Task' taskPathPattern: \Microsoft\Windows\UpdateOrchestrator\ taskNamePattern: UUS Failover Task - grantPermissions: true # 🔒 No permissions, tested since [≥ Windows 11 22H2] + grantPermissions: 'true' # 🔒 No permissions, tested since [≥ Windows 11 22H2] - name: Disable "PerformRemediation" task docs: |- @@ -14093,7 +14093,7 @@ actions: # Check: Get-ScheduledTask -TaskPath '\Microsoft\Windows\WaaSMedic\' -TaskName 'PerformRemediation' taskPathPattern: \Microsoft\Windows\WaaSMedic\ taskNamePattern: PerformRemediation - grantPermissions: true # 🔒 No permissions, tested since [≥ Windows 10 22H2] [≥ Windows 11 22H2] + grantPermissions: 'true' # 🔒 No permissions, tested since [≥ Windows 10 22H2] [≥ Windows 11 22H2] - name: Disable outdated Windows Update tasks docs: |- @@ -19014,7 +19014,7 @@ actions: function: DeleteDirectory parameters: directoryGlob: '%LOCALAPPDATA%\Microsoft\OneDrive' - grantPermissions: true + grantPermissions: 'true' - function: DeleteDirectory parameters: @@ -19465,7 +19465,7 @@ actions: function: DisableWindowsFeature parameters: featureName: DirectPlay # Get-WindowsOptionalFeature -FeatureName 'DirectPlay' -Online - disabledByDefault: true + disabledByDefault: 'true' - name: Disable "Internet Explorer" feature docs: |- @@ -19483,17 +19483,17 @@ actions: function: DisableWindowsFeature parameters: featureName: Internet-Explorer-Optional-x64 # Get-WindowsOptionalFeature -FeatureName 'Internet-Explorer-Optional-x64' -Online - ignoreMissingOnRevert: true + ignoreMissingOnRevert: 'true' - function: DisableWindowsFeature parameters: featureName: Internet-Explorer-Optional-x84 # Get-WindowsOptionalFeature -FeatureName 'Internet-Explorer-Optional-x84' -Online - ignoreMissingOnRevert: true + ignoreMissingOnRevert: 'true' - function: DisableWindowsFeature parameters: featureName: Internet-Explorer-Optional-amd64 # Get-WindowsOptionalFeature -FeatureName 'Internet-Explorer-Optional-amd64' -Online - ignoreMissingOnRevert: true + ignoreMissingOnRevert: 'true' - name: Disable "Legacy Components" feature docs: |- @@ -19510,7 +19510,7 @@ actions: function: DisableWindowsFeature parameters: featureName: LegacyComponents # Get-WindowsOptionalFeature -FeatureName 'LegacyComponents' -Online - disabledByDefault: true + disabledByDefault: 'true' - category: Disable Hyper-V virtualization features children: @@ -19530,7 +19530,7 @@ actions: function: DisableWindowsFeature parameters: featureName: Microsoft-Hyper-V-All # Get-WindowsOptionalFeature -FeatureName 'Microsoft-Hyper-V-All' -Online - disabledByDefault: true + disabledByDefault: 'true' - name: Disable "Hyper-V GUI Management Tools" feature docs: |- @@ -19547,7 +19547,7 @@ actions: function: DisableWindowsFeature parameters: featureName: Microsoft-Hyper-V-Management-Clients # Get-WindowsOptionalFeature -FeatureName 'Microsoft-Hyper-V-Management-Clients' -Online - disabledByDefault: true + disabledByDefault: 'true' - name: Disable "Hyper-V Management Tools" feature docs: |- @@ -19564,7 +19564,7 @@ actions: function: DisableWindowsFeature parameters: featureName: Microsoft-Hyper-V-Tools-All # Get-WindowsOptionalFeature -FeatureName 'Microsoft-Hyper-V-Tools-All' -Online - disabledByDefault: true # Default: Disabled (tested: Windows 10 22H2, Windows 11 23H2) + disabledByDefault: 'true' # Default: Disabled (tested: Windows 10 22H2, Windows 11 23H2) - name: Disable "Hyper-V Module for Windows PowerShell" feature docs: |- @@ -19581,7 +19581,7 @@ actions: function: DisableWindowsFeature parameters: featureName: Microsoft-Hyper-V-Management-PowerShell # Get-WindowsOptionalFeature -FeatureName 'Microsoft-Hyper-V-Management-PowerShell' -Online - disabledByDefault: true + disabledByDefault: 'true' - category: Disable printing features children: @@ -19620,7 +19620,7 @@ actions: function: DisableWindowsFeature parameters: featureName: Printing-Foundation-LPDPrintService # Get-WindowsOptionalFeature -FeatureName 'Printing-Foundation-LPDPrintService' -Online - disabledByDefault: true + disabledByDefault: 'true' - name: Disable "LPR Port Monitor" feature docs: |- @@ -19637,7 +19637,7 @@ actions: function: DisableWindowsFeature parameters: featureName: Printing-Foundation-LPRPortMonitor # Get-WindowsOptionalFeature -FeatureName 'Printing-Foundation-LPRPortMonitor' -Online - disabledByDefault: true + disabledByDefault: 'true' - name: Disable "Microsoft Print to PDF" feature docs: |- @@ -19707,7 +19707,7 @@ actions: function: DisableWindowsFeature parameters: featureName: Printing-XPSServices-Features # Get-WindowsOptionalFeature -FeatureName 'Printing-XPSServices-Features' -Online - disabledByDefault: true + disabledByDefault: 'true' - name: Disable "XPS Viewer" feature recommend: standard # Deprecated and missing on modern versions of Windows @@ -19729,7 +19729,7 @@ actions: function: DisableWindowsFeature parameters: featureName: Xps-Foundation-Xps-Viewer # Get-WindowsOptionalFeature -FeatureName 'Xps-Foundation-Xps-Viewer' -Online - ignoreMissingOnRevert: true + ignoreMissingOnRevert: 'true' - name: Disable "Media Features" feature docs: |- @@ -19767,7 +19767,7 @@ actions: function: DisableWindowsFeature parameters: featureName: ScanManagementConsole # Get-WindowsOptionalFeature -FeatureName 'ScanManagementConsole' -Online - ignoreMissingOnRevert: true + ignoreMissingOnRevert: 'true' - name: Disable "Windows Fax and Scan" feature recommend: standard # Deprecated and missing on modern versions of Windows @@ -19789,7 +19789,7 @@ actions: function: DisableWindowsFeature parameters: featureName: FaxServicesClientPackage # Get-WindowsOptionalFeature -FeatureName 'FaxServicesClientPackage' -Online - ignoreMissingOnRevert: true + ignoreMissingOnRevert: 'true' - name: Disable "Windows Media Player" feature docs: |- @@ -20667,8 +20667,8 @@ actions: function: ShowMessage parameters: message: Disabling Network settings on Windows 10 is known to break Network settings. - ignoreWindows11: true - warn: true + ignoreWindows11: 'true' + warn: 'true' - function: DisableService parameters: diff --git a/tests/integration/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator.spec.ts b/tests/integration/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator.spec.ts new file mode 100644 index 00000000..4c8546d4 --- /dev/null +++ b/tests/integration/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator.spec.ts @@ -0,0 +1,51 @@ +import { describe, it, expect } from 'vitest'; +import { validateParameterName } from '@/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator'; + +describe('ParameterNameValidator', () => { + describe('accepts when valid', () => { + // arrange + const validValues: readonly string[] = [ + 'lowercase', + 'onlyLetters', + 'l3tt3rsW1thNumb3rs', + ]; + validValues.forEach((validValue) => { + it(validValue, () => { + // act + const act = () => validateParameterName(validValue); + // assert + expect(act).to.not.throw(); + }); + }); + }); + describe('throws if invalid', () => { + // arrange + const testScenarios: readonly { + readonly description: string; + readonly value: string; + }[] = [ + { + description: 'empty name', + value: '', + }, + { + description: 'has @', + value: 'b@d', + }, + { + description: 'has {', + value: 'b{a}d', + }, + ]; + testScenarios.forEach(( + { description, value }, + ) => { + it(description, () => { + // act + const act = () => validateParameterName(value); + // assert + expect(act).to.throw(); + }); + }); + }); +}); diff --git a/tests/unit/application/Parser/Common/TypeValidator.spec.ts b/tests/unit/application/Parser/Common/TypeValidator.spec.ts index 4b8a273a..5bac11a2 100644 --- a/tests/unit/application/Parser/Common/TypeValidator.spec.ts +++ b/tests/unit/application/Parser/Common/TypeValidator.spec.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; -import { createTypeValidator } from '@/application/Parser/Common/TypeValidator'; -import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests'; +import { createTypeValidator, type NonEmptyStringAssertion, type RegexValidationRule } from '@/application/Parser/Common/TypeValidator'; +import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests'; describe('createTypeValidator', () => { describe('assertObject', () => { @@ -146,6 +146,108 @@ describe('createTypeValidator', () => { }); }); }); + describe('assertNonEmptyString', () => { + describe('with valid string', () => { + it('accepts non-empty string without regex rule', () => { + // arrange + const nonEmptyString = 'hello'; + const { assertNonEmptyString } = createTypeValidator(); + // act + const act = () => assertNonEmptyString({ value: nonEmptyString, valueName: 'unimportant name' }); + // assert + expect(act).to.not.throw(); + }); + it('accepts if the string matches the regex', () => { + // arrange + const regex: RegExp = /goodbye/; + const stringMatchingRegex = 'Valid string containing "goodbye"'; + const rule: RegexValidationRule = { + expectedMatch: regex, + errorMessage: 'String contain "goodbye"', + }; + const { assertNonEmptyString } = createTypeValidator(); + // act + const act = () => assertNonEmptyString({ + value: stringMatchingRegex, + valueName: 'unimportant name', + rule, + }); + // assert + expect(act).to.not.throw(); + }); + }); + describe('with invalid string', () => { + describe('throws error for missing string', () => { + itEachAbsentStringValue((absentValue) => { + // arrange + const valueName = 'absent string value'; + const expectedMessage = `'${valueName}' is missing.`; + const { assertNonEmptyString } = createTypeValidator(); + // act + const act = () => assertNonEmptyString({ value: absentValue, valueName }); + // assert + expect(act).to.throw(expectedMessage); + }); + }); + describe('throws error for non string values', () => { + // arrange + const testScenarios: readonly { + readonly description: string; + readonly invalidValue: unknown; + }[] = [ + { + description: 'number', + invalidValue: 42, + }, + { + description: 'boolean', + invalidValue: true, + }, + { + description: 'object', + invalidValue: { property: 'value' }, + }, + { + description: 'array', + invalidValue: ['a', 'r', 'r', 'a', 'y'], + }, + ]; + testScenarios.forEach(({ + description, invalidValue, + }) => { + it(description, () => { + const valueName = 'invalidValue'; + const expectedMessage = `'${valueName}' should be of type 'string', but is of type '${typeof invalidValue}'.`; + const { assertNonEmptyString } = createTypeValidator(); + // act + const act = () => assertNonEmptyString({ value: invalidValue, valueName }); + // assert + expect(act).to.throw(expectedMessage); + }); + }); + }); + it('throws an error if the string does not match the regex', () => { + // arrange + const regex: RegExp = /goodbye/; + const stringNotMatchingRegex = 'Hello'; + const expectedMessage = 'String should contain "goodbye"'; + const rule: RegexValidationRule = { + expectedMatch: regex, + errorMessage: expectedMessage, + }; + const assertion: NonEmptyStringAssertion = { + value: stringNotMatchingRegex, + valueName: 'non-important-value-name', + rule, + }; + const { assertNonEmptyString } = createTypeValidator(); + // act + const act = () => assertNonEmptyString(assertion); + // assert + expect(act).to.throw(expectedMessage); + }); + }); + }); }); function createObjectWithProperties(properties: readonly string[]): object { diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument.spec.ts index bd3cb4b1..b35f6157 100644 --- a/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument.spec.ts +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument.spec.ts @@ -1,52 +1,110 @@ -import { describe, expect } from 'vitest'; -import { FunctionCallArgument } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; -import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests'; -import { testParameterName } from '../../../ParameterNameTestRunner'; +import { describe, expect, it } from 'vitest'; +import { createFunctionCallArgument } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; +import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub'; +import type { NonEmptyStringAssertion, TypeValidator } from '@/application/Parser/Common/TypeValidator'; +import { createParameterNameValidatorStub } from '@tests/unit/shared/Stubs/ParameterNameValidatorStub'; +import type { ParameterNameValidator } from '@/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator'; describe('FunctionCallArgument', () => { - describe('ctor', () => { + describe('createFunctionCallArgument', () => { describe('parameter name', () => { - testParameterName( - (parameterName) => new FunctionCallArgumentBuilder() - .withParameterName(parameterName) - .build() - .parameterName, - ); - }); - describe('throws if argument value is absent', () => { - itEachAbsentStringValue((absentValue) => { + it('assigns correctly', () => { // arrange - const parameterName = 'paramName'; - const expectedError = `Missing argument value for the parameter "${parameterName}".`; - const argumentValue = absentValue; + const expectedName = 'expected parameter name'; + const context = new TestContext() + .withParameterName(expectedName); // act - const act = () => new FunctionCallArgumentBuilder() - .withParameterName(parameterName) - .withArgumentValue(argumentValue) - .build(); + const actualArgument = context.create(); // assert - expect(act).to.throw(expectedError); - }, { excludeNull: true, excludeUndefined: true }); + const actualName = actualArgument.parameterName; + expect(actualName).toEqual(expectedName); + }); + it('validates parameter name', () => { + // arrange + const validator = createParameterNameValidatorStub(); + const expectedParameterName = 'parameter name expected to be validated'; + const context = new TestContext() + .withParameterName(expectedParameterName) + .withParameterNameValidator(validator.validator); + // act + context.create(); + // assert + expect(validator.validatedNames).to.have.lengthOf(1); + expect(validator.validatedNames).to.include(expectedParameterName); + }); + }); + describe('argument value', () => { + it('assigns correctly', () => { + // arrange + const expectedValue = 'expected argument value'; + const context = new TestContext() + .withArgumentValue(expectedValue); + // act + const actualArgument = context.create(); + // assert + const actualValue = actualArgument.argumentValue; + expect(actualValue).toEqual(expectedValue); + }); + it('validates argument value', () => { + // arrange + const parameterNameInError = 'expected parameter with argument error'; + const expectedArgumentValue = 'argument value to be validated'; + const expectedAssertion: NonEmptyStringAssertion = { + value: expectedArgumentValue, + valueName: `Missing argument value for the parameter "${parameterNameInError}".`, + }; + const typeValidator = new TypeValidatorStub(); + const context = new TestContext() + .withArgumentValue(expectedArgumentValue) + .withParameterName(parameterNameInError) + .withTypeValidator(typeValidator); + // act + context.create(); + // assert + typeValidator.assertNonEmptyString(expectedAssertion); + }); }); }); }); -class FunctionCallArgumentBuilder { - private parameterName = 'default-parameter-name'; +class TestContext { + private parameterName = `[${TestContext.name}] default-parameter-name`; - private argumentValue = 'default-argument-value'; + private argumentValue = `[${TestContext.name}] default-argument-value`; - public withParameterName(parameterName: string) { + private typeValidator: TypeValidator = new TypeValidatorStub(); + + private parameterNameValidator + : ParameterNameValidator = createParameterNameValidatorStub().validator; + + public withParameterName(parameterName: string): this { this.parameterName = parameterName; return this; } - public withArgumentValue(argumentValue: string) { + public withArgumentValue(argumentValue: string): this { this.argumentValue = argumentValue; return this; } - public build() { - return new FunctionCallArgument(this.parameterName, this.argumentValue); + public withTypeValidator(typeValidator: TypeValidator): this { + this.typeValidator = typeValidator; + return this; + } + + public withParameterNameValidator(parameterNameValidator: ParameterNameValidator): this { + this.parameterNameValidator = parameterNameValidator; + return this; + } + + public create(): ReturnType { + return createFunctionCallArgument( + this.parameterName, + this.argumentValue, + { + typeValidator: this.typeValidator, + validateParameterName: this.parameterNameValidator, + }, + ); } } diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection.spec.ts index b29122ca..08c333bf 100644 --- a/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection.spec.ts +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection.spec.ts @@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest'; import { FunctionCallArgumentCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection'; import { FunctionCallArgumentStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentStub'; import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests'; -import type { IFunctionCallArgument } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/IFunctionCallArgument'; +import type { FunctionCallArgument } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; describe('FunctionCallArgumentCollection', () => { describe('addArgument', () => { @@ -25,7 +25,7 @@ describe('FunctionCallArgumentCollection', () => { // arrange const testCases: ReadonlyArray<{ readonly description: string; - readonly args: readonly IFunctionCallArgument[]; + readonly args: readonly FunctionCallArgument[]; readonly expectedParameterNames: string[]; }> = [{ description: 'no args', diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler.spec.ts index edac5c28..868cb138 100644 --- a/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler.spec.ts +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler.spec.ts @@ -14,6 +14,8 @@ import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFun import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester'; import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError'; import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub'; +import type { FunctionCallArgumentFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; +import { FunctionCallArgumentFactoryStub } from '../../../../../../../../../../../shared/Stubs/FunctionCallArgumentFactoryStub'; describe('NestedFunctionArgumentCompiler', () => { describe('createCompiledNestedCall', () => { @@ -266,6 +268,9 @@ class NestedFunctionArgumentCompilerBuilder implements ArgumentCompiler { private wrapError: ErrorWithContextWrapper = errorWithContextWrapperStub; + private callArgumentFactory + : FunctionCallArgumentFactory = new FunctionCallArgumentFactoryStub().factory; + public withExpressionsCompiler(expressionsCompiler: IExpressionsCompiler): this { this.expressionsCompiler = expressionsCompiler; return this; @@ -292,10 +297,11 @@ class NestedFunctionArgumentCompilerBuilder implements ArgumentCompiler { } public createCompiledNestedCall(): FunctionCall { - const compiler = new NestedFunctionArgumentCompiler( - this.expressionsCompiler, - this.wrapError, - ); + const compiler = new NestedFunctionArgumentCompiler({ + expressionsCompiler: this.expressionsCompiler, + wrapError: this.wrapError, + createCallArgument: this.callArgumentFactory, + }); return compiler.createCompiledNestedCall( this.nestedFunctionCall, this.parentFunctionCall, diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser.spec.ts index 99ff12d1..1a318ef4 100644 --- a/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser.spec.ts +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser.spec.ts @@ -3,8 +3,12 @@ import type { FunctionCallsData, FunctionCallData } from '@/application/collecti import { parseFunctionCalls } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser'; import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub'; import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests'; -import type { NonEmptyCollectionAssertion, ObjectAssertion, TypeValidator } from '@/application/Parser/Common/TypeValidator'; +import type { + NonEmptyCollectionAssertion, ObjectAssertion, TypeValidator, +} from '@/application/Parser/Common/TypeValidator'; import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub'; +import type { FunctionCallArgumentFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; +import { FunctionCallArgumentFactoryStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentFactoryStub'; describe('FunctionCallsParser', () => { describe('parseFunctionCalls', () => { @@ -174,12 +178,15 @@ describe('FunctionCallsParser', () => { }); class TestContext { - private validator: TypeValidator = new TypeValidatorStub(); + private typeValidator: TypeValidator = new TypeValidatorStub(); + + private createCallArgument + : FunctionCallArgumentFactory = new FunctionCallArgumentFactoryStub().factory; private calls: FunctionCallsData = [new FunctionCallDataStub()]; public withTypeValidator(typeValidator: TypeValidator): this { - this.validator = typeValidator; + this.typeValidator = typeValidator; return this; } @@ -191,7 +198,10 @@ class TestContext { public parse(): ReturnType { return parseFunctionCalls( this.calls, - this.validator, + { + typeValidator: this.typeValidator, + createCallArgument: this.createCallArgument, + }, ); } } diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameter.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameter.spec.ts deleted file mode 100644 index 0d16dcd9..00000000 --- a/tests/unit/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameter.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { FunctionParameter } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameter'; -import { testParameterName } from '../../ParameterNameTestRunner'; - -describe('FunctionParameter', () => { - describe('name', () => { - testParameterName( - (parameterName) => new FunctionParameterBuilder() - .withName(parameterName) - .build() - .name, - ); - }); - describe('isOptional', () => { - describe('sets as expected', () => { - // arrange - const expectedValues = [true, false]; - for (const expected of expectedValues) { - it(expected.toString(), () => { - // act - const sut = new FunctionParameterBuilder() - .withIsOptional(expected) - .build(); - // expect - expect(sut.isOptional).to.equal(expected); - }); - } - }); - }); -}); - -class FunctionParameterBuilder { - private name = 'parameterFromParameterBuilder'; - - private isOptional = false; - - public withName(name: string) { - this.name = name; - return this; - } - - public withIsOptional(isOptional: boolean) { - this.isOptional = isOptional; - return this; - } - - public build() { - return new FunctionParameter(this.name, this.isOptional); - } -} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterParser.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterParser.spec.ts new file mode 100644 index 00000000..fde3ee51 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterParser.spec.ts @@ -0,0 +1,83 @@ +import { describe, it, expect } from 'vitest'; +import type { ParameterDefinitionData } from '@/application/collections/'; +import type { ParameterNameValidator } from '@/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator'; +import { createParameterNameValidatorStub } from '@tests/unit/shared/Stubs/ParameterNameValidatorStub'; +import { parseFunctionParameter } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterParser'; +import { ParameterDefinitionDataStub } from '@tests/unit/shared/Stubs/ParameterDefinitionDataStub'; + +describe('FunctionParameterParser', () => { + describe('parseFunctionParameter', () => { + describe('name', () => { + it('assigns correctly', () => { + // arrange + const expectedName = 'expected-function-name'; + const data = new ParameterDefinitionDataStub() + .withName(expectedName); + // act + const actual = new TestContext() + .withData(data) + .parse(); + // expect + const actualName = actual.name; + expect(actualName).to.equal(expectedName); + }); + it('validates correctly', () => { + // arrange + const expectedName = 'expected-function-name'; + const { validator, validatedNames } = createParameterNameValidatorStub(); + const data = new ParameterDefinitionDataStub() + .withName(expectedName); + // act + new TestContext() + .withData(data) + .withValidator(validator) + .parse(); + // expect + expect(validatedNames).to.have.lengthOf(1); + expect(validatedNames).to.contain(expectedName); + }); + }); + describe('isOptional', () => { + describe('assigns correctly', () => { + // arrange + const expectedValues = [true, false]; + for (const expected of expectedValues) { + it(expected.toString(), () => { + const data = new ParameterDefinitionDataStub() + .withOptionality(expected); + // act + const actual = new TestContext() + .withData(data) + .parse(); + // expect + expect(actual.isOptional).to.equal(expected); + }); + } + }); + }); + }); +}); + +class TestContext { + private data: ParameterDefinitionData = new ParameterDefinitionDataStub() + .withName(`[${TestContext.name}]function-name`); + + private validator: ParameterNameValidator = createParameterNameValidatorStub().validator; + + public withData(data: ParameterDefinitionData) { + this.data = data; + return this; + } + + public withValidator(parameterNameValidator: ParameterNameValidator): this { + this.validator = parameterNameValidator; + return this; + } + + public parse() { + return parseFunctionParameter( + this.data, + this.validator, + ); + } +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser.spec.ts index 47e1bbd2..adac3332 100644 --- a/tests/unit/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser.spec.ts +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser.spec.ts @@ -4,7 +4,7 @@ import type { ParameterDefinitionData, FunctionCallsData, } from '@/application/collections/'; import type { ISharedFunction } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunction'; -import { parseSharedFunctions, type FunctionParameterFactory } from '@/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser'; +import { parseSharedFunctions } from '@/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser'; import { createFunctionDataWithCall, createFunctionDataWithCode, createFunctionDataWithoutCallOrCode } from '@tests/unit/shared/Stubs/FunctionDataStub'; import { ParameterDefinitionDataStub } from '@tests/unit/shared/Stubs/ParameterDefinitionDataStub'; import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub'; @@ -16,7 +16,6 @@ import type { ICodeValidator } from '@/application/Parser/Executable/Script/Vali import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines'; import { NoDuplicatedLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoDuplicatedLines'; import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError'; -import { FunctionParameterStub } from '@tests/unit/shared/Stubs/FunctionParameterStub'; import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub'; import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester'; import type { FunctionParameterCollectionFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollectionFactory'; @@ -26,6 +25,8 @@ import { createFunctionCallsParserStub } from '@tests/unit/shared/Stubs/Function import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub'; import type { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/IFunctionParameterCollection'; import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall'; +import type { FunctionParameterParser } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterParser'; +import { createFunctionParameterParserStub } from '@tests/unit/shared/Stubs/FunctionParameterParserStub'; import { expectCallsFunctionBody, expectCodeFunctionBody } from './ExpectFunctionBodyType'; describe('SharedFunctionsParser', () => { @@ -185,7 +186,7 @@ describe('SharedFunctionsParser', () => { const functionName = 'functionName'; const expectedErrorMessage = `Failed to create parameter: ${invalidParameterName} for function "${functionName}"`; const expectedInnerError = new Error('injected error'); - const parameterFactory: FunctionParameterFactory = () => { + const parser: FunctionParameterParser = () => { throw expectedInnerError; }; const functionData = createFunctionDataWithCode() @@ -196,7 +197,7 @@ describe('SharedFunctionsParser', () => { throwingAction: (wrapError) => { new TestContext() .withFunctions([functionData]) - .withFunctionParameterFactory(parameterFactory) + .withFunctionParameterParser(parser) .withErrorWrapper(wrapError) .parseFunctions(); }, @@ -415,12 +416,7 @@ class TestContext { private functionCallsParser: FunctionCallsParser = createFunctionCallsParserStub().parser; - private parameterFactory: FunctionParameterFactory = ( - name: string, - isOptional: boolean, - ) => new FunctionParameterStub() - .withName(name) - .withOptional(isOptional); + private functionParameterParser: FunctionParameterParser = createFunctionParameterParserStub; private parameterCollectionFactory : FunctionParameterCollectionFactory = () => new FunctionParameterCollectionStub(); @@ -450,8 +446,8 @@ class TestContext { return this; } - public withFunctionParameterFactory(parameterFactory: FunctionParameterFactory): this { - this.parameterFactory = parameterFactory; + public withFunctionParameterParser(functionParameterParser: FunctionParameterParser): this { + this.functionParameterParser = functionParameterParser; return this; } @@ -469,7 +465,7 @@ class TestContext { { codeValidator: this.codeValidator, wrapError: this.wrapError, - createParameter: this.parameterFactory, + parseParameter: this.functionParameterParser, createParameterCollection: this.parameterCollectionFactory, parseFunctionCalls: this.functionCallsParser, }, diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/ParameterNameTestRunner.ts b/tests/unit/application/Parser/Executable/Script/Compiler/ParameterNameTestRunner.ts deleted file mode 100644 index d8216502..00000000 --- a/tests/unit/application/Parser/Executable/Script/Compiler/ParameterNameTestRunner.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -export function testParameterName(action: (parameterName: string) => string) { - describe('name', () => { - describe('sets as expected', () => { - // arrange - const expectedValues: readonly string[] = [ - 'lowercase', - 'onlyLetters', - 'l3tt3rsW1thNumb3rs', - ]; - for (const expected of expectedValues) { - it(expected, () => { - // act - const value = action(expected); - // assert - expect(value).to.equal(expected); - }); - } - }); - describe('throws if invalid', () => { - // arrange - const testScenarios: readonly { - readonly description: string; - readonly value: string; - readonly expectedError: string; - }[] = [ - { - description: 'empty Name', - value: '', - expectedError: 'missing parameter name', - }, - { - description: 'has @', - value: 'b@d', - expectedError: 'parameter name must be alphanumeric but it was "b@d"', - }, - { - description: 'has {', - value: 'b{a}d', - expectedError: 'parameter name must be alphanumeric but it was "b{a}d"', - }, - ]; - for (const { description, value, expectedError } of testScenarios) { - it(description, () => { - // act - const act = () => action(value); - // assert - expect(act).to.throw(expectedError); - }); - } - }); - }); -} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/ParameterNameValidator.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/ParameterNameValidator.spec.ts new file mode 100644 index 00000000..7a1bcbe2 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/ParameterNameValidator.spec.ts @@ -0,0 +1,24 @@ +import { describe, it } from 'vitest'; +import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub'; +import { validateParameterName } from '@/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator'; +import type { NonEmptyStringAssertion } from '@/application/Parser/Common/TypeValidator'; + +describe('ParameterNameValidator', () => { + it('asserts correctly', () => { + // arrange + const parameterName = 'expected-parameter-name'; + const validator = new TypeValidatorStub(); + const expectedAssertion: NonEmptyStringAssertion = { + value: parameterName, + valueName: 'parameter name', + rule: { + expectedMatch: /^[0-9a-zA-Z]+$/, + errorMessage: `parameter name must be alphanumeric but it was "${parameterName}".`, + }, + }; + // act + validateParameterName(parameterName, validator); + // assert + validator.assertNonEmptyString(expectedAssertion); + }); +}); diff --git a/tests/unit/application/Parser/ScriptingDefinition/CodeSubstituter.spec.ts b/tests/unit/application/Parser/ScriptingDefinition/CodeSubstituter.spec.ts index 337a5b55..ff6c5b30 100644 --- a/tests/unit/application/Parser/ScriptingDefinition/CodeSubstituter.spec.ts +++ b/tests/unit/application/Parser/ScriptingDefinition/CodeSubstituter.spec.ts @@ -1,91 +1,120 @@ import { describe, it, expect } from 'vitest'; -import { CodeSubstituter } from '@/application/Parser/ScriptingDefinition/CodeSubstituter'; import type { IExpressionsCompiler } from '@/application/Parser/Executable/Script/Compiler/Expressions/IExpressionsCompiler'; import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub'; import { ExpressionsCompilerStub } from '@tests/unit/shared/Stubs/ExpressionsCompilerStub'; import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests'; +import { substituteCode } from '@/application/Parser/ScriptingDefinition/CodeSubstituter'; +import type { ProjectDetails } from '@/domain/Project/ProjectDetails'; +import type { FunctionCallArgumentFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; +import { FunctionCallArgumentFactoryStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentFactoryStub'; describe('CodeSubstituter', () => { - describe('throws if code is empty', () => { - itEachAbsentStringValue((emptyCode) => { - // arrange - const expectedError = 'missing code'; - const code = emptyCode; - const projectDetails = new ProjectDetailsStub(); - const sut = new CodeSubstituterBuilder().build(); - // act - const act = () => sut.substitute(code, projectDetails); - // assert - expect(act).to.throw(expectedError); - }, { excludeNull: true, excludeUndefined: true }); - }); - describe('substitutes parameters as expected values', () => { - // arrange - const projectDetails = new ProjectDetailsStub(); - const date = new Date(); - const testCases: Array<{ parameter: string, argument: string }> = [ - { - parameter: 'homepage', - argument: projectDetails.homepage, - }, - { - parameter: 'version', - argument: projectDetails.version.toString(), - }, - { - parameter: 'date', - argument: date.toUTCString(), - }, - ]; - for (const testCase of testCases) { - it(`substitutes ${testCase.parameter} as expected`, () => { - const compilerStub = new ExpressionsCompilerStub(); - const sut = new CodeSubstituterBuilder() - .withCompiler(compilerStub) - .withDate(date) - .build(); + describe('substituteCode', () => { + describe('throws if code is empty', () => { + itEachAbsentStringValue((emptyCode) => { + // arrange + const expectedError = 'missing code'; + const context = new TestContext() + .withCode(emptyCode); // act - sut.substitute('non empty code', projectDetails); + const act = () => context.substitute(); // assert - expect(compilerStub.callHistory).to.have.lengthOf(1); - const parameters = compilerStub.callHistory[0].args[1]; - expect(parameters.hasArgument(testCase.parameter)); - const { argumentValue } = parameters.getArgument(testCase.parameter); - expect(argumentValue).to.equal(testCase.argument); - }); - } - }); - it('returns code as it is', () => { - // arrange - const expected = 'expected-code'; - const compilerStub = new ExpressionsCompilerStub(); - const sut = new CodeSubstituterBuilder() - .withCompiler(compilerStub) - .build(); - // act - sut.substitute(expected, new ProjectDetailsStub()); - // assert - expect(compilerStub.callHistory).to.have.lengthOf(1); - expect(compilerStub.callHistory[0].args[0]).to.equal(expected); + expect(act).to.throw(expectedError); + }, { excludeNull: true, excludeUndefined: true }); + }); + describe('substitutes parameters as expected values', () => { + // arrange + const projectDetails = new ProjectDetailsStub(); + const date = new Date(); + const testCases: Array<{ parameter: string, argument: string }> = [ + { + parameter: 'homepage', + argument: projectDetails.homepage, + }, + { + parameter: 'version', + argument: projectDetails.version.toString(), + }, + { + parameter: 'date', + argument: date.toUTCString(), + }, + ]; + for (const testCase of testCases) { + it(`substitutes ${testCase.parameter} as expected`, () => { + const compilerStub = new ExpressionsCompilerStub(); + const context = new TestContext() + .withCompiler(compilerStub) + .withDate(date) + .withProjectDetails(projectDetails); + // act + context.substitute(); + // assert + expect(compilerStub.callHistory).to.have.lengthOf(1); + const parameters = compilerStub.callHistory[0].args[1]; + expect(parameters.hasArgument(testCase.parameter)); + const { argumentValue } = parameters.getArgument(testCase.parameter); + expect(argumentValue).to.equal(testCase.argument); + }); + } + }); + it('returns code as it is', () => { + // arrange + const expected = 'expected-code'; + const compilerStub = new ExpressionsCompilerStub(); + const context = new TestContext() + .withCompiler(compilerStub) + .withCode(expected); + // act + context.substitute(); + // assert + expect(compilerStub.callHistory).to.have.lengthOf(1); + expect(compilerStub.callHistory[0].args[0]).to.equal(expected); + }); }); }); -class CodeSubstituterBuilder { +class TestContext { private compiler: IExpressionsCompiler = new ExpressionsCompilerStub(); private date = new Date(); - public withCompiler(compiler: IExpressionsCompiler) { + private code = `[${TestContext.name}] default code for testing`; + + private projectDetails: ProjectDetails = new ProjectDetailsStub(); + + private callArgumentFactory + : FunctionCallArgumentFactory = new FunctionCallArgumentFactoryStub().factory; + + public withCompiler(compiler: IExpressionsCompiler): this { this.compiler = compiler; return this; } - public withDate(date: Date) { + public withDate(date: Date): this { this.date = date; return this; } - public build() { - return new CodeSubstituter(this.compiler, this.date); + public withCode(code: string): this { + this.code = code; + return this; + } + + public withProjectDetails(projectDetails: ProjectDetails): this { + this.projectDetails = projectDetails; + return this; + } + + public substitute(): ReturnType { + return substituteCode( + this.code, + this.projectDetails, + { + compiler: this.compiler, + provideDate: () => this.date, + createCallArgument: this.callArgumentFactory, + }, + ); } } diff --git a/tests/unit/application/Parser/ScriptingDefinition/ScriptingDefinitionParser.spec.ts b/tests/unit/application/Parser/ScriptingDefinition/ScriptingDefinitionParser.spec.ts index df35a4fa..e6ebea13 100644 --- a/tests/unit/application/Parser/ScriptingDefinition/ScriptingDefinitionParser.spec.ts +++ b/tests/unit/application/Parser/ScriptingDefinition/ScriptingDefinitionParser.spec.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import type { EnumParser } from '@/application/Common/Enum'; -import type { ICodeSubstituter } from '@/application/Parser/ScriptingDefinition/ICodeSubstituter'; +import type { CodeSubstituter } from '@/application/Parser/ScriptingDefinition/CodeSubstituter'; import type { IScriptingDefinition } from '@/domain/IScriptingDefinition'; import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub'; import { EnumParserStub } from '@tests/unit/shared/Stubs/EnumParserStub'; @@ -83,7 +83,7 @@ describe('ScriptingDefinitionParser', () => { const context = new TestContext() .withData(data) .withProjectDetails(projectDetails) - .withSubstituter(substituterMock); + .withSubstituter(substituterMock.substitute); // act const definition = context.parseScriptingDefinition(); // assert @@ -99,7 +99,7 @@ class TestContext { private languageParser: EnumParser = new EnumParserStub() .setupDefaultValue(ScriptingLanguage.shellscript); - private codeSubstituter: ICodeSubstituter = new CodeSubstituterStub(); + private codeSubstituter: CodeSubstituter = new CodeSubstituterStub().substitute; private validator: TypeValidator = new TypeValidatorStub(); @@ -122,7 +122,7 @@ class TestContext { return this; } - public withSubstituter(substituter: ICodeSubstituter): this { + public withSubstituter(substituter: CodeSubstituter): this { this.codeSubstituter = substituter; return this; } diff --git a/tests/unit/shared/Stubs/CodeSubstituterStub.ts b/tests/unit/shared/Stubs/CodeSubstituterStub.ts index 97425f5a..344bce22 100644 --- a/tests/unit/shared/Stubs/CodeSubstituterStub.ts +++ b/tests/unit/shared/Stubs/CodeSubstituterStub.ts @@ -1,22 +1,22 @@ import type { ProjectDetails } from '@/domain/Project/ProjectDetails'; -import type { ICodeSubstituter } from '@/application/Parser/ScriptingDefinition/ICodeSubstituter'; +import type { CodeSubstituter } from '@/application/Parser/ScriptingDefinition/CodeSubstituter'; -export class CodeSubstituterStub implements ICodeSubstituter { +export class CodeSubstituterStub { private readonly scenarios = new Array<{ code: string, projectDetails: ProjectDetails, result: string }>(); - public substitute(code: string, projectDetails: ProjectDetails): string { + public setup(code: string, projectDetails: ProjectDetails, result: string) { + this.scenarios.push({ code, projectDetails, result }); + return this; + } + + public substitute: CodeSubstituter = (code: string, projectDetails: ProjectDetails) => { const scenario = this.scenarios.find( (s) => s.code === code && s.projectDetails === projectDetails, ); if (scenario) { return scenario.result; } - return `[CodeSubstituterStub] - code: ${code}`; - } - - public setup(code: string, projectDetails: ProjectDetails, result: string) { - this.scenarios.push({ code, projectDetails, result }); - return this; - } + return `[${CodeSubstituterStub.name}] - code: ${code}`; + }; } diff --git a/tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub.ts b/tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub.ts index fd92c13e..d9bb9cd0 100644 --- a/tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub.ts +++ b/tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub.ts @@ -1,9 +1,9 @@ -import type { IFunctionCallArgument } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/IFunctionCallArgument'; +import type { FunctionCallArgument } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; import type { IFunctionCallArgumentCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection'; import { FunctionCallArgumentStub } from './FunctionCallArgumentStub'; export class FunctionCallArgumentCollectionStub implements IFunctionCallArgumentCollection { - private args = new Array(); + private args = new Array(); public withEmptyArguments(): this { this.args.length = 0; @@ -36,7 +36,7 @@ export class FunctionCallArgumentCollectionStub implements IFunctionCallArgument return this.args.some((a) => a.parameterName === parameterName); } - public addArgument(argument: IFunctionCallArgument): void { + public addArgument(argument: FunctionCallArgument): void { this.args.push(argument); } @@ -44,7 +44,7 @@ export class FunctionCallArgumentCollectionStub implements IFunctionCallArgument return this.args.map((a) => a.parameterName); } - public getArgument(parameterName: string): IFunctionCallArgument { + public getArgument(parameterName: string): FunctionCallArgument { const arg = this.args.find((a) => a.parameterName === parameterName); if (!arg) { throw new Error(`no argument exists for parameter "${parameterName}"`); diff --git a/tests/unit/shared/Stubs/FunctionCallArgumentFactoryStub.ts b/tests/unit/shared/Stubs/FunctionCallArgumentFactoryStub.ts new file mode 100644 index 00000000..afb4da94 --- /dev/null +++ b/tests/unit/shared/Stubs/FunctionCallArgumentFactoryStub.ts @@ -0,0 +1,10 @@ +import type { FunctionCallArgumentFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; +import { FunctionCallArgumentStub } from './FunctionCallArgumentStub'; + +export class FunctionCallArgumentFactoryStub { + public factory: FunctionCallArgumentFactory = (parameterName, argumentValue) => { + return new FunctionCallArgumentStub() + .withParameterName(parameterName) + .withArgumentValue(argumentValue); + }; +} diff --git a/tests/unit/shared/Stubs/FunctionCallArgumentStub.ts b/tests/unit/shared/Stubs/FunctionCallArgumentStub.ts index 9a458a48..6634f5ca 100644 --- a/tests/unit/shared/Stubs/FunctionCallArgumentStub.ts +++ b/tests/unit/shared/Stubs/FunctionCallArgumentStub.ts @@ -1,6 +1,6 @@ -import type { IFunctionCallArgument } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/IFunctionCallArgument'; +import type { FunctionCallArgument } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; -export class FunctionCallArgumentStub implements IFunctionCallArgument { +export class FunctionCallArgumentStub implements FunctionCallArgument { public parameterName = 'stub-parameter-name'; public argumentValue = 'stub-arg-name'; diff --git a/tests/unit/shared/Stubs/FunctionParameterCollectionStub.ts b/tests/unit/shared/Stubs/FunctionParameterCollectionStub.ts index 01f6cbe7..f9071a6b 100644 --- a/tests/unit/shared/Stubs/FunctionParameterCollectionStub.ts +++ b/tests/unit/shared/Stubs/FunctionParameterCollectionStub.ts @@ -1,15 +1,15 @@ -import type { IFunctionParameter } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/IFunctionParameter'; import type { IFunctionParameterCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/IFunctionParameterCollection'; +import type { FunctionParameter } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameter'; import { FunctionParameterStub } from './FunctionParameterStub'; export class FunctionParameterCollectionStub implements IFunctionParameterCollection { - private parameters = new Array(); + private parameters = new Array(); - public addParameter(parameter: IFunctionParameter): void { + public addParameter(parameter: FunctionParameter): void { this.parameters.push(parameter); } - public get all(): readonly IFunctionParameter[] { + public get all(): readonly FunctionParameter[] { return this.parameters; } diff --git a/tests/unit/shared/Stubs/FunctionParameterParserStub.ts b/tests/unit/shared/Stubs/FunctionParameterParserStub.ts new file mode 100644 index 00000000..dd7c1df1 --- /dev/null +++ b/tests/unit/shared/Stubs/FunctionParameterParserStub.ts @@ -0,0 +1,8 @@ +import type { FunctionParameterParser } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterParser'; +import { FunctionParameterStub } from './FunctionParameterStub'; + +export const createFunctionParameterParserStub: FunctionParameterParser = (parameters) => { + return new FunctionParameterStub() + .withName(parameters.name) + .withOptional(parameters.optional || false); +}; diff --git a/tests/unit/shared/Stubs/FunctionParameterStub.ts b/tests/unit/shared/Stubs/FunctionParameterStub.ts index 8495aee7..ac5a8392 100644 --- a/tests/unit/shared/Stubs/FunctionParameterStub.ts +++ b/tests/unit/shared/Stubs/FunctionParameterStub.ts @@ -1,6 +1,6 @@ -import type { IFunctionParameter } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/IFunctionParameter'; +import type { FunctionParameter } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameter'; -export class FunctionParameterStub implements IFunctionParameter { +export class FunctionParameterStub implements FunctionParameter { public name = 'function-parameter-stub'; public isOptional = true; diff --git a/tests/unit/shared/Stubs/ParameterNameValidatorStub.ts b/tests/unit/shared/Stubs/ParameterNameValidatorStub.ts new file mode 100644 index 00000000..01f8a773 --- /dev/null +++ b/tests/unit/shared/Stubs/ParameterNameValidatorStub.ts @@ -0,0 +1,12 @@ +import type { ParameterNameValidator } from '@/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator'; + +export const createParameterNameValidatorStub = () => { + const validatedNames = new Array(); + const validator: ParameterNameValidator = (name) => { + validatedNames.push(name); + }; + return { + validator, + validatedNames, + }; +}; diff --git a/tests/unit/shared/Stubs/TypeValidatorStub.ts b/tests/unit/shared/Stubs/TypeValidatorStub.ts index b84ce673..da3b65e5 100644 --- a/tests/unit/shared/Stubs/TypeValidatorStub.ts +++ b/tests/unit/shared/Stubs/TypeValidatorStub.ts @@ -1,4 +1,7 @@ -import type { NonEmptyCollectionAssertion, ObjectAssertion, TypeValidator } from '@/application/Parser/Common/TypeValidator'; +import type { + NonEmptyCollectionAssertion, NonEmptyStringAssertion, + ObjectAssertion, TypeValidator, +} from '@/application/Parser/Common/TypeValidator'; import type { FunctionKeys } from '@/TypeHelpers'; import { expectDeepIncludes } from '@tests/shared/Assertions/ExpectDeepIncludes'; import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls'; @@ -22,6 +25,13 @@ export class TypeValidatorStub }); } + public assertNonEmptyString(assertion: NonEmptyStringAssertion): void { + this.registerMethodCall({ + methodName: 'assertNonEmptyString', + args: [assertion], + }); + } + public expectObjectAssertion( expectedAssertion: ObjectAssertion, ): void { @@ -34,6 +44,12 @@ export class TypeValidatorStub this.expectAssertion('assertNonEmptyCollection', expectedAssertion); } + public expectNonEmptyStringAssertion( + expectedAssertion: NonEmptyStringAssertion, + ): void { + this.expectAssertion('assertNonEmptyString', expectedAssertion); + } + private expectAssertion>( methodName: T, expectedAssertion: Parameters[0],