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:
@@ -0,0 +1,37 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { ICodeValidationRule, IInvalidCodeLine } from '@/application/Parser/Script/Validation/ICodeValidationRule';
|
||||
import { ICodeLine } from '@/application/Parser/Script/Validation/ICodeLine';
|
||||
|
||||
interface ICodeValidationRuleTestCase {
|
||||
testName: string;
|
||||
codeLines: readonly string[];
|
||||
expected: readonly IInvalidCodeLine[];
|
||||
sut: ICodeValidationRule;
|
||||
}
|
||||
|
||||
export function testCodeValidationRule(testCases: readonly ICodeValidationRuleTestCase[]) {
|
||||
for (const testCase of testCases) {
|
||||
it(testCase.testName, () => {
|
||||
// arrange
|
||||
const { sut } = testCase;
|
||||
const codeLines = createCodeLines(testCase.codeLines);
|
||||
// act
|
||||
const actual = sut.analyze(codeLines);
|
||||
// assert
|
||||
function sort(lines: readonly IInvalidCodeLine[]) { // To ignore order
|
||||
return Array.from(lines).sort((a, b) => a.index - b.index);
|
||||
}
|
||||
expect(sort(actual)).to.deep.equal(sort(testCase.expected));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createCodeLines(lines: readonly string[]): ICodeLine[] {
|
||||
return lines.map((lineText, index): ICodeLine => (
|
||||
{
|
||||
index: index + 1,
|
||||
text: lineText,
|
||||
}
|
||||
));
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines';
|
||||
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
||||
import { IInvalidCodeLine } from '@/application/Parser/Script/Validation/ICodeValidationRule';
|
||||
import { testCodeValidationRule } from './CodeValidationRuleTestRunner';
|
||||
|
||||
describe('NoDuplicatedLines', () => {
|
||||
describe('ctor', () => {
|
||||
describe('throws if syntax is missing', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing syntax';
|
||||
const syntax = absentValue;
|
||||
// act
|
||||
const act = () => new NoDuplicatedLines(syntax);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('analyze', () => {
|
||||
testCodeValidationRule([
|
||||
{
|
||||
testName: 'no results when code is valid',
|
||||
codeLines: ['unique1', 'unique2', 'unique3', 'unique4'],
|
||||
expected: [],
|
||||
sut: new NoDuplicatedLines(new LanguageSyntaxStub()),
|
||||
},
|
||||
{
|
||||
testName: 'detects single duplicated line as expected',
|
||||
codeLines: ['duplicate', 'duplicate', 'unique', 'duplicate'],
|
||||
expected: expectInvalidCodeLines([1, 2, 4]),
|
||||
sut: new NoDuplicatedLines(new LanguageSyntaxStub()),
|
||||
},
|
||||
{
|
||||
testName: 'detects multiple duplicated lines as expected',
|
||||
codeLines: ['duplicate1', 'duplicate2', 'unique', 'duplicate1', 'unique2', 'duplicate2'],
|
||||
expected: expectInvalidCodeLines([1, 4], [2, 6]),
|
||||
sut: new NoDuplicatedLines(new LanguageSyntaxStub()),
|
||||
},
|
||||
{
|
||||
testName: 'common code parts: does not detect multiple common code part usages as duplicates',
|
||||
codeLines: ['good', 'good', 'bad', 'bad', 'good', 'also-good', 'also-good', 'unique'],
|
||||
expected: expectInvalidCodeLines([3, 4]),
|
||||
sut: new NoDuplicatedLines(new LanguageSyntaxStub()
|
||||
.withCommonCodeParts('good', 'also-good')),
|
||||
},
|
||||
{
|
||||
testName: 'common code parts: does not detect multiple common code part used in same code line as duplicates',
|
||||
codeLines: ['bad', 'bad', 'good1 good2', 'good1 good2', 'good2 good1', 'good2 good1'],
|
||||
expected: expectInvalidCodeLines([1, 2]),
|
||||
sut: new NoDuplicatedLines(new LanguageSyntaxStub()
|
||||
.withCommonCodeParts('good2', 'good1')),
|
||||
},
|
||||
{
|
||||
testName: 'common code parts: detects when common code parts used in conjunction with unique words',
|
||||
codeLines: [
|
||||
'common-part1', 'common-part1', 'common-part1 common-part2', 'common-part1 unique', 'common-part1 unique',
|
||||
'common-part2', 'common-part2 common-part1', 'unique common-part2', 'unique common-part2',
|
||||
],
|
||||
expected: expectInvalidCodeLines([4, 5], [8, 9]),
|
||||
sut: new NoDuplicatedLines(new LanguageSyntaxStub()
|
||||
.withCommonCodeParts('common-part1', 'common-part2')),
|
||||
},
|
||||
{
|
||||
testName: 'comments: does not when lines start with comment',
|
||||
codeLines: ['#abc', '#abc', 'abc', 'unique', 'abc', '//abc', '//abc', '//unique', '#unique'],
|
||||
expected: expectInvalidCodeLines([3, 5]),
|
||||
sut: new NoDuplicatedLines(new LanguageSyntaxStub()
|
||||
.withCommentDelimiters('#', '//')),
|
||||
},
|
||||
{
|
||||
testName: 'comments: does when comments come after lien start',
|
||||
codeLines: ['test #comment', 'test #comment', 'test2 # comment', 'test2 # comment'],
|
||||
expected: expectInvalidCodeLines([1, 2], [3, 4]),
|
||||
sut: new NoDuplicatedLines(new LanguageSyntaxStub()
|
||||
.withCommentDelimiters('#')),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
function expectInvalidCodeLines(
|
||||
...lines: readonly ReadonlyArray<number>[]
|
||||
): IInvalidCodeLine[] {
|
||||
return lines.flatMap((occurrenceIndices): readonly IInvalidCodeLine[] => occurrenceIndices
|
||||
.map((index): IInvalidCodeLine => ({
|
||||
index,
|
||||
error: `Line is duplicated at line numbers ${occurrenceIndices.join(',')}.`,
|
||||
})));
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||
import { testCodeValidationRule } from './CodeValidationRuleTestRunner';
|
||||
|
||||
describe('NoEmptyLines', () => {
|
||||
describe('analyze', () => {
|
||||
testCodeValidationRule([
|
||||
{
|
||||
testName: 'no results when code is valid',
|
||||
codeLines: ['non-empty-line1', 'none-empty-line2'],
|
||||
expected: [],
|
||||
sut: new NoEmptyLines(),
|
||||
},
|
||||
{
|
||||
testName: 'shows error for empty line',
|
||||
codeLines: ['first line', '', 'third line'],
|
||||
expected: [{ index: 2, error: 'Empty line' }],
|
||||
sut: new NoEmptyLines(),
|
||||
},
|
||||
{
|
||||
testName: 'shows error for multiple empty lines',
|
||||
codeLines: ['first line', '', 'third line', ''],
|
||||
expected: [2, 4].map((index) => ({ index, error: 'Empty line' })),
|
||||
sut: new NoEmptyLines(),
|
||||
},
|
||||
{
|
||||
testName: 'shows error for undefined and null lines',
|
||||
codeLines: ['first line', undefined, 'third line', null],
|
||||
expected: [2, 4].map((index) => ({ index, error: 'Empty line' })),
|
||||
sut: new NoEmptyLines(),
|
||||
},
|
||||
{
|
||||
testName: 'shows error for whitespace-only lines',
|
||||
codeLines: ['first line', ' ', 'third line'],
|
||||
expected: [{ index: 2, error: 'Empty line: "{whitespace}{whitespace}"' }],
|
||||
sut: new NoEmptyLines(),
|
||||
},
|
||||
{
|
||||
testName: 'shows error for tab-only lines',
|
||||
codeLines: ['first line', '\t\t', 'third line'],
|
||||
expected: [{ index: 2, error: 'Empty line: "{tab}{tab}"' }],
|
||||
sut: new NoEmptyLines(),
|
||||
},
|
||||
{
|
||||
testName: 'shows error for lines that consists of whitespace and tabs',
|
||||
codeLines: ['first line', '\t \t', 'third line', ' \t '],
|
||||
expected: [{ index: 2, error: 'Empty line: "{tab}{whitespace}{tab}"' }, { index: 4, error: 'Empty line: "{whitespace}{tab}{whitespace}"' }],
|
||||
sut: new NoEmptyLines(),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user