This commit adds validation logic in compiler to check for max allowed characters per line for scripts. This allows preventing bugs caused by limitation of terminal emulators. Other supporting changes: - Rename/refactor related code for clarity and better maintainability. - Drop `I` prefix from interfaces to align with latest convention. - Refactor CodeValidator to be functional rather than object-oriented for simplicity. - Refactor syntax definition construction to be functional and be part of rule for better separation of concerns. - Refactored validation logic to use an enum-based factory pattern for improved maintainability and scalability.
227 lines
8.2 KiB
TypeScript
227 lines
8.2 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { CodeValidationAnalyzerStub } from '@tests/unit/shared/Stubs/CodeValidationAnalyzerStub';
|
|
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
|
import { indentText } from '@/application/Common/Text/IndentText';
|
|
import type { CodeLine, InvalidCodeLine } from '@/application/Parser/Executable/Script/Validation/Analyzers/CodeValidationAnalyzer';
|
|
import { validateCode } from '@/application/Parser/Executable/Script/Validation/CodeValidator';
|
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
|
import { CodeValidationRule } from '@/application/Parser/Executable/Script/Validation/CodeValidationRule';
|
|
import type { ValidationRuleAnalyzerFactory } from '@/application/Parser/Executable/Script/Validation/ValidationRuleAnalyzerFactory';
|
|
|
|
describe('validateCode', () => {
|
|
describe('does not throw if code is absent', () => {
|
|
itEachAbsentStringValue((absentValue) => {
|
|
// arrange
|
|
const code = absentValue;
|
|
// act
|
|
const act = () => new TestContext()
|
|
.withCode(code)
|
|
.validate();
|
|
// assert
|
|
expect(act).to.not.throw();
|
|
}, { excludeNull: true, excludeUndefined: true });
|
|
});
|
|
describe('line splitting', () => {
|
|
it('supports all line separators', () => {
|
|
// arrange
|
|
const expectedLineTexts = ['line1', 'line2', 'line3', 'line4'];
|
|
const code = 'line1\r\nline2\rline3\nline4';
|
|
const analyzer = new CodeValidationAnalyzerStub();
|
|
const analyzerFactory: ValidationRuleAnalyzerFactory = () => [analyzer.get()];
|
|
// act
|
|
new TestContext()
|
|
.withCode(code)
|
|
.withAnalyzerFactory(analyzerFactory)
|
|
.validate();
|
|
// expect
|
|
expect(analyzer.receivedLines).has.lengthOf(1);
|
|
const actualLineTexts = analyzer.receivedLines[0].map((line) => line.text);
|
|
expect(actualLineTexts).to.deep.equal(expectedLineTexts);
|
|
});
|
|
it('uses 1-indexed line numbering', () => {
|
|
// arrange
|
|
const expectedLineNumbers = [1, 2, 3];
|
|
const code = ['line1', 'line2', 'line3'].join('\n');
|
|
const analyzer = new CodeValidationAnalyzerStub();
|
|
const analyzerFactory: ValidationRuleAnalyzerFactory = () => [analyzer.get()];
|
|
// act
|
|
new TestContext()
|
|
.withCode(code)
|
|
.withAnalyzerFactory(analyzerFactory)
|
|
.validate();
|
|
// expect
|
|
expect(analyzer.receivedLines).has.lengthOf(1);
|
|
const actualLineIndexes = analyzer.receivedLines[0].map((line) => line.lineNumber);
|
|
expect(actualLineIndexes).to.deep.equal(expectedLineNumbers);
|
|
});
|
|
it('includes empty lines in count', () => {
|
|
// arrange
|
|
const expectedEmptyLineCount = 4;
|
|
const code = '\n'.repeat(expectedEmptyLineCount - 1);
|
|
const analyzer = new CodeValidationAnalyzerStub();
|
|
const analyzerFactory: ValidationRuleAnalyzerFactory = () => [analyzer.get()];
|
|
// act
|
|
new TestContext()
|
|
.withCode(code)
|
|
.withAnalyzerFactory(analyzerFactory)
|
|
.validate();
|
|
// expect
|
|
expect(analyzer.receivedLines).has.lengthOf(1);
|
|
const actualLines = analyzer.receivedLines[0];
|
|
expect(actualLines).to.have.lengthOf(expectedEmptyLineCount);
|
|
});
|
|
it('correctly matches line numbers with text', () => {
|
|
// arrange
|
|
const expected: readonly CodeLine[] = [
|
|
{ lineNumber: 1, text: 'first' },
|
|
{ lineNumber: 2, text: 'second' },
|
|
];
|
|
const code = expected.map((line) => line.text).join('\n');
|
|
const analyzer = new CodeValidationAnalyzerStub();
|
|
const analyzerFactory: ValidationRuleAnalyzerFactory = () => [analyzer.get()];
|
|
// act
|
|
new TestContext()
|
|
.withCode(code)
|
|
.withAnalyzerFactory(analyzerFactory)
|
|
.validate();
|
|
// expect
|
|
expect(analyzer.receivedLines).has.lengthOf(1);
|
|
expect(analyzer.receivedLines[0]).to.deep.equal(expected);
|
|
});
|
|
});
|
|
it('analyzes lines for correct language', () => {
|
|
// arrange
|
|
const expectedLanguage = ScriptingLanguage.batchfile;
|
|
const analyzers = [
|
|
new CodeValidationAnalyzerStub(),
|
|
new CodeValidationAnalyzerStub(),
|
|
new CodeValidationAnalyzerStub(),
|
|
];
|
|
const analyzerFactory: ValidationRuleAnalyzerFactory = () => analyzers.map((s) => s.get());
|
|
// act
|
|
new TestContext()
|
|
.withAnalyzerFactory(analyzerFactory)
|
|
.validate();
|
|
// assert
|
|
const actualLanguages = analyzers.flatMap((a) => a.receivedLanguages);
|
|
const unexpectedLanguages = actualLanguages.filter((l) => l !== expectedLanguage);
|
|
expect(unexpectedLanguages).to.have.lengthOf(0);
|
|
});
|
|
describe('throwing invalid lines', () => {
|
|
it('throws error for invalid line from single rule', () => {
|
|
// arrange
|
|
const errorText = 'error';
|
|
const expectedError = constructExpectedValidationErrorMessage([
|
|
{ text: 'line1' },
|
|
{ text: 'line2', error: errorText },
|
|
{ text: 'line3' },
|
|
{ text: 'line4' },
|
|
]);
|
|
const code = ['line1', 'line2', 'line3', 'line4'].join('\n');
|
|
const invalidLines: readonly InvalidCodeLine[] = [
|
|
{ lineNumber: 2, error: errorText },
|
|
];
|
|
const invalidAnalyzer = new CodeValidationAnalyzerStub()
|
|
.withReturnValue(invalidLines);
|
|
const noopAnalyzer = new CodeValidationAnalyzerStub()
|
|
.withReturnValue([]);
|
|
const analyzerFactory: ValidationRuleAnalyzerFactory = () => [
|
|
invalidAnalyzer, noopAnalyzer,
|
|
].map((s) => s.get());
|
|
// act
|
|
const act = () => new TestContext()
|
|
.withCode(code)
|
|
.withAnalyzerFactory(analyzerFactory)
|
|
.validate();
|
|
// assert
|
|
expect(act).to.throw(expectedError);
|
|
});
|
|
it('throws error with combined invalid lines from multiple rules', () => {
|
|
// arrange
|
|
const firstError = 'firstError';
|
|
const secondError = 'firstError';
|
|
const expectedError = constructExpectedValidationErrorMessage([
|
|
{ text: 'line1' },
|
|
{ text: 'line2', error: firstError },
|
|
{ text: 'line3' },
|
|
{ text: 'line4', error: secondError },
|
|
]);
|
|
const code = ['line1', 'line2', 'line3', 'line4'].join('\n');
|
|
const firstRuleError: readonly InvalidCodeLine[] = [
|
|
{ lineNumber: 2, error: firstError },
|
|
];
|
|
const secondRuleError: readonly InvalidCodeLine[] = [
|
|
{ lineNumber: 4, error: secondError },
|
|
];
|
|
const firstRule = new CodeValidationAnalyzerStub().withReturnValue(firstRuleError);
|
|
const secondRule = new CodeValidationAnalyzerStub().withReturnValue(secondRuleError);
|
|
const analyzerFactory: ValidationRuleAnalyzerFactory = () => [
|
|
firstRule, secondRule,
|
|
].map((s) => s.get());
|
|
// act
|
|
const act = () => new TestContext()
|
|
.withCode(code)
|
|
.withAnalyzerFactory(analyzerFactory)
|
|
.validate();
|
|
// assert
|
|
expect(act).to.throw(expectedError);
|
|
});
|
|
});
|
|
});
|
|
|
|
function constructExpectedValidationErrorMessage(
|
|
lines: readonly {
|
|
readonly text: string,
|
|
readonly error?: string,
|
|
}[],
|
|
): string {
|
|
return [
|
|
'Errors with the code.',
|
|
...lines.flatMap((line, index): string[] => {
|
|
const textPrefix = line.error ? '❌' : '✅';
|
|
const lineNumber = `[${index + 1}]`;
|
|
const formattedLine = `${lineNumber} ${textPrefix} ${line.text}`;
|
|
return [
|
|
formattedLine,
|
|
...(line.error ? [indentText(`⟶ ${line.error}`)] : []),
|
|
];
|
|
}),
|
|
].join('\n');
|
|
}
|
|
|
|
class TestContext {
|
|
private code = `[${TestContext.name}] code`;
|
|
|
|
private language: ScriptingLanguage = ScriptingLanguage.batchfile;
|
|
|
|
private rules: readonly CodeValidationRule[] = [CodeValidationRule.NoDuplicatedLines];
|
|
|
|
private analyzerFactory: ValidationRuleAnalyzerFactory = () => [
|
|
new CodeValidationAnalyzerStub().get(),
|
|
];
|
|
|
|
public withCode(code: string): this {
|
|
this.code = code;
|
|
return this;
|
|
}
|
|
|
|
public withRules(rules: readonly CodeValidationRule[]): this {
|
|
this.rules = rules;
|
|
return this;
|
|
}
|
|
|
|
public withAnalyzerFactory(analyzerFactory: ValidationRuleAnalyzerFactory): this {
|
|
this.analyzerFactory = analyzerFactory;
|
|
return this;
|
|
}
|
|
|
|
public validate(): ReturnType<typeof validateCode> {
|
|
return validateCode(
|
|
this.code,
|
|
this.language,
|
|
this.rules,
|
|
this.analyzerFactory,
|
|
);
|
|
}
|
|
}
|