win: fix incomplete VSCEIP, location scripts

This commit improves the validation logic in parser, corrects Windows
collection files to adhere to expected structure. This validation helps
catch errors that previously led to incomplete generated code in scripts
for disabling VSCEIP and location settings.

Changes:

- Add type validation for function call structures in the
  parser/compiler. This helps prevent runtime errors by ensuring that
  only correctly structured data is processed.
- Fix scripts in the Windows collection that previoulsy had incomplete
  `code` or `revertCode` values. These corrections ensure that the
  scripts function as intended.
- Refactor related logic within the compiler/parser to improve
  testability and maintainability.
This commit is contained in:
undergroundwires
2024-06-18 17:59:32 +02:00
parent dc03bff324
commit 48761f62a2
14 changed files with 646 additions and 329 deletions

View File

@@ -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,
};
};

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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}`);
}
}
}

View File

@@ -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:

View File

@@ -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()

View File

@@ -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);
}
});
});
});

View File

@@ -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<FunctionCallData> = {
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<FunctionCallData> = {
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<typeof parseFunctionCalls> {
return parseFunctionCalls(
this.calls,
this.validator,
);
}
}

View File

@@ -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]}`,

View File

@@ -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<FunctionData>((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<FunctionCallsParser>,
}[] = [
{
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<typeof parseSharedFunctions> {
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);
}

View File

@@ -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,
},
);
}
}

View File

@@ -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<FunctionCallsData, FunctionCall[]>();
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,
};
}

View File

@@ -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,
};
}