Fix compiler bug with nested optional arguments

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

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

Context:

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

View File

@@ -1,7 +1,7 @@
import { expect } from 'vitest';
// `toThrowError` does not assert the error type (https://github.com/vitest-dev/vitest/blob/v0.34.2/docs/api/expect.md#tothrowerror)
export function expectThrowsError<T extends Error>(delegate: () => void, expected: T) {
export function expectDeepThrowsError<T extends Error>(delegate: () => void, expected: T) {
// arrange
if (!expected) {
throw new Error('missing expected');

View File

@@ -0,0 +1,37 @@
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
import { ArgumentCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/ArgumentCompiler';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { FunctionCallStub } from './FunctionCallStub';
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
export class ArgumentCompilerStub
extends StubWithObservableMethodCalls<ArgumentCompiler>
implements ArgumentCompiler {
private readonly scenarios = new Array<ArgumentCompilationScenario>();
public createCompiledNestedCall(
nestedFunctionCall: FunctionCall,
parentFunctionCall: FunctionCall,
context: FunctionCallCompilationContext,
): FunctionCall {
this.registerMethodCall({
methodName: 'createCompiledNestedCall',
args: [nestedFunctionCall, parentFunctionCall, context],
});
const scenario = this.scenarios.find((s) => s.givenNestedFunctionCall === nestedFunctionCall);
if (scenario) {
return scenario.result;
}
return new FunctionCallStub();
}
public withScenario(scenario: ArgumentCompilationScenario): this {
this.scenarios.push(scenario);
return this;
}
}
interface ArgumentCompilationScenario {
readonly givenNestedFunctionCall: FunctionCall;
readonly result: FunctionCall;
}

View File

@@ -0,0 +1,16 @@
import { CodeSegmentMerger } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/CodeSegmentMerger';
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { CompiledCodeStub } from './CompiledCodeStub';
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
export class CodeSegmentMergerStub
extends StubWithObservableMethodCalls<CodeSegmentMerger>
implements CodeSegmentMerger {
public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode {
this.registerMethodCall({
methodName: 'mergeCodeParts',
args: [codeSegments],
});
return new CompiledCodeStub();
}
}

View File

@@ -0,0 +1,17 @@
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
export class CompiledCodeStub implements CompiledCode {
public code = `${CompiledCodeStub.name}: code`;
public revertCode?: string = `${CompiledCodeStub.name}: revertCode`;
public withCode(code: string): this {
this.code = code;
return this;
}
public withRevertCode(revertCode?: string): this {
this.revertCode = revertCode;
return this;
}
}

View File

@@ -3,14 +3,14 @@ import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Sc
import { scrambledEqual } from '@/application/Common/Array';
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
export class ExpressionsCompilerStub implements IExpressionsCompiler {
public readonly callHistory = new Array<{
code: string, parameters: IReadOnlyFunctionCallArgumentCollection }>();
export class ExpressionsCompilerStub
extends StubWithObservableMethodCalls<IExpressionsCompiler>
implements IExpressionsCompiler {
private readonly scenarios = new Array<ExpressionCompilationScenario>();
private readonly scenarios = new Array<ITestScenario>();
public setup(scenario: ITestScenario): ExpressionsCompilerStub {
public setup(scenario: ExpressionCompilationScenario): ExpressionsCompilerStub {
this.scenarios.push(scenario);
return this;
}
@@ -28,7 +28,10 @@ export class ExpressionsCompilerStub implements IExpressionsCompiler {
code: string,
parameters: IReadOnlyFunctionCallArgumentCollection,
): string {
this.callHistory.push({ code, parameters });
this.registerMethodCall({
methodName: 'compileExpressions',
args: [code, parameters],
});
const scenario = this.scenarios.find(
(s) => s.givenCode === code && deepEqual(s.givenArgs, parameters),
);
@@ -43,7 +46,7 @@ export class ExpressionsCompilerStub implements IExpressionsCompiler {
}
}
interface ITestScenario {
interface ExpressionCompilationScenario {
readonly givenCode: string;
readonly givenArgs: IReadOnlyFunctionCallArgumentCollection;
readonly result: string;

View File

@@ -5,7 +5,19 @@ import { FunctionCallArgumentStub } from './FunctionCallArgumentStub';
export class FunctionCallArgumentCollectionStub implements IFunctionCallArgumentCollection {
private args = new Array<IFunctionCallArgument>();
public withArgument(parameterName: string, argumentValue: string) {
public withEmptyArguments(): this {
this.args.length = 0;
return this;
}
public withSomeArguments(): this {
return this
.withArgument('firstTestParameterName', 'first-parameter-argument-value')
.withArgument('secondTestParameterName', 'second-parameter-argument-value')
.withArgument('thirdTestParameterName', 'third-parameter-argument-value');
}
public withArgument(parameterName: string, argumentValue: string): this {
const arg = new FunctionCallArgumentStub()
.withParameterName(parameterName)
.withArgumentValue(argumentValue);
@@ -13,7 +25,7 @@ export class FunctionCallArgumentCollectionStub implements IFunctionCallArgument
return this;
}
public withArguments(args: { readonly [index: string]: string }) {
public withArguments(args: { readonly [index: string]: string }): this {
for (const [name, value] of Object.entries(args)) {
this.withArgument(name, value);
}

View File

@@ -0,0 +1,27 @@
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
import { SingleCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
import { SingleCallCompilerStub } from './SingleCallCompilerStub';
import { FunctionCallStub } from './FunctionCallStub';
import { SharedFunctionCollectionStub } from './SharedFunctionCollectionStub';
export class FunctionCallCompilationContextStub implements FunctionCallCompilationContext {
public allFunctions: ISharedFunctionCollection = new SharedFunctionCollectionStub();
public rootCallSequence: readonly FunctionCall[] = [
new FunctionCallStub(), new FunctionCallStub(),
];
public singleCallCompiler: SingleCallCompiler = new SingleCallCompilerStub();
public withSingleCallCompiler(singleCallCompiler: SingleCallCompiler): this {
this.singleCallCompiler = singleCallCompiler;
return this;
}
public withAllFunctions(allFunctions: ISharedFunctionCollection): this {
this.allFunctions = allFunctions;
return this;
}
}

View File

@@ -1,29 +1,29 @@
import { ICompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/ICompiledCode';
import { IFunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/IFunctionCallCompiler';
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { FunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler';
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
interface IScenario {
calls: IFunctionCall[];
calls: FunctionCall[];
functions: ISharedFunctionCollection;
result: ICompiledCode;
result: CompiledCode;
}
export class FunctionCallCompilerStub implements IFunctionCallCompiler {
export class FunctionCallCompilerStub implements FunctionCallCompiler {
public scenarios = new Array<IScenario>();
public setup(
calls: IFunctionCall[],
calls: FunctionCall[],
functions: ISharedFunctionCollection,
result: ICompiledCode,
result: CompiledCode,
) {
this.scenarios.push({ calls, functions, result });
}
public compileCall(
calls: IFunctionCall[],
public compileFunctionCalls(
calls: readonly FunctionCall[],
functions: ISharedFunctionCollection,
): ICompiledCode {
): CompiledCode {
const predefined = this.scenarios
.find((s) => areEqual(s.calls, calls) && s.functions === functions);
if (predefined) {
@@ -37,12 +37,12 @@ export class FunctionCallCompilerStub implements IFunctionCallCompiler {
}
function areEqual(
first: readonly IFunctionCall[],
second: readonly IFunctionCall[],
first: readonly FunctionCall[],
second: readonly FunctionCall[],
) {
const comparer = (a: IFunctionCall, b: IFunctionCall) => a.functionName
const comparer = (a: FunctionCall, b: FunctionCall) => a.functionName
.localeCompare(b.functionName);
const printSorted = (calls: readonly IFunctionCall[]) => JSON
const printSorted = (calls: readonly FunctionCall[]) => JSON
.stringify([...calls].sort(comparer));
return printSorted(first) === printSorted(second);
}

View File

@@ -1,7 +1,7 @@
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { FunctionCallArgumentCollectionStub } from './FunctionCallArgumentCollectionStub';
export class FunctionCallStub implements IFunctionCall {
export class FunctionCallStub implements FunctionCall {
public functionName = 'functionCallStub';
public args = new FunctionCallArgumentCollectionStub();

View File

@@ -5,7 +5,7 @@ import { SharedFunctionStub } from './SharedFunctionStub';
export class SharedFunctionCollectionStub implements ISharedFunctionCollection {
private readonly functions = new Map<string, ISharedFunction>();
public withFunction(...funcs: readonly ISharedFunction[]) {
public withFunctions(...funcs: readonly ISharedFunction[]): this {
for (const func of funcs) {
this.functions.set(func.name, func);
}
@@ -21,4 +21,12 @@ export class SharedFunctionCollectionStub implements ISharedFunctionCollection {
.withCode('code by SharedFunctionCollectionStub')
.withRevertCode('revert-code by SharedFunctionCollectionStub');
}
public getRequiredParameterNames(functionName: string): string[] {
return this.getFunctionByName(functionName)
.parameters
.all
.filter((p) => !p.isOptional)
.map((p) => p.name);
}
}

View File

@@ -1,6 +1,6 @@
import { ISharedFunction, ISharedFunctionBody, FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/IFunctionParameterCollection';
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { FunctionParameterCollectionStub } from './FunctionParameterCollectionStub';
import { FunctionCallStub } from './FunctionCallStub';
@@ -16,7 +16,7 @@ export class SharedFunctionStub implements ISharedFunction {
private bodyType: FunctionBodyType = FunctionBodyType.Code;
private calls: IFunctionCall[] = [new FunctionCallStub()];
private calls: FunctionCall[] = [new FunctionCallStub()];
constructor(type: FunctionBodyType) {
this.bodyType = type;
@@ -53,7 +53,11 @@ export class SharedFunctionStub implements ISharedFunction {
return this;
}
public withCalls(...calls: readonly IFunctionCall[]) {
public withSomeCalls() {
return this.withCalls(new FunctionCallStub(), new FunctionCallStub());
}
public withCalls(...calls: readonly FunctionCall[]) {
this.calls = [...calls];
return this;
}

View File

@@ -0,0 +1,45 @@
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
import { SingleCallCompilerStrategy } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompilerStrategy';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
import { CompiledCodeStub } from './CompiledCodeStub';
export class SingleCallCompilerStrategyStub
extends StubWithObservableMethodCalls<SingleCallCompilerStrategy>
implements SingleCallCompilerStrategy {
private canCompileResult = true;
private compiledFunctionResult: CompiledCode[] = [new CompiledCodeStub()];
public canCompile(func: ISharedFunction): boolean {
this.registerMethodCall({
methodName: 'canCompile',
args: [func],
});
return this.canCompileResult;
}
public compileFunction(
calledFunction: ISharedFunction,
callToFunction: FunctionCall,
context: FunctionCallCompilationContext,
): CompiledCode[] {
this.registerMethodCall({
methodName: 'compileFunction',
args: [calledFunction, callToFunction, context],
});
return this.compiledFunctionResult;
}
public withCanCompileResult(canCompileResult: boolean): this {
this.canCompileResult = canCompileResult;
return this;
}
public withCompiledFunctionResult(compiledFunctionResult: CompiledCode[]): this {
this.compiledFunctionResult = compiledFunctionResult;
return this;
}
}

View File

@@ -0,0 +1,47 @@
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
import { SingleCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
import { CompiledCodeStub } from './CompiledCodeStub';
interface CallCompilationScenario {
readonly givenCall: FunctionCall;
readonly result: CompiledCode[];
}
export class SingleCallCompilerStub
extends StubWithObservableMethodCalls<SingleCallCompiler>
implements SingleCallCompiler {
private readonly callCompilationScenarios = new Array<CallCompilationScenario>();
public withCallCompilationScenarios(scenarios: Map<FunctionCall, CompiledCode[]>): this {
for (const [call, result] of scenarios) {
this.withCallCompilationScenario({
givenCall: call,
result,
});
}
return this;
}
public withCallCompilationScenario(scenario: CallCompilationScenario): this {
this.callCompilationScenarios.push(scenario);
return this;
}
public compileSingleCall(
call: FunctionCall,
context: FunctionCallCompilationContext,
): CompiledCode[] {
this.registerMethodCall({
methodName: 'compileSingleCall',
args: [call, context],
});
const callCompilation = this.callCompilationScenarios.find((s) => s.givenCall === call);
if (callCompilation) {
return callCompilation.result;
}
return [new CompiledCodeStub()];
}
}