diff --git a/src/application/Parser/Executable/CategoryCollectionSpecificUtilities.ts b/src/application/Parser/Executable/CategoryCollectionSpecificUtilities.ts index 17a6e0d5..dfb92ef1 100644 --- a/src/application/Parser/Executable/CategoryCollectionSpecificUtilities.ts +++ b/src/application/Parser/Executable/CategoryCollectionSpecificUtilities.ts @@ -18,7 +18,10 @@ export const createCollectionUtilities: CategoryCollectionSpecificUtilitiesFacto ) => { const syntax = syntaxFactory.create(scripting.language); return { - compiler: new ScriptCompiler(functionsData ?? [], syntax), + compiler: new ScriptCompiler({ + functions: functionsData ?? [], + syntax, + }), syntax, }; }; diff --git a/src/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallParser.ts b/src/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser.ts similarity index 57% rename from src/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallParser.ts rename to src/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser.ts index fbd66585..3c8c54c4 100644 --- a/src/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallParser.ts +++ b/src/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser.ts @@ -1,27 +1,50 @@ 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 type { FunctionCall } from './FunctionCall'; -export function parseFunctionCalls(calls: FunctionCallsData): FunctionCall[] { - const sequence = getCallSequence(calls); - return sequence.map((call) => parseFunctionCall(call)); +export interface FunctionCallsParser { + ( + calls: FunctionCallsData, + validator?: TypeValidator, + ): FunctionCall[]; } -function getCallSequence(calls: FunctionCallsData): FunctionCallData[] { +export const parseFunctionCalls: FunctionCallsParser = ( + calls, + validator = createTypeValidator(), +) => { + const sequence = getCallSequence(calls, validator); + return sequence.map((call) => parseFunctionCall(call, validator)); +}; + +function getCallSequence(calls: FunctionCallsData, validator: TypeValidator): FunctionCallData[] { if (!isPlainObject(calls) && !isArray(calls)) { throw new Error('called function(s) must be an object or array'); } if (isArray(calls)) { + validator.assertNonEmptyCollection({ + value: calls, + valueName: 'function call sequence', + }); return calls as FunctionCallData[]; } const singleCall = calls as FunctionCallData; return [singleCall]; } -function parseFunctionCall(call: FunctionCallData): FunctionCall { +function parseFunctionCall( + call: FunctionCallData, + validator: TypeValidator, +): FunctionCall { + validator.assertObject({ + value: call, + valueName: 'function call', + allowedProperties: ['function', 'parameters'], + }); const callArgs = parseArgs(call.parameters); return new ParsedFunctionCall(call.function, callArgs); } diff --git a/src/application/Parser/Executable/Script/Compiler/Function/ISharedFunctionsParser.ts b/src/application/Parser/Executable/Script/Compiler/Function/ISharedFunctionsParser.ts deleted file mode 100644 index daffe498..00000000 --- a/src/application/Parser/Executable/Script/Compiler/Function/ISharedFunctionsParser.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { FunctionData } from '@/application/collections/'; -import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax'; -import type { ISharedFunctionCollection } from './ISharedFunctionCollection'; - -export interface ISharedFunctionsParser { - parseFunctions( - functions: readonly FunctionData[], - syntax: ILanguageSyntax, - ): ISharedFunctionCollection; -} diff --git a/src/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser.ts b/src/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser.ts index f7d7f22e..60f9dde0 100644 --- a/src/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser.ts +++ b/src/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser.ts @@ -12,50 +12,52 @@ import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/ import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction'; import { SharedFunctionCollection } from './SharedFunctionCollection'; import { FunctionParameter } from './Parameter/FunctionParameter'; -import { parseFunctionCalls } from './Call/FunctionCallParser'; +import { parseFunctionCalls, type FunctionCallsParser } from './Call/FunctionCallsParser'; import { createFunctionParameterCollection, type FunctionParameterCollectionFactory } from './Parameter/FunctionParameterCollectionFactory'; import type { ISharedFunctionCollection } from './ISharedFunctionCollection'; -import type { ISharedFunctionsParser } from './ISharedFunctionsParser'; import type { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection'; import type { ISharedFunction } from './ISharedFunction'; -const DefaultSharedFunctionsParsingUtilities: SharedFunctionsParsingUtilities = { +export interface SharedFunctionsParser { + ( + functions: readonly FunctionData[], + syntax: ILanguageSyntax, + utilities?: SharedFunctionsParsingUtilities, + ): ISharedFunctionCollection; +} + +export const parseSharedFunctions: SharedFunctionsParser = ( + functions: readonly FunctionData[], + syntax: ILanguageSyntax, + utilities = DefaultUtilities, +) => { + const collection = new SharedFunctionCollection(); + if (!functions.length) { + return collection; + } + ensureValidFunctions(functions); + return functions + .map((func) => parseFunction(func, syntax, utilities)) + .reduce((acc, func) => { + acc.addFunction(func); + return acc; + }, collection); +}; + +const DefaultUtilities: SharedFunctionsParsingUtilities = { wrapError: wrapErrorWithAdditionalContext, createParameter: (...args) => new FunctionParameter(...args), codeValidator: CodeValidator.instance, createParameterCollection: createFunctionParameterCollection, + parseFunctionCalls, }; -export class SharedFunctionsParser implements ISharedFunctionsParser { - public static readonly instance: ISharedFunctionsParser = new SharedFunctionsParser(); - - constructor( - private readonly utilities = DefaultSharedFunctionsParsingUtilities, - ) { } - - public parseFunctions( - functions: readonly FunctionData[], - syntax: ILanguageSyntax, - ): ISharedFunctionCollection { - const collection = new SharedFunctionCollection(); - if (!functions.length) { - return collection; - } - ensureValidFunctions(functions); - return functions - .map((func) => parseFunction(func, syntax, this.utilities)) - .reduce((acc, func) => { - acc.addFunction(func); - return acc; - }, collection); - } -} - interface SharedFunctionsParsingUtilities { readonly wrapError: ErrorWithContextWrapper; readonly createParameter: FunctionParameterFactory; readonly codeValidator: ICodeValidator; readonly createParameterCollection: FunctionParameterCollectionFactory; + readonly parseFunctionCalls: FunctionCallsParser; } export type FunctionParameterFactory = ( @@ -74,7 +76,7 @@ function parseFunction( return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode); } // Has call - const calls = parseFunctionCalls(data.call); + const calls = utilities.parseFunctionCalls(data.call); return createCallerFunction(name, parameters, calls); } diff --git a/src/application/Parser/Executable/Script/Compiler/ScriptCompiler.ts b/src/application/Parser/Executable/Script/Compiler/ScriptCompiler.ts index 68cf1fd0..5ad8d6c9 100644 --- a/src/application/Parser/Executable/Script/Compiler/ScriptCompiler.ts +++ b/src/application/Parser/Executable/Script/Compiler/ScriptCompiler.ts @@ -6,28 +6,46 @@ import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/ import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator'; import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError'; import { createScriptCode, type ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory'; -import { SharedFunctionsParser } from './Function/SharedFunctionsParser'; import { FunctionCallSequenceCompiler } from './Function/Call/Compiler/FunctionCallSequenceCompiler'; -import { parseFunctionCalls } from './Function/Call/FunctionCallParser'; +import { parseFunctionCalls } from './Function/Call/FunctionCallsParser'; +import { parseSharedFunctions, type SharedFunctionsParser } from './Function/SharedFunctionsParser'; import type { CompiledCode } from './Function/Call/Compiler/CompiledCode'; import type { IScriptCompiler } from './IScriptCompiler'; import type { ISharedFunctionCollection } from './Function/ISharedFunctionCollection'; import type { FunctionCallCompiler } from './Function/Call/Compiler/FunctionCallCompiler'; -import type { ISharedFunctionsParser } from './Function/ISharedFunctionsParser'; + +interface ScriptCompilerUtilities { + readonly sharedFunctionsParser: SharedFunctionsParser; + readonly callCompiler: FunctionCallCompiler; + readonly codeValidator: ICodeValidator; + readonly wrapError: ErrorWithContextWrapper; + readonly scriptCodeFactory: ScriptCodeFactory; +} + +const DefaultUtilities: ScriptCompilerUtilities = { + sharedFunctionsParser: parseSharedFunctions, + callCompiler: FunctionCallSequenceCompiler.instance, + codeValidator: CodeValidator.instance, + wrapError: wrapErrorWithAdditionalContext, + scriptCodeFactory: createScriptCode, +}; + +interface CategoryCollectionDataContext { + readonly functions: readonly FunctionData[]; + readonly syntax: ILanguageSyntax; +} export class ScriptCompiler implements IScriptCompiler { private readonly functions: ISharedFunctionCollection; constructor( - functions: readonly FunctionData[], - syntax: ILanguageSyntax, - sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance, - private readonly callCompiler: FunctionCallCompiler = FunctionCallSequenceCompiler.instance, - private readonly codeValidator: ICodeValidator = CodeValidator.instance, - private readonly wrapError: ErrorWithContextWrapper = wrapErrorWithAdditionalContext, - private readonly scriptCodeFactory: ScriptCodeFactory = createScriptCode, + categoryContext: CategoryCollectionDataContext, + private readonly utilities: ScriptCompilerUtilities = DefaultUtilities, ) { - this.functions = sharedFunctionsParser.parseFunctions(functions, syntax); + this.functions = this.utilities.sharedFunctionsParser( + categoryContext.functions, + categoryContext.syntax, + ); } public canCompile(script: ScriptData): boolean { @@ -40,14 +58,14 @@ export class ScriptCompiler implements IScriptCompiler { throw new Error('Script does include any calls.'); } const calls = parseFunctionCalls(script.call); - const compiledCode = this.callCompiler.compileFunctionCalls(calls, this.functions); - validateCompiledCode(compiledCode, this.codeValidator); - return this.scriptCodeFactory( + const compiledCode = this.utilities.callCompiler.compileFunctionCalls(calls, this.functions); + validateCompiledCode(compiledCode, this.utilities.codeValidator); + return this.utilities.scriptCodeFactory( compiledCode.code, compiledCode.revertCode, ); } catch (error) { - throw this.wrapError(error, `Failed to compile script: ${script.name}`); + throw this.utilities.wrapError(error, `Failed to compile script: ${script.name}`); } } } diff --git a/src/application/collections/windows.yaml b/src/application/collections/windows.yaml index e23a8018..fe11ebbd 100644 --- a/src/application/collections/windows.yaml +++ b/src/application/collections/windows.yaml @@ -4362,9 +4362,10 @@ actions: deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2) - function: RunInlineCode - code: reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Sensor\Overrides\{BFA794E4-F964-4FDB-90F6-51056BFE4B44}" /v "SensorPermissionState" /d "0" /t REG_DWORD /f - revertCode: >- # Default value is `1` since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2) - reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Sensor\Overrides\{BFA794E4-F964-4FDB-90F6-51056BFE4B44}" /v "SensorPermissionState" /d "1" /t REG_DWORD /f + parameters: + code: reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Sensor\Overrides\{BFA794E4-F964-4FDB-90F6-51056BFE4B44}" /v "SensorPermissionState" /d "0" /t REG_DWORD /f + revertCode: >- # Default value is `1` since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2) + reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Sensor\Overrides\{BFA794E4-F964-4FDB-90F6-51056BFE4B44}" /v "SensorPermissionState" /d "1" /t REG_DWORD /f - name: Disable device sensors recommend: standard @@ -5764,30 +5765,31 @@ actions: - # Using OS keys function: RunInlineCode - code: |- - if %PROCESSOR_ARCHITECTURE%==x86 ( REM is 32 bit? - reg add "HKLM\SOFTWARE\Microsoft\VSCommon\14.0\SQM" /v "OptIn" /t REG_DWORD /d 0 /f - reg add "HKLM\SOFTWARE\Microsoft\VSCommon\15.0\SQM" /v "OptIn" /t REG_DWORD /d 0 /f - reg add "HKLM\SOFTWARE\Microsoft\VSCommon\16.0\SQM" /v "OptIn" /t REG_DWORD /d 0 /f - reg add "HKLM\SOFTWARE\Microsoft\VSCommon\17.0\SQM" /v "OptIn" /t REG_DWORD /d 0 /f - ) else ( - reg add "HKLM\SOFTWARE\Wow6432Node\Microsoft\VSCommon\14.0\SQM" /v "OptIn" /t REG_DWORD /d 0 /f - reg add "HKLM\SOFTWARE\Wow6432Node\Microsoft\VSCommon\15.0\SQM" /v "OptIn" /t REG_DWORD /d 0 /f - reg add "HKLM\SOFTWARE\Wow6432Node\Microsoft\VSCommon\16.0\SQM" /v "OptIn" /t REG_DWORD /d 0 /f - reg add "HKLM\SOFTWARE\Wow6432Node\Microsoft\VSCommon\17.0\SQM" /v "OptIn" /t REG_DWORD /d 0 /f - ) - revertCode: |- - if %PROCESSOR_ARCHITECTURE%==x86 ( REM is 32 bit? - reg add "HKLM\SOFTWARE\Microsoft\VSCommon\14.0\SQM" /v "OptIn" /t REG_DWORD /d 1 /f - reg add "HKLM\SOFTWARE\Microsoft\VSCommon\15.0\SQM" /v "OptIn" /t REG_DWORD /d 1 /f - reg add "HKLM\SOFTWARE\Microsoft\VSCommon\16.0\SQM" /v "OptIn" /t REG_DWORD /d 1 /f - reg add "HKLM\SOFTWARE\Microsoft\VSCommon\17.0\SQM" /v "OptIn" /t REG_DWORD /d 1 /f - ) else ( - reg add "HKLM\SOFTWARE\Wow6432Node\Microsoft\VSCommon\14.0\SQM" /v "OptIn" /t REG_DWORD /d 1 /f - reg add "HKLM\SOFTWARE\Wow6432Node\Microsoft\VSCommon\15.0\SQM" /v "OptIn" /t REG_DWORD /d 1 /f - reg add "HKLM\SOFTWARE\Wow6432Node\Microsoft\VSCommon\16.0\SQM" /v "OptIn" /t REG_DWORD /d 1 /f - reg add "HKLM\SOFTWARE\Wow6432Node\Microsoft\VSCommon\17.0\SQM" /v "OptIn" /t REG_DWORD /d 1 /f - ) + parameters: + code: |- + if %PROCESSOR_ARCHITECTURE%==x86 ( REM is 32 bit? + reg add "HKLM\SOFTWARE\Microsoft\VSCommon\14.0\SQM" /v "OptIn" /t REG_DWORD /d 0 /f + reg add "HKLM\SOFTWARE\Microsoft\VSCommon\15.0\SQM" /v "OptIn" /t REG_DWORD /d 0 /f + reg add "HKLM\SOFTWARE\Microsoft\VSCommon\16.0\SQM" /v "OptIn" /t REG_DWORD /d 0 /f + reg add "HKLM\SOFTWARE\Microsoft\VSCommon\17.0\SQM" /v "OptIn" /t REG_DWORD /d 0 /f + ) else ( + reg add "HKLM\SOFTWARE\Wow6432Node\Microsoft\VSCommon\14.0\SQM" /v "OptIn" /t REG_DWORD /d 0 /f + reg add "HKLM\SOFTWARE\Wow6432Node\Microsoft\VSCommon\15.0\SQM" /v "OptIn" /t REG_DWORD /d 0 /f + reg add "HKLM\SOFTWARE\Wow6432Node\Microsoft\VSCommon\16.0\SQM" /v "OptIn" /t REG_DWORD /d 0 /f + reg add "HKLM\SOFTWARE\Wow6432Node\Microsoft\VSCommon\17.0\SQM" /v "OptIn" /t REG_DWORD /d 0 /f + ) + revertCode: |- + if %PROCESSOR_ARCHITECTURE%==x86 ( REM is 32 bit? + reg add "HKLM\SOFTWARE\Microsoft\VSCommon\14.0\SQM" /v "OptIn" /t REG_DWORD /d 1 /f + reg add "HKLM\SOFTWARE\Microsoft\VSCommon\15.0\SQM" /v "OptIn" /t REG_DWORD /d 1 /f + reg add "HKLM\SOFTWARE\Microsoft\VSCommon\16.0\SQM" /v "OptIn" /t REG_DWORD /d 1 /f + reg add "HKLM\SOFTWARE\Microsoft\VSCommon\17.0\SQM" /v "OptIn" /t REG_DWORD /d 1 /f + ) else ( + reg add "HKLM\SOFTWARE\Wow6432Node\Microsoft\VSCommon\14.0\SQM" /v "OptIn" /t REG_DWORD /d 1 /f + reg add "HKLM\SOFTWARE\Wow6432Node\Microsoft\VSCommon\15.0\SQM" /v "OptIn" /t REG_DWORD /d 1 /f + reg add "HKLM\SOFTWARE\Wow6432Node\Microsoft\VSCommon\16.0\SQM" /v "OptIn" /t REG_DWORD /d 1 /f + reg add "HKLM\SOFTWARE\Wow6432Node\Microsoft\VSCommon\17.0\SQM" /v "OptIn" /t REG_DWORD /d 1 /f + ) - function: SetRegistryValue parameters: diff --git a/tests/unit/application/Parser/Executable/CategoryCollectionSpecificUtilities.spec.ts b/tests/unit/application/Parser/Executable/CategoryCollectionSpecificUtilities.spec.ts index 82250473..f09372f6 100644 --- a/tests/unit/application/Parser/Executable/CategoryCollectionSpecificUtilities.spec.ts +++ b/tests/unit/application/Parser/Executable/CategoryCollectionSpecificUtilities.spec.ts @@ -32,7 +32,10 @@ describe('CategoryCollectionSpecificUtilities', () => { // arrange const functionsData = [createFunctionDataWithCode()]; const syntax = new LanguageSyntaxStub(); - const expected = new ScriptCompiler(functionsData, syntax); + const expected = new ScriptCompiler({ + functions: functionsData, + syntax, + }); const language = ScriptingLanguage.shellscript; const factoryMock = createSyntaxFactoryStub(language, syntax); const definition = new ScriptingDefinitionStub() diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallParser.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallParser.spec.ts deleted file mode 100644 index 152df25e..00000000 --- a/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallParser.spec.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { parseFunctionCalls } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallParser'; -import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub'; -import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests'; - -describe('FunctionCallParser', () => { - describe('parseFunctionCalls', () => { - it('throws if call is not an object', () => { - // arrange - const expectedError = 'called function(s) must be an object'; - const invalidCalls = ['string', 33, false]; - invalidCalls.forEach((invalidCall) => { - // act - const act = () => parseFunctionCalls(invalidCall as never); - // assert - expect(act).to.throw(expectedError); - }); - }); - describe('throws if call sequence has undefined function name', () => { - itEachAbsentStringValue((absentValue) => { - // arrange - const expectedError = 'missing function name in function call'; - const data = [ - new FunctionCallDataStub().withName('function-name'), - new FunctionCallDataStub().withName(absentValue), - ]; - // act - const act = () => parseFunctionCalls(data); - // assert - expect(act).to.throw(expectedError); - }, { excludeNull: true, excludeUndefined: true }); - }); - it('parses single call as expected', () => { - // arrange - const expectedFunctionName = 'functionName'; - const expectedParameterName = 'parameterName'; - const expectedArgumentValue = 'argumentValue'; - const data = new FunctionCallDataStub() - .withName(expectedFunctionName) - .withParameters({ [expectedParameterName]: expectedArgumentValue }); - // act - const actual = parseFunctionCalls(data); - // assert - expect(actual).to.have.lengthOf(1); - const call = actual[0]; - expect(call.functionName).to.equal(expectedFunctionName); - const { args } = call; - expect(args.getAllParameterNames()).to.have.lengthOf(1); - expect(args.hasArgument(expectedParameterName)).to.equal( - true, - `Does not include expected parameter: "${expectedParameterName}"\n` - + `But includes: "${args.getAllParameterNames()}"`, - ); - const argument = args.getArgument(expectedParameterName); - expect(argument.parameterName).to.equal(expectedParameterName); - expect(argument.argumentValue).to.equal(expectedArgumentValue); - }); - it('parses multiple calls as expected', () => { - // arrange - const getFunctionName = (index: number) => `functionName${index}`; - const getParameterName = (index: number) => `parameterName${index}`; - const getArgumentValue = (index: number) => `argumentValue${index}`; - const createCall = (index: number) => new FunctionCallDataStub() - .withName(getFunctionName(index)) - .withParameters({ [getParameterName(index)]: getArgumentValue(index) }); - const calls = [createCall(0), createCall(1), createCall(2), createCall(3)]; - // act - const actual = parseFunctionCalls(calls); - // assert - expect(actual).to.have.lengthOf(calls.length); - for (let i = 0; i < calls.length; i++) { - const call = actual[i]; - const expectedParameterName = getParameterName(i); - const expectedArgumentValue = getArgumentValue(i); - expect(call.functionName).to.equal(getFunctionName(i)); - expect(call.args.getAllParameterNames()).to.have.lengthOf(1); - expect(call.args.hasArgument(expectedParameterName)).to.equal(true); - const argument = call.args.getArgument(expectedParameterName); - expect(argument.parameterName).to.equal(expectedParameterName); - expect(argument.argumentValue).to.equal(expectedArgumentValue); - } - }); - }); -}); 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 new file mode 100644 index 00000000..99ff12d1 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser.spec.ts @@ -0,0 +1,197 @@ +import { describe, it, expect } from 'vitest'; +import type { FunctionCallsData, FunctionCallData } from '@/application/collections/'; +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 { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub'; + +describe('FunctionCallsParser', () => { + describe('parseFunctionCalls', () => { + describe('throws if single call is not an object or array', () => { + // arrange + const expectedError = 'called function(s) must be an object or array'; + const testScenarios: readonly { + readonly description: string; + readonly invalidData: FunctionCallsData; + }[] = [ + { + description: 'given a string', + invalidData: 'string' as unknown as FunctionCallsData, + }, + { + description: 'given a number', + invalidData: 33 as unknown as FunctionCallsData, + }, + { + description: 'given a boolean', + invalidData: false as unknown as FunctionCallsData, + }, + { + description: 'given null', + invalidData: null as unknown as FunctionCallsData, + }, + { + description: 'given undefined', + invalidData: undefined as unknown as FunctionCallsData, + }, + ]; + testScenarios.forEach(({ description, invalidData }) => { + it(description, () => { + const context = new TestContext() + .withData(invalidData); + // act + const act = () => context.parse(); + // assert + expect(act).to.throw(expectedError); + }); + }); + }); + describe('given a single call', () => { + it('validates single call as object', () => { + // arrange + const data = new FunctionCallDataStub(); + const expectedAssertion: ObjectAssertion = { + value: data, + valueName: 'function call', + allowedProperties: [ + 'function', 'parameters', + ], + }; + const validator = new TypeValidatorStub(); + const context = new TestContext() + .withData(data) + .withTypeValidator(validator); + // act + context.parse(); + // assert + validator.expectObjectAssertion(expectedAssertion); + }); + it('parses single call as expected', () => { + // arrange + const expectedFunctionName = 'functionName'; + const expectedParameterName = 'parameterName'; + const expectedArgumentValue = 'argumentValue'; + const data = new FunctionCallDataStub() + .withName(expectedFunctionName) + .withParameters({ [expectedParameterName]: expectedArgumentValue }); + // act + const actual = parseFunctionCalls(data); + // assert + expect(actual).to.have.lengthOf(1); + const call = actual[0]; + expect(call.functionName).to.equal(expectedFunctionName); + const { args } = call; + expect(args.getAllParameterNames()).to.have.lengthOf(1); + expect(args.hasArgument(expectedParameterName)).to.equal( + true, + `Does not include expected parameter: "${expectedParameterName}"\n` + + `But includes: "${args.getAllParameterNames()}"`, + ); + const argument = args.getArgument(expectedParameterName); + expect(argument.parameterName).to.equal(expectedParameterName); + expect(argument.argumentValue).to.equal(expectedArgumentValue); + }); + }); + describe('given a call sequence', () => { + describe('throws if call sequence has undefined function name', () => { + itEachAbsentStringValue((absentValue) => { + // arrange + const expectedError = 'missing function name in function call'; + const data = [ + new FunctionCallDataStub().withName('function-name'), + new FunctionCallDataStub().withName(absentValue), + ]; + // act + const act = () => parseFunctionCalls(data); + // assert + expect(act).to.throw(expectedError); + }, { excludeNull: true, excludeUndefined: true }); + }); + it('validates call sequence as non empty collection', () => { + // arrange + const data: FunctionCallsData = [new FunctionCallDataStub()]; + const expectedAssertion: NonEmptyCollectionAssertion = { + value: data, + valueName: 'function call sequence', + }; + const validator = new TypeValidatorStub(); + const context = new TestContext() + .withData(data) + .withTypeValidator(validator); + // act + context.parse(); + // assert + validator.expectNonEmptyCollectionAssertion(expectedAssertion); + }); + it('validates a call in call sequence as object', () => { + // arrange + const expectedValidatedCallData = new FunctionCallDataStub(); + const data: FunctionCallsData = [expectedValidatedCallData]; + const expectedAssertion: ObjectAssertion = { + value: expectedValidatedCallData, + valueName: 'function call', + allowedProperties: [ + 'function', 'parameters', + ], + }; + const validator = new TypeValidatorStub(); + const context = new TestContext() + .withData(data) + .withTypeValidator(validator); + // act + context.parse(); + // assert + validator.expectObjectAssertion(expectedAssertion); + }); + it('parses multiple calls as expected', () => { + // arrange + const getFunctionName = (index: number) => `functionName${index}`; + const getParameterName = (index: number) => `parameterName${index}`; + const getArgumentValue = (index: number) => `argumentValue${index}`; + const createCall = (index: number) => new FunctionCallDataStub() + .withName(getFunctionName(index)) + .withParameters({ [getParameterName(index)]: getArgumentValue(index) }); + const calls = [createCall(0), createCall(1), createCall(2), createCall(3)]; + // act + const actual = parseFunctionCalls(calls); + // assert + expect(actual).to.have.lengthOf(calls.length); + for (let i = 0; i < calls.length; i++) { + const call = actual[i]; + const expectedParameterName = getParameterName(i); + const expectedArgumentValue = getArgumentValue(i); + expect(call.functionName).to.equal(getFunctionName(i)); + expect(call.args.getAllParameterNames()).to.have.lengthOf(1); + expect(call.args.hasArgument(expectedParameterName)).to.equal(true); + const argument = call.args.getArgument(expectedParameterName); + expect(argument.parameterName).to.equal(expectedParameterName); + expect(argument.argumentValue).to.equal(expectedArgumentValue); + } + }); + }); + }); +}); + +class TestContext { + private validator: TypeValidator = new TypeValidatorStub(); + + private calls: FunctionCallsData = [new FunctionCallDataStub()]; + + public withTypeValidator(typeValidator: TypeValidator): this { + this.validator = typeValidator; + return this; + } + + public withData(calls: FunctionCallsData): this { + this.calls = calls; + return this; + } + + public parse(): ReturnType { + return parseFunctionCalls( + this.calls, + this.validator, + ); + } +} diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/Function/ExpectFunctionBodyType.ts b/tests/unit/application/Parser/Executable/Script/Compiler/Function/ExpectFunctionBodyType.ts index fd355d44..52c62e6b 100644 --- a/tests/unit/application/Parser/Executable/Script/Compiler/Function/ExpectFunctionBodyType.ts +++ b/tests/unit/application/Parser/Executable/Script/Compiler/Function/ExpectFunctionBodyType.ts @@ -2,6 +2,7 @@ import type { CallFunctionBody, CodeFunctionBody, SharedFunctionBody, } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunction'; import { FunctionBodyType } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunction'; +import { expectExists } from '@tests/shared/Assertions/ExpectExists'; import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; export function expectCodeFunctionBody( @@ -18,6 +19,7 @@ export function expectCallsFunctionBody( function expectBodyType(body: SharedFunctionBody, expectedType: FunctionBodyType) { const actualType = body.type; + expectExists(actualType, 'Function has no body'); expect(actualType).to.equal(expectedType, formatAssertionMessage([ `Actual: ${FunctionBodyType[actualType]}`, `Expected: ${FunctionBodyType[expectedType]}`, 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 96852733..47e1bbd2 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 @@ -1,14 +1,16 @@ import { describe, it, expect } from 'vitest'; -import type { FunctionData, CodeInstruction } from '@/application/collections/'; +import type { + FunctionData, CodeInstruction, + ParameterDefinitionData, FunctionCallsData, +} from '@/application/collections/'; import type { ISharedFunction } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunction'; -import { SharedFunctionsParser, type FunctionParameterFactory } from '@/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser'; -import { createFunctionDataWithCode, createFunctionDataWithoutCallOrCode } from '@tests/unit/shared/Stubs/FunctionDataStub'; +import { parseSharedFunctions, type FunctionParameterFactory } 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'; import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests'; import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax'; import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub'; -import { itIsSingletonFactory } from '@tests/unit/shared/TestCases/SingletonFactoryTests'; import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub'; import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator'; import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines'; @@ -19,18 +21,17 @@ import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithC import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester'; import type { FunctionParameterCollectionFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollectionFactory'; import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub'; +import type { FunctionCallsParser } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser'; +import { createFunctionCallsParserStub } from '@tests/unit/shared/Stubs/FunctionCallsParserStub'; +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 { expectCallsFunctionBody, expectCodeFunctionBody } from './ExpectFunctionBodyType'; describe('SharedFunctionsParser', () => { - describe('instance', () => { - itIsSingletonFactory({ - getter: () => SharedFunctionsParser.instance, - expectedType: SharedFunctionsParser, - }); - }); - describe('parseFunctions', () => { + describe('parseSharedFunctions', () => { describe('validates functions', () => { - it('throws when functions have no names', () => { + it('throws when no name is provided', () => { // arrange const invalidFunctions = [ createFunctionDataWithCode() @@ -45,13 +46,13 @@ describe('SharedFunctionsParser', () => { ]; const expectedError = `Some function(s) have no names:\n${invalidFunctions.map((f) => JSON.stringify(f)).join('\n')}`; // act - const act = () => new ParseFunctionsCallerWithDefaults() + const act = () => new TestContext() .withFunctions(invalidFunctions) .parseFunctions(); // assert expect(act).to.throw(expectedError); }); - it('throws when functions have same names', () => { + it('throws when functions have duplicate names', () => { // arrange const name = 'same-func-name'; const expectedError = `duplicate function name: "${name}"`; @@ -60,14 +61,14 @@ describe('SharedFunctionsParser', () => { createFunctionDataWithCode().withName(name), ]; // act - const act = () => new ParseFunctionsCallerWithDefaults() + const act = () => new TestContext() .withFunctions(functions) .parseFunctions(); // assert expect(act).to.throw(expectedError); }); - describe('throws when when function have duplicate code', () => { - it('code', () => { + describe('throws when functions have duplicate code', () => { + it('throws on code duplication', () => { // arrange const code = 'duplicate-code'; const expectedError = `duplicate "code" in functions: "${code}"`; @@ -76,13 +77,13 @@ describe('SharedFunctionsParser', () => { createFunctionDataWithoutCallOrCode().withName('func-2').withCode(code), ]; // act - const act = () => new ParseFunctionsCallerWithDefaults() + const act = () => new TestContext() .withFunctions(functions) .parseFunctions(); // assert expect(act).to.throw(expectedError); }); - it('revertCode', () => { + it('throws on revert code duplication', () => { // arrange const revertCode = 'duplicate-revert-code'; const expectedError = `duplicate "revertCode" in functions: "${revertCode}"`; @@ -93,15 +94,15 @@ describe('SharedFunctionsParser', () => { .withName('func-2').withCode('code-2').withRevertCode(revertCode), ]; // act - const act = () => new ParseFunctionsCallerWithDefaults() + const act = () => new TestContext() .withFunctions(functions) .parseFunctions(); // assert expect(act).to.throw(expectedError); }); }); - describe('ensures either call or code is defined', () => { - it('both code and call are defined', () => { + describe('throws when both or neither code and call are defined', () => { + it('throws when both code and call are defined', () => { // arrange const functionName = 'invalid-function'; const expectedError = `both "code" and "call" are defined in "${functionName}"`; @@ -110,45 +111,48 @@ describe('SharedFunctionsParser', () => { .withCode('code') .withMockCall(); // act - const act = () => new ParseFunctionsCallerWithDefaults() + const act = () => new TestContext() .withFunctions([invalidFunction]) .parseFunctions(); // assert expect(act).to.throw(expectedError); }); - it('neither code and call is defined', () => { + it('throws when neither code nor call is defined', () => { // arrange const functionName = 'invalid-function'; const expectedError = `neither "code" or "call" is defined in "${functionName}"`; const invalidFunction = createFunctionDataWithoutCallOrCode() .withName(functionName); // act - const act = () => new ParseFunctionsCallerWithDefaults() + const act = () => new TestContext() .withFunctions([invalidFunction]) .parseFunctions(); // assert expect(act).to.throw(expectedError); }); }); - describe('throws when parameters type is not as expected', () => { - const testScenarios = [ + describe('throws when parameter types are invalid', () => { + const testScenarios: readonly { + readonly description: string; + readonly invalidType: unknown; + }[] = [ { - state: 'when not an array', + description: 'parameter is not an array', invalidType: 5, }, { - state: 'when array but not of objects', + description: 'parameter array contains non-objects', invalidType: ['a', { a: 'b' }], }, ]; for (const testCase of testScenarios) { - it(testCase.state, () => { + it(testCase.description, () => { // arrange const func = createFunctionDataWithCode() .withParametersObject(testCase.invalidType as never); const expectedError = `parameters must be an array of objects in function(s) "${func.name}"`; // act - const act = () => new ParseFunctionsCallerWithDefaults() + const act = () => new TestContext() .withFunctions([func]) .parseFunctions(); // assert @@ -164,7 +168,7 @@ describe('SharedFunctionsParser', () => { .withRevertCode('expected revert code to be validated'); const validator = new CodeValidatorStub(); // act - new ParseFunctionsCallerWithDefaults() + new TestContext() .withFunctions([functionData]) .withValidator(validator) .parseFunctions(); @@ -190,7 +194,7 @@ describe('SharedFunctionsParser', () => { itThrowsContextualError({ // act throwingAction: (wrapError) => { - new ParseFunctionsCallerWithDefaults() + new TestContext() .withFunctions([functionData]) .withFunctionParameterFactory(parameterFactory) .withErrorWrapper(wrapError) @@ -203,10 +207,10 @@ describe('SharedFunctionsParser', () => { }); }); }); - describe('given empty functions, returns empty collection', () => { + describe('handles empty function data', () => { itEachAbsentCollectionValue((absentValue) => { // act - const actual = new ParseFunctionsCallerWithDefaults() + const actual = new TestContext() .withFunctions(absentValue) .parseFunctions(); // assert @@ -226,68 +230,181 @@ describe('SharedFunctionsParser', () => { new ParameterDefinitionDataStub().withName('expectedParameter2').withOptionality(false), ); // act - const collection = new ParseFunctionsCallerWithDefaults() + const collection = new TestContext() .withFunctions([expected]) .parseFunctions(); // expect const actual = collection.getFunctionByName(name); expectEqualName(expected, actual); - expectEqualParameters(expected, actual); + expectEqualParameters(expected.parameters, actual.parameters); expectEqualFunctionWithInlineCode(expected, actual); }); }); describe('function with calls', () => { - it('parses single function with call as expected', () => { - // arrange - const call = new FunctionCallDataStub() - .withName('calleeFunction') - .withParameters({ test: 'value' }); - const data = createFunctionDataWithoutCallOrCode() - .withName('caller-function') - .withCall(call); - // act - const collection = new ParseFunctionsCallerWithDefaults() - .withFunctions([data]) - .parseFunctions(); - // expect - const actual = collection.getFunctionByName(data.name); - expectEqualName(data, actual); - expectEqualParameters(data, actual); - expectEqualCalls([call], actual); + describe('parses single function correctly', () => { + it('parses name correctly', () => { + // arrange + const expectedName = 'expected-function-name'; + const data = createFunctionDataWithCode() + .withName(expectedName); + // act + const collection = new TestContext() + .withFunctions([data]) + .parseFunctions(); + // expect + const actual = collection.getFunctionByName(expectedName); + expect(actual.name).to.equal(expectedName); + expectEqualName(data, actual); + }); + it('parses parameters correctly', () => { + // arrange + const functionCallsParserStub = createFunctionCallsParserStub(); + const expectedParameters: readonly ParameterDefinitionData[] = [ + new ParameterDefinitionDataStub().withName('expectedParameter').withOptionality(true), + new ParameterDefinitionDataStub().withName('expectedParameter2').withOptionality(false), + ]; + const data = createFunctionDataWithCode() + .withParameters(...expectedParameters); + // act + const collection = new TestContext() + .withFunctions([data]) + .withFunctionCallsParser(functionCallsParserStub.parser) + .parseFunctions(); + // expect + const actual = collection.getFunctionByName(data.name); + expectEqualParameters(expectedParameters, actual.parameters); + }); + it('parses call correctly', () => { + // arrange + const functionCallsParserStub = createFunctionCallsParserStub(); + const inputCallData = new FunctionCallDataStub() + .withName('function-input-call'); + const data = createFunctionDataWithoutCallOrCode() + .withCall(inputCallData); + const expectedCall = new FunctionCallStub() + .withFunctionName('function-expected-call'); + functionCallsParserStub.setup(inputCallData, [expectedCall]); + // act + const collection = new TestContext() + .withFunctions([data]) + .withFunctionCallsParser(functionCallsParserStub.parser) + .parseFunctions(); + // expect + const actualFunction = collection.getFunctionByName(data.name); + expectEqualFunctionWithCalls([expectedCall], actualFunction); + }); }); - it('parses multiple functions with call as expected', () => { - // arrange - const call1 = new FunctionCallDataStub() - .withName('calleeFunction1') - .withParameters({ param: 'value' }); - const call2 = new FunctionCallDataStub() - .withName('calleeFunction2') - .withParameters({ param2: 'value2' }); - const caller1 = createFunctionDataWithoutCallOrCode() - .withName('caller-function') - .withCall(call1); - const caller2 = createFunctionDataWithoutCallOrCode() - .withName('caller-function-2') - .withCall([call1, call2]); - // act - const collection = new ParseFunctionsCallerWithDefaults() - .withFunctions([caller1, caller2]) - .parseFunctions(); - // expect - const compiledCaller1 = collection.getFunctionByName(caller1.name); - expectEqualName(caller1, compiledCaller1); - expectEqualParameters(caller1, compiledCaller1); - expectEqualCalls([call1], compiledCaller1); - const compiledCaller2 = collection.getFunctionByName(caller2.name); - expectEqualName(caller2, compiledCaller2); - expectEqualParameters(caller2, compiledCaller2); - expectEqualCalls([call1, call2], compiledCaller2); + describe('parses multiple functions correctly', () => { + it('parses names correctly', () => { + // arrange + const expectedNames: readonly string[] = [ + 'expected-function-name-1', + 'expected-function-name-2', + 'expected-function-name-3', + ]; + const data: readonly FunctionData[] = expectedNames.map( + (functionName) => createFunctionDataWithCall() + .withName(functionName), + ); + // act + const collection = new TestContext() + .withFunctions(data) + .parseFunctions(); + // expect + expectedNames.forEach((name, index) => { + const compiledFunction = collection.getFunctionByName(name); + expectEqualName(data[index], compiledFunction); + }); + }); + it('parses parameters correctly', () => { + // arrange + const testData: readonly { + readonly functionName: string; + readonly inputParameterData: readonly ParameterDefinitionData[]; + }[] = [ + { + functionName: 'func1', + inputParameterData: [ + new ParameterDefinitionDataStub().withName('func1-first-parameter'), + new ParameterDefinitionDataStub().withName('func1-second-parameter'), + ], + }, + { + functionName: 'func2', + inputParameterData: [ + new ParameterDefinitionDataStub().withName('func2-optional-parameter').withOptionality(true), + new ParameterDefinitionDataStub().withName('func2-required-parameter').withOptionality(false), + ], + }, + ]; + const data: readonly FunctionData[] = testData.map( + (d) => createFunctionDataWithCall() + .withName(d.functionName) + .withParameters(...d.inputParameterData), + ); + // act + const collection = new TestContext() + .withFunctions(data) + .parseFunctions(); + // expect + testData.forEach(({ functionName, inputParameterData }) => { + const actualFunction = collection.getFunctionByName(functionName); + expectEqualParameters(inputParameterData, actualFunction.parameters); + }); + }); + it('parses call correctly', () => { + // arrange + const functionCallsParserStub = createFunctionCallsParserStub(); + const callData: readonly { + readonly functionName: string; + readonly inputData: FunctionCallsData, + readonly expectedCalls: ReturnType, + }[] = [ + { + functionName: 'function-1', + inputData: new FunctionCallDataStub().withName('function-1-input-function-call'), + expectedCalls: [ + new FunctionCallStub().withFunctionName('function-1-compiled-function-call'), + ], + }, + { + functionName: 'function-2', + inputData: [ + new FunctionCallDataStub().withName('function-2-input-function-call-1'), + new FunctionCallDataStub().withName('function-2-input-function-call-2'), + ], + expectedCalls: [ + new FunctionCallStub().withFunctionName('function-2-compiled-function-call-1'), + new FunctionCallStub().withFunctionName('function-2-compiled-function-call-2'), + ], + }, + ]; + const data: readonly FunctionData[] = callData.map( + ({ functionName, inputData }) => createFunctionDataWithoutCallOrCode() + .withName(functionName) + .withCall(inputData), + ); + callData.forEach(({ + inputData, + expectedCalls, + }) => functionCallsParserStub.setup(inputData, expectedCalls)); + // act + const collection = new TestContext() + .withFunctions(data) + .withFunctionCallsParser(functionCallsParserStub.parser) + .parseFunctions(); + // expect + callData.forEach(({ functionName, expectedCalls }) => { + const actualFunction = collection.getFunctionByName(functionName); + expectEqualFunctionWithCalls(expectedCalls, actualFunction); + }); + }); }); }); }); }); -class ParseFunctionsCallerWithDefaults { +class TestContext { private syntax: ILanguageSyntax = new LanguageSyntaxStub(); private codeValidator: ICodeValidator = new CodeValidatorStub(); @@ -296,6 +413,8 @@ class ParseFunctionsCallerWithDefaults { private wrapError: ErrorWithContextWrapper = errorWithContextWrapperStub; + private functionCallsParser: FunctionCallsParser = createFunctionCallsParserStub().parser; + private parameterFactory: FunctionParameterFactory = ( name: string, isOptional: boolean, @@ -306,17 +425,22 @@ class ParseFunctionsCallerWithDefaults { private parameterCollectionFactory : FunctionParameterCollectionFactory = () => new FunctionParameterCollectionStub(); - public withSyntax(syntax: ILanguageSyntax) { + public withSyntax(syntax: ILanguageSyntax): this { this.syntax = syntax; return this; } - public withValidator(codeValidator: ICodeValidator) { + public withValidator(codeValidator: ICodeValidator): this { this.codeValidator = codeValidator; return this; } - public withFunctions(functions: readonly FunctionData[]) { + public withFunctionCallsParser(functionCallsParser: FunctionCallsParser): this { + this.functionCallsParser = functionCallsParser; + return this; + } + + public withFunctions(functions: readonly FunctionData[]): this { this.functions = functions; return this; } @@ -338,16 +462,18 @@ class ParseFunctionsCallerWithDefaults { return this; } - public parseFunctions() { - const sut = new SharedFunctionsParser( + public parseFunctions(): ReturnType { + return parseSharedFunctions( + this.functions, + this.syntax, { codeValidator: this.codeValidator, wrapError: this.wrapError, createParameter: this.parameterFactory, createParameterCollection: this.parameterCollectionFactory, + parseFunctionCalls: this.functionCallsParser, }, ); - return sut.parseFunctions(this.functions, this.syntax); } } @@ -355,12 +481,15 @@ function expectEqualName(expected: FunctionData, actual: ISharedFunction): void expect(actual.name).to.equal(expected.name); } -function expectEqualParameters(expected: FunctionData, actual: ISharedFunction): void { - const actualSimplifiedParameters = actual.parameters.all.map((parameter) => ({ +function expectEqualParameters( + expected: readonly ParameterDefinitionData[] | undefined, + actual: IReadOnlyFunctionParameterCollection, +): void { + const actualSimplifiedParameters = actual.all.map((parameter) => ({ name: parameter.name, optional: parameter.isOptional, })); - const expectedSimplifiedParameters = expected.parameters?.map((parameter) => ({ + const expectedSimplifiedParameters = expected?.map((parameter) => ({ name: parameter.name, optional: parameter.optional || false, })) || []; @@ -371,33 +500,18 @@ function expectEqualFunctionWithInlineCode( expected: CodeInstruction, actual: ISharedFunction, ): void { - expect(actual.body, `function "${actual.name}" has no body`); expectCodeFunctionBody(actual.body); expect(actual.body.code, `function "${actual.name}" has no code`); expect(actual.body.code.execute).to.equal(expected.code); expect(actual.body.code.revert).to.equal(expected.revertCode); } -function expectEqualCalls( - expected: FunctionCallDataStub[], - actual: ISharedFunction, -) { - expect(actual.body, `function "${actual.name}" has no body`); - expectCallsFunctionBody(actual.body); - expect(actual.body.calls, `function "${actual.name}" has no calls`); - const actualSimplifiedCalls = actual.body.calls - .map((call) => ({ - function: call.functionName, - params: call.args.getAllParameterNames().map((name) => ({ - name, value: call.args.getArgument(name).argumentValue, - })), - })); - const expectedSimplifiedCalls = expected - .map((call) => ({ - function: call.function, - params: Object.keys(call.parameters).map((key) => ( - { name: key, value: call.parameters[key] } - )), - })); - expect(actualSimplifiedCalls).to.deep.equal(expectedSimplifiedCalls, 'Unequal calls'); +function expectEqualFunctionWithCalls( + expectedCalls: readonly FunctionCall[], + actualFunction: ISharedFunction, +): void { + expectCallsFunctionBody(actualFunction.body); + const actualCalls = actualFunction.body.calls; + expect(actualCalls.length).to.equal(expectedCalls.length); + expect(actualCalls).to.have.members(expectedCalls); } diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/ScriptCompiler.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/ScriptCompiler.spec.ts index d014d4a1..5e985a85 100644 --- a/tests/unit/application/Parser/Executable/Script/Compiler/ScriptCompiler.spec.ts +++ b/tests/unit/application/Parser/Executable/Script/Compiler/ScriptCompiler.spec.ts @@ -1,14 +1,13 @@ import { describe, it, expect } from 'vitest'; import type { FunctionData } from '@/application/collections/'; import { ScriptCompiler } from '@/application/Parser/Executable/Script/Compiler/ScriptCompiler'; -import type { ISharedFunctionsParser } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunctionsParser'; import type { FunctionCallCompiler } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler'; import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub'; import { createFunctionDataWithCode } from '@tests/unit/shared/Stubs/FunctionDataStub'; import { FunctionCallCompilerStub } from '@tests/unit/shared/Stubs/FunctionCallCompilerStub'; -import { SharedFunctionsParserStub } from '@tests/unit/shared/Stubs/SharedFunctionsParserStub'; +import { createSharedFunctionsParserStub } from '@tests/unit/shared/Stubs/SharedFunctionsParserStub'; import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub'; -import { parseFunctionCalls } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallParser'; +import { parseFunctionCalls } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser'; import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub'; import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax'; import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator'; @@ -22,6 +21,7 @@ import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub'; import type { ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory'; import { createScriptCodeFactoryStub } from '@tests/unit/shared/Stubs/ScriptCodeFactoryStub'; import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester'; +import type { SharedFunctionsParser } from '@/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser'; describe('ScriptCompiler', () => { describe('canCompile', () => { @@ -90,7 +90,7 @@ describe('ScriptCompiler', () => { const script = createScriptDataWithCall(call); const functions = [createFunctionDataWithCode().withName('existing-func')]; const compiledFunctions = new SharedFunctionCollectionStub(); - const functionParserMock = new SharedFunctionsParserStub(); + const functionParserMock = createSharedFunctionsParserStub(); functionParserMock.setup(functions, compiledFunctions); const callCompilerMock = new FunctionCallCompilerStub(); callCompilerMock.setup( @@ -102,7 +102,7 @@ describe('ScriptCompiler', () => { ); const sut = new ScriptCompilerBuilder() .withFunctions(...functions) - .withSharedFunctionsParser(functionParserMock) + .withSharedFunctionsParser(functionParserMock.parser) .withFunctionCallCompiler(callCompilerMock) .withScriptCodeFactory(scriptCodeFactory) .build(); @@ -118,33 +118,35 @@ describe('ScriptCompiler', () => { it('parses functions with expected syntax', () => { // arrange const expected: ILanguageSyntax = new LanguageSyntaxStub(); - const parser = new SharedFunctionsParserStub(); + const functionParserMock = createSharedFunctionsParserStub(); const sut = new ScriptCompilerBuilder() .withSomeFunctions() .withSyntax(expected) - .withSharedFunctionsParser(parser) + .withSharedFunctionsParser(functionParserMock.parser) .build(); const scriptData = createScriptDataWithCall(); // act sut.compile(scriptData); // assert - expect(parser.callHistory.length).to.equal(1); - expect(parser.callHistory[0].syntax).to.equal(expected); + const parserCalls = functionParserMock.callHistory; + expect(parserCalls.length).to.equal(1); + expect(parserCalls[0].syntax).to.equal(expected); }); it('parses given functions', () => { // arrange const expectedFunctions = [createFunctionDataWithCode().withName('existing-func')]; - const parser = new SharedFunctionsParserStub(); + const functionParserMock = createSharedFunctionsParserStub(); const sut = new ScriptCompilerBuilder() .withFunctions(...expectedFunctions) - .withSharedFunctionsParser(parser) + .withSharedFunctionsParser(functionParserMock.parser) .build(); const scriptData = createScriptDataWithCall(); // act sut.compile(scriptData); // assert - expect(parser.callHistory.length).to.equal(1); - expect(parser.callHistory[0].functions).to.deep.equal(expectedFunctions); + const parserCalls = functionParserMock.callHistory; + expect(parserCalls.length).to.equal(1); + expect(parserCalls[0].functions).to.deep.equal(expectedFunctions); }); }); describe('rethrows error with script name', () => { @@ -243,7 +245,7 @@ class ScriptCompilerBuilder { private syntax: ILanguageSyntax = new LanguageSyntaxStub(); - private sharedFunctionsParser: ISharedFunctionsParser = new SharedFunctionsParserStub(); + private sharedFunctionsParser: SharedFunctionsParser = createSharedFunctionsParserStub().parser; private callCompiler: FunctionCallCompiler = new FunctionCallCompilerStub(); @@ -281,7 +283,7 @@ class ScriptCompilerBuilder { } public withSharedFunctionsParser( - sharedFunctionsParser: ISharedFunctionsParser, + sharedFunctionsParser: SharedFunctionsParser, ): this { this.sharedFunctionsParser = sharedFunctionsParser; return this; @@ -314,13 +316,17 @@ class ScriptCompilerBuilder { throw new Error('Function behavior not defined'); } return new ScriptCompiler( - this.functions, - this.syntax, - this.sharedFunctionsParser, - this.callCompiler, - this.codeValidator, - this.wrapError, - this.scriptCodeFactory, + { + functions: this.functions, + syntax: this.syntax, + }, + { + sharedFunctionsParser: this.sharedFunctionsParser, + callCompiler: this.callCompiler, + codeValidator: this.codeValidator, + wrapError: this.wrapError, + scriptCodeFactory: this.scriptCodeFactory, + }, ); } } diff --git a/tests/unit/shared/Stubs/FunctionCallsParserStub.ts b/tests/unit/shared/Stubs/FunctionCallsParserStub.ts new file mode 100644 index 00000000..42d75c44 --- /dev/null +++ b/tests/unit/shared/Stubs/FunctionCallsParserStub.ts @@ -0,0 +1,30 @@ +import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall'; +import type { FunctionCallsParser } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser'; +import type { FunctionCallsData } from '@/application/collections/'; +import { FunctionCallStub } from './FunctionCallStub'; + +export function createFunctionCallsParserStub() { + const setupResults = new Map(); + const parser: FunctionCallsParser = (rawData) => { + if (setupResults.size === 0) { + return [ + new FunctionCallStub().withFunctionName('function created by parser stub'), + ]; + } + const setupResult = setupResults.get(rawData); + if (setupResult === undefined) { + throw new Error(`Stub error: Expected pre-configured input data was not found.\n + Received input: ${JSON.stringify(rawData, null, 2)}\n + Number of configurations available: ${setupResults.size}\n + Available configurations: ${JSON.stringify([...setupResults.keys()].map((key) => JSON.stringify(key, null, 2)), null, 2)}`); + } + return setupResult; + }; + const setup = (rawData: FunctionCallsData, parsedData: FunctionCall[]) => { + setupResults.set(rawData, parsedData); + }; + return { + parser, + setup, + }; +} diff --git a/tests/unit/shared/Stubs/SharedFunctionsParserStub.ts b/tests/unit/shared/Stubs/SharedFunctionsParserStub.ts index 50418c81..734ac4d3 100644 --- a/tests/unit/shared/Stubs/SharedFunctionsParserStub.ts +++ b/tests/unit/shared/Stubs/SharedFunctionsParserStub.ts @@ -1,40 +1,51 @@ import type { FunctionData } from '@/application/collections/'; import { sequenceEqual } from '@/application/Common/Array'; import type { ISharedFunctionCollection } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunctionCollection'; -import type { ISharedFunctionsParser } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunctionsParser'; +import type { SharedFunctionsParser } from '@/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser'; import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax'; import { SharedFunctionCollectionStub } from './SharedFunctionCollectionStub'; -export class SharedFunctionsParserStub implements ISharedFunctionsParser { - public callHistory = new Array<{ - functions: readonly FunctionData[], - syntax: ILanguageSyntax, +export function createSharedFunctionsParserStub() { + const callHistory = new Array<{ + readonly functions: readonly FunctionData[], + readonly syntax: ILanguageSyntax, }>(); - private setupResults = new Array<{ - functions: readonly FunctionData[], - result: ISharedFunctionCollection, + const setupResults = new Array<{ + readonly functions: readonly FunctionData[], + readonly result: ISharedFunctionCollection, }>(); - public setup(functions: readonly FunctionData[], result: ISharedFunctionCollection) { - this.setupResults.push({ functions, result }); - } + const findResult = ( + functions: readonly FunctionData[], + ): ISharedFunctionCollection | undefined => { + return setupResults + .find((result) => sequenceEqual(result.functions, functions)) + ?.result; + }; - public parseFunctions( + const parser: SharedFunctionsParser = ( functions: readonly FunctionData[], syntax: ILanguageSyntax, - ): ISharedFunctionCollection { - this.callHistory.push({ + ) => { + callHistory.push({ functions: Array.from(functions), syntax, }); - const result = this.findResult(functions); + const result = findResult(functions); return result || new SharedFunctionCollectionStub(); - } + }; - private findResult(functions: readonly FunctionData[]): ISharedFunctionCollection | undefined { - return this.setupResults - .find((result) => sequenceEqual(result.functions, functions)) - ?.result; - } + const setup = ( + functions: readonly FunctionData[], + result: ISharedFunctionCollection, + ) => { + setupResults.push({ functions, result }); + }; + + return { + parser, + setup, + callHistory, + }; }