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