Relax and improve code validation

Rework code validation to be bound to a context and not
context-independent. It means that the generated code is validated based
on different phases during the compilation. This is done by moving
validation from `ScriptCode` constructor to a different callable
function.

It removes duplicate detection for function calls once a call is fully
compiled, but still checks for duplicates inside each function body that
has inline code. This allows for having duplicates in final scripts
(thus relaxing the duplicate detection), e.g., when multiple calls to
the same function is made.

It fixes non-duplicates (when using common syntax) being misrepresented
as duplicate lines.

It improves the output of errors, such as printing valid lines, to give
more context. This improvement also fixes empty line validation not
showing the right empty lines in the error output. Empty line validation
shows tabs and whitespaces more clearly.

Finally, it adds more tests including tests for existing logic, such as
singleton factories.
This commit is contained in:
undergroundwires
2022-10-29 20:03:06 +02:00
parent f4a7ca76b8
commit e8199932b4
44 changed files with 1095 additions and 392 deletions

View File

@@ -1,6 +1,6 @@
import { ICategoryCollectionParseContext } from '@/application/Parser/Script/ICategoryCollectionParseContext';
import { IScriptCompiler } from '@/application/Parser/Script/Compiler/IScriptCompiler';
import { ILanguageSyntax } from '@/domain/ScriptCode';
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
import { ScriptCompilerStub } from './ScriptCompilerStub';
import { LanguageSyntaxStub } from './LanguageSyntaxStub';

View File

@@ -0,0 +1,18 @@
import { ICodeLine } from '@/application/Parser/Script/Validation/ICodeLine';
import { ICodeValidationRule, IInvalidCodeLine } from '@/application/Parser/Script/Validation/ICodeValidationRule';
export class CodeValidationRuleStub implements ICodeValidationRule {
public readonly receivedLines = new Array<readonly ICodeLine[]>();
private returnValue: IInvalidCodeLine[] = [];
public withReturnValue(lines: readonly IInvalidCodeLine[]) {
this.returnValue = [...lines];
return this;
}
public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] {
this.receivedLines.push(...[lines]);
return this.returnValue;
}
}

View File

@@ -0,0 +1,34 @@
import { expect } from 'chai';
import { ICodeValidationRule } from '@/application/Parser/Script/Validation/ICodeValidationRule';
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
import { Type } from '../Type';
export class CodeValidatorStub implements ICodeValidator {
public callHistory = new Array<{
code: string,
rules: readonly ICodeValidationRule[],
}>();
public throwIfInvalid(
code: string,
rules: readonly ICodeValidationRule[],
): void {
this.callHistory.push({
code,
rules: Array.from(rules),
});
}
public assertHistory(expected: {
validatedCodes: readonly string[],
rules: readonly Type<ICodeValidationRule>[],
}) {
expect(this.callHistory).to.have.lengthOf(expected.validatedCodes.length);
const actualValidatedCodes = this.callHistory.map((args) => args.code);
expect(actualValidatedCodes.sort()).deep.equal([...expected.validatedCodes].sort());
for (const call of this.callHistory) {
const actualRules = call.rules.map((rule) => rule.constructor);
expect(actualRules.sort()).to.deep.equal([...expected.rules].sort());
}
}
}

View File

@@ -20,7 +20,7 @@ export class ExpressionsCompilerStub implements IExpressionsCompiler {
givenArgs: FunctionCallArgumentCollectionStub,
) {
return this
.setup({ givenCode: func.body.code.do, givenArgs, result: func.body.code.do })
.setup({ givenCode: func.body.code.execute, givenArgs, result: func.body.code.execute })
.setup({ givenCode: func.body.code.revert, givenArgs, result: func.body.code.revert });
}

View File

@@ -1,12 +1,12 @@
import { IFunctionCode } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
export class FunctionCodeStub implements IFunctionCode {
public do = 'do code (function-code-stub)';
public execute = 'execute code (function-code-stub)';
public revert? = 'revert code (function-code-stub)';
public withDo(code: string) {
this.do = code;
public withExecute(code: string) {
this.execute = code;
return this;
}

View File

@@ -1,4 +1,4 @@
import { ILanguageSyntax } from '@/domain/ScriptCode';
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
export class LanguageSyntaxStub implements ILanguageSyntax {
public commentDelimiters = [];

View File

@@ -26,7 +26,7 @@ export class SharedFunctionStub implements ISharedFunction {
return {
type: this.bodyType,
code: this.bodyType === FunctionBodyType.Code ? {
do: this.code,
execute: this.code,
revert: this.revertCode,
} : undefined,
calls: this.bodyType === FunctionBodyType.Calls ? this.calls : undefined,

View File

@@ -2,9 +2,15 @@ import type { FunctionData } from '@/application/collections/';
import { sequenceEqual } from '@/application/Common/Array';
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
import { ISharedFunctionsParser } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionsParser';
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
import { SharedFunctionCollectionStub } from './SharedFunctionCollectionStub';
export class SharedFunctionsParserStub implements ISharedFunctionsParser {
public callHistory = new Array<{
functions: readonly FunctionData[],
syntax: ILanguageSyntax,
}>();
private setupResults = new Array<{
functions: readonly FunctionData[],
result: ISharedFunctionCollection,
@@ -14,7 +20,14 @@ export class SharedFunctionsParserStub implements ISharedFunctionsParser {
this.setupResults.push({ functions, result });
}
public parseFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection {
public parseFunctions(
functions: readonly FunctionData[],
syntax: ILanguageSyntax,
): ISharedFunctionCollection {
this.callHistory.push({
functions: Array.from(functions),
syntax,
});
const result = this.findResult(functions);
return result || new SharedFunctionCollectionStub();
}

View File

@@ -0,0 +1,24 @@
import 'mocha';
import { expect } from 'chai';
import { Type } from '../Type';
interface ISingletonTestData<T> {
getter: () => T;
expectedType: Type<T>;
}
export function itIsSingleton<T>(test: ISingletonTestData<T>): void {
it('gets the expected type', () => {
// act
const instance = test.getter();
// assert
expect(instance).to.be.instanceOf(test.expectedType);
});
it('multiple calls get the same instance', () => {
// act
const instance1 = test.getter();
const instance2 = test.getter();
// assert
expect(instance1).to.equal(instance2);
});
}

View File

@@ -0,0 +1,5 @@
// eslint-disable-next-line @typescript-eslint/ban-types
export type Type<T, TArgs extends unknown[] = never> = Function & {
prototype: T,
apply: (this: unknown, args: TArgs) => void
};