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.
258 lines
8.1 KiB
TypeScript
258 lines
8.1 KiB
TypeScript
import 'mocha';
|
|
import { expect } from 'chai';
|
|
import { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/IFunctionParameterCollection';
|
|
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
|
|
import { createCallerFunction, createFunctionWithInlineCode } from '@/application/Parser/Script/Compiler/Function/SharedFunction';
|
|
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
|
|
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
|
|
import { FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
|
import {
|
|
AbsentStringTestCases, itEachAbsentCollectionValue, itEachAbsentObjectValue,
|
|
itEachAbsentStringValue,
|
|
} from '@tests/unit/shared/TestCases/AbsentTests';
|
|
|
|
describe('SharedFunction', () => {
|
|
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('createFunctionWithInlineCode', () => {
|
|
describe('code', () => {
|
|
it('sets as expected', () => {
|
|
// arrange
|
|
const expected = 'expected-code';
|
|
// act
|
|
const sut = new SharedFunctionBuilder()
|
|
.withCode(expected)
|
|
.createFunctionWithInlineCode();
|
|
// assert
|
|
expect(sut.body.code.execute).equal(expected);
|
|
});
|
|
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 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.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);
|
|
});
|
|
});
|
|
});
|
|
|
|
function runForEachFactoryMethod(
|
|
act: (action: (sut: SharedFunctionBuilder) => ISharedFunction) => void,
|
|
): void {
|
|
describe('createCallerFunction', () => {
|
|
const action = (builder: SharedFunctionBuilder) => builder.createCallerFunction();
|
|
act(action);
|
|
});
|
|
describe('createFunctionWithInlineCode', () => {
|
|
const action = (builder: SharedFunctionBuilder) => builder.createFunctionWithInlineCode();
|
|
act(action);
|
|
});
|
|
}
|
|
|
|
/*
|
|
Using an abstraction here allows for easy refactorings in
|
|
parameters or moving between functional and object-oriented
|
|
solutions without refactorings all tests.
|
|
*/
|
|
class SharedFunctionBuilder {
|
|
private name = 'name';
|
|
|
|
private parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollectionStub();
|
|
|
|
private callSequence: readonly IFunctionCall[] = [new FunctionCallStub()];
|
|
|
|
private code = 'code';
|
|
|
|
private revertCode = 'revert-code';
|
|
|
|
public createCallerFunction(): ISharedFunction {
|
|
return createCallerFunction(
|
|
this.name,
|
|
this.parameters,
|
|
this.callSequence,
|
|
);
|
|
}
|
|
|
|
public createFunctionWithInlineCode(): ISharedFunction {
|
|
return createFunctionWithInlineCode(
|
|
this.name,
|
|
this.parameters,
|
|
this.code,
|
|
this.revertCode,
|
|
);
|
|
}
|
|
|
|
public withName(name: string) {
|
|
this.name = name;
|
|
return this;
|
|
}
|
|
|
|
public withParameters(parameters: IReadOnlyFunctionParameterCollection) {
|
|
this.parameters = parameters;
|
|
return this;
|
|
}
|
|
|
|
public withCode(code: string) {
|
|
this.code = code;
|
|
return this;
|
|
}
|
|
|
|
public withRevertCode(revertCode: string) {
|
|
this.revertCode = revertCode;
|
|
return this;
|
|
}
|
|
|
|
public withCallSequence(callSequence: readonly IFunctionCall[]) {
|
|
this.callSequence = callSequence;
|
|
return this;
|
|
}
|
|
}
|