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

@@ -11,8 +11,15 @@ import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/Fun
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', () => {
@@ -172,7 +179,7 @@ describe('FunctionCallCompiler', () => {
const args = new FunctionCallArgumentCollectionStub().withArguments(testCase.callArgs);
const { code } = func.body;
const expressionsCompilerMock = new ExpressionsCompilerStub()
.setup({ givenCode: code.do, givenArgs: args, result: expected.execute })
.setup({ givenCode: code.execute, givenArgs: args, result: expected.execute })
.setup({ givenCode: code.revert, givenArgs: args, result: expected.revert });
const sut = new MockableFunctionCallCompiler(expressionsCompilerMock);
// act
@@ -209,7 +216,7 @@ describe('FunctionCallCompiler', () => {
const expressionsCompilerMock = new ExpressionsCompilerStub()
.setupToReturnFunctionCode(firstFunction, firstFunctionCallArgs)
.setupToReturnFunctionCode(secondFunction, secondFunctionCallArgs);
const expectedExecute = `${firstFunction.body.code.do}\n${secondFunction.body.code.do}`;
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)
@@ -244,7 +251,7 @@ describe('FunctionCallCompiler', () => {
};
const expressionsCompilerMock = new ExpressionsCompilerStub()
.setup({
givenCode: functions.deep.body.code.do,
givenCode: functions.deep.body.code.execute,
givenArgs: emptyArgs,
result: expected.code,
})
@@ -312,7 +319,7 @@ describe('FunctionCallCompiler', () => {
})
// set-up compiling of deep, compiled argument should be sent
.setup({
givenCode: scenario.deep.getFunction().body.code.do,
givenCode: scenario.deep.getFunction().body.code.execute,
givenArgs: scenario.front.callArgs.expectedCallDeep(),
result: expected.code,
})
@@ -407,7 +414,7 @@ describe('FunctionCallCompiler', () => {
})
// Compiling of third functions code with expected arguments
.setup({
givenCode: scenario.third.getFunction().body.code.do,
givenCode: scenario.third.getFunction().body.code.execute,
givenArgs: scenario.second.callArgs.expectedToThird(),
result: expected.code,
})
@@ -491,7 +498,7 @@ describe('FunctionCallCompiler', () => {
.setupToReturnFunctionCode(functions.call2.deep.getFunction(), emptyArgs);
const sut = new MockableFunctionCallCompiler(expressionsCompilerMock);
const expected = {
code: `${functions.call1.deep.getFunction().body.code.do}\n${functions.call2.deep.getFunction().body.code.do}`,
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

View File

@@ -12,174 +12,174 @@ import {
} from '@tests/unit/shared/TestCases/AbsentTests';
describe('SharedFunction', () => {
describe('name', () => {
runForEachFactoryMethod((build) => {
it('sets as expected', () => {
// arrange
const expected = 'expected-function-name';
const builder = new SharedFunctionBuilder()
.withName(expected);
// act
const sut = build(builder);
// assert
expect(sut.name).equal(expected);
});
it('throws when absent', () => {
itEachAbsentStringValue((absentValue) => {
// arrange
const expectedError = 'missing function name';
const builder = new SharedFunctionBuilder()
.withName(absentValue);
// act
const act = () => build(builder);
// assert
expect(act).to.throw(expectedError);
});
});
});
});
describe('parameters', () => {
runForEachFactoryMethod((build) => {
it('sets as expected', () => {
// arrange
const expected = new FunctionParameterCollectionStub()
.withParameterName('test-parameter');
const builder = new SharedFunctionBuilder()
.withParameters(expected);
// act
const sut = build(builder);
// assert
expect(sut.parameters).equal(expected);
});
describe('throws if missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing parameters';
const parameters = absentValue;
const builder = new SharedFunctionBuilder()
.withParameters(parameters);
// act
const act = () => build(builder);
// assert
expect(act).to.throw(expectedError);
});
});
});
});
describe('body', () => {
describe('createFunctionWithInlineCode', () => {
describe('code', () => {
describe('SharedFunction', () => {
describe('name', () => {
runForEachFactoryMethod((build) => {
it('sets as expected', () => {
// arrange
const expected = 'expected-code';
const expected = 'expected-function-name';
const builder = new SharedFunctionBuilder()
.withName(expected);
// act
const sut = new SharedFunctionBuilder()
.withCode(expected)
.createFunctionWithInlineCode();
const sut = build(builder);
// assert
expect(sut.body.code.do).equal(expected);
expect(sut.name).equal(expected);
});
describe('throws if absent', () => {
it('throws when absent', () => {
itEachAbsentStringValue((absentValue) => {
// arrange
const functionName = 'expected-function-name';
const expectedError = `undefined code in function "${functionName}"`;
const invalidValue = absentValue;
const expectedError = 'missing function name';
const builder = new SharedFunctionBuilder()
.withName(absentValue);
// act
const act = () => new SharedFunctionBuilder()
.withName(functionName)
.withCode(invalidValue)
.createFunctionWithInlineCode();
const act = () => build(builder);
// assert
expect(act).to.throw(expectedError);
});
});
});
describe('revertCode', () => {
it('sets as expected', () => {
// arrange
const testData = [
'expected-revert-code',
...AbsentStringTestCases.map((testCase) => testCase.absentValue),
];
for (const data of testData) {
// act
const sut = new SharedFunctionBuilder()
.withRevertCode(data)
.createFunctionWithInlineCode();
// assert
expect(sut.body.code.revert).equal(data);
}
});
});
it('sets type as expected', () => {
// arrange
const expectedType = FunctionBodyType.Code;
// act
const sut = new SharedFunctionBuilder()
.createFunctionWithInlineCode();
// assert
expect(sut.body.type).equal(expectedType);
});
it('calls are undefined', () => {
// arrange
const expectedCalls = undefined;
// act
const sut = new SharedFunctionBuilder()
.createFunctionWithInlineCode();
// assert
expect(sut.body.calls).equal(expectedCalls);
});
});
describe('createCallerFunction', () => {
describe('callSequence', () => {
describe('parameters', () => {
runForEachFactoryMethod((build) => {
it('sets as expected', () => {
// arrange
const expected = [
new FunctionCallStub().withFunctionName('firstFunction'),
new FunctionCallStub().withFunctionName('secondFunction'),
];
const expected = new FunctionParameterCollectionStub()
.withParameterName('test-parameter');
const builder = new SharedFunctionBuilder()
.withParameters(expected);
// act
const sut = new SharedFunctionBuilder()
.withCallSequence(expected)
.createCallerFunction();
const sut = build(builder);
// assert
expect(sut.body.calls).equal(expected);
expect(sut.parameters).equal(expected);
});
describe('throws if missing', () => {
itEachAbsentCollectionValue((absentValue) => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const functionName = 'invalidFunction';
const callSequence = absentValue;
const expectedError = `missing call sequence in function "${functionName}"`;
const expectedError = 'missing parameters';
const parameters = absentValue;
const builder = new SharedFunctionBuilder()
.withParameters(parameters);
// act
const act = () => new SharedFunctionBuilder()
.withName(functionName)
.withCallSequence(callSequence)
.createCallerFunction();
const act = () => build(builder);
// assert
expect(act).to.throw(expectedError);
});
});
});
it('sets type as expected', () => {
});
});
describe('createFunctionWithInlineCode', () => {
describe('code', () => {
it('sets as expected', () => {
// arrange
const expectedType = FunctionBodyType.Calls;
const expected = 'expected-code';
// act
const sut = new SharedFunctionBuilder()
.createCallerFunction();
.withCode(expected)
.createFunctionWithInlineCode();
// assert
expect(sut.body.type).equal(expectedType);
expect(sut.body.code.execute).equal(expected);
});
it('code is undefined', () => {
describe('throws if absent', () => {
itEachAbsentStringValue((absentValue) => {
// arrange
const functionName = 'expected-function-name';
const expectedError = `undefined code in function "${functionName}"`;
const invalidValue = absentValue;
// act
const act = () => new SharedFunctionBuilder()
.withName(functionName)
.withCode(invalidValue)
.createFunctionWithInlineCode();
// assert
expect(act).to.throw(expectedError);
});
});
});
describe('revertCode', () => {
it('sets as expected', () => {
// arrange
const expectedCode = undefined;
const testData = [
'expected-revert-code',
...AbsentStringTestCases.map((testCase) => testCase.absentValue),
];
for (const data of testData) {
// act
const sut = new SharedFunctionBuilder()
.withRevertCode(data)
.createFunctionWithInlineCode();
// assert
expect(sut.body.code.revert).equal(data);
}
});
});
it('sets type as expected', () => {
// arrange
const expectedType = FunctionBodyType.Code;
// act
const sut = new SharedFunctionBuilder()
.createFunctionWithInlineCode();
// assert
expect(sut.body.type).equal(expectedType);
});
it('calls are undefined', () => {
// arrange
const expectedCalls = undefined;
// act
const sut = new SharedFunctionBuilder()
.createFunctionWithInlineCode();
// assert
expect(sut.body.calls).equal(expectedCalls);
});
});
describe('createCallerFunction', () => {
describe('callSequence', () => {
it('sets as expected', () => {
// arrange
const expected = [
new FunctionCallStub().withFunctionName('firstFunction'),
new FunctionCallStub().withFunctionName('secondFunction'),
];
// act
const sut = new SharedFunctionBuilder()
.withCallSequence(expected)
.createCallerFunction();
// assert
expect(sut.body.code).equal(expectedCode);
expect(sut.body.calls).equal(expected);
});
describe('throws if missing', () => {
itEachAbsentCollectionValue((absentValue) => {
// arrange
const functionName = 'invalidFunction';
const callSequence = absentValue;
const expectedError = `missing call sequence in function "${functionName}"`;
// act
const act = () => new SharedFunctionBuilder()
.withName(functionName)
.withCallSequence(callSequence)
.createCallerFunction();
// assert
expect(act).to.throw(expectedError);
});
});
});
it('sets type as expected', () => {
// arrange
const expectedType = FunctionBodyType.Calls;
// act
const sut = new SharedFunctionBuilder()
.createCallerFunction();
// assert
expect(sut.body.type).equal(expectedType);
});
it('code is undefined', () => {
// arrange
const expectedCode = undefined;
// act
const sut = new SharedFunctionBuilder()
.createCallerFunction();
// assert
expect(sut.body.code).equal(expectedCode);
});
});
});

View File

@@ -8,18 +8,46 @@ import { ParameterDefinitionDataStub } from '@tests/unit/shared/Stubs/ParameterD
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
import { itEachAbsentCollectionValue, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
import { itIsSingleton } from '@tests/unit/shared/TestCases/SingletonTests';
import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines';
describe('SharedFunctionsParser', () => {
describe('instance', () => {
itIsSingleton({
getter: () => SharedFunctionsParser.instance,
expectedType: SharedFunctionsParser,
});
});
describe('parseFunctions', () => {
describe('throws if syntax is missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing syntax';
const syntax = absentValue;
// act
const act = () => new ParseFunctionsCallerWithDefaults()
.withSyntax(syntax)
.parseFunctions();
// assert
expect(act).to.throw(expectedError);
});
});
describe('validates functions', () => {
describe('throws if one of the functions is undefined', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'some functions are undefined';
const functions = [FunctionDataStub.createWithCode(), absentValue];
const sut = new SharedFunctionsParser();
const sut = new ParseFunctionsCallerWithDefaults();
// act
const act = () => sut.parseFunctions(functions);
const act = () => sut
.withFunctions(functions)
.parseFunctions();
// assert
expect(act).to.throw(expectedError);
});
@@ -32,9 +60,10 @@ describe('SharedFunctionsParser', () => {
FunctionDataStub.createWithCode().withName(name),
FunctionDataStub.createWithCode().withName(name),
];
const sut = new SharedFunctionsParser();
// act
const act = () => sut.parseFunctions(functions);
const act = () => new ParseFunctionsCallerWithDefaults()
.withFunctions(functions)
.parseFunctions();
// assert
expect(act).to.throw(expectedError);
});
@@ -47,9 +76,10 @@ describe('SharedFunctionsParser', () => {
FunctionDataStub.createWithoutCallOrCodes().withName('func-1').withCode(code),
FunctionDataStub.createWithoutCallOrCodes().withName('func-2').withCode(code),
];
const sut = new SharedFunctionsParser();
// act
const act = () => sut.parseFunctions(functions);
const act = () => new ParseFunctionsCallerWithDefaults()
.withFunctions(functions)
.parseFunctions();
// assert
expect(act).to.throw(expectedError);
});
@@ -63,9 +93,10 @@ describe('SharedFunctionsParser', () => {
FunctionDataStub.createWithoutCallOrCodes()
.withName('func-2').withCode('code-2').withRevertCode(revertCode),
];
const sut = new SharedFunctionsParser();
// act
const act = () => sut.parseFunctions(functions);
const act = () => new ParseFunctionsCallerWithDefaults()
.withFunctions(functions)
.parseFunctions();
// assert
expect(act).to.throw(expectedError);
});
@@ -79,9 +110,10 @@ describe('SharedFunctionsParser', () => {
.withName(functionName)
.withCode('code')
.withMockCall();
const sut = new SharedFunctionsParser();
// act
const act = () => sut.parseFunctions([invalidFunction]);
const act = () => new ParseFunctionsCallerWithDefaults()
.withFunctions([invalidFunction])
.parseFunctions();
// assert
expect(act).to.throw(expectedError);
});
@@ -91,9 +123,10 @@ describe('SharedFunctionsParser', () => {
const expectedError = `neither "code" or "call" is defined in "${functionName}"`;
const invalidFunction = FunctionDataStub.createWithoutCallOrCodes()
.withName(functionName);
const sut = new SharedFunctionsParser();
// act
const act = () => sut.parseFunctions([invalidFunction]);
const act = () => new ParseFunctionsCallerWithDefaults()
.withFunctions([invalidFunction])
.parseFunctions();
// assert
expect(act).to.throw(expectedError);
});
@@ -116,14 +149,34 @@ describe('SharedFunctionsParser', () => {
.createWithCall()
.withParametersObject(testCase.invalidType as never);
const expectedError = `parameters must be an array of objects in function(s) "${func.name}"`;
const sut = new SharedFunctionsParser();
// act
const act = () => sut.parseFunctions([func]);
const act = () => new ParseFunctionsCallerWithDefaults()
.withFunctions([func])
.parseFunctions();
// assert
expect(act).to.throw(expectedError);
});
}
});
it('validates function code as expected when code is defined', () => {
// arrange
const expectedRules = [NoEmptyLines, NoDuplicatedLines];
const functionData = FunctionDataStub
.createWithCode()
.withCode('expected code to be validated')
.withRevertCode('expected revert code to be validated');
const validator = new CodeValidatorStub();
// act
new ParseFunctionsCallerWithDefaults()
.withFunctions([functionData])
.withValidator(validator)
.parseFunctions();
// assert
validator.assertHistory({
validatedCodes: [functionData.code, functionData.revertCode],
rules: expectedRules,
});
});
it('rethrows including function name when FunctionParameter throws', () => {
// arrange
const invalidParameterName = 'invalid function p@r4meter name';
@@ -139,8 +192,9 @@ describe('SharedFunctionsParser', () => {
.withParameters(new ParameterDefinitionDataStub().withName(invalidParameterName));
// act
const sut = new SharedFunctionsParser();
const act = () => sut.parseFunctions([functionData]);
const act = () => new ParseFunctionsCallerWithDefaults()
.withFunctions([functionData])
.parseFunctions();
// assert
expect(act).to.throw(expectedError);
@@ -148,10 +202,10 @@ describe('SharedFunctionsParser', () => {
});
describe('given empty functions, returns empty collection', () => {
itEachAbsentCollectionValue((absentValue) => {
// arrange
const sut = new SharedFunctionsParser();
// act
const actual = sut.parseFunctions(absentValue);
const actual = new ParseFunctionsCallerWithDefaults()
.withFunctions(absentValue)
.parseFunctions();
// assert
expect(actual).to.not.equal(undefined);
});
@@ -169,9 +223,10 @@ describe('SharedFunctionsParser', () => {
new ParameterDefinitionDataStub().withName('expectedParameter').withOptionality(true),
new ParameterDefinitionDataStub().withName('expectedParameter2').withOptionality(false),
);
const sut = new SharedFunctionsParser();
// act
const collection = sut.parseFunctions([expected]);
const collection = new ParseFunctionsCallerWithDefaults()
.withFunctions([expected])
.parseFunctions();
// expect
const actual = collection.getFunctionByName(name);
expectEqualName(expected, actual);
@@ -188,9 +243,10 @@ describe('SharedFunctionsParser', () => {
const data = FunctionDataStub.createWithoutCallOrCodes()
.withName('caller-function')
.withCall(call);
const sut = new SharedFunctionsParser();
// act
const collection = sut.parseFunctions([data]);
const collection = new ParseFunctionsCallerWithDefaults()
.withFunctions([data])
.parseFunctions();
// expect
const actual = collection.getFunctionByName(data.name);
expectEqualName(data, actual);
@@ -211,9 +267,10 @@ describe('SharedFunctionsParser', () => {
const caller2 = FunctionDataStub.createWithoutCallOrCodes()
.withName('caller-function-2')
.withCall([call1, call2]);
const sut = new SharedFunctionsParser();
// act
const collection = sut.parseFunctions([caller1, caller2]);
const collection = new ParseFunctionsCallerWithDefaults()
.withFunctions([caller1, caller2])
.parseFunctions();
// expect
const compiledCaller1 = collection.getFunctionByName(caller1.name);
expectEqualName(caller1, compiledCaller1);
@@ -228,6 +285,34 @@ describe('SharedFunctionsParser', () => {
});
});
class ParseFunctionsCallerWithDefaults {
private syntax: ILanguageSyntax = new LanguageSyntaxStub();
private codeValidator: ICodeValidator = new CodeValidatorStub();
private functions: readonly FunctionData[] = [FunctionDataStub.createWithCode()];
public withSyntax(syntax: ILanguageSyntax) {
this.syntax = syntax;
return this;
}
public withValidator(codeValidator: ICodeValidator) {
this.codeValidator = codeValidator;
return this;
}
public withFunctions(functions: readonly FunctionData[]) {
this.functions = functions;
return this;
}
public parseFunctions() {
const sut = new SharedFunctionsParser(this.codeValidator);
return sut.parseFunctions(this.functions, this.syntax);
}
}
function expectEqualName(expected: FunctionDataStub, actual: ISharedFunction): void {
expect(actual.name).to.equal(expected.name);
}
@@ -250,7 +335,7 @@ function expectEqualFunctionWithInlineCode(
): void {
expect(actual.body, `function "${actual.name}" has no body`);
expect(actual.body.code, `function "${actual.name}" has no code`);
expect(actual.body.code.do).to.equal(expected.code);
expect(actual.body.code.execute).to.equal(expected.code);
expect(actual.body.code.revert).to.equal(expected.revertCode);
}