Fix compiler bug with nested optional arguments

This commit fixes compiler bug where it fails when optional values are
compiled into absent values in nested calls.

- Throw exception with more context for easier future debugging.
- Add better validation of argument values for nested calls.
- Refactor `FunctionCallCompiler` for better clarity and modularize it
  to make it more maintainable and testable.
- Refactor related interface to not have `I` prefix, and
  function/variable names for better clarity.

Context:

Discovered this issue while attempting to call
`RunInlineCodeAsTrustedInstaller` which in turn invokes `RunPowerShell`
for issue #246. This led to the realization that despite parameters
flagged as optional, the nested argument compilation didn't support
them.
This commit is contained in:
undergroundwires
2023-09-16 16:11:41 +02:00
parent a1f2497381
commit 53222fd83c
49 changed files with 1938 additions and 772 deletions

View File

@@ -0,0 +1,5 @@
import { CompiledCode } from '../CompiledCode';
export interface CodeSegmentMerger {
mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode;
}

View File

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

View File

@@ -1,4 +1,4 @@
export interface ICompiledCode {
export interface CompiledCode {
readonly code: string;
readonly revertCode?: string;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +0,0 @@
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
export interface IFunctionCall {
readonly functionName: string;
readonly args: IReadOnlyFunctionCallArgumentCollection;
}

View File

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

View File

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

View File

@@ -2,4 +2,5 @@ import { ISharedFunction } from './ISharedFunction';
export interface ISharedFunctionCollection {
getFunctionByName(name: string): ISharedFunction;
getRequiredParameterNames(functionName: string): string[];
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<T extends Error>(delegate: () => void, expected: T) {
export function expectDeepThrowsError<T extends Error>(delegate: () => void, expected: T) {
// arrange
if (!expected) {
throw new Error('missing expected');

View File

@@ -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<ArgumentCompiler>
implements ArgumentCompiler {
private readonly scenarios = new Array<ArgumentCompilationScenario>();
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;
}

View File

@@ -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<CodeSegmentMerger>
implements CodeSegmentMerger {
public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode {
this.registerMethodCall({
methodName: 'mergeCodeParts',
args: [codeSegments],
});
return new CompiledCodeStub();
}
}

View File

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

View File

@@ -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<IExpressionsCompiler>
implements IExpressionsCompiler {
private readonly scenarios = new Array<ExpressionCompilationScenario>();
private readonly scenarios = new Array<ITestScenario>();
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;

View File

@@ -5,7 +5,19 @@ import { FunctionCallArgumentStub } from './FunctionCallArgumentStub';
export class FunctionCallArgumentCollectionStub implements IFunctionCallArgumentCollection {
private args = new Array<IFunctionCallArgument>();
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);
}

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import { SharedFunctionStub } from './SharedFunctionStub';
export class SharedFunctionCollectionStub implements ISharedFunctionCollection {
private readonly functions = new Map<string, ISharedFunction>();
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);
}
}

View File

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

View File

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

View File

@@ -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<SingleCallCompiler>
implements SingleCallCompiler {
private readonly callCompilationScenarios = new Array<CallCompilationScenario>();
public withCallCompilationScenarios(scenarios: Map<FunctionCall, CompiledCode[]>): 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()];
}
}