Add validation for max line length in compiler
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.
This commit is contained in:
21
tests/unit/shared/Stubs/CategoryCollectionContextStub.ts
Normal file
21
tests/unit/shared/Stubs/CategoryCollectionContextStub.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { CategoryCollectionContext } from '@/application/Parser/Executable/CategoryCollectionContext';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import type { ScriptCompiler } from '@/application/Parser/Executable/Script/Compiler/ScriptCompiler';
|
||||
import { ScriptCompilerStub } from './ScriptCompilerStub';
|
||||
|
||||
export class CategoryCollectionContextStub
|
||||
implements CategoryCollectionContext {
|
||||
public compiler: ScriptCompiler = new ScriptCompilerStub();
|
||||
|
||||
public language: ScriptingLanguage = ScriptingLanguage.shellscript;
|
||||
|
||||
public withCompiler(compiler: ScriptCompiler) {
|
||||
this.compiler = compiler;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withLanguage(language: ScriptingLanguage) {
|
||||
this.language = language;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import type { IScriptCompiler } from '@/application/Parser/Executable/Script/Compiler/IScriptCompiler';
|
||||
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
|
||||
import type { CategoryCollectionSpecificUtilities } from '@/application/Parser/Executable/CategoryCollectionSpecificUtilities';
|
||||
import { ScriptCompilerStub } from './ScriptCompilerStub';
|
||||
import { LanguageSyntaxStub } from './LanguageSyntaxStub';
|
||||
|
||||
export class CategoryCollectionSpecificUtilitiesStub
|
||||
implements CategoryCollectionSpecificUtilities {
|
||||
public compiler: IScriptCompiler = new ScriptCompilerStub();
|
||||
|
||||
public syntax: ILanguageSyntax = new LanguageSyntaxStub();
|
||||
|
||||
public withCompiler(compiler: IScriptCompiler) {
|
||||
this.compiler = compiler;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withSyntax(syntax: ILanguageSyntax) {
|
||||
this.syntax = syntax;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { CategoryParser } from '@/application/Parser/Executable/CategoryParser';
|
||||
import type { CategoryData } from '@/application/collections/';
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import type { CategoryCollectionSpecificUtilities } from '@/application/Parser/Executable/CategoryCollectionSpecificUtilities';
|
||||
import type { CategoryCollectionContext } from '@/application/Parser/Executable/CategoryCollectionContext';
|
||||
import { CategoryStub } from './CategoryStub';
|
||||
|
||||
export class CategoryParserStub {
|
||||
private configuredParseResults = new Map<CategoryData, Category>();
|
||||
|
||||
private usedUtilities = new Array<CategoryCollectionSpecificUtilities>();
|
||||
private usedUtilities = new Array<CategoryCollectionContext>();
|
||||
|
||||
public get(): CategoryParser {
|
||||
return (category, utilities) => {
|
||||
@@ -28,7 +28,7 @@ export class CategoryParserStub {
|
||||
return this;
|
||||
}
|
||||
|
||||
public getUsedUtilities(): readonly CategoryCollectionSpecificUtilities[] {
|
||||
public getUsedContext(): readonly CategoryCollectionContext[] {
|
||||
return this.usedUtilities;
|
||||
}
|
||||
}
|
||||
|
||||
23
tests/unit/shared/Stubs/CodeValidationAnalyzerStub.ts
Normal file
23
tests/unit/shared/Stubs/CodeValidationAnalyzerStub.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { CodeLine, InvalidCodeLine, CodeValidationAnalyzer } from '@/application/Parser/Executable/Script/Validation/Analyzers/CodeValidationAnalyzer';
|
||||
import type { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
|
||||
export class CodeValidationAnalyzerStub {
|
||||
public readonly receivedLines = new Array<readonly CodeLine[]>();
|
||||
|
||||
public readonly receivedLanguages = new Array<ScriptingLanguage>();
|
||||
|
||||
private returnValue: InvalidCodeLine[] = [];
|
||||
|
||||
public withReturnValue(lines: readonly InvalidCodeLine[]) {
|
||||
this.returnValue = [...lines];
|
||||
return this;
|
||||
}
|
||||
|
||||
public get(): CodeValidationAnalyzer {
|
||||
return (lines, language) => {
|
||||
this.receivedLines.push(...[lines]);
|
||||
this.receivedLanguages.push(language);
|
||||
return this.returnValue;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { ICodeLine } from '@/application/Parser/Executable/Script/Validation/ICodeLine';
|
||||
import type { ICodeValidationRule, IInvalidCodeLine } from '@/application/Parser/Executable/Script/Validation/ICodeValidationRule';
|
||||
|
||||
export class CodeValidationRuleStub implements ICodeValidationRule {
|
||||
public readonly receivedLines = new Array<readonly ICodeLine[]>();
|
||||
|
||||
private returnValue: IInvalidCodeLine[] = [];
|
||||
|
||||
public withReturnValue(lines: readonly IInvalidCodeLine[]) {
|
||||
this.returnValue = [...lines];
|
||||
return this;
|
||||
}
|
||||
|
||||
public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] {
|
||||
this.receivedLines.push(...[lines]);
|
||||
return this.returnValue;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,86 @@
|
||||
import { expect } from 'vitest';
|
||||
import type { Constructible } from '@/TypeHelpers';
|
||||
import type { ICodeValidationRule } from '@/application/Parser/Executable/Script/Validation/ICodeValidationRule';
|
||||
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
||||
import type { CodeValidator } from '@/application/Parser/Executable/Script/Validation/CodeValidator';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import type { CodeValidationRule } from '@/application/Parser/Executable/Script/Validation/CodeValidationRule';
|
||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||
|
||||
export class CodeValidatorStub implements ICodeValidator {
|
||||
public callHistory = new Array<{
|
||||
code: string,
|
||||
rules: readonly ICodeValidationRule[],
|
||||
}>();
|
||||
export class CodeValidatorStub {
|
||||
public callHistory = new Array<Parameters<CodeValidator>>();
|
||||
|
||||
public throwIfInvalid(
|
||||
code: string,
|
||||
rules: readonly ICodeValidationRule[],
|
||||
): void {
|
||||
this.callHistory.push({
|
||||
code,
|
||||
rules: Array.from(rules),
|
||||
});
|
||||
public get(): CodeValidator {
|
||||
return (...args) => {
|
||||
this.callHistory.push(args);
|
||||
};
|
||||
}
|
||||
|
||||
public assertHistory(expectation: {
|
||||
validatedCodes: readonly (string | undefined)[],
|
||||
rules: readonly Constructible<ICodeValidationRule>[],
|
||||
}) {
|
||||
expect(this.callHistory).to.have.lengthOf(expectation.validatedCodes.length);
|
||||
const actualValidatedCodes = this.callHistory.map((args) => args.code);
|
||||
expect(actualValidatedCodes.sort()).deep.equal([...expectation.validatedCodes].sort());
|
||||
for (const call of this.callHistory) {
|
||||
const actualRules = call.rules.map((rule) => rule.constructor);
|
||||
expect(actualRules.sort()).to.deep.equal([...expectation.rules].sort());
|
||||
}
|
||||
public assertValidatedCodes(
|
||||
validatedCodes: readonly string[],
|
||||
) {
|
||||
expectExpectedCodes(this, validatedCodes);
|
||||
}
|
||||
|
||||
public assertValidatedRules(
|
||||
rules: readonly CodeValidationRule[],
|
||||
) {
|
||||
expectExpectedRules(this, rules);
|
||||
}
|
||||
|
||||
public assertValidatedLanguage(
|
||||
language: ScriptingLanguage,
|
||||
) {
|
||||
expectExpectedLanguage(this, language);
|
||||
}
|
||||
}
|
||||
|
||||
function expectExpectedCodes(
|
||||
validator: CodeValidatorStub,
|
||||
expectedCodes: readonly string[],
|
||||
): void {
|
||||
expect(validator.callHistory).to.have.lengthOf(expectedCodes.length, formatAssertionMessage([
|
||||
'Mismatch in number of validated codes',
|
||||
`Expected: ${expectedCodes.length}`,
|
||||
`Actual: ${validator.callHistory.length}`,
|
||||
]));
|
||||
const actualValidatedCodes = validator.callHistory.map((args) => {
|
||||
const [code] = args;
|
||||
return code;
|
||||
});
|
||||
expect(actualValidatedCodes).to.have.members(expectedCodes, formatAssertionMessage([
|
||||
'Mismatch in validated codes',
|
||||
`Expected: ${JSON.stringify(expectedCodes)}`,
|
||||
`Actual: ${JSON.stringify(actualValidatedCodes)}`,
|
||||
]));
|
||||
}
|
||||
|
||||
function expectExpectedRules(
|
||||
validator: CodeValidatorStub,
|
||||
expectedRules: readonly CodeValidationRule[],
|
||||
): void {
|
||||
for (const call of validator.callHistory) {
|
||||
const [,,actualRules] = call;
|
||||
expect(actualRules).to.have.lengthOf(expectedRules.length, formatAssertionMessage([
|
||||
'Mismatch in number of validation rules for a call.',
|
||||
`Expected: ${expectedRules.length}`,
|
||||
`Actual: ${actualRules.length}`,
|
||||
]));
|
||||
expect(actualRules).to.have.members(expectedRules, formatAssertionMessage([
|
||||
'Mismatch in validation rules for for a call.',
|
||||
`Expected: ${JSON.stringify(expectedRules)}`,
|
||||
`Actual: ${JSON.stringify(actualRules)}`,
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
function expectExpectedLanguage(
|
||||
validator: CodeValidatorStub,
|
||||
expectedLanguage: ScriptingLanguage,
|
||||
): void {
|
||||
for (const call of validator.callHistory) {
|
||||
const [,language] = call;
|
||||
expect(language).to.equal(expectedLanguage, formatAssertionMessage([
|
||||
'Mismatch in scripting language',
|
||||
`Expected: ${ScriptingLanguage[expectedLanguage]}`,
|
||||
`Actual: ${ScriptingLanguage[language]}`,
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
|
||||
import type { LanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Analyzers/Syntax/LanguageSyntax';
|
||||
|
||||
export class LanguageSyntaxStub implements ILanguageSyntax {
|
||||
export class LanguageSyntaxStub implements LanguageSyntax {
|
||||
public commentDelimiters: string[] = [];
|
||||
|
||||
public commonCodeParts: string[] = [];
|
||||
|
||||
20
tests/unit/shared/Stubs/ScriptCompilerFactoryStub.ts
Normal file
20
tests/unit/shared/Stubs/ScriptCompilerFactoryStub.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { ScriptCompilerFactory, ScriptCompilerInitParameters } from '@/application/Parser/Executable/Script/Compiler/ScriptCompilerFactory';
|
||||
import type { ScriptCompiler } from '@/application/Parser/Executable/Script/Compiler/ScriptCompiler';
|
||||
import { ScriptCompilerStub } from './ScriptCompilerStub';
|
||||
|
||||
export function createScriptCompilerFactorySpy(): {
|
||||
readonly instance: ScriptCompilerFactory;
|
||||
getInitParameters: (
|
||||
compiler: ScriptCompiler,
|
||||
) => ScriptCompilerInitParameters | undefined;
|
||||
} {
|
||||
const createdCompilers = new Map<ScriptCompiler, ScriptCompilerInitParameters>();
|
||||
return {
|
||||
instance: (parameters) => {
|
||||
const compiler = new ScriptCompilerStub();
|
||||
createdCompilers.set(compiler, parameters);
|
||||
return compiler;
|
||||
},
|
||||
getInitParameters: (category) => createdCompilers.get(category),
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { ScriptData } from '@/application/collections/';
|
||||
import type { IScriptCompiler } from '@/application/Parser/Executable/Script/Compiler/IScriptCompiler';
|
||||
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
||||
import type { ScriptCompiler } from '@/application/Parser/Executable/Script/Compiler/ScriptCompiler';
|
||||
import { ScriptCodeStub } from './ScriptCodeStub';
|
||||
|
||||
export class ScriptCompilerStub implements IScriptCompiler {
|
||||
export class ScriptCompilerStub implements ScriptCompiler {
|
||||
public compilableScripts = new Map<ScriptData, ScriptCode>();
|
||||
|
||||
public canCompile(script: ScriptData): boolean {
|
||||
|
||||
@@ -2,13 +2,13 @@ import type { FunctionData } from '@/application/collections/';
|
||||
import { sequenceEqual } from '@/application/Common/Array';
|
||||
import type { ISharedFunctionCollection } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunctionCollection';
|
||||
import type { SharedFunctionsParser } from '@/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser';
|
||||
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
|
||||
import type { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { SharedFunctionCollectionStub } from './SharedFunctionCollectionStub';
|
||||
|
||||
export function createSharedFunctionsParserStub() {
|
||||
const callHistory = new Array<{
|
||||
readonly functions: readonly FunctionData[],
|
||||
readonly syntax: ILanguageSyntax,
|
||||
readonly language: ScriptingLanguage,
|
||||
}>();
|
||||
|
||||
const setupResults = new Array<{
|
||||
@@ -26,11 +26,11 @@ export function createSharedFunctionsParserStub() {
|
||||
|
||||
const parser: SharedFunctionsParser = (
|
||||
functions: readonly FunctionData[],
|
||||
syntax: ILanguageSyntax,
|
||||
language: ScriptingLanguage,
|
||||
) => {
|
||||
callHistory.push({
|
||||
functions: Array.from(functions),
|
||||
syntax,
|
||||
language,
|
||||
});
|
||||
const result = findResult(functions);
|
||||
return result || new SharedFunctionCollectionStub();
|
||||
|
||||
@@ -1,18 +1,31 @@
|
||||
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
|
||||
import type { ISyntaxFactory } from '@/application/Parser/Executable/Script/Validation/Syntax/ISyntaxFactory';
|
||||
import type { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import type { LanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Analyzers/Syntax/LanguageSyntax';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import type { SyntaxFactory } from '@/application/Parser/Executable/Script/Validation/Analyzers/Syntax/SyntaxFactory';
|
||||
import { LanguageSyntaxStub } from './LanguageSyntaxStub';
|
||||
|
||||
export function createSyntaxFactoryStub(
|
||||
expectedLanguage?: ScriptingLanguage,
|
||||
result?: ILanguageSyntax,
|
||||
): ISyntaxFactory {
|
||||
return {
|
||||
create: (language: ScriptingLanguage) => {
|
||||
if (expectedLanguage !== undefined && language !== expectedLanguage) {
|
||||
throw new Error('unexpected language');
|
||||
}
|
||||
return result ?? new LanguageSyntaxStub();
|
||||
},
|
||||
};
|
||||
interface PredeterminedSyntax {
|
||||
readonly givenLanguage: ScriptingLanguage;
|
||||
readonly predeterminedSyntax: LanguageSyntax;
|
||||
}
|
||||
|
||||
export class SyntaxFactoryStub {
|
||||
private readonly predeterminedResults = new Array<PredeterminedSyntax>();
|
||||
|
||||
public withPredeterminedSyntax(scenario: PredeterminedSyntax): this {
|
||||
this.predeterminedResults.push(scenario);
|
||||
return this;
|
||||
}
|
||||
|
||||
public get(): SyntaxFactory {
|
||||
return (language): LanguageSyntax => {
|
||||
const results = this.predeterminedResults.filter((r) => r.givenLanguage === language);
|
||||
if (results.length === 0) {
|
||||
return new LanguageSyntaxStub();
|
||||
}
|
||||
if (results.length > 1) {
|
||||
throw new Error(`Logical error: More than single predetermined results for ${ScriptingLanguage[language]}`);
|
||||
}
|
||||
return results[0].predeterminedSyntax;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user