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:
@@ -0,0 +1,5 @@
|
||||
import { CompiledCode } from '../CompiledCode';
|
||||
|
||||
export interface CodeSegmentMerger {
|
||||
mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode;
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface ICompiledCode {
|
||||
export interface CompiledCode {
|
||||
readonly code: string;
|
||||
readonly revertCode?: string;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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'),
|
||||
);
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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[],
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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),
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
|
||||
|
||||
export interface IFunctionCall {
|
||||
readonly functionName: string;
|
||||
readonly args: IReadOnlyFunctionCallArgumentCollection;
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -2,4 +2,5 @@ import { ISharedFunction } from './ISharedFunction';
|
||||
|
||||
export interface ISharedFunctionCollection {
|
||||
getFunctionByName(name: string): ISharedFunction;
|
||||
getRequiredParameterNames(functionName: string): string[];
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()]),
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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');
|
||||
37
tests/unit/shared/Stubs/ArgumentCompilerStub.ts
Normal file
37
tests/unit/shared/Stubs/ArgumentCompilerStub.ts
Normal 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;
|
||||
}
|
||||
16
tests/unit/shared/Stubs/CodeSegmentMergerStub.ts
Normal file
16
tests/unit/shared/Stubs/CodeSegmentMergerStub.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
17
tests/unit/shared/Stubs/CompiledCodeStub.ts
Normal file
17
tests/unit/shared/Stubs/CompiledCodeStub.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
45
tests/unit/shared/Stubs/SingleCallCompilerStrategyStub.ts
Normal file
45
tests/unit/shared/Stubs/SingleCallCompilerStrategyStub.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
47
tests/unit/shared/Stubs/SingleCallCompilerStub.ts
Normal file
47
tests/unit/shared/Stubs/SingleCallCompilerStub.ts
Normal 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()];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user