diff --git a/src/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/CodeSegmentMerger.ts b/src/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/CodeSegmentMerger.ts new file mode 100644 index 00000000..139277d1 --- /dev/null +++ b/src/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/CodeSegmentMerger.ts @@ -0,0 +1,5 @@ +import { CompiledCode } from '../CompiledCode'; + +export interface CodeSegmentMerger { + mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode; +} diff --git a/src/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger.ts b/src/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger.ts new file mode 100644 index 00000000..fab561ea --- /dev/null +++ b/src/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger.ts @@ -0,0 +1,20 @@ +import { CompiledCode } from '../CompiledCode'; +import { CodeSegmentMerger } from './CodeSegmentMerger'; + +export class NewlineCodeSegmentMerger implements CodeSegmentMerger { + public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode { + if (!codeSegments?.length) { + throw new Error('missing segments'); + } + return { + code: joinCodeParts(codeSegments.map((f) => f.code)), + revertCode: joinCodeParts(codeSegments.map((f) => f.revertCode)), + }; + } +} + +function joinCodeParts(codeSegments: readonly string[]): string { + return codeSegments + .filter((segment) => segment?.length > 0) + .join('\n'); +} diff --git a/src/application/Parser/Script/Compiler/Function/Call/Compiler/ICompiledCode.ts b/src/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode.ts similarity index 64% rename from src/application/Parser/Script/Compiler/Function/Call/Compiler/ICompiledCode.ts rename to src/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode.ts index a776459c..cb4ff889 100644 --- a/src/application/Parser/Script/Compiler/Function/Call/Compiler/ICompiledCode.ts +++ b/src/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode.ts @@ -1,4 +1,4 @@ -export interface ICompiledCode { +export interface CompiledCode { readonly code: string; readonly revertCode?: string; } diff --git a/src/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext.ts b/src/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext.ts new file mode 100644 index 00000000..02be45ba --- /dev/null +++ b/src/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext.ts @@ -0,0 +1,9 @@ +import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection'; +import { FunctionCall } from '../FunctionCall'; +import type { SingleCallCompiler } from './SingleCall/SingleCallCompiler'; + +export interface FunctionCallCompilationContext { + readonly allFunctions: ISharedFunctionCollection; + readonly rootCallSequence: readonly FunctionCall[]; + readonly singleCallCompiler: SingleCallCompiler; +} diff --git a/src/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler.ts b/src/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler.ts index c470c29a..466a8f58 100644 --- a/src/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler.ts +++ b/src/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler.ts @@ -1,149 +1,10 @@ -import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection'; -import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall'; -import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; -import { ISharedFunctionCollection } from '../../ISharedFunctionCollection'; -import { IExpressionsCompiler } from '../../../Expressions/IExpressionsCompiler'; -import { ExpressionsCompiler } from '../../../Expressions/ExpressionsCompiler'; -import { ISharedFunction, IFunctionCode } from '../../ISharedFunction'; +import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection'; import { FunctionCall } from '../FunctionCall'; -import { FunctionCallArgumentCollection } from '../Argument/FunctionCallArgumentCollection'; -import { IFunctionCallCompiler } from './IFunctionCallCompiler'; -import { ICompiledCode } from './ICompiledCode'; +import { CompiledCode } from './CompiledCode'; -export class FunctionCallCompiler implements IFunctionCallCompiler { - public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler(); - - protected constructor( - private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(), - ) { - } - - public compileCall( - calls: IFunctionCall[], +export interface FunctionCallCompiler { + compileFunctionCalls( + calls: readonly FunctionCall[], functions: ISharedFunctionCollection, - ): ICompiledCode { - if (!functions) { throw new Error('missing functions'); } - if (!calls) { throw new Error('missing calls'); } - if (calls.some((f) => !f)) { throw new Error('missing function call'); } - const context: ICompilationContext = { - allFunctions: functions, - callSequence: calls, - expressionsCompiler: this.expressionsCompiler, - }; - const code = compileCallSequence(context); - return code; - } -} - -interface ICompilationContext { - allFunctions: ISharedFunctionCollection; - callSequence: readonly IFunctionCall[]; - expressionsCompiler: IExpressionsCompiler; -} - -interface ICompiledFunctionCall { - readonly code: string; - readonly revertCode: string; -} - -function compileCallSequence(context: ICompilationContext): ICompiledFunctionCall { - const compiledFunctions = context.callSequence - .flatMap((call) => compileSingleCall(call, context)); - return { - code: merge(compiledFunctions.map((f) => f.code)), - revertCode: merge(compiledFunctions.map((f) => f.revertCode)), - }; -} - -function compileSingleCall( - call: IFunctionCall, - context: ICompilationContext, -): ICompiledFunctionCall[] { - const func = context.allFunctions.getFunctionByName(call.functionName); - ensureThatCallArgumentsExistInParameterDefinition(func, call.args); - if (func.body.code) { // Function with inline code - const compiledCode = compileCode(func.body.code, call.args, context.expressionsCompiler); - return [compiledCode]; - } - // Function with inner calls - return func.body.calls - .map((innerCall) => { - const compiledArgs = compileArgs(innerCall.args, call.args, context.expressionsCompiler); - const compiledCall = new FunctionCall(innerCall.functionName, compiledArgs); - return compileSingleCall(compiledCall, context); - }) - .flat(); -} - -function compileCode( - code: IFunctionCode, - args: IReadOnlyFunctionCallArgumentCollection, - compiler: IExpressionsCompiler, -): ICompiledFunctionCall { - return { - code: compiler.compileExpressions(code.execute, args), - revertCode: compiler.compileExpressions(code.revert, args), - }; -} - -function compileArgs( - argsToCompile: IReadOnlyFunctionCallArgumentCollection, - args: IReadOnlyFunctionCallArgumentCollection, - compiler: IExpressionsCompiler, -): IReadOnlyFunctionCallArgumentCollection { - return argsToCompile - .getAllParameterNames() - .map((parameterName) => { - const { argumentValue } = argsToCompile.getArgument(parameterName); - const compiledValue = compiler.compileExpressions(argumentValue, args); - return new FunctionCallArgument(parameterName, compiledValue); - }) - .reduce((compiledArgs, arg) => { - compiledArgs.addArgument(arg); - return compiledArgs; - }, new FunctionCallArgumentCollection()); -} - -function merge(codeParts: readonly string[]): string { - return codeParts - .filter((part) => part?.length > 0) - .join('\n'); -} - -function ensureThatCallArgumentsExistInParameterDefinition( - func: ISharedFunction, - args: IReadOnlyFunctionCallArgumentCollection, -): void { - const callArgumentNames = args.getAllParameterNames(); - const functionParameterNames = func.parameters.all.map((param) => param.name) || []; - const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames); - throwIfNotEmpty(func.name, unexpectedParameters, functionParameterNames); -} - -function findUnexpectedParameters( - callArgumentNames: string[], - functionParameterNames: string[], -): string[] { - if (!callArgumentNames.length && !functionParameterNames.length) { - return []; - } - return callArgumentNames - .filter((callParam) => !functionParameterNames.includes(callParam)); -} - -function throwIfNotEmpty( - functionName: string, - unexpectedParameters: string[], - expectedParameters: string[], -) { - if (!unexpectedParameters.length) { - return; - } - throw new Error( - // eslint-disable-next-line prefer-template - `Function "${functionName}" has unexpected parameter(s) provided: ` - + `"${unexpectedParameters.join('", "')}"` - + '. Expected parameter(s): ' - + (expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'), - ); + ): CompiledCode; } diff --git a/src/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallSequenceCompiler.ts b/src/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallSequenceCompiler.ts new file mode 100644 index 00000000..85ea8b73 --- /dev/null +++ b/src/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallSequenceCompiler.ts @@ -0,0 +1,36 @@ +import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; +import { ISharedFunctionCollection } from '../../ISharedFunctionCollection'; +import { FunctionCallCompiler } from './FunctionCallCompiler'; +import { CompiledCode } from './CompiledCode'; +import { FunctionCallCompilationContext } from './FunctionCallCompilationContext'; +import { SingleCallCompiler } from './SingleCall/SingleCallCompiler'; +import { AdaptiveFunctionCallCompiler } from './SingleCall/AdaptiveFunctionCallCompiler'; +import { CodeSegmentMerger } from './CodeSegmentJoin/CodeSegmentMerger'; +import { NewlineCodeSegmentMerger } from './CodeSegmentJoin/NewlineCodeSegmentMerger'; + +export class FunctionCallSequenceCompiler implements FunctionCallCompiler { + public static readonly instance: FunctionCallCompiler = new FunctionCallSequenceCompiler(); + + /* The constructor is protected to enforce the singleton pattern. */ + protected constructor( + private readonly singleCallCompiler: SingleCallCompiler = new AdaptiveFunctionCallCompiler(), + private readonly codeSegmentMerger: CodeSegmentMerger = new NewlineCodeSegmentMerger(), + ) { } + + public compileFunctionCalls( + calls: readonly FunctionCall[], + functions: ISharedFunctionCollection, + ): CompiledCode { + if (!functions) { throw new Error('missing functions'); } + if (!calls?.length) { throw new Error('missing calls'); } + if (calls.some((f) => !f)) { throw new Error('missing function call'); } + const context: FunctionCallCompilationContext = { + allFunctions: functions, + rootCallSequence: calls, + singleCallCompiler: this.singleCallCompiler, + }; + const codeSegments = context.rootCallSequence + .flatMap((call) => this.singleCallCompiler.compileSingleCall(call, context)); + return this.codeSegmentMerger.mergeCodeParts(codeSegments); + } +} diff --git a/src/application/Parser/Script/Compiler/Function/Call/Compiler/IFunctionCallCompiler.ts b/src/application/Parser/Script/Compiler/Function/Call/Compiler/IFunctionCallCompiler.ts deleted file mode 100644 index 7ccb35a4..00000000 --- a/src/application/Parser/Script/Compiler/Function/Call/Compiler/IFunctionCallCompiler.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection'; -import { IFunctionCall } from '../IFunctionCall'; -import { ICompiledCode } from './ICompiledCode'; - -export interface IFunctionCallCompiler { - compileCall( - calls: IFunctionCall[], - functions: ISharedFunctionCollection): ICompiledCode; -} diff --git a/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/AdaptiveFunctionCallCompiler.ts b/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/AdaptiveFunctionCallCompiler.ts new file mode 100644 index 00000000..8e2a2c9a --- /dev/null +++ b/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/AdaptiveFunctionCallCompiler.ts @@ -0,0 +1,78 @@ +import { FunctionCall } from '../../FunctionCall'; +import { CompiledCode } from '../CompiledCode'; +import { FunctionCallCompilationContext } from '../FunctionCallCompilationContext'; +import { IReadOnlyFunctionCallArgumentCollection } from '../../Argument/IFunctionCallArgumentCollection'; +import { ISharedFunction } from '../../../ISharedFunction'; +import { SingleCallCompiler } from './SingleCallCompiler'; +import { SingleCallCompilerStrategy } from './SingleCallCompilerStrategy'; +import { InlineFunctionCallCompiler } from './Strategies/InlineFunctionCallCompiler'; +import { NestedFunctionCallCompiler } from './Strategies/NestedFunctionCallCompiler'; + +export class AdaptiveFunctionCallCompiler implements SingleCallCompiler { + public constructor( + private readonly strategies: SingleCallCompilerStrategy[] = [ + new InlineFunctionCallCompiler(), + new NestedFunctionCallCompiler(), + ], + ) { + } + + public compileSingleCall( + call: FunctionCall, + context: FunctionCallCompilationContext, + ): CompiledCode[] { + const func = context.allFunctions.getFunctionByName(call.functionName); + ensureThatCallArgumentsExistInParameterDefinition(func, call.args); + const strategy = this.findStrategy(func); + return strategy.compileFunction(func, call, context); + } + + private findStrategy(func: ISharedFunction): SingleCallCompilerStrategy { + const strategies = this.strategies.filter((strategy) => strategy.canCompile(func)); + if (strategies.length > 1) { + throw new Error('Multiple strategies found to compile the function call.'); + } + if (strategies.length === 0) { + throw new Error('No strategies found to compile the function call.'); + } + return strategies[0]; + } +} + +function ensureThatCallArgumentsExistInParameterDefinition( + func: ISharedFunction, + callArguments: IReadOnlyFunctionCallArgumentCollection, +): void { + const callArgumentNames = callArguments.getAllParameterNames(); + const functionParameterNames = func.parameters.all.map((param) => param.name) || []; + const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames); + throwIfUnexpectedParametersExist(func.name, unexpectedParameters, functionParameterNames); +} + +function findUnexpectedParameters( + callArgumentNames: string[], + functionParameterNames: string[], +): string[] { + if (!callArgumentNames.length && !functionParameterNames.length) { + return []; + } + return callArgumentNames + .filter((callParam) => !functionParameterNames.includes(callParam)); +} + +function throwIfUnexpectedParametersExist( + functionName: string, + unexpectedParameters: string[], + expectedParameters: string[], +) { + if (!unexpectedParameters.length) { + return; + } + throw new Error( + // eslint-disable-next-line prefer-template + `Function "${functionName}" has unexpected parameter(s) provided: ` + + `"${unexpectedParameters.join('", "')}"` + + '. Expected parameter(s): ' + + (expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'), + ); +} diff --git a/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler.ts b/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler.ts new file mode 100644 index 00000000..6b7727a9 --- /dev/null +++ b/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler.ts @@ -0,0 +1,10 @@ +import { FunctionCall } from '../../FunctionCall'; +import { CompiledCode } from '../CompiledCode'; +import { FunctionCallCompilationContext } from '../FunctionCallCompilationContext'; + +export interface SingleCallCompiler { + compileSingleCall( + call: FunctionCall, + context: FunctionCallCompilationContext, + ): CompiledCode[]; +} diff --git a/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompilerStrategy.ts b/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompilerStrategy.ts new file mode 100644 index 00000000..47127c24 --- /dev/null +++ b/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompilerStrategy.ts @@ -0,0 +1,13 @@ +import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; +import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; +import { CompiledCode } from '../CompiledCode'; +import { FunctionCallCompilationContext } from '../FunctionCallCompilationContext'; + +export interface SingleCallCompilerStrategy { + canCompile(func: ISharedFunction): boolean; + compileFunction( + calledFunction: ISharedFunction, + callToFunction: FunctionCall, + context: FunctionCallCompilationContext, + ): CompiledCode[], +} diff --git a/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/ArgumentCompiler.ts b/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/ArgumentCompiler.ts new file mode 100644 index 00000000..4f309fb1 --- /dev/null +++ b/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/ArgumentCompiler.ts @@ -0,0 +1,10 @@ +import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; +import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext'; + +export interface ArgumentCompiler { + createCompiledNestedCall( + nestedFunctionCall: FunctionCall, + parentFunctionCall: FunctionCall, + context: FunctionCallCompilationContext, + ): FunctionCall; +} diff --git a/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler.ts b/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler.ts new file mode 100644 index 00000000..58882618 --- /dev/null +++ b/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler.ts @@ -0,0 +1,109 @@ +import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection'; +import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; +import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection'; +import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler'; +import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler'; +import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; +import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext'; +import { ParsedFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall'; +import { ArgumentCompiler } from './ArgumentCompiler'; + +export class NestedFunctionArgumentCompiler implements ArgumentCompiler { + constructor( + private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(), + ) { } + + public createCompiledNestedCall( + nestedFunction: FunctionCall, + parentFunction: FunctionCall, + context: FunctionCallCompilationContext, + ): FunctionCall { + const compiledArgs = compileNestedFunctionArguments( + nestedFunction, + parentFunction.args, + context, + this.expressionsCompiler, + ); + const compiledCall = new ParsedFunctionCall(nestedFunction.functionName, compiledArgs); + return compiledCall; + } +} + +function compileNestedFunctionArguments( + nestedFunction: FunctionCall, + parentFunctionArgs: IReadOnlyFunctionCallArgumentCollection, + context: FunctionCallCompilationContext, + expressionsCompiler: IExpressionsCompiler, +): IReadOnlyFunctionCallArgumentCollection { + const requiredParameterNames = context + .allFunctions + .getRequiredParameterNames(nestedFunction.functionName); + const compiledArguments = nestedFunction.args + .getAllParameterNames() + // Compile each argument value + .map((paramName) => ({ + parameterName: paramName, + compiledArgumentValue: compileArgument( + paramName, + nestedFunction, + parentFunctionArgs, + expressionsCompiler, + ), + })) + // Filter out arguments with absent values + .filter(({ + parameterName, + compiledArgumentValue, + }) => isValidNonAbsentArgumentValue( + parameterName, + compiledArgumentValue, + requiredParameterNames, + )) + /* + Create argument object with non-absent values. + This is done after eliminating absent values because otherwise creating argument object + with absent values throws error. + */ + .map(({ + parameterName, + compiledArgumentValue, + }) => new FunctionCallArgument(parameterName, compiledArgumentValue)); + return buildArgumentCollectionFromArguments(compiledArguments); +} + +function isValidNonAbsentArgumentValue( + parameterName: string, + argumentValue: string | undefined, + requiredParameterNames: string[], +): boolean { + if (argumentValue) { + return true; + } + if (!requiredParameterNames.includes(parameterName)) { + return false; + } + throw new Error(`Compilation resulted in empty value for required parameter: "${parameterName}"`); +} + +function compileArgument( + parameterName: string, + nestedFunction: FunctionCall, + parentFunctionArgs: IReadOnlyFunctionCallArgumentCollection, + expressionsCompiler: IExpressionsCompiler, +): string { + try { + const { argumentValue: codeInArgument } = nestedFunction.args.getArgument(parameterName); + return expressionsCompiler.compileExpressions(codeInArgument, parentFunctionArgs); + } catch (err) { + throw new AggregateError([err], `Error when compiling argument for "${parameterName}"`); + } +} + +function buildArgumentCollectionFromArguments( + args: FunctionCallArgument[], +): FunctionCallArgumentCollection { + return args.reduce((compiledArgs, arg) => { + compiledArgs.addArgument(arg); + return compiledArgs; + }, new FunctionCallArgumentCollection()); +} diff --git a/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler.ts b/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler.ts new file mode 100644 index 00000000..7886723d --- /dev/null +++ b/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler.ts @@ -0,0 +1,31 @@ +import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler'; +import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler'; +import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; +import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; +import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode'; +import { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy'; + +export class InlineFunctionCallCompiler implements SingleCallCompilerStrategy { + public constructor( + private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(), + ) { + } + + public canCompile(func: ISharedFunction): boolean { + return func.body.code !== undefined; + } + + public compileFunction( + calledFunction: ISharedFunction, + callToFunction: FunctionCall, + ): CompiledCode[] { + const { code } = calledFunction.body; + const { args } = callToFunction; + return [ + { + code: this.expressionsCompiler.compileExpressions(code.execute, args), + revertCode: this.expressionsCompiler.compileExpressions(code.revert, args), + }, + ]; + } +} diff --git a/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/NestedFunctionCallCompiler.ts b/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/NestedFunctionCallCompiler.ts new file mode 100644 index 00000000..baaea003 --- /dev/null +++ b/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/NestedFunctionCallCompiler.ts @@ -0,0 +1,37 @@ +import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; +import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; +import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext'; +import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode'; +import { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy'; +import { ArgumentCompiler } from './Argument/ArgumentCompiler'; +import { NestedFunctionArgumentCompiler } from './Argument/NestedFunctionArgumentCompiler'; + +export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy { + public constructor( + private readonly argumentCompiler: ArgumentCompiler = new NestedFunctionArgumentCompiler(), + ) { + } + + public canCompile(func: ISharedFunction): boolean { + return func.body.calls !== undefined; + } + + public compileFunction( + calledFunction: ISharedFunction, + callToFunction: FunctionCall, + context: FunctionCallCompilationContext, + ): CompiledCode[] { + const nestedCalls = calledFunction.body.calls; + return nestedCalls.map((nestedCall) => { + try { + const compiledParentCall = this.argumentCompiler + .createCompiledNestedCall(nestedCall, callToFunction, context); + const compiledNestedCall = context.singleCallCompiler + .compileSingleCall(compiledParentCall, context); + return compiledNestedCall; + } catch (err) { + throw new AggregateError([err], `Error with call to "${nestedCall.functionName}" function from "${callToFunction.functionName}" function`); + } + }).flat(); + } +} diff --git a/src/application/Parser/Script/Compiler/Function/Call/FunctionCall.ts b/src/application/Parser/Script/Compiler/Function/Call/FunctionCall.ts index 9554cb81..0a81c52a 100644 --- a/src/application/Parser/Script/Compiler/Function/Call/FunctionCall.ts +++ b/src/application/Parser/Script/Compiler/Function/Call/FunctionCall.ts @@ -1,16 +1,6 @@ import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection'; -import { IFunctionCall } from './IFunctionCall'; -export class FunctionCall implements IFunctionCall { - constructor( - public readonly functionName: string, - public readonly args: IReadOnlyFunctionCallArgumentCollection, - ) { - if (!functionName) { - throw new Error('missing function name in function call'); - } - if (!args) { - throw new Error('missing args'); - } - } +export interface FunctionCall { + readonly functionName: string; + readonly args: IReadOnlyFunctionCallArgumentCollection; } diff --git a/src/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.ts b/src/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.ts index 2a3078a1..bb1f696a 100644 --- a/src/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.ts +++ b/src/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.ts @@ -1,10 +1,10 @@ import type { FunctionCallData, FunctionCallsData, FunctionCallParametersData } from '@/application/collections/'; -import { IFunctionCall } from './IFunctionCall'; +import { FunctionCall } from './FunctionCall'; import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection'; import { FunctionCallArgument } from './Argument/FunctionCallArgument'; -import { FunctionCall } from './FunctionCall'; +import { ParsedFunctionCall } from './ParsedFunctionCall'; -export function parseFunctionCalls(calls: FunctionCallsData): IFunctionCall[] { +export function parseFunctionCalls(calls: FunctionCallsData): FunctionCall[] { if (calls === undefined) { throw new Error('missing call data'); } @@ -22,12 +22,12 @@ function getCallSequence(calls: FunctionCallsData): FunctionCallData[] { return [calls as FunctionCallData]; } -function parseFunctionCall(call: FunctionCallData): IFunctionCall { +function parseFunctionCall(call: FunctionCallData): FunctionCall { if (!call) { throw new Error('missing call data'); } const callArgs = parseArgs(call.parameters); - return new FunctionCall(call.function, callArgs); + return new ParsedFunctionCall(call.function, callArgs); } function parseArgs( diff --git a/src/application/Parser/Script/Compiler/Function/Call/IFunctionCall.ts b/src/application/Parser/Script/Compiler/Function/Call/IFunctionCall.ts deleted file mode 100644 index 752d54dc..00000000 --- a/src/application/Parser/Script/Compiler/Function/Call/IFunctionCall.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection'; - -export interface IFunctionCall { - readonly functionName: string; - readonly args: IReadOnlyFunctionCallArgumentCollection; -} diff --git a/src/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall.ts b/src/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall.ts new file mode 100644 index 00000000..71b41469 --- /dev/null +++ b/src/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall.ts @@ -0,0 +1,16 @@ +import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection'; +import { FunctionCall } from './FunctionCall'; + +export class ParsedFunctionCall implements FunctionCall { + constructor( + public readonly functionName: string, + public readonly args: IReadOnlyFunctionCallArgumentCollection, + ) { + if (!functionName) { + throw new Error('missing function name in function call'); + } + if (!args) { + throw new Error('missing args'); + } + } +} diff --git a/src/application/Parser/Script/Compiler/Function/ISharedFunction.ts b/src/application/Parser/Script/Compiler/Function/ISharedFunction.ts index 09f3e4a9..de0267f5 100644 --- a/src/application/Parser/Script/Compiler/Function/ISharedFunction.ts +++ b/src/application/Parser/Script/Compiler/Function/ISharedFunction.ts @@ -1,5 +1,5 @@ import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection'; -import { IFunctionCall } from './Call/IFunctionCall'; +import { FunctionCall } from './Call/FunctionCall'; export interface ISharedFunction { readonly name: string; @@ -9,8 +9,8 @@ export interface ISharedFunction { export interface ISharedFunctionBody { readonly type: FunctionBodyType; - readonly code: IFunctionCode; - readonly calls: readonly IFunctionCall[]; + readonly code: IFunctionCode | undefined; + readonly calls: readonly FunctionCall[] | undefined; } export enum FunctionBodyType { diff --git a/src/application/Parser/Script/Compiler/Function/ISharedFunctionCollection.ts b/src/application/Parser/Script/Compiler/Function/ISharedFunctionCollection.ts index 3d69acc7..d68ef504 100644 --- a/src/application/Parser/Script/Compiler/Function/ISharedFunctionCollection.ts +++ b/src/application/Parser/Script/Compiler/Function/ISharedFunctionCollection.ts @@ -2,4 +2,5 @@ import { ISharedFunction } from './ISharedFunction'; export interface ISharedFunctionCollection { getFunctionByName(name: string): ISharedFunction; + getRequiredParameterNames(functionName: string): string[]; } diff --git a/src/application/Parser/Script/Compiler/Function/SharedFunction.ts b/src/application/Parser/Script/Compiler/Function/SharedFunction.ts index 9945bbde..5108a510 100644 --- a/src/application/Parser/Script/Compiler/Function/SharedFunction.ts +++ b/src/application/Parser/Script/Compiler/Function/SharedFunction.ts @@ -1,4 +1,4 @@ -import { IFunctionCall } from './Call/IFunctionCall'; +import { FunctionCall } from './Call/FunctionCall'; import { FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody, @@ -8,7 +8,7 @@ import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParam export function createCallerFunction( name: string, parameters: IReadOnlyFunctionParameterCollection, - callSequence: readonly IFunctionCall[], + callSequence: readonly FunctionCall[], ): ISharedFunction { if (!callSequence || !callSequence.length) { throw new Error(`missing call sequence in function "${name}"`); @@ -38,7 +38,7 @@ class SharedFunction implements ISharedFunction { constructor( public readonly name: string, public readonly parameters: IReadOnlyFunctionParameterCollection, - content: IFunctionCode | readonly IFunctionCall[], + content: IFunctionCode | readonly FunctionCall[], bodyType: FunctionBodyType, ) { if (!name) { throw new Error('missing function name'); } @@ -46,7 +46,7 @@ class SharedFunction implements ISharedFunction { this.body = { type: bodyType, code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined, - calls: bodyType === FunctionBodyType.Calls ? content as readonly IFunctionCall[] : undefined, + calls: bodyType === FunctionBodyType.Calls ? content as readonly FunctionCall[] : undefined, }; } } diff --git a/src/application/Parser/Script/Compiler/Function/SharedFunctionCollection.ts b/src/application/Parser/Script/Compiler/Function/SharedFunctionCollection.ts index a8679ea1..9cba7948 100644 --- a/src/application/Parser/Script/Compiler/Function/SharedFunctionCollection.ts +++ b/src/application/Parser/Script/Compiler/Function/SharedFunctionCollection.ts @@ -21,6 +21,15 @@ export class SharedFunctionCollection implements ISharedFunctionCollection { return func; } + public getRequiredParameterNames(functionName: string): string[] { + return this + .getFunctionByName(functionName) + .parameters + .all + .filter((parameter) => !parameter.isOptional) + .map((parameter) => parameter.name); + } + private has(functionName: string) { return this.functionsByName.has(functionName); } diff --git a/src/application/Parser/Script/Compiler/ScriptCompiler.ts b/src/application/Parser/Script/Compiler/ScriptCompiler.ts index a20c4a09..38c07a9c 100644 --- a/src/application/Parser/Script/Compiler/ScriptCompiler.ts +++ b/src/application/Parser/Script/Compiler/ScriptCompiler.ts @@ -7,12 +7,12 @@ import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmp import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator'; import { IScriptCompiler } from './IScriptCompiler'; import { ISharedFunctionCollection } from './Function/ISharedFunctionCollection'; -import { IFunctionCallCompiler } from './Function/Call/Compiler/IFunctionCallCompiler'; +import { FunctionCallSequenceCompiler } from './Function/Call/Compiler/FunctionCallSequenceCompiler'; import { FunctionCallCompiler } from './Function/Call/Compiler/FunctionCallCompiler'; import { ISharedFunctionsParser } from './Function/ISharedFunctionsParser'; import { SharedFunctionsParser } from './Function/SharedFunctionsParser'; import { parseFunctionCalls } from './Function/Call/FunctionCallParser'; -import { ICompiledCode } from './Function/Call/Compiler/ICompiledCode'; +import { CompiledCode } from './Function/Call/Compiler/CompiledCode'; export class ScriptCompiler implements IScriptCompiler { private readonly functions: ISharedFunctionCollection; @@ -21,7 +21,7 @@ export class ScriptCompiler implements IScriptCompiler { functions: readonly FunctionData[] | undefined, syntax: ILanguageSyntax, sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance, - private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance, + private readonly callCompiler: FunctionCallCompiler = FunctionCallSequenceCompiler.instance, private readonly codeValidator: ICodeValidator = CodeValidator.instance, ) { if (!syntax) { throw new Error('missing syntax'); } @@ -40,7 +40,7 @@ export class ScriptCompiler implements IScriptCompiler { if (!script) { throw new Error('missing script'); } try { const calls = parseFunctionCalls(script.call); - const compiledCode = this.callCompiler.compileCall(calls, this.functions); + const compiledCode = this.callCompiler.compileFunctionCalls(calls, this.functions); validateCompiledCode(compiledCode, this.codeValidator); return new ScriptCode( compiledCode.code, @@ -52,7 +52,7 @@ export class ScriptCompiler implements IScriptCompiler { } } -function validateCompiledCode(compiledCode: ICompiledCode, validator: ICodeValidator): void { +function validateCompiledCode(compiledCode: CompiledCode, validator: ICodeValidator): void { [compiledCode.code, compiledCode.revertCode].forEach( (code) => validator.throwIfInvalid(code, [new NoEmptyLines()]), ); diff --git a/tests/unit/application/Parser/NodeValidation/NodeValidator.spec.ts b/tests/unit/application/Parser/NodeValidation/NodeValidator.spec.ts index 2586f740..d3214bc7 100644 --- a/tests/unit/application/Parser/NodeValidation/NodeValidator.spec.ts +++ b/tests/unit/application/Parser/NodeValidation/NodeValidator.spec.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest'; import { NodeDataError } from '@/application/Parser/NodeValidation/NodeDataError'; import { NodeValidator } from '@/application/Parser/NodeValidation/NodeValidator'; -import { expectThrowsError } from '@tests/unit/shared/Assertions/ExpectThrowsError'; +import { expectDeepThrowsError } from '@tests/unit/shared/Assertions/ExpectDeepThrowsError'; import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub'; import { NodeDataErrorContextStub } from '@tests/unit/shared/Stubs/NodeDataErrorContextStub'; import { NodeData } from '@/application/Parser/NodeValidation/NodeData'; @@ -67,7 +67,7 @@ describe('NodeValidator', () => { // act const act = () => sut.assert(falsePredicate, message); // assert - expectThrowsError(act, expected); + expectDeepThrowsError(act, expected); }); it('does not throw if condition is true', () => { // arrange @@ -89,7 +89,7 @@ describe('NodeValidator', () => { // act const act = () => sut.throw(message); // assert - expectThrowsError(act, expected); + expectDeepThrowsError(act, expected); }); }); }); diff --git a/tests/unit/application/Parser/NodeValidation/NodeValidatorTestRunner.ts b/tests/unit/application/Parser/NodeValidation/NodeValidatorTestRunner.ts index 383eb94c..f91505d0 100644 --- a/tests/unit/application/Parser/NodeValidation/NodeValidatorTestRunner.ts +++ b/tests/unit/application/Parser/NodeValidation/NodeValidatorTestRunner.ts @@ -2,7 +2,7 @@ import { describe, it } from 'vitest'; import { NodeDataError, INodeDataErrorContext } from '@/application/Parser/NodeValidation/NodeDataError'; import { NodeData } from '@/application/Parser/NodeValidation/NodeData'; import { getAbsentObjectTestCases, getAbsentStringTestCases, itEachAbsentTestCase } from '@tests/unit/shared/TestCases/AbsentTests'; -import { expectThrowsError } from '@tests/unit/shared/Assertions/ExpectThrowsError'; +import { expectDeepThrowsError } from '@tests/unit/shared/Assertions/ExpectDeepThrowsError'; export interface ITestScenario { readonly act: () => void; @@ -82,6 +82,6 @@ export function expectThrowsNodeError( // act const act = () => test.act(); // assert - expectThrowsError(act, expected); + expectDeepThrowsError(act, expected); return this; } diff --git a/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger.spec.ts b/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger.spec.ts new file mode 100644 index 00000000..be66c367 --- /dev/null +++ b/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger.spec.ts @@ -0,0 +1,101 @@ +import { expect, describe, it } from 'vitest'; +import { NewlineCodeSegmentMerger } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger'; +import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub'; +import { getAbsentStringTestCases, itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests'; + +describe('NewlineCodeSegmentMerger', () => { + describe('mergeCodeParts', () => { + describe('throws given empty segments', () => { + itEachAbsentCollectionValue((absentValue) => { + // arrange + const expectedError = 'missing segments'; + const segments = absentValue; + const merger = new NewlineCodeSegmentMerger(); + // act + const act = () => merger.mergeCodeParts(segments); + // assert + expect(act).to.throw(expectedError); + }); + }); + describe('merges correctly', () => { + const testCases: ReadonlyArray<{ + readonly description: string, + readonly segments: CompiledCodeStub[], + readonly expected: { + readonly code: string, + readonly revertCode?: string, + }, + }> = [ + { + description: 'given `code` and `revertCode`', + segments: [ + new CompiledCodeStub().withCode('code1').withRevertCode('revert1'), + new CompiledCodeStub().withCode('code2').withRevertCode('revert2'), + new CompiledCodeStub().withCode('code3').withRevertCode('revert3'), + ], + expected: { + code: 'code1\ncode2\ncode3', + revertCode: 'revert1\nrevert2\nrevert3', + }, + }, + ...getAbsentStringTestCases().map((absentTestCase) => ({ + description: `filter out ${absentTestCase.valueName} \`revertCode\``, + segments: [ + new CompiledCodeStub().withCode('code1').withRevertCode('revert1'), + new CompiledCodeStub().withCode('code2').withRevertCode(absentTestCase.absentValue), + new CompiledCodeStub().withCode('code3').withRevertCode('revert3'), + ], + expected: { + code: 'code1\ncode2\ncode3', + revertCode: 'revert1\nrevert3', + }, + })), + { + description: 'given only `code` in segments', + segments: [ + new CompiledCodeStub().withCode('code1').withRevertCode(''), + new CompiledCodeStub().withCode('code2').withRevertCode(''), + ], + expected: { + code: 'code1\ncode2', + revertCode: '', + }, + }, + { + description: 'given mix of segments with only `code` or `revertCode`', + segments: [ + new CompiledCodeStub().withCode('code1').withRevertCode(''), + new CompiledCodeStub().withCode('').withRevertCode('revert2'), + new CompiledCodeStub().withCode('code3').withRevertCode(''), + ], + expected: { + code: 'code1\ncode3', + revertCode: 'revert2', + }, + }, + { + description: 'given only `revertCode` in segments', + segments: [ + new CompiledCodeStub().withCode('').withRevertCode('revert1'), + new CompiledCodeStub().withCode('').withRevertCode('revert2'), + ], + expected: { + code: '', + revertCode: 'revert1\nrevert2', + }, + }, + ]; + for (const { segments, expected, description } of testCases) { + it(description, () => { + // arrange + const merger = new NewlineCodeSegmentMerger(); + // act + const actual = merger.mergeCodeParts(segments); + // assert + expect(actual.code).to.equal(expected.code); + expect(actual.revertCode).to.equal(expected.revertCode); + }); + } + }); + }); +}); diff --git a/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler.spec.ts b/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler.spec.ts deleted file mode 100644 index 6ba90cdd..00000000 --- a/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler.spec.ts +++ /dev/null @@ -1,522 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import type { FunctionCallParametersData } from '@/application/collections/'; -import { FunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler'; -import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection'; -import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler'; -import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub'; -import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; -import { SharedFunctionStub } from '@tests/unit/shared/Stubs/SharedFunctionStub'; -import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub'; -import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub'; -import { ExpressionsCompilerStub } from '@tests/unit/shared/Stubs/ExpressionsCompilerStub'; -import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests'; -import { itIsSingleton } from '@tests/unit/shared/TestCases/SingletonTests'; - -describe('FunctionCallCompiler', () => { - describe('instance', () => { - itIsSingleton({ - getter: () => FunctionCallCompiler.instance, - expectedType: FunctionCallCompiler, - }); - }); - describe('compileCall', () => { - describe('parameter validation', () => { - describe('call', () => { - describe('throws with missing call', () => { - itEachAbsentObjectValue((absentValue) => { - // arrange - const expectedError = 'missing calls'; - const call = absentValue; - const functions = new SharedFunctionCollectionStub(); - const sut = new MockableFunctionCallCompiler(); - // act - const act = () => sut.compileCall(call, functions); - // assert - expect(act).to.throw(expectedError); - }); - }); - describe('throws if call sequence has absent call', () => { - itEachAbsentObjectValue((absentValue) => { - // arrange - const expectedError = 'missing function call'; - const call = [ - new FunctionCallStub(), - absentValue, - ]; - const functions = new SharedFunctionCollectionStub(); - const sut = new MockableFunctionCallCompiler(); - // act - const act = () => sut.compileCall(call, functions); - // assert - expect(act).to.throw(expectedError); - }); - }); - describe('throws if call parameters does not match function parameters', () => { - // arrange - const functionName = 'test-function-name'; - const testCases = [ - { - name: 'provided: single unexpected parameter, when: another expected', - functionParameters: ['expected-parameter'], - callParameters: ['unexpected-parameter'], - expectedError: - `Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"` - + '. Expected parameter(s): "expected-parameter"', - }, - { - name: 'provided: multiple unexpected parameters, when: different one is expected', - functionParameters: ['expected-parameter'], - callParameters: ['unexpected-parameter1', 'unexpected-parameter2'], - expectedError: - `Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter1", "unexpected-parameter2"` - + '. Expected parameter(s): "expected-parameter"', - }, - { - name: 'provided: an unexpected parameter, when: multiple parameters are expected', - functionParameters: ['expected-parameter1', 'expected-parameter2'], - callParameters: ['expected-parameter1', 'expected-parameter2', 'unexpected-parameter'], - expectedError: - `Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"` - + '. Expected parameter(s): "expected-parameter1", "expected-parameter2"', - }, - { - name: 'provided: an unexpected parameter, when: none required', - functionParameters: [], - callParameters: ['unexpected-call-parameter'], - expectedError: - `Function "${functionName}" has unexpected parameter(s) provided: "unexpected-call-parameter"` - + '. Expected parameter(s): none', - }, - { - name: 'provided: expected and unexpected parameter, when: one of them is expected', - functionParameters: ['expected-parameter'], - callParameters: ['expected-parameter', 'unexpected-parameter'], - expectedError: - `Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"` - + '. Expected parameter(s): "expected-parameter"', - }, - ]; - for (const testCase of testCases) { - it(testCase.name, () => { - const func = new SharedFunctionStub(FunctionBodyType.Code) - .withName('test-function-name') - .withParameterNames(...testCase.functionParameters); - const params = testCase.callParameters - .reduce((result, parameter) => { - return { ...result, [parameter]: 'defined-parameter-value ' }; - }, {} as FunctionCallParametersData); - const call = new FunctionCallStub() - .withFunctionName(func.name) - .withArguments(params); - const functions = new SharedFunctionCollectionStub() - .withFunction(func); - const sut = new MockableFunctionCallCompiler(); - // act - const act = () => sut.compileCall([call], functions); - // assert - expect(act).to.throw(testCase.expectedError); - }); - } - }); - }); - describe('functions', () => { - describe('throws with missing functions', () => { - itEachAbsentObjectValue((absentValue) => { - // arrange - const expectedError = 'missing functions'; - const call = new FunctionCallStub(); - const functions = absentValue; - const sut = new MockableFunctionCallCompiler(); - // act - const act = () => sut.compileCall([call], functions); - // assert - expect(act).to.throw(expectedError); - }); - }); - it('throws if function does not exist', () => { - // arrange - const expectedError = 'function does not exist'; - const call = new FunctionCallStub(); - const functions: ISharedFunctionCollection = { - getFunctionByName: () => { throw new Error(expectedError); }, - }; - const sut = new MockableFunctionCallCompiler(); - // act - const act = () => sut.compileCall([call], functions); - // assert - expect(act).to.throw(expectedError); - }); - }); - }); - describe('builds code as expected', () => { - describe('builds single call as expected', () => { - // arrange - const parametersTestCases = [ - { - name: 'empty parameters', - parameters: [], - callArgs: { }, - }, - { - name: 'non-empty parameters', - parameters: ['param1', 'param2'], - callArgs: { param1: 'value1', param2: 'value2' }, - }, - ]; - for (const testCase of parametersTestCases) { - it(testCase.name, () => { - const expected = { - execute: 'expected code (execute)', - revert: 'expected code (revert)', - }; - const func = new SharedFunctionStub(FunctionBodyType.Code) - .withParameterNames(...testCase.parameters); - const functions = new SharedFunctionCollectionStub().withFunction(func); - const call = new FunctionCallStub() - .withFunctionName(func.name) - .withArguments(testCase.callArgs); - const args = new FunctionCallArgumentCollectionStub().withArguments(testCase.callArgs); - const { code } = func.body; - const expressionsCompilerMock = new ExpressionsCompilerStub() - .setup({ givenCode: code.execute, givenArgs: args, result: expected.execute }) - .setup({ givenCode: code.revert, givenArgs: args, result: expected.revert }); - const sut = new MockableFunctionCallCompiler(expressionsCompilerMock); - // act - const actual = sut.compileCall([call], functions); - // assert - expect(actual.code).to.equal(expected.execute); - expect(actual.revertCode).to.equal(expected.revert); - }); - } - }); - it('builds call sequence as expected', () => { - // arrange - const firstFunction = new SharedFunctionStub(FunctionBodyType.Code) - .withName('first-function-name') - .withCode('first-function-code') - .withRevertCode('first-function-revert-code'); - const secondFunction = new SharedFunctionStub(FunctionBodyType.Code) - .withName('second-function-name') - .withParameterNames('testParameter') - .withCode('second-function-code') - .withRevertCode('second-function-revert-code'); - const secondCallArguments = { testParameter: 'testValue' }; - const calls = [ - new FunctionCallStub() - .withFunctionName(firstFunction.name) - .withArguments({}), - new FunctionCallStub() - .withFunctionName(secondFunction.name) - .withArguments(secondCallArguments), - ]; - const firstFunctionCallArgs = new FunctionCallArgumentCollectionStub(); - const secondFunctionCallArgs = new FunctionCallArgumentCollectionStub() - .withArguments(secondCallArguments); - const expressionsCompilerMock = new ExpressionsCompilerStub() - .setupToReturnFunctionCode(firstFunction, firstFunctionCallArgs) - .setupToReturnFunctionCode(secondFunction, secondFunctionCallArgs); - const expectedExecute = `${firstFunction.body.code.execute}\n${secondFunction.body.code.execute}`; - const expectedRevert = `${firstFunction.body.code.revert}\n${secondFunction.body.code.revert}`; - const functions = new SharedFunctionCollectionStub() - .withFunction(firstFunction) - .withFunction(secondFunction); - const sut = new MockableFunctionCallCompiler(expressionsCompilerMock); - // act - const actual = sut.compileCall(calls, functions); - // assert - expect(actual.code).to.equal(expectedExecute); - expect(actual.revertCode).to.equal(expectedRevert); - }); - describe('can compile a call tree (function calling another)', () => { - describe('single deep function call', () => { - it('builds 2nd level of depth without arguments', () => { - // arrange - const emptyArgs = new FunctionCallArgumentCollectionStub(); - const deepFunctionName = 'deepFunction'; - const functions = { - deep: new SharedFunctionStub(FunctionBodyType.Code) - .withName(deepFunctionName) - .withCode('deep function code') - .withRevertCode('deep function final code'), - front: new SharedFunctionStub(FunctionBodyType.Calls) - .withName('frontFunction') - .withCalls(new FunctionCallStub() - .withFunctionName(deepFunctionName) - .withArgumentCollection(emptyArgs)), - }; - const expected = { - code: 'final code', - revert: 'final revert code', - }; - const expressionsCompilerMock = new ExpressionsCompilerStub() - .setup({ - givenCode: functions.deep.body.code.execute, - givenArgs: emptyArgs, - result: expected.code, - }) - .setup({ - givenCode: functions.deep.body.code.revert, - givenArgs: emptyArgs, - result: expected.revert, - }); - const mainCall = new FunctionCallStub() - .withFunctionName(functions.front.name) - .withArgumentCollection(emptyArgs); - const sut = new MockableFunctionCallCompiler(expressionsCompilerMock); - // act - const actual = sut.compileCall( - [mainCall], - new SharedFunctionCollectionStub().withFunction(functions.deep, functions.front), - ); - // assert - expect(actual.code).to.equal(expected.code); - expect(actual.revertCode).to.equal(expected.revert); - }); - it('builds 2nd level of depth by compiling arguments', () => { - // arrange - const scenario = { - front: { - functionName: 'frontFunction', - parameterName: 'frontFunctionParameterName', - args: { - fromMainCall: 'initial argument to be compiled', - toNextStatic: 'value from "front" to "deep" in function definition', - toNextCompiled: 'argument from "front" to "deep" (compiled)', - }, - callArgs: { - initialFromMainCall: () => new FunctionCallArgumentCollectionStub() - .withArgument(scenario.front.parameterName, scenario.front.args.fromMainCall), - expectedCallDeep: () => new FunctionCallArgumentCollectionStub() - .withArgument(scenario.deep.parameterName, scenario.front.args.toNextCompiled), - }, - getFunction: () => new SharedFunctionStub(FunctionBodyType.Calls) - .withName(scenario.front.functionName) - .withParameterNames(scenario.front.parameterName) - .withCalls(new FunctionCallStub() - .withFunctionName(scenario.deep.functionName) - .withArgument(scenario.deep.parameterName, scenario.front.args.toNextStatic)), - }, - deep: { - functionName: 'deepFunction', - parameterName: 'deepFunctionParameterName', - getFunction: () => new SharedFunctionStub(FunctionBodyType.Code) - .withName(scenario.deep.functionName) - .withParameterNames(scenario.deep.parameterName) - .withCode(`${scenario.deep.functionName} function code`) - .withRevertCode(`${scenario.deep.functionName} function revert code`), - }, - }; - const expected = { - code: 'final code', - revert: 'final revert code', - }; - const expressionsCompilerMock = new ExpressionsCompilerStub() - .setup({ // Front ===args===> Deep - givenCode: scenario.front.args.toNextStatic, - givenArgs: scenario.front.callArgs.initialFromMainCall(), - result: scenario.front.args.toNextCompiled, - }) - // set-up compiling of deep, compiled argument should be sent - .setup({ - givenCode: scenario.deep.getFunction().body.code.execute, - givenArgs: scenario.front.callArgs.expectedCallDeep(), - result: expected.code, - }) - .setup({ - givenCode: scenario.deep.getFunction().body.code.revert, - givenArgs: scenario.front.callArgs.expectedCallDeep(), - result: expected.revert, - }); - const sut = new MockableFunctionCallCompiler(expressionsCompilerMock); - // act - const actual = sut.compileCall( - [ - new FunctionCallStub() - .withFunctionName(scenario.front.functionName) - .withArgumentCollection(scenario.front.callArgs.initialFromMainCall()), - ], - new SharedFunctionCollectionStub().withFunction( - scenario.deep.getFunction(), - scenario.front.getFunction(), - ), - ); - // assert - expect(actual.code).to.equal(expected.code); - expect(actual.revertCode).to.equal(expected.revert); - }); - it('builds 3rd level of depth by compiling arguments', () => { - // arrange - const scenario = { - first: { - functionName: 'firstFunction', - parameter: 'firstParameter', - args: { - fromMainCall: 'initial argument to be compiled', - toNextStatic: 'value from "first" to "second" in function definition', - toNextCompiled: 'argument from "first" to "second" (compiled)', - }, - callArgs: { - initialFromMainCall: () => new FunctionCallArgumentCollectionStub() - .withArgument(scenario.first.parameter, scenario.first.args.fromMainCall), - expectedToSecond: () => new FunctionCallArgumentCollectionStub() - .withArgument(scenario.second.parameter, scenario.first.args.toNextCompiled), - }, - getFunction: () => new SharedFunctionStub(FunctionBodyType.Calls) - .withName(scenario.first.functionName) - .withParameterNames(scenario.first.parameter) - .withCalls(new FunctionCallStub() - .withFunctionName(scenario.second.functionName) - .withArgument(scenario.second.parameter, scenario.first.args.toNextStatic)), - }, - second: { - functionName: 'secondFunction', - parameter: 'secondParameter', - args: { - toNextCompiled: 'argument second to third (compiled)', - toNextStatic: 'calling second to third', - }, - callArgs: { - expectedToThird: () => new FunctionCallArgumentCollectionStub() - .withArgument(scenario.third.parameter, scenario.second.args.toNextCompiled), - }, - getFunction: () => new SharedFunctionStub(FunctionBodyType.Calls) - .withName(scenario.second.functionName) - .withParameterNames(scenario.second.parameter) - .withCalls(new FunctionCallStub() - .withFunctionName(scenario.third.functionName) - .withArgument(scenario.third.parameter, scenario.second.args.toNextStatic)), - }, - third: { - functionName: 'thirdFunction', - parameter: 'thirdParameter', - getFunction: () => new SharedFunctionStub(FunctionBodyType.Code) - .withName(scenario.third.functionName) - .withParameterNames(scenario.third.parameter) - .withCode(`${scenario.third.functionName} function code`) - .withRevertCode(`${scenario.third.functionName} function revert code`), - }, - }; - const expected = { - code: 'final code', - revert: 'final revert code', - }; - const expressionsCompilerMock = new ExpressionsCompilerStub() - .setup({ // First ===args===> Second - givenCode: scenario.first.args.toNextStatic, - givenArgs: scenario.first.callArgs.initialFromMainCall(), - result: scenario.first.args.toNextCompiled, - }) - .setup({ // Second ===args===> third - givenCode: scenario.second.args.toNextStatic, - givenArgs: scenario.first.callArgs.expectedToSecond(), - result: scenario.second.args.toNextCompiled, - }) - // Compiling of third functions code with expected arguments - .setup({ - givenCode: scenario.third.getFunction().body.code.execute, - givenArgs: scenario.second.callArgs.expectedToThird(), - result: expected.code, - }) - .setup({ - givenCode: scenario.third.getFunction().body.code.revert, - givenArgs: scenario.second.callArgs.expectedToThird(), - result: expected.revert, - }); - const sut = new MockableFunctionCallCompiler(expressionsCompilerMock); - const mainCall = new FunctionCallStub() - .withFunctionName(scenario.first.functionName) - .withArgumentCollection(scenario.first.callArgs.initialFromMainCall()); - // act - const actual = sut.compileCall( - [mainCall], - new SharedFunctionCollectionStub().withFunction( - scenario.first.getFunction(), - scenario.second.getFunction(), - scenario.third.getFunction(), - ), - ); - // assert - expect(actual.code).to.equal(expected.code); - expect(actual.revertCode).to.equal(expected.revert); - }); - }); - describe('multiple deep function calls', () => { - it('builds 2nd level of depth without arguments', () => { - // arrange - const emptyArgs = new FunctionCallArgumentCollectionStub(); - const functions = { - call1: { - deep: { - functionName: 'deepFunction', - getFunction: () => new SharedFunctionStub(FunctionBodyType.Code) - .withName(functions.call1.deep.functionName) - .withCode('deep function (1) code') - .withRevertCode('deep function (1) final code'), - }, - front: { - getFunction: () => new SharedFunctionStub(FunctionBodyType.Calls) - .withName('frontFunction') - .withCalls(new FunctionCallStub() - .withFunctionName(functions.call1.deep.functionName) - .withArgumentCollection(emptyArgs)), - }, - }, - call2: { - deep: { - functionName: 'deepFunction2', - getFunction: () => new SharedFunctionStub(FunctionBodyType.Code) - .withName(functions.call2.deep.functionName) - .withCode('deep function (2) code') - .withRevertCode('deep function (2) final code'), - }, - front: { - getFunction: () => new SharedFunctionStub(FunctionBodyType.Calls) - .withName('frontFunction2') - .withCalls(new FunctionCallStub() - .withFunctionName(functions.call2.deep.functionName) - .withArgumentCollection(emptyArgs)), - }, - }, - getMainCall: () => [ - new FunctionCallStub() - .withFunctionName(functions.call1.front.getFunction().name) - .withArgumentCollection(emptyArgs), - new FunctionCallStub() - .withFunctionName(functions.call2.front.getFunction().name) - .withArgumentCollection(emptyArgs), - ], - getCollection: () => new SharedFunctionCollectionStub().withFunction( - functions.call1.deep.getFunction(), - functions.call1.front.getFunction(), - functions.call2.deep.getFunction(), - functions.call2.front.getFunction(), - ), - }; - const expressionsCompilerMock = new ExpressionsCompilerStub() - .setupToReturnFunctionCode(functions.call1.deep.getFunction(), emptyArgs) - .setupToReturnFunctionCode(functions.call2.deep.getFunction(), emptyArgs); - const sut = new MockableFunctionCallCompiler(expressionsCompilerMock); - const expected = { - code: `${functions.call1.deep.getFunction().body.code.execute}\n${functions.call2.deep.getFunction().body.code.execute}`, - revert: `${functions.call1.deep.getFunction().body.code.revert}\n${functions.call2.deep.getFunction().body.code.revert}`, - }; - // act - const actual = sut.compileCall( - functions.getMainCall(), - functions.getCollection(), - ); - // assert - expect(actual.code).to.equal(expected.code); - expect(actual.revertCode).to.equal(expected.revert); - }); - }); - }); - }); - }); -}); - -class MockableFunctionCallCompiler extends FunctionCallCompiler { - constructor(expressionsCompiler: IExpressionsCompiler = new ExpressionsCompilerStub()) { - super(expressionsCompiler); - } -} diff --git a/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallSequenceCompiler.spec.ts b/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallSequenceCompiler.spec.ts new file mode 100644 index 00000000..fd446978 --- /dev/null +++ b/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallSequenceCompiler.spec.ts @@ -0,0 +1,251 @@ +/* eslint-disable max-classes-per-file */ +import { describe, it, expect } from 'vitest'; +import { FunctionCallSequenceCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallSequenceCompiler'; +import { itIsSingleton } from '@tests/unit/shared/TestCases/SingletonTests'; +import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests'; +import { SingleCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler'; +import { CodeSegmentMerger } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/CodeSegmentMerger'; +import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection'; +import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; +import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub'; +import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub'; +import { SingleCallCompilerStub } from '@tests/unit/shared/Stubs/SingleCallCompilerStub'; +import { CodeSegmentMergerStub } from '@tests/unit/shared/Stubs/CodeSegmentMergerStub'; +import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub'; +import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode'; + +describe('FunctionCallSequenceCompiler', () => { + describe('instance', () => { + itIsSingleton({ + getter: () => FunctionCallSequenceCompiler.instance, + expectedType: FunctionCallSequenceCompiler, + }); + }); + describe('compileFunctionCalls', () => { + describe('parameter validation', () => { + describe('calls', () => { + describe('throws with missing call', () => { + itEachAbsentObjectValue((absentValue) => { + // arrange + const expectedError = 'missing calls'; + const calls = absentValue; + const builder = new FunctionCallSequenceCompilerBuilder() + .withCalls(calls); + // act + const act = () => builder.compileFunctionCalls(); + // assert + expect(act).to.throw(expectedError); + }); + }); + describe('throws if call sequence has absent call', () => { + itEachAbsentObjectValue((absentValue) => { + // arrange + const expectedError = 'missing function call'; + const calls = [ + new FunctionCallStub(), + absentValue, + ]; + const builder = new FunctionCallSequenceCompilerBuilder() + .withCalls(calls); + // act + const act = () => builder.compileFunctionCalls(); + // assert + expect(act).to.throw(expectedError); + }); + }); + }); + describe('functions', () => { + describe('throws with missing functions', () => { + itEachAbsentObjectValue((absentValue) => { + // arrange + const expectedError = 'missing functions'; + const functions = absentValue; + const builder = new FunctionCallSequenceCompilerBuilder() + .withFunctions(functions); + // act + const act = () => builder.compileFunctionCalls(); + // assert + expect(act).to.throw(expectedError); + }); + }); + }); + }); + describe('invokes single call compiler correctly', () => { + describe('calls', () => { + it('with expected call', () => { + // arrange + const singleCallCompilerStub = new SingleCallCompilerStub(); + const expectedCall = new FunctionCallStub(); + const builder = new FunctionCallSequenceCompilerBuilder() + .withSingleCallCompiler(singleCallCompilerStub) + .withCalls([expectedCall]); + // act + builder.compileFunctionCalls(); + // assert + expect(singleCallCompilerStub.callHistory).to.have.lengthOf(1); + const calledMethod = singleCallCompilerStub.callHistory.find((m) => m.methodName === 'compileSingleCall'); + expect(calledMethod).toBeDefined(); + expect(calledMethod.args[0]).to.equal(expectedCall); + }); + it('with every call', () => { + // arrange + const singleCallCompilerStub = new SingleCallCompilerStub(); + const expectedCalls = [ + new FunctionCallStub(), new FunctionCallStub(), new FunctionCallStub(), + ]; + const builder = new FunctionCallSequenceCompilerBuilder() + .withSingleCallCompiler(singleCallCompilerStub) + .withCalls(expectedCalls); + // act + builder.compileFunctionCalls(); + // assert + const calledMethods = singleCallCompilerStub.callHistory.filter((m) => m.methodName === 'compileSingleCall'); + expect(calledMethods).to.have.lengthOf(expectedCalls.length); + const callArguments = calledMethods.map((c) => c.args[0]); + expect(expectedCalls).to.have.members(callArguments); + }); + }); + describe('context', () => { + it('with expected functions', () => { + // arrange + const singleCallCompilerStub = new SingleCallCompilerStub(); + const expectedFunctions = new SharedFunctionCollectionStub(); + const builder = new FunctionCallSequenceCompilerBuilder() + .withSingleCallCompiler(singleCallCompilerStub) + .withFunctions(expectedFunctions); + // act + builder.compileFunctionCalls(); + // assert + expect(singleCallCompilerStub.callHistory).to.have.lengthOf(1); + const calledMethod = singleCallCompilerStub.callHistory.find((m) => m.methodName === 'compileSingleCall'); + expect(calledMethod).toBeDefined(); + const actualFunctions = calledMethod.args[1].allFunctions; + expect(actualFunctions).to.equal(expectedFunctions); + }); + it('with expected call sequence', () => { + // arrange + const singleCallCompilerStub = new SingleCallCompilerStub(); + const expectedCallSequence = [new FunctionCallStub(), new FunctionCallStub()]; + const builder = new FunctionCallSequenceCompilerBuilder() + .withSingleCallCompiler(singleCallCompilerStub) + .withCalls(expectedCallSequence); + // act + builder.compileFunctionCalls(); + // assert + const calledMethods = singleCallCompilerStub.callHistory.filter((m) => m.methodName === 'compileSingleCall'); + expect(calledMethods).to.have.lengthOf(expectedCallSequence.length); + const calledSequenceArgs = calledMethods.map((call) => call.args[1].rootCallSequence); + expect(calledSequenceArgs.every((sequence) => sequence === expectedCallSequence)); + }); + it('with expected call compiler', () => { + // arrange + const expectedCompiler = new SingleCallCompilerStub(); + const rootCallSequence = [new FunctionCallStub(), new FunctionCallStub()]; + const builder = new FunctionCallSequenceCompilerBuilder() + .withCalls(rootCallSequence) + .withSingleCallCompiler(expectedCompiler); + // act + builder.compileFunctionCalls(); + // assert + const calledMethods = expectedCompiler.callHistory.filter((m) => m.methodName === 'compileSingleCall'); + expect(calledMethods).to.have.lengthOf(rootCallSequence.length); + const compilerArgs = calledMethods.map((call) => call.args[1].singleCallCompiler); + expect(compilerArgs.every((compiler) => compiler === expectedCompiler)); + }); + }); + }); + describe('code segment merger', () => { + it('invokes code segment merger correctly', () => { + // arrange + const singleCallCompilationScenario = new Map([ + [new FunctionCallStub(), [new CompiledCodeStub()]], + [new FunctionCallStub(), [new CompiledCodeStub(), new CompiledCodeStub()]], + ]); + const expectedFlattenedSegments = [...singleCallCompilationScenario.values()].flat(); + const calls = [...singleCallCompilationScenario.keys()]; + const singleCallCompiler = new SingleCallCompilerStub() + .withCallCompilationScenarios(singleCallCompilationScenario); + const codeSegmentMergerStub = new CodeSegmentMergerStub(); + const builder = new FunctionCallSequenceCompilerBuilder() + .withCalls(calls) + .withSingleCallCompiler(singleCallCompiler) + .withCodeSegmentMerger(codeSegmentMergerStub); + // act + builder.compileFunctionCalls(); + // assert + const [actualSegments] = codeSegmentMergerStub.callHistory.find((c) => c.methodName === 'mergeCodeParts').args; + expect(expectedFlattenedSegments).to.have.lengthOf(actualSegments.length); + expect(expectedFlattenedSegments).to.have.deep.members(actualSegments); + }); + it('returns code segment merger result', () => { + // arrange + const expectedResult = new CompiledCodeStub(); + const codeSegmentMergerStub = new CodeSegmentMergerStub(); + codeSegmentMergerStub.mergeCodeParts = () => expectedResult; + const builder = new FunctionCallSequenceCompilerBuilder() + .withCodeSegmentMerger(codeSegmentMergerStub); + // act + const actualResult = builder.compileFunctionCalls(); + // assert + expect(actualResult).to.equal(expectedResult); + }); + }); + }); +}); + +class FunctionCallSequenceCompilerBuilder { + private singleCallCompiler: SingleCallCompiler = new SingleCallCompilerStub(); + + private codeSegmentMerger: CodeSegmentMerger = new CodeSegmentMergerStub(); + + private functions: ISharedFunctionCollection = new SharedFunctionCollectionStub(); + + private calls: readonly FunctionCall[] = [ + new FunctionCallStub(), + ]; + + public withSingleCallCompiler(compiler: SingleCallCompiler): this { + this.singleCallCompiler = compiler; + return this; + } + + public withCodeSegmentMerger(merger: CodeSegmentMerger): this { + this.codeSegmentMerger = merger; + return this; + } + + public withCalls(calls: readonly FunctionCall[]): this { + this.calls = calls; + return this; + } + + public withFunctions(functions: ISharedFunctionCollection): this { + this.functions = functions; + return this; + } + + public compileFunctionCalls() { + const compiler = new TestableFunctionCallSequenceCompiler({ + singleCallCompiler: this.singleCallCompiler, + codeSegmentMerger: this.codeSegmentMerger, + }); + return compiler.compileFunctionCalls( + this.calls, + this.functions, + ); + } +} + +interface FunctionCallSequenceCompilerStubs { + readonly singleCallCompiler?: SingleCallCompiler; + readonly codeSegmentMerger: CodeSegmentMerger; +} + +class TestableFunctionCallSequenceCompiler extends FunctionCallSequenceCompiler { + public constructor(options: FunctionCallSequenceCompilerStubs) { + super( + options.singleCallCompiler, + options.codeSegmentMerger, + ); + } +} diff --git a/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/NestedFunctionCallCompiler.spec.ts b/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/NestedFunctionCallCompiler.spec.ts new file mode 100644 index 00000000..726d2b74 --- /dev/null +++ b/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/NestedFunctionCallCompiler.spec.ts @@ -0,0 +1,240 @@ +import { expect, describe, it } from 'vitest'; +import { SharedFunctionStub } from '@tests/unit/shared/Stubs/SharedFunctionStub'; +import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; +import { NestedFunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/NestedFunctionCallCompiler'; +import { ArgumentCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/ArgumentCompiler'; +import { ArgumentCompilerStub } from '@tests/unit/shared/Stubs/ArgumentCompilerStub'; +import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub'; +import { FunctionCallCompilationContextStub } from '@tests/unit/shared/Stubs/FunctionCallCompilationContextStub'; +import { SingleCallCompilerStub } from '@tests/unit/shared/Stubs/SingleCallCompilerStub'; +import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub'; +import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; +import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode'; +import { expectDeepThrowsError } from '@tests/unit/shared/Assertions/ExpectDeepThrowsError'; + +describe('NestedFunctionCallCompiler', () => { + describe('canCompile', () => { + it('returns `true` for code body function', () => { + // arrange + const expected = true; + const func = new SharedFunctionStub(FunctionBodyType.Calls) + .withSomeCalls(); + const compiler = new NestedFunctionCallCompilerBuilder() + .build(); + // act + const actual = compiler.canCompile(func); + // assert + expect(actual).to.equal(expected); + }); + it('returns `false` for non-code body function', () => { + // arrange + const expected = false; + const func = new SharedFunctionStub(FunctionBodyType.Code); + const compiler = new NestedFunctionCallCompilerBuilder() + .build(); + // act + const actual = compiler.canCompile(func); + // assert + expect(actual).to.equal(expected); + }); + }); + describe('compile', () => { + describe('argument compilation', () => { + it('uses correct context', () => { + // arrange + const argumentCompiler = new ArgumentCompilerStub(); + const expectedContext = new FunctionCallCompilationContextStub(); + const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc(); + const compiler = new NestedFunctionCallCompilerBuilder() + .withArgumentCompiler(argumentCompiler) + .build(); + // act + compiler.compileFunction(frontFunc, callToFrontFunc, expectedContext); + // assert + const calls = argumentCompiler.callHistory.filter((call) => call.methodName === 'createCompiledNestedCall'); + expect(calls).have.lengthOf(1); + const [,,actualContext] = calls[0].args; + expect(actualContext).to.equal(expectedContext); + }); + it('uses correct parent call', () => { + // arrange + const argumentCompiler = new ArgumentCompilerStub(); + const expectedContext = new FunctionCallCompilationContextStub(); + const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc(); + const compiler = new NestedFunctionCallCompilerBuilder() + .withArgumentCompiler(argumentCompiler) + .build(); + // act + compiler.compileFunction(frontFunc, callToFrontFunc, expectedContext); + // assert + const calls = argumentCompiler.callHistory.filter((call) => call.methodName === 'createCompiledNestedCall'); + expect(calls).have.lengthOf(1); + const [,actualParentCall] = calls[0].args; + expect(actualParentCall).to.equal(callToFrontFunc); + }); + it('uses correct nested call', () => { + // arrange + const argumentCompiler = new ArgumentCompilerStub(); + const expectedContext = new FunctionCallCompilationContextStub(); + const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc(); + const compiler = new NestedFunctionCallCompilerBuilder() + .withArgumentCompiler(argumentCompiler) + .build(); + // act + compiler.compileFunction(frontFunc, callToFrontFunc, expectedContext); + // assert + const calls = argumentCompiler.callHistory.filter((call) => call.methodName === 'createCompiledNestedCall'); + expect(calls).have.lengthOf(1); + const [actualNestedCall] = calls[0].args; + expect(actualNestedCall).to.deep.equal(callToFrontFunc); + }); + }); + describe('re-compilation with compiled args', () => { + it('uses correct context', () => { + // arrange + const singleCallCompilerStub = new SingleCallCompilerStub(); + const expectedContext = new FunctionCallCompilationContextStub() + .withSingleCallCompiler(singleCallCompilerStub); + const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc(); + const compiler = new NestedFunctionCallCompilerBuilder() + .build(); + // act + compiler.compileFunction(frontFunc, callToFrontFunc, expectedContext); + // assert + const calls = singleCallCompilerStub.callHistory.filter((call) => call.methodName === 'compileSingleCall'); + expect(calls).have.lengthOf(1); + const [,actualContext] = calls[0].args; + expect(expectedContext).to.equal(actualContext); + }); + it('uses compiled nested call', () => { + // arrange + const expectedCall = new FunctionCallStub(); + const argumentCompilerStub = new ArgumentCompilerStub(); + argumentCompilerStub.createCompiledNestedCall = () => expectedCall; + const singleCallCompilerStub = new SingleCallCompilerStub(); + const context = new FunctionCallCompilationContextStub() + .withSingleCallCompiler(singleCallCompilerStub); + const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc(); + const compiler = new NestedFunctionCallCompilerBuilder() + .withArgumentCompiler(argumentCompilerStub) + .build(); + // act + compiler.compileFunction(frontFunc, callToFrontFunc, context); + // assert + const calls = singleCallCompilerStub.callHistory.filter((call) => call.methodName === 'compileSingleCall'); + expect(calls).have.lengthOf(1); + const [actualNestedCall] = calls[0].args; + expect(expectedCall).to.equal(actualNestedCall); + }); + }); + it('flattens re-compiled functions', () => { + // arrange + const deepFunc1 = new SharedFunctionStub(FunctionBodyType.Code); + const deepFunc2 = new SharedFunctionStub(FunctionBodyType.Code); + const callToDeepFunc1 = new FunctionCallStub().withFunctionName(deepFunc1.name); + const callToDeepFunc2 = new FunctionCallStub().withFunctionName(deepFunc2.name); + const singleCallCompilationScenario = new Map([ + [callToDeepFunc1, [new CompiledCodeStub()]], + [callToDeepFunc2, [new CompiledCodeStub(), new CompiledCodeStub()]], + ]); + const argumentCompiler = new ArgumentCompilerStub() + .withScenario({ givenNestedFunctionCall: callToDeepFunc1, result: callToDeepFunc1 }) + .withScenario({ givenNestedFunctionCall: callToDeepFunc2, result: callToDeepFunc2 }); + const expectedFlattenedCodes = [...singleCallCompilationScenario.values()].flat(); + const frontFunc = new SharedFunctionStub(FunctionBodyType.Calls) + .withCalls(callToDeepFunc1, callToDeepFunc2); + const callToFrontFunc = new FunctionCallStub().withFunctionName(frontFunc.name); + const singleCallCompilerStub = new SingleCallCompilerStub() + .withCallCompilationScenarios(singleCallCompilationScenario); + const expectedContext = new FunctionCallCompilationContextStub() + .withSingleCallCompiler(singleCallCompilerStub); + const compiler = new NestedFunctionCallCompilerBuilder() + .withArgumentCompiler(argumentCompiler) + .build(); + // act + const actualCodes = compiler.compileFunction(frontFunc, callToFrontFunc, expectedContext); + // assert + expect(actualCodes).have.lengthOf(expectedFlattenedCodes.length); + expect(actualCodes).to.have.members(expectedFlattenedCodes); + }); + describe('error handling', () => { + it('handles argument compiler errors', () => { + // arrange + const argumentCompilerError = new Error('Test error'); + const argumentCompilerStub = new ArgumentCompilerStub(); + argumentCompilerStub.createCompiledNestedCall = () => { + throw argumentCompilerError; + }; + const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc(); + const expectedError = new AggregateError( + [argumentCompilerError], + `Error with call to "${callToFrontFunc.functionName}" function from "${callToFrontFunc.functionName}" function`, + ); + const compiler = new NestedFunctionCallCompilerBuilder() + .withArgumentCompiler(argumentCompilerStub) + .build(); + // act + const act = () => compiler.compileFunction( + frontFunc, + callToFrontFunc, + new FunctionCallCompilationContextStub(), + ); + // assert + expectDeepThrowsError(act, expectedError); + }); + it('handles single call compiler errors', () => { + // arrange + const singleCallCompilerError = new Error('Test error'); + const singleCallCompiler = new SingleCallCompilerStub(); + singleCallCompiler.compileSingleCall = () => { + throw singleCallCompilerError; + }; + const context = new FunctionCallCompilationContextStub() + .withSingleCallCompiler(singleCallCompiler); + const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc(); + const expectedError = new AggregateError( + [singleCallCompilerError], + `Error with call to "${callToFrontFunc.functionName}" function from "${callToFrontFunc.functionName}" function`, + ); + const compiler = new NestedFunctionCallCompilerBuilder() + .build(); + // act + const act = () => compiler.compileFunction( + frontFunc, + callToFrontFunc, + context, + ); + // assert + expectDeepThrowsError(act, expectedError); + }); + }); + }); +}); + +function createSingleFuncCallingAnotherFunc() { + const deepFunc = new SharedFunctionStub(FunctionBodyType.Code); + const callToDeepFunc = new FunctionCallStub().withFunctionName(deepFunc.name); + const frontFunc = new SharedFunctionStub(FunctionBodyType.Calls).withCalls(callToDeepFunc); + const callToFrontFunc = new FunctionCallStub().withFunctionName(frontFunc.name); + return { + deepFunc, + frontFunc, + callToFrontFunc, + callToDeepFunc, + }; +} + +class NestedFunctionCallCompilerBuilder { + private argumentCompiler: ArgumentCompiler = new ArgumentCompilerStub(); + + public withArgumentCompiler(argumentCompiler: ArgumentCompiler): this { + this.argumentCompiler = argumentCompiler; + return this; + } + + public build(): NestedFunctionCallCompiler { + return new NestedFunctionCallCompiler( + this.argumentCompiler, + ); + } +} diff --git a/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/AdaptiveFunctionCallCompiler.spec.ts b/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/AdaptiveFunctionCallCompiler.spec.ts new file mode 100644 index 00000000..7822d1c9 --- /dev/null +++ b/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/AdaptiveFunctionCallCompiler.spec.ts @@ -0,0 +1,259 @@ +import { expect, describe, it } from 'vitest'; +import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; +import { SharedFunctionStub } from '@tests/unit/shared/Stubs/SharedFunctionStub'; +import type { FunctionCallParametersData } from '@/application/collections/'; +import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub'; +import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub'; +import { AdaptiveFunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/AdaptiveFunctionCallCompiler'; +import { SingleCallCompilerStrategy } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompilerStrategy'; +import { SingleCallCompilerStrategyStub } from '@tests/unit/shared/Stubs/SingleCallCompilerStrategyStub'; +import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; +import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext'; +import { FunctionCallCompilationContextStub } from '@tests/unit/shared/Stubs/FunctionCallCompilationContextStub'; +import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub'; +import { SingleCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler'; + +describe('AdaptiveFunctionCallCompiler', () => { + describe('compileSingleCall', () => { + describe('throws if call parameters does not match function parameters', () => { + // arrange + const functionName = 'test-function-name'; + const testCases: Array<{ + readonly description: string, + readonly functionParameters: string[], + readonly callParameters: string[] + readonly expectedError: string; + }> = [ + { + description: 'provided: single unexpected parameter, when: another expected', + functionParameters: ['expected-parameter'], + callParameters: ['unexpected-parameter'], + expectedError: + `Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"` + + '. Expected parameter(s): "expected-parameter"', + }, + { + description: 'provided: multiple unexpected parameters, when: different one is expected', + functionParameters: ['expected-parameter'], + callParameters: ['unexpected-parameter1', 'unexpected-parameter2'], + expectedError: + `Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter1", "unexpected-parameter2"` + + '. Expected parameter(s): "expected-parameter"', + }, + { + description: 'provided: an unexpected parameter, when: multiple parameters are expected', + functionParameters: ['expected-parameter1', 'expected-parameter2'], + callParameters: ['expected-parameter1', 'expected-parameter2', 'unexpected-parameter'], + expectedError: + `Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"` + + '. Expected parameter(s): "expected-parameter1", "expected-parameter2"', + }, + { + description: 'provided: an unexpected parameter, when: none required', + functionParameters: [], + callParameters: ['unexpected-call-parameter'], + expectedError: + `Function "${functionName}" has unexpected parameter(s) provided: "unexpected-call-parameter"` + + '. Expected parameter(s): none', + }, + { + description: 'provided: expected and unexpected parameter, when: one of them is expected', + functionParameters: ['expected-parameter'], + callParameters: ['expected-parameter', 'unexpected-parameter'], + expectedError: + `Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"` + + '. Expected parameter(s): "expected-parameter"', + }, + ]; + testCases.forEach(({ + description, functionParameters, callParameters, expectedError, + }) => { + it(description, () => { + // arrange + const func = new SharedFunctionStub(FunctionBodyType.Code) + .withName('test-function-name') + .withParameterNames(...functionParameters); + const params = callParameters + .reduce((result, parameter) => { + return { ...result, [parameter]: 'defined-parameter-value' }; + }, {} as FunctionCallParametersData); + const call = new FunctionCallStub() + .withFunctionName(func.name) + .withArguments(params); + const builder = new AdaptiveFunctionCallCompilerBuilder() + .withContext(new FunctionCallCompilationContextStub() + .withAllFunctions( + new SharedFunctionCollectionStub().withFunctions(func), + )) + .withCall(call); + // act + const act = () => builder.compileSingleCall(); + // assert + expect(act).to.throw(expectedError); + }); + }); + }); + describe('strategy selection', () => { + it('uses the matching strategy among multiple', () => { + // arrange + const matchedStrategy = new SingleCallCompilerStrategyStub() + .withCanCompileResult(true); + const unmatchedStrategy = new SingleCallCompilerStrategyStub() + .withCanCompileResult(false); + const builder = new AdaptiveFunctionCallCompilerBuilder() + .withStrategies([matchedStrategy, unmatchedStrategy]); + // act + builder.compileSingleCall(); + // assert + expect(matchedStrategy.callHistory.filter((c) => c.methodName === 'compileFunction')).to.have.lengthOf(1); + expect(unmatchedStrategy.callHistory.filter((c) => c.methodName === 'compileFunction')).to.have.lengthOf(0); + }); + it('throws if multiple strategies can compile', () => { + // arrange + const expectedError = 'Multiple strategies found to compile the function call.'; + const matchedStrategy1 = new SingleCallCompilerStrategyStub().withCanCompileResult(true); + const matchedStrategy2 = new SingleCallCompilerStrategyStub().withCanCompileResult(true); + const builder = new AdaptiveFunctionCallCompilerBuilder().withStrategies( + [matchedStrategy1, matchedStrategy2], + ); + // act + const act = () => builder.compileSingleCall(); + // assert + expect(act).to.throw(expectedError); + }); + it('throws if no strategy can compile', () => { + // arrange + const expectedError = 'No strategies found to compile the function call.'; + const unmatchedStrategy = new SingleCallCompilerStrategyStub() + .withCanCompileResult(false); + const builder = new AdaptiveFunctionCallCompilerBuilder() + .withStrategies([unmatchedStrategy]); + // act + const act = () => builder.compileSingleCall(); + // assert + expect(act).to.throw(expectedError); + }); + }); + describe('strategy invocation', () => { + it('passes correct function for compilation ability check', () => { + // arrange + const expectedFunction = new SharedFunctionStub(FunctionBodyType.Code); + const strategy = new SingleCallCompilerStrategyStub() + .withCanCompileResult(true); + const builder = new AdaptiveFunctionCallCompilerBuilder() + .withContext(new FunctionCallCompilationContextStub() + .withAllFunctions( + new SharedFunctionCollectionStub().withFunctions(expectedFunction), + )) + .withCall(new FunctionCallStub().withFunctionName(expectedFunction.name)) + .withStrategies([strategy]); + // act + builder.compileSingleCall(); + // assert + const call = strategy.callHistory.filter((c) => c.methodName === 'canCompile'); + expect(call).to.have.lengthOf(1); + expect(call[0].args[0]).to.equal(expectedFunction); + }); + describe('compilation arguments', () => { + it('uses correct function', () => { + // arrange + const expectedFunction = new SharedFunctionStub(FunctionBodyType.Code); + const strategy = new SingleCallCompilerStrategyStub() + .withCanCompileResult(true); + const builder = new AdaptiveFunctionCallCompilerBuilder() + .withContext(new FunctionCallCompilationContextStub() + .withAllFunctions( + new SharedFunctionCollectionStub().withFunctions(expectedFunction), + )) + .withCall(new FunctionCallStub().withFunctionName(expectedFunction.name)) + .withStrategies([strategy]); + // act + builder.compileSingleCall(); + // assert + const call = strategy.callHistory.filter((c) => c.methodName === 'compileFunction'); + expect(call).to.have.lengthOf(1); + const [actualFunction] = call[0].args; + expect(actualFunction).to.equal(expectedFunction); + }); + it('uses correct call', () => { + // arrange + const expectedCall = new FunctionCallStub(); + const strategy = new SingleCallCompilerStrategyStub() + .withCanCompileResult(true); + const builder = new AdaptiveFunctionCallCompilerBuilder() + .withStrategies([strategy]) + .withCall(expectedCall); + // act + builder.compileSingleCall(); + // assert + const call = strategy.callHistory.filter((c) => c.methodName === 'compileFunction'); + expect(call).to.have.lengthOf(1); + const [,actualCall] = call[0].args; + expect(actualCall).to.equal(expectedCall); + }); + it('uses correct context', () => { + // arrange + const expectedContext = new FunctionCallCompilationContextStub(); + const strategy = new SingleCallCompilerStrategyStub() + .withCanCompileResult(true); + const builder = new AdaptiveFunctionCallCompilerBuilder() + .withStrategies([strategy]) + .withContext(expectedContext); + // act + builder.compileSingleCall(); + // assert + const call = strategy.callHistory.filter((c) => c.methodName === 'compileFunction'); + expect(call).to.have.lengthOf(1); + const [,,actualContext] = call[0].args; + expect(actualContext).to.equal(expectedContext); + }); + }); + }); + it('returns compiled code from strategy', () => { + // arrange + const expectedResult = [new CompiledCodeStub(), new CompiledCodeStub()]; + const strategy = new SingleCallCompilerStrategyStub() + .withCanCompileResult(true) + .withCompiledFunctionResult(expectedResult); + const builder = new AdaptiveFunctionCallCompilerBuilder() + .withStrategies([strategy]); + // act + const actualResult = builder.compileSingleCall(); + // assert + expect(expectedResult).to.equal(actualResult); + }); + }); +}); + +class AdaptiveFunctionCallCompilerBuilder implements SingleCallCompiler { + private strategies: SingleCallCompilerStrategy[] = [ + new SingleCallCompilerStrategyStub().withCanCompileResult(true), + ]; + + private call: FunctionCall = new FunctionCallStub(); + + private context: FunctionCallCompilationContext = new FunctionCallCompilationContextStub(); + + public withCall(call: FunctionCall): this { + this.call = call; + return this; + } + + public withContext(context: FunctionCallCompilationContext): this { + this.context = context; + return this; + } + + public withStrategies(strategies: SingleCallCompilerStrategy[]): this { + this.strategies = strategies; + return this; + } + + public compileSingleCall() { + const compiler = new AdaptiveFunctionCallCompiler(this.strategies); + return compiler.compileSingleCall( + this.call, + this.context, + ); + } +} diff --git a/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler.spec.ts b/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler.spec.ts new file mode 100644 index 00000000..0b72aedf --- /dev/null +++ b/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler.spec.ts @@ -0,0 +1,290 @@ +import { expect, describe, it } from 'vitest'; +import { ArgumentCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/ArgumentCompiler'; +import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext'; +import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; +import { NestedFunctionArgumentCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler'; +import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler'; +import { ExpressionsCompilerStub } from '@tests/unit/shared/Stubs/ExpressionsCompilerStub'; +import { FunctionCallCompilationContextStub } from '@tests/unit/shared/Stubs/FunctionCallCompilationContextStub'; +import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub'; +import { expectDeepThrowsError } from '@tests/unit/shared/Assertions/ExpectDeepThrowsError'; +import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub'; +import { SharedFunctionStub } from '@tests/unit/shared/Stubs/SharedFunctionStub'; +import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub'; +import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub'; +import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; + +describe('NestedFunctionArgumentCompiler', () => { + describe('createCompiledNestedCall', () => { + it('should handle error from expressions compiler', () => { + // arrange + const parameterName = 'parameterName'; + const nestedCall = new FunctionCallStub() + .withFunctionName('nested-function-call') + .withArgumentCollection(new FunctionCallArgumentCollectionStub() + .withArgument(parameterName, 'unimportant-value')); + const parentCall = new FunctionCallStub() + .withFunctionName('parent-function-call'); + const expressionsCompilerError = new Error('child-'); + const expectedError = new AggregateError( + [expressionsCompilerError], + `Error when compiling argument for "${parameterName}"`, + ); + const expressionsCompiler = new ExpressionsCompilerStub(); + expressionsCompiler.compileExpressions = () => { throw expressionsCompilerError; }; + const builder = new NestedFunctionArgumentCompilerBuilder() + .withParentFunctionCall(parentCall) + .withNestedFunctionCall(nestedCall) + .withExpressionsCompiler(expressionsCompiler); + // act + const act = () => builder.createCompiledNestedCall(); + // assert + expectDeepThrowsError(act, expectedError); + }); + describe('compilation', () => { + describe('without arguments', () => { + it('matches nested call name', () => { + // arrange + const expectedCall = new FunctionCallStub() + .withArgumentCollection(new FunctionCallArgumentCollectionStub().withEmptyArguments()); + const builder = new NestedFunctionArgumentCompilerBuilder() + .withNestedFunctionCall(expectedCall); + // act + const actualCall = builder.createCompiledNestedCall(); + // assert + expect(actualCall.functionName).to.equal(expectedCall.functionName); + }); + it('has no arguments or parameters', () => { + // arrange + const expectedCall = new FunctionCallStub() + .withArgumentCollection(new FunctionCallArgumentCollectionStub().withEmptyArguments()); + const builder = new NestedFunctionArgumentCompilerBuilder() + .withNestedFunctionCall(expectedCall); + // act + const actualCall = builder.createCompiledNestedCall(); + // assert + expect(actualCall.args.getAllParameterNames()).to.have.lengthOf(0); + }); + it('does not compile expressions', () => { + // arrange + const expressionsCompilerStub = new ExpressionsCompilerStub(); + const call = new FunctionCallStub() + .withArgumentCollection(new FunctionCallArgumentCollectionStub().withEmptyArguments()); + const builder = new NestedFunctionArgumentCompilerBuilder() + .withNestedFunctionCall(call) + .withExpressionsCompiler(expressionsCompilerStub); + // act + builder.createCompiledNestedCall(); + // assert + expect(expressionsCompilerStub.callHistory).to.have.lengthOf(0); + }); + }); + describe('with arguments', () => { + it('matches nested call name', () => { + // arrange + const expectedName = 'expected-nested-function-call-name'; + const nestedCall = new FunctionCallStub() + .withFunctionName(expectedName) + .withArgumentCollection(new FunctionCallArgumentCollectionStub().withSomeArguments()); + const builder = new NestedFunctionArgumentCompilerBuilder() + .withNestedFunctionCall(nestedCall); + // act + const call = builder.createCompiledNestedCall(); + // assert + expect(call.functionName).to.equal(expectedName); + }); + it('matches nested call parameters', () => { + // arrange + const expectedParameterNames = ['expectedFirstParameterName', 'expectedSecondParameterName']; + const nestedCall = new FunctionCallStub() + .withArgumentCollection(new FunctionCallArgumentCollectionStub() + .withArguments(expectedParameterNames.reduce((acc, name) => ({ ...acc, ...{ [name]: 'unimportant-value' } }), {}))); + const builder = new NestedFunctionArgumentCompilerBuilder() + .withNestedFunctionCall(nestedCall); + // act + const call = builder.createCompiledNestedCall(); + // assert + const actualParameterNames = call.args.getAllParameterNames(); + expect(actualParameterNames.length).to.equal(expectedParameterNames.length); + expect(actualParameterNames).to.have.members(expectedParameterNames); + }); + it('compiles args using parent parameters', () => { + // arrange + const expressionsCompilerStub = new ExpressionsCompilerStub(); + const testParameterScenarios = [ + { + parameterName: 'firstParameterName', + rawArgumentValue: 'first-raw-argument-value', + compiledArgumentValue: 'first-compiled-argument-value', + }, + { + parameterName: 'secondParameterName', + rawArgumentValue: 'second-raw-argument-value', + compiledArgumentValue: 'second-compiled-argument-value', + }, + ]; + const parentCall = new FunctionCallStub().withArgumentCollection( + new FunctionCallArgumentCollectionStub().withSomeArguments(), + ); + testParameterScenarios.forEach(({ rawArgumentValue }) => { + expressionsCompilerStub.setup({ + givenCode: rawArgumentValue, + givenArgs: parentCall.args, + result: testParameterScenarios.find( + (r) => r.rawArgumentValue === rawArgumentValue, + ).compiledArgumentValue, + }); + }); + const nestedCallArgs = new FunctionCallArgumentCollectionStub() + .withArguments(testParameterScenarios.reduce(( + acc, + { parameterName, rawArgumentValue }, + ) => ({ ...acc, ...{ [parameterName]: rawArgumentValue } }), {})); + const nestedCall = new FunctionCallStub() + .withArgumentCollection(nestedCallArgs); + const builder = new NestedFunctionArgumentCompilerBuilder() + .withExpressionsCompiler(expressionsCompilerStub) + .withParentFunctionCall(parentCall) + .withNestedFunctionCall(nestedCall); + // act + const compiledCall = builder.createCompiledNestedCall(); + // assert + const expectedParameterNames = testParameterScenarios.map((p) => p.parameterName); + const actualParameterNames = compiledCall.args.getAllParameterNames(); + expect(expectedParameterNames.length).to.equal(actualParameterNames.length); + expect(expectedParameterNames).to.have.members(actualParameterNames); + const getActualArgumentValue = (parameterName: string) => compiledCall + .args + .getArgument(parameterName) + .argumentValue; + testParameterScenarios.forEach(({ parameterName, compiledArgumentValue }) => { + expect(getActualArgumentValue(parameterName)).to.equal(compiledArgumentValue); + }); + }); + describe('when expression compiler returns empty', () => { + it('throws for required parameter', () => { + // arrange + const parameterName = 'requiredParameter'; + const initialValue = 'initial-value'; + const compiledValue = undefined; + const expectedError = `Compilation resulted in empty value for required parameter: "${parameterName}"`; + const nestedCall = new FunctionCallStub() + .withArgumentCollection(new FunctionCallArgumentCollectionStub() + .withArgument(parameterName, initialValue)); + const parentCall = new FunctionCallStub().withArgumentCollection( + new FunctionCallArgumentCollectionStub().withSomeArguments(), + ); + const context = createContextWithParameter({ + existingFunctionName: nestedCall.functionName, + existingParameterName: parameterName, + isExistingParameterOptional: false, + }); + const expressionsCompilerStub = new ExpressionsCompilerStub() + .setup({ + givenCode: initialValue, + givenArgs: parentCall.args, + result: compiledValue, + }); + const builder = new NestedFunctionArgumentCompilerBuilder() + .withExpressionsCompiler(expressionsCompilerStub) + .withParentFunctionCall(parentCall) + .withContext(context) + .withNestedFunctionCall(nestedCall); + // act + const act = () => builder.createCompiledNestedCall(); + // assert + expect(act).to.throw(expectedError); + }); + it('succeeds for optional parameter', () => { + // arrange + const parameterName = 'optionalParameter'; + const initialValue = 'initial-value'; + const compiledValue = undefined; + const nestedCall = new FunctionCallStub() + .withArgumentCollection(new FunctionCallArgumentCollectionStub() + .withArgument(parameterName, initialValue)); + const parentCall = new FunctionCallStub().withArgumentCollection( + new FunctionCallArgumentCollectionStub().withSomeArguments(), + ); + const context = createContextWithParameter({ + existingFunctionName: nestedCall.functionName, + existingParameterName: parameterName, + isExistingParameterOptional: true, + }); + const expressionsCompilerStub = new ExpressionsCompilerStub() + .setup({ + givenCode: initialValue, + givenArgs: parentCall.args, + result: compiledValue, + }); + const builder = new NestedFunctionArgumentCompilerBuilder() + .withExpressionsCompiler(expressionsCompilerStub) + .withParentFunctionCall(parentCall) + .withContext(context) + .withNestedFunctionCall(nestedCall); + // act + const compiledCall = builder.createCompiledNestedCall(); + // assert + expect(compiledCall.args.hasArgument(parameterName)).toBeFalsy(); + }); + }); + }); + }); + }); +}); + +function createContextWithParameter(options: { + readonly existingFunctionName: string, + readonly existingParameterName: string, + readonly isExistingParameterOptional: boolean, +}): FunctionCallCompilationContext { + const parameters = new FunctionParameterCollectionStub() + .withParameterName(options.existingParameterName, options.isExistingParameterOptional); + const func = new SharedFunctionStub(FunctionBodyType.Code) + .withName(options.existingFunctionName) + .withParameters(parameters); + const functions = new SharedFunctionCollectionStub() + .withFunctions(func); + const context = new FunctionCallCompilationContextStub() + .withAllFunctions(functions); + return context; +} + +class NestedFunctionArgumentCompilerBuilder implements ArgumentCompiler { + private expressionsCompiler: IExpressionsCompiler = new ExpressionsCompilerStub(); + + private nestedFunctionCall: FunctionCall = new FunctionCallStub(); + + private parentFunctionCall: FunctionCall = new FunctionCallStub(); + + private context: FunctionCallCompilationContext = new FunctionCallCompilationContextStub(); + + public withExpressionsCompiler(expressionsCompiler: IExpressionsCompiler): this { + this.expressionsCompiler = expressionsCompiler; + return this; + } + + public withParentFunctionCall(parentFunctionCall: FunctionCall): this { + this.parentFunctionCall = parentFunctionCall; + return this; + } + + public withNestedFunctionCall(nestedFunctionCall: FunctionCall): this { + this.nestedFunctionCall = nestedFunctionCall; + return this; + } + + public withContext(context: FunctionCallCompilationContext): this { + this.context = context; + return this; + } + + public createCompiledNestedCall(): FunctionCall { + const compiler = new NestedFunctionArgumentCompiler(this.expressionsCompiler); + return compiler.createCompiledNestedCall( + this.nestedFunctionCall, + this.parentFunctionCall, + this.context, + ); + } +} diff --git a/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler.spec.ts b/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler.spec.ts new file mode 100644 index 00000000..3b54fc27 --- /dev/null +++ b/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler.spec.ts @@ -0,0 +1,111 @@ +import { expect, describe, it } from 'vitest'; +import { InlineFunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler'; +import { SharedFunctionStub } from '@tests/unit/shared/Stubs/SharedFunctionStub'; +import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; +import { ExpressionsCompilerStub } from '@tests/unit/shared/Stubs/ExpressionsCompilerStub'; +import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler'; +import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub'; +import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub'; + +describe('InlineFunctionCallCompiler', () => { + describe('canCompile', () => { + it('returns `true` if function has code body', () => { + // arrange + const expected = true; + const func = new SharedFunctionStub(FunctionBodyType.Code); + const compiler = new InlineFunctionCallCompilerBuilder() + .build(); + // act + const actual = compiler.canCompile(func); + // assert + expect(actual).to.equal(expected); + }); + it('returns `false` if function does not have code body', () => { + // arrange + const expected = false; + const func = new SharedFunctionStub(FunctionBodyType.Calls); + const compiler = new InlineFunctionCallCompilerBuilder() + .build(); + // act + const actual = compiler.canCompile(func); + // assert + expect(actual).to.equal(expected); + }); + }); + describe('compile', () => { + it('compiles expressions with correct arguments', () => { + // arrange + const expressionsCompilerStub = new ExpressionsCompilerStub(); + const expectedArgs = new FunctionCallArgumentCollectionStub(); + const compiler = new InlineFunctionCallCompilerBuilder() + .withExpressionsCompiler(expressionsCompilerStub) + .build(); + // act + compiler.compileFunction( + new SharedFunctionStub(FunctionBodyType.Code), + new FunctionCallStub() + .withArgumentCollection(expectedArgs), + ); + // assert + const actualArgs = expressionsCompilerStub.callHistory.map((call) => call.args[1]); + expect(actualArgs.every((arg) => arg === expectedArgs)); + }); + it('creates compiled code with compiled `execute`', () => { + // arrange + const func = new SharedFunctionStub(FunctionBodyType.Code); + const args = new FunctionCallArgumentCollectionStub(); + const expectedCode = 'expected-code'; + const expressionsCompilerStub = new ExpressionsCompilerStub() + .setup({ + givenCode: func.body.code.execute, + givenArgs: args, + result: expectedCode, + }); + const compiler = new InlineFunctionCallCompilerBuilder() + .withExpressionsCompiler(expressionsCompilerStub) + .build(); + // act + const compiledCodes = compiler + .compileFunction(func, new FunctionCallStub().withArgumentCollection(args)); + // assert + expect(compiledCodes).to.have.lengthOf(1); + const actualCode = compiledCodes[0].code; + expect(actualCode).to.equal(expectedCode); + }); + it('creates compiled revert code with compiled `revert`', () => { + // arrange + const func = new SharedFunctionStub(FunctionBodyType.Code); + const args = new FunctionCallArgumentCollectionStub(); + const expectedRevertCode = 'expected-revert-code'; + const expressionsCompilerStub = new ExpressionsCompilerStub() + .setup({ + givenCode: func.body.code.revert, + givenArgs: args, + result: expectedRevertCode, + }); + const compiler = new InlineFunctionCallCompilerBuilder() + .withExpressionsCompiler(expressionsCompilerStub) + .build(); + // act + const compiledCodes = compiler + .compileFunction(func, new FunctionCallStub().withArgumentCollection(args)); + // assert + expect(compiledCodes).to.have.lengthOf(1); + const actualRevertCode = compiledCodes[0].revertCode; + expect(actualRevertCode).to.equal(expectedRevertCode); + }); + }); +}); + +class InlineFunctionCallCompilerBuilder { + private expressionsCompiler: IExpressionsCompiler = new ExpressionsCompilerStub(); + + public build(): InlineFunctionCallCompiler { + return new InlineFunctionCallCompiler(this.expressionsCompiler); + } + + public withExpressionsCompiler(expressionsCompiler: IExpressionsCompiler): this { + this.expressionsCompiler = expressionsCompiler; + return this; + } +} diff --git a/tests/unit/application/Parser/Script/Compiler/Function/Call/FunctionCall.spec.ts b/tests/unit/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall.spec.ts similarity index 92% rename from tests/unit/application/Parser/Script/Compiler/Function/Call/FunctionCall.spec.ts rename to tests/unit/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall.spec.ts index 636187fd..191bc152 100644 --- a/tests/unit/application/Parser/Script/Compiler/Function/Call/FunctionCall.spec.ts +++ b/tests/unit/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall.spec.ts @@ -1,10 +1,10 @@ import { describe, it, expect } from 'vitest'; -import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; +import { ParsedFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall'; import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection'; import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub'; import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests'; -describe('FunctionCall', () => { +describe('ParsedFunctionCall', () => { describe('ctor', () => { describe('args', () => { describe('throws when args is missing', () => { @@ -76,6 +76,6 @@ class FunctionCallBuilder { } public build() { - return new FunctionCall(this.functionName, this.args); + return new ParsedFunctionCall(this.functionName, this.args); } } diff --git a/tests/unit/application/Parser/Script/Compiler/Function/SharedFunction.spec.ts b/tests/unit/application/Parser/Script/Compiler/Function/SharedFunction.spec.ts index 0d1d746b..edba477a 100644 --- a/tests/unit/application/Parser/Script/Compiler/Function/SharedFunction.spec.ts +++ b/tests/unit/application/Parser/Script/Compiler/Function/SharedFunction.spec.ts @@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest'; import { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/IFunctionParameterCollection'; import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub'; import { createCallerFunction, createFunctionWithInlineCode } from '@/application/Parser/Script/Compiler/Function/SharedFunction'; -import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall'; +import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub'; import { FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; import { @@ -132,7 +132,7 @@ describe('SharedFunction', () => { }); }); describe('createCallerFunction', () => { - describe('callSequence', () => { + describe('rootCallSequence', () => { it('sets as expected', () => { // arrange const expected = [ @@ -141,7 +141,7 @@ describe('SharedFunction', () => { ]; // act const sut = new SharedFunctionBuilder() - .withCallSequence(expected) + .withRootCallSequence(expected) .createCallerFunction(); // assert expect(sut.body.calls).equal(expected); @@ -150,12 +150,12 @@ describe('SharedFunction', () => { itEachAbsentCollectionValue((absentValue) => { // arrange const functionName = 'invalidFunction'; - const callSequence = absentValue; + const rootCallSequence = absentValue; const expectedError = `missing call sequence in function "${functionName}"`; // act const act = () => new SharedFunctionBuilder() .withName(functionName) - .withCallSequence(callSequence) + .withRootCallSequence(rootCallSequence) .createCallerFunction(); // assert expect(act).to.throw(expectedError); @@ -206,7 +206,7 @@ class SharedFunctionBuilder { private parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollectionStub(); - private callSequence: readonly IFunctionCall[] = [new FunctionCallStub()]; + private callSequence: readonly FunctionCall[] = [new FunctionCallStub()]; private code = 'code'; @@ -249,7 +249,7 @@ class SharedFunctionBuilder { return this; } - public withCallSequence(callSequence: readonly IFunctionCall[]) { + public withRootCallSequence(callSequence: readonly FunctionCall[]) { this.callSequence = callSequence; return this; } diff --git a/tests/unit/application/Parser/Script/Compiler/ScriptCompiler.spec.ts b/tests/unit/application/Parser/Script/Compiler/ScriptCompiler.spec.ts index bef4ec21..351faf9c 100644 --- a/tests/unit/application/Parser/Script/Compiler/ScriptCompiler.spec.ts +++ b/tests/unit/application/Parser/Script/Compiler/ScriptCompiler.spec.ts @@ -3,8 +3,8 @@ import type { FunctionData } from '@/application/collections/'; import { ScriptCode } from '@/domain/ScriptCode'; import { ScriptCompiler } from '@/application/Parser/Script/Compiler/ScriptCompiler'; import { ISharedFunctionsParser } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionsParser'; -import { ICompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/ICompiledCode'; -import { IFunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/IFunctionCallCompiler'; +import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode'; +import { FunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler'; import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub'; import { ScriptDataStub } from '@tests/unit/shared/Stubs/ScriptDataStub'; import { FunctionDataStub } from '@tests/unit/shared/Stubs/FunctionDataStub'; @@ -91,7 +91,7 @@ describe('ScriptCompiler', () => { }); it('returns code as expected', () => { // arrange - const expected: ICompiledCode = { + const expected: CompiledCode = { code: 'expected-code', revertCode: 'expected-revert-code', }; @@ -152,8 +152,8 @@ describe('ScriptCompiler', () => { const scriptName = 'scriptName'; const innerError = 'innerError'; const expectedError = `Script "${scriptName}" ${innerError}`; - const callCompiler: IFunctionCallCompiler = { - compileCall: () => { throw new Error(innerError); }, + const callCompiler: FunctionCallCompiler = { + compileFunctionCalls: () => { throw new Error(innerError); }, }; const scriptData = ScriptDataStub.createWithCall() .withName(scriptName); @@ -170,13 +170,13 @@ describe('ScriptCompiler', () => { // arrange const scriptName = 'scriptName'; const syntax = new LanguageSyntaxStub(); - const invalidCode: ICompiledCode = { code: undefined, revertCode: undefined }; + const invalidCode: CompiledCode = { code: undefined, revertCode: undefined }; const realExceptionMessage = collectExceptionMessage( () => new ScriptCode(invalidCode.code, invalidCode.revertCode), ); const expectedError = `Script "${scriptName}" ${realExceptionMessage}`; - const callCompiler: IFunctionCallCompiler = { - compileCall: () => invalidCode, + const callCompiler: FunctionCallCompiler = { + compileFunctionCalls: () => invalidCode, }; const scriptData = ScriptDataStub.createWithCall() .withName(scriptName); @@ -226,7 +226,7 @@ class ScriptCompilerBuilder { private sharedFunctionsParser: ISharedFunctionsParser = new SharedFunctionsParserStub(); - private callCompiler: IFunctionCallCompiler = new FunctionCallCompilerStub(); + private callCompiler: FunctionCallCompiler = new FunctionCallCompilerStub(); private codeValidator: ICodeValidator = new CodeValidatorStub(); @@ -269,7 +269,7 @@ class ScriptCompilerBuilder { return this; } - public withFunctionCallCompiler(callCompiler: IFunctionCallCompiler): ScriptCompilerBuilder { + public withFunctionCallCompiler(callCompiler: FunctionCallCompiler): ScriptCompilerBuilder { this.callCompiler = callCompiler; return this; } diff --git a/tests/unit/application/Parser/ScriptingDefinition/CodeSubstituter.spec.ts b/tests/unit/application/Parser/ScriptingDefinition/CodeSubstituter.spec.ts index 861f680b..9828b9ae 100644 --- a/tests/unit/application/Parser/ScriptingDefinition/CodeSubstituter.spec.ts +++ b/tests/unit/application/Parser/ScriptingDefinition/CodeSubstituter.spec.ts @@ -67,7 +67,7 @@ describe('CodeSubstituter', () => { sut.substitute('non empty code', info); // assert expect(compilerStub.callHistory).to.have.lengthOf(1); - const { parameters } = compilerStub.callHistory[0]; + const parameters = compilerStub.callHistory[0].args[1]; expect(parameters.hasArgument(testCase.parameter)); const { argumentValue } = parameters.getArgument(testCase.parameter); expect(argumentValue).to.equal(testCase.argument); @@ -85,7 +85,7 @@ describe('CodeSubstituter', () => { sut.substitute(expected, new ProjectInformationStub()); // assert expect(compilerStub.callHistory).to.have.lengthOf(1); - expect(compilerStub.callHistory[0].code).to.equal(expected); + expect(compilerStub.callHistory[0].args[0]).to.equal(expected); }); }); diff --git a/tests/unit/shared/Assertions/ExpectThrowsError.ts b/tests/unit/shared/Assertions/ExpectDeepThrowsError.ts similarity index 94% rename from tests/unit/shared/Assertions/ExpectThrowsError.ts rename to tests/unit/shared/Assertions/ExpectDeepThrowsError.ts index e81e9eb5..e163fe63 100644 --- a/tests/unit/shared/Assertions/ExpectThrowsError.ts +++ b/tests/unit/shared/Assertions/ExpectDeepThrowsError.ts @@ -1,7 +1,7 @@ import { expect } from 'vitest'; // `toThrowError` does not assert the error type (https://github.com/vitest-dev/vitest/blob/v0.34.2/docs/api/expect.md#tothrowerror) -export function expectThrowsError(delegate: () => void, expected: T) { +export function expectDeepThrowsError(delegate: () => void, expected: T) { // arrange if (!expected) { throw new Error('missing expected'); diff --git a/tests/unit/shared/Stubs/ArgumentCompilerStub.ts b/tests/unit/shared/Stubs/ArgumentCompilerStub.ts new file mode 100644 index 00000000..dad231d4 --- /dev/null +++ b/tests/unit/shared/Stubs/ArgumentCompilerStub.ts @@ -0,0 +1,37 @@ +import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext'; +import { ArgumentCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/ArgumentCompiler'; +import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; +import { FunctionCallStub } from './FunctionCallStub'; +import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls'; + +export class ArgumentCompilerStub + extends StubWithObservableMethodCalls + implements ArgumentCompiler { + private readonly scenarios = new Array(); + + public createCompiledNestedCall( + nestedFunctionCall: FunctionCall, + parentFunctionCall: FunctionCall, + context: FunctionCallCompilationContext, + ): FunctionCall { + this.registerMethodCall({ + methodName: 'createCompiledNestedCall', + args: [nestedFunctionCall, parentFunctionCall, context], + }); + const scenario = this.scenarios.find((s) => s.givenNestedFunctionCall === nestedFunctionCall); + if (scenario) { + return scenario.result; + } + return new FunctionCallStub(); + } + + public withScenario(scenario: ArgumentCompilationScenario): this { + this.scenarios.push(scenario); + return this; + } +} + +interface ArgumentCompilationScenario { + readonly givenNestedFunctionCall: FunctionCall; + readonly result: FunctionCall; +} diff --git a/tests/unit/shared/Stubs/CodeSegmentMergerStub.ts b/tests/unit/shared/Stubs/CodeSegmentMergerStub.ts new file mode 100644 index 00000000..6815cdee --- /dev/null +++ b/tests/unit/shared/Stubs/CodeSegmentMergerStub.ts @@ -0,0 +1,16 @@ +import { CodeSegmentMerger } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/CodeSegmentMerger'; +import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode'; +import { CompiledCodeStub } from './CompiledCodeStub'; +import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls'; + +export class CodeSegmentMergerStub + extends StubWithObservableMethodCalls + implements CodeSegmentMerger { + public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode { + this.registerMethodCall({ + methodName: 'mergeCodeParts', + args: [codeSegments], + }); + return new CompiledCodeStub(); + } +} diff --git a/tests/unit/shared/Stubs/CompiledCodeStub.ts b/tests/unit/shared/Stubs/CompiledCodeStub.ts new file mode 100644 index 00000000..74f2bd06 --- /dev/null +++ b/tests/unit/shared/Stubs/CompiledCodeStub.ts @@ -0,0 +1,17 @@ +import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode'; + +export class CompiledCodeStub implements CompiledCode { + public code = `${CompiledCodeStub.name}: code`; + + public revertCode?: string = `${CompiledCodeStub.name}: revertCode`; + + public withCode(code: string): this { + this.code = code; + return this; + } + + public withRevertCode(revertCode?: string): this { + this.revertCode = revertCode; + return this; + } +} diff --git a/tests/unit/shared/Stubs/ExpressionsCompilerStub.ts b/tests/unit/shared/Stubs/ExpressionsCompilerStub.ts index 9a64a3aa..99cfe5b1 100644 --- a/tests/unit/shared/Stubs/ExpressionsCompilerStub.ts +++ b/tests/unit/shared/Stubs/ExpressionsCompilerStub.ts @@ -3,14 +3,14 @@ import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Sc import { scrambledEqual } from '@/application/Common/Array'; import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub'; +import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls'; -export class ExpressionsCompilerStub implements IExpressionsCompiler { - public readonly callHistory = new Array<{ - code: string, parameters: IReadOnlyFunctionCallArgumentCollection }>(); +export class ExpressionsCompilerStub + extends StubWithObservableMethodCalls + implements IExpressionsCompiler { + private readonly scenarios = new Array(); - private readonly scenarios = new Array(); - - public setup(scenario: ITestScenario): ExpressionsCompilerStub { + public setup(scenario: ExpressionCompilationScenario): ExpressionsCompilerStub { this.scenarios.push(scenario); return this; } @@ -28,7 +28,10 @@ export class ExpressionsCompilerStub implements IExpressionsCompiler { code: string, parameters: IReadOnlyFunctionCallArgumentCollection, ): string { - this.callHistory.push({ code, parameters }); + this.registerMethodCall({ + methodName: 'compileExpressions', + args: [code, parameters], + }); const scenario = this.scenarios.find( (s) => s.givenCode === code && deepEqual(s.givenArgs, parameters), ); @@ -43,7 +46,7 @@ export class ExpressionsCompilerStub implements IExpressionsCompiler { } } -interface ITestScenario { +interface ExpressionCompilationScenario { readonly givenCode: string; readonly givenArgs: IReadOnlyFunctionCallArgumentCollection; readonly result: string; diff --git a/tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub.ts b/tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub.ts index 8a8b69f1..ab0c556b 100644 --- a/tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub.ts +++ b/tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub.ts @@ -5,7 +5,19 @@ import { FunctionCallArgumentStub } from './FunctionCallArgumentStub'; export class FunctionCallArgumentCollectionStub implements IFunctionCallArgumentCollection { private args = new Array(); - public withArgument(parameterName: string, argumentValue: string) { + public withEmptyArguments(): this { + this.args.length = 0; + return this; + } + + public withSomeArguments(): this { + return this + .withArgument('firstTestParameterName', 'first-parameter-argument-value') + .withArgument('secondTestParameterName', 'second-parameter-argument-value') + .withArgument('thirdTestParameterName', 'third-parameter-argument-value'); + } + + public withArgument(parameterName: string, argumentValue: string): this { const arg = new FunctionCallArgumentStub() .withParameterName(parameterName) .withArgumentValue(argumentValue); @@ -13,7 +25,7 @@ export class FunctionCallArgumentCollectionStub implements IFunctionCallArgument return this; } - public withArguments(args: { readonly [index: string]: string }) { + public withArguments(args: { readonly [index: string]: string }): this { for (const [name, value] of Object.entries(args)) { this.withArgument(name, value); } diff --git a/tests/unit/shared/Stubs/FunctionCallCompilationContextStub.ts b/tests/unit/shared/Stubs/FunctionCallCompilationContextStub.ts new file mode 100644 index 00000000..194ecc39 --- /dev/null +++ b/tests/unit/shared/Stubs/FunctionCallCompilationContextStub.ts @@ -0,0 +1,27 @@ +import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext'; +import { SingleCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler'; +import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; +import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection'; +import { SingleCallCompilerStub } from './SingleCallCompilerStub'; +import { FunctionCallStub } from './FunctionCallStub'; +import { SharedFunctionCollectionStub } from './SharedFunctionCollectionStub'; + +export class FunctionCallCompilationContextStub implements FunctionCallCompilationContext { + public allFunctions: ISharedFunctionCollection = new SharedFunctionCollectionStub(); + + public rootCallSequence: readonly FunctionCall[] = [ + new FunctionCallStub(), new FunctionCallStub(), + ]; + + public singleCallCompiler: SingleCallCompiler = new SingleCallCompilerStub(); + + public withSingleCallCompiler(singleCallCompiler: SingleCallCompiler): this { + this.singleCallCompiler = singleCallCompiler; + return this; + } + + public withAllFunctions(allFunctions: ISharedFunctionCollection): this { + this.allFunctions = allFunctions; + return this; + } +} diff --git a/tests/unit/shared/Stubs/FunctionCallCompilerStub.ts b/tests/unit/shared/Stubs/FunctionCallCompilerStub.ts index 50e7b50d..24058da6 100644 --- a/tests/unit/shared/Stubs/FunctionCallCompilerStub.ts +++ b/tests/unit/shared/Stubs/FunctionCallCompilerStub.ts @@ -1,29 +1,29 @@ -import { ICompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/ICompiledCode'; -import { IFunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/IFunctionCallCompiler'; +import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode'; +import { FunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler'; import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection'; -import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall'; +import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; interface IScenario { - calls: IFunctionCall[]; + calls: FunctionCall[]; functions: ISharedFunctionCollection; - result: ICompiledCode; + result: CompiledCode; } -export class FunctionCallCompilerStub implements IFunctionCallCompiler { +export class FunctionCallCompilerStub implements FunctionCallCompiler { public scenarios = new Array(); public setup( - calls: IFunctionCall[], + calls: FunctionCall[], functions: ISharedFunctionCollection, - result: ICompiledCode, + result: CompiledCode, ) { this.scenarios.push({ calls, functions, result }); } - public compileCall( - calls: IFunctionCall[], + public compileFunctionCalls( + calls: readonly FunctionCall[], functions: ISharedFunctionCollection, - ): ICompiledCode { + ): CompiledCode { const predefined = this.scenarios .find((s) => areEqual(s.calls, calls) && s.functions === functions); if (predefined) { @@ -37,12 +37,12 @@ export class FunctionCallCompilerStub implements IFunctionCallCompiler { } function areEqual( - first: readonly IFunctionCall[], - second: readonly IFunctionCall[], + first: readonly FunctionCall[], + second: readonly FunctionCall[], ) { - const comparer = (a: IFunctionCall, b: IFunctionCall) => a.functionName + const comparer = (a: FunctionCall, b: FunctionCall) => a.functionName .localeCompare(b.functionName); - const printSorted = (calls: readonly IFunctionCall[]) => JSON + const printSorted = (calls: readonly FunctionCall[]) => JSON .stringify([...calls].sort(comparer)); return printSorted(first) === printSorted(second); } diff --git a/tests/unit/shared/Stubs/FunctionCallStub.ts b/tests/unit/shared/Stubs/FunctionCallStub.ts index 514ea17a..d1d9f3fe 100644 --- a/tests/unit/shared/Stubs/FunctionCallStub.ts +++ b/tests/unit/shared/Stubs/FunctionCallStub.ts @@ -1,7 +1,7 @@ -import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall'; +import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; import { FunctionCallArgumentCollectionStub } from './FunctionCallArgumentCollectionStub'; -export class FunctionCallStub implements IFunctionCall { +export class FunctionCallStub implements FunctionCall { public functionName = 'functionCallStub'; public args = new FunctionCallArgumentCollectionStub(); diff --git a/tests/unit/shared/Stubs/SharedFunctionCollectionStub.ts b/tests/unit/shared/Stubs/SharedFunctionCollectionStub.ts index 3b270cb9..f338dcc2 100644 --- a/tests/unit/shared/Stubs/SharedFunctionCollectionStub.ts +++ b/tests/unit/shared/Stubs/SharedFunctionCollectionStub.ts @@ -5,7 +5,7 @@ import { SharedFunctionStub } from './SharedFunctionStub'; export class SharedFunctionCollectionStub implements ISharedFunctionCollection { private readonly functions = new Map(); - public withFunction(...funcs: readonly ISharedFunction[]) { + public withFunctions(...funcs: readonly ISharedFunction[]): this { for (const func of funcs) { this.functions.set(func.name, func); } @@ -21,4 +21,12 @@ export class SharedFunctionCollectionStub implements ISharedFunctionCollection { .withCode('code by SharedFunctionCollectionStub') .withRevertCode('revert-code by SharedFunctionCollectionStub'); } + + public getRequiredParameterNames(functionName: string): string[] { + return this.getFunctionByName(functionName) + .parameters + .all + .filter((p) => !p.isOptional) + .map((p) => p.name); + } } diff --git a/tests/unit/shared/Stubs/SharedFunctionStub.ts b/tests/unit/shared/Stubs/SharedFunctionStub.ts index f3a2e4bd..d112a36b 100644 --- a/tests/unit/shared/Stubs/SharedFunctionStub.ts +++ b/tests/unit/shared/Stubs/SharedFunctionStub.ts @@ -1,6 +1,6 @@ import { ISharedFunction, ISharedFunctionBody, FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; import { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/IFunctionParameterCollection'; -import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall'; +import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; import { FunctionParameterCollectionStub } from './FunctionParameterCollectionStub'; import { FunctionCallStub } from './FunctionCallStub'; @@ -16,7 +16,7 @@ export class SharedFunctionStub implements ISharedFunction { private bodyType: FunctionBodyType = FunctionBodyType.Code; - private calls: IFunctionCall[] = [new FunctionCallStub()]; + private calls: FunctionCall[] = [new FunctionCallStub()]; constructor(type: FunctionBodyType) { this.bodyType = type; @@ -53,7 +53,11 @@ export class SharedFunctionStub implements ISharedFunction { return this; } - public withCalls(...calls: readonly IFunctionCall[]) { + public withSomeCalls() { + return this.withCalls(new FunctionCallStub(), new FunctionCallStub()); + } + + public withCalls(...calls: readonly FunctionCall[]) { this.calls = [...calls]; return this; } diff --git a/tests/unit/shared/Stubs/SingleCallCompilerStrategyStub.ts b/tests/unit/shared/Stubs/SingleCallCompilerStrategyStub.ts new file mode 100644 index 00000000..d8298971 --- /dev/null +++ b/tests/unit/shared/Stubs/SingleCallCompilerStrategyStub.ts @@ -0,0 +1,45 @@ +import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode'; +import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext'; +import { SingleCallCompilerStrategy } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompilerStrategy'; +import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; +import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; +import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls'; +import { CompiledCodeStub } from './CompiledCodeStub'; + +export class SingleCallCompilerStrategyStub + extends StubWithObservableMethodCalls + implements SingleCallCompilerStrategy { + private canCompileResult = true; + + private compiledFunctionResult: CompiledCode[] = [new CompiledCodeStub()]; + + public canCompile(func: ISharedFunction): boolean { + this.registerMethodCall({ + methodName: 'canCompile', + args: [func], + }); + return this.canCompileResult; + } + + public compileFunction( + calledFunction: ISharedFunction, + callToFunction: FunctionCall, + context: FunctionCallCompilationContext, + ): CompiledCode[] { + this.registerMethodCall({ + methodName: 'compileFunction', + args: [calledFunction, callToFunction, context], + }); + return this.compiledFunctionResult; + } + + public withCanCompileResult(canCompileResult: boolean): this { + this.canCompileResult = canCompileResult; + return this; + } + + public withCompiledFunctionResult(compiledFunctionResult: CompiledCode[]): this { + this.compiledFunctionResult = compiledFunctionResult; + return this; + } +} diff --git a/tests/unit/shared/Stubs/SingleCallCompilerStub.ts b/tests/unit/shared/Stubs/SingleCallCompilerStub.ts new file mode 100644 index 00000000..ca7e1a73 --- /dev/null +++ b/tests/unit/shared/Stubs/SingleCallCompilerStub.ts @@ -0,0 +1,47 @@ +import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode'; +import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext'; +import { SingleCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler'; +import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; +import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls'; +import { CompiledCodeStub } from './CompiledCodeStub'; + +interface CallCompilationScenario { + readonly givenCall: FunctionCall; + readonly result: CompiledCode[]; +} + +export class SingleCallCompilerStub + extends StubWithObservableMethodCalls + implements SingleCallCompiler { + private readonly callCompilationScenarios = new Array(); + + public withCallCompilationScenarios(scenarios: Map): this { + for (const [call, result] of scenarios) { + this.withCallCompilationScenario({ + givenCall: call, + result, + }); + } + return this; + } + + public withCallCompilationScenario(scenario: CallCompilationScenario): this { + this.callCompilationScenarios.push(scenario); + return this; + } + + public compileSingleCall( + call: FunctionCall, + context: FunctionCallCompilationContext, + ): CompiledCode[] { + this.registerMethodCall({ + methodName: 'compileSingleCall', + args: [call, context], + }); + const callCompilation = this.callCompilationScenarios.find((s) => s.givenCall === call); + if (callCompilation) { + return callCompilation.result; + } + return [new CompiledCodeStub()]; + } +}