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:
undergroundwires
2024-08-27 11:32:52 +02:00
parent db090f3696
commit dc5c87376b
65 changed files with 2217 additions and 1350 deletions

View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View 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;
};
}
}

View File

@@ -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;
}
}

View File

@@ -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]}`,
]));
}
}

View File

@@ -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[] = [];

View 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),
};
}

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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;
};
}
}