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

@@ -81,7 +81,7 @@ function compileCode(
compiler: IExpressionsCompiler,
): ICompiledFunctionCall {
return {
code: compiler.compileExpressions(code.do, args),
code: compiler.compileExpressions(code.execute, args),
revertCode: compiler.compileExpressions(code.revert, args),
};
}

View File

@@ -19,6 +19,6 @@ export enum FunctionBodyType {
}
export interface IFunctionCode {
readonly do: string;
readonly execute: string;
readonly revert?: string;
}

View File

@@ -1,6 +1,10 @@
import type { FunctionData } from '@/application/collections/';
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
export interface ISharedFunctionsParser {
parseFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection;
parseFunctions(
functions: readonly FunctionData[],
syntax: ILanguageSyntax,
): ISharedFunctionCollection;
}

View File

@@ -1,4 +1,5 @@
import { IFunctionCall } from './Call/IFunctionCall';
import {
FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody,
} from './ISharedFunction';
@@ -25,7 +26,7 @@ export function createFunctionWithInlineCode(
throw new Error(`undefined code in function "${name}"`);
}
const content: IFunctionCode = {
do: code,
execute: code,
revert: revertCode,
};
return new SharedFunction(name, parameters, content, FunctionBodyType.Code);

View File

@@ -1,4 +1,9 @@
import type { FunctionData, InstructionHolder } from '@/application/collections/';
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines';
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
import { SharedFunctionCollection } from './SharedFunctionCollection';
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
@@ -12,16 +17,20 @@ import { parseFunctionCalls } from './Call/FunctionCallParser';
export class SharedFunctionsParser implements ISharedFunctionsParser {
public static readonly instance: ISharedFunctionsParser = new SharedFunctionsParser();
constructor(private readonly codeValidator: ICodeValidator = CodeValidator.instance) { }
public parseFunctions(
functions: readonly FunctionData[],
syntax: ILanguageSyntax,
): ISharedFunctionCollection {
if (!syntax) { throw new Error('missing syntax'); }
const collection = new SharedFunctionCollection();
if (!functions || !functions.length) {
return collection;
}
ensureValidFunctions(functions);
return functions
.map((func) => parseFunction(func))
.map((func) => parseFunction(func, syntax, this.codeValidator))
.reduce((acc, func) => {
acc.addFunction(func);
return acc;
@@ -29,10 +38,15 @@ export class SharedFunctionsParser implements ISharedFunctionsParser {
}
}
function parseFunction(data: FunctionData): ISharedFunction {
function parseFunction(
data: FunctionData,
syntax: ILanguageSyntax,
validator: ICodeValidator,
): ISharedFunction {
const { name } = data;
const parameters = parseParameters(data);
if (hasCode(data)) {
validateCode(data, syntax, validator);
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
}
// Has call
@@ -40,6 +54,19 @@ function parseFunction(data: FunctionData): ISharedFunction {
return createCallerFunction(name, parameters, calls);
}
function validateCode(
data: FunctionData,
syntax: ILanguageSyntax,
validator: ICodeValidator,
): void {
[data.code, data.revertCode].forEach(
(code) => validator.throwIfInvalid(
code,
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
),
);
}
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
return (data.parameters || [])
.map((parameter) => {

View File

@@ -1,6 +1,10 @@
import type { FunctionData, ScriptData } from '@/application/collections/';
import { IScriptCode } from '@/domain/IScriptCode';
import { ScriptCode, ILanguageSyntax } from '@/domain/ScriptCode';
import { ScriptCode } from '@/domain/ScriptCode';
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
import { IScriptCompiler } from './IScriptCompiler';
import { ISharedFunctionCollection } from './Function/ISharedFunctionCollection';
import { IFunctionCallCompiler } from './Function/Call/Compiler/IFunctionCallCompiler';
@@ -8,18 +12,20 @@ import { FunctionCallCompiler } from './Function/Call/Compiler/FunctionCallCompi
import { ISharedFunctionsParser } from './Function/ISharedFunctionsParser';
import { SharedFunctionsParser } from './Function/SharedFunctionsParser';
import { parseFunctionCalls } from './Function/Call/FunctionCallParser';
import { ICompiledCode } from './Function/Call/Compiler/ICompiledCode';
export class ScriptCompiler implements IScriptCompiler {
private readonly functions: ISharedFunctionCollection;
constructor(
functions: readonly FunctionData[] | undefined,
private readonly syntax: ILanguageSyntax,
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
syntax: ILanguageSyntax,
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
private readonly codeValidator: ICodeValidator = CodeValidator.instance,
) {
if (!syntax) { throw new Error('missing syntax'); }
this.functions = sharedFunctionsParser.parseFunctions(functions);
this.functions = sharedFunctionsParser.parseFunctions(functions, syntax);
}
public canCompile(script: ScriptData): boolean {
@@ -35,13 +41,19 @@ export class ScriptCompiler implements IScriptCompiler {
try {
const calls = parseFunctionCalls(script.call);
const compiledCode = this.callCompiler.compileCall(calls, this.functions);
validateCompiledCode(compiledCode, this.codeValidator);
return new ScriptCode(
compiledCode.code,
compiledCode.revertCode,
this.syntax,
);
} catch (error) {
throw Error(`Script "${script.name}" ${error.message}`);
}
}
}
function validateCompiledCode(compiledCode: ICompiledCode, validator: ICodeValidator): void {
[compiledCode.code, compiledCode.revertCode].forEach(
(code) => validator.throwIfInvalid(code, [new NoEmptyLines()]),
);
}