Skip to content

Commit

Permalink
Add validation for max line length in compiler
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
undergroundwires committed Aug 27, 2024
1 parent db090f3 commit dc5c873
Show file tree
Hide file tree
Showing 65 changed files with 2,215 additions and 1,348 deletions.
10 changes: 5 additions & 5 deletions src/application/Parser/CategoryCollectionParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { createEnumParser, type EnumParser } from '../Common/Enum';
import { parseCategory, type CategoryParser } from './Executable/CategoryParser';
import { parseScriptingDefinition, type ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
import { createTypeValidator, type TypeValidator } from './Common/TypeValidator';
import { createCollectionUtilities, type CategoryCollectionSpecificUtilitiesFactory } from './Executable/CategoryCollectionSpecificUtilities';
import { createCategoryCollectionContext, type CategoryCollectionContextFactory } from './Executable/CategoryCollectionContext';

export const parseCategoryCollection: CategoryCollectionParser = (
content,
Expand All @@ -16,9 +16,9 @@ export const parseCategoryCollection: CategoryCollectionParser = (
) => {
validateCollection(content, utilities.validator);
const scripting = utilities.parseScriptingDefinition(content.scripting, projectDetails);
const collectionUtilities = utilities.createUtilities(content.functions, scripting);
const collectionContext = utilities.createContext(content.functions, scripting.language);
const categories = content.actions.map(
(action) => utilities.parseCategory(action, collectionUtilities),
(action) => utilities.parseCategory(action, collectionContext),
);
const os = utilities.osParser.parseEnum(content.os, 'os');
const collection = utilities.createCategoryCollection({
Expand Down Expand Up @@ -60,7 +60,7 @@ interface CategoryCollectionParserUtilities {
readonly osParser: EnumParser<OperatingSystem>;
readonly validator: TypeValidator;
readonly parseScriptingDefinition: ScriptingDefinitionParser;
readonly createUtilities: CategoryCollectionSpecificUtilitiesFactory;
readonly createContext: CategoryCollectionContextFactory;
readonly parseCategory: CategoryParser;
readonly createCategoryCollection: CategoryCollectionFactory;
}
Expand All @@ -69,7 +69,7 @@ const DefaultUtilities: CategoryCollectionParserUtilities = {
osParser: createEnumParser(OperatingSystem),
validator: createTypeValidator(),
parseScriptingDefinition,
createUtilities: createCollectionUtilities,
createContext: createCategoryCollectionContext,
parseCategory,
createCategoryCollection: (...args) => new CategoryCollection(...args),
};
33 changes: 33 additions & 0 deletions src/application/Parser/Executable/CategoryCollectionContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { FunctionData } from '@/application/collections/';
import type { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { createScriptCompiler, type ScriptCompilerFactory } from './Script/Compiler/ScriptCompilerFactory';
import type { ScriptCompiler } from './Script/Compiler/ScriptCompiler';

export interface CategoryCollectionContext {
readonly compiler: ScriptCompiler;
readonly language: ScriptingLanguage;
}

export interface CategoryCollectionContextFactory {
(
functionsData: ReadonlyArray<FunctionData> | undefined,
language: ScriptingLanguage,
compilerFactory?: ScriptCompilerFactory,
): CategoryCollectionContext;
}

export const createCategoryCollectionContext: CategoryCollectionContextFactory = (
functionsData: ReadonlyArray<FunctionData> | undefined,
language: ScriptingLanguage,
compilerFactory: ScriptCompilerFactory = createScriptCompiler,
) => {
return {
compiler: compilerFactory({
categoryContext: {
functions: functionsData ?? [],
language,
},
}),
language,
};
};

This file was deleted.

18 changes: 9 additions & 9 deletions src/application/Parser/Executable/CategoryParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,31 @@ import { parseDocs, type DocsParser } from './DocumentationParser';
import { parseScript, type ScriptParser } from './Script/ScriptParser';
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from './Validation/ExecutableValidator';
import { ExecutableType } from './Validation/ExecutableType';
import type { CategoryCollectionSpecificUtilities } from './CategoryCollectionSpecificUtilities';
import type { CategoryCollectionContext } from './CategoryCollectionContext';

export const parseCategory: CategoryParser = (
category: CategoryData,
collectionUtilities: CategoryCollectionSpecificUtilities,
collectionContext: CategoryCollectionContext,
categoryUtilities: CategoryParserUtilities = DefaultCategoryParserUtilities,
) => {
return parseCategoryRecursively({
categoryData: category,
collectionUtilities,
collectionContext,
categoryUtilities,
});
};

export interface CategoryParser {
(
category: CategoryData,
collectionUtilities: CategoryCollectionSpecificUtilities,
collectionContext: CategoryCollectionContext,
categoryUtilities?: CategoryParserUtilities,
): Category;
}

interface CategoryParseContext {
readonly categoryData: CategoryData;
readonly collectionUtilities: CategoryCollectionSpecificUtilities;
readonly collectionContext: CategoryCollectionContext;
readonly parentCategory?: CategoryData;
readonly categoryUtilities: CategoryParserUtilities;
}
Expand All @@ -52,7 +52,7 @@ function parseCategoryRecursively(
children,
parent: context.categoryData,
categoryUtilities: context.categoryUtilities,
collectionUtilities: context.collectionUtilities,
collectionContext: context.collectionContext,
});
}
try {
Expand Down Expand Up @@ -104,7 +104,7 @@ interface ExecutableParseContext {
readonly data: ExecutableData;
readonly children: CategoryChildren;
readonly parent: CategoryData;
readonly collectionUtilities: CategoryCollectionSpecificUtilities;
readonly collectionContext: CategoryCollectionContext;
readonly categoryUtilities: CategoryParserUtilities;
}

Expand All @@ -124,13 +124,13 @@ function parseUnknownExecutable(context: ExecutableParseContext) {
if (isCategory(context.data)) {
const subCategory = parseCategoryRecursively({
categoryData: context.data,
collectionUtilities: context.collectionUtilities,
collectionContext: context.collectionContext,
parentCategory: context.parent,
categoryUtilities: context.categoryUtilities,
});
context.children.subcategories.push(subCategory);
} else { // A script
const script = context.categoryUtilities.parseScript(context.data, context.collectionUtilities);
const script = context.categoryUtilities.parseScript(context.data, context.collectionContext);
context.children.subscripts.push(script);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@ import type {
FunctionData, CodeInstruction, CodeFunctionData, CallFunctionData,
CallInstruction, ParameterDefinitionData,
} from '@/application/collections/';
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
import { CodeValidator } from '@/application/Parser/Executable/Script/Validation/CodeValidator';
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
import { NoDuplicatedLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoDuplicatedLines';
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
import { validateCode, type CodeValidator } from '@/application/Parser/Executable/Script/Validation/CodeValidator';
import { isArray, isNullOrUndefined, isPlainObject } from '@/TypeHelpers';
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import { filterEmptyStrings } from '@/application/Common/Text/FilterEmptyStrings';
import type { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { CodeValidationRule } from '@/application/Parser/Executable/Script/Validation/CodeValidationRule';
import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
import { SharedFunctionCollection } from './SharedFunctionCollection';
import { parseFunctionCalls, type FunctionCallsParser } from './Call/FunctionCallsParser';
Expand All @@ -23,14 +21,14 @@ import type { ISharedFunction } from './ISharedFunction';
export interface SharedFunctionsParser {
(
functions: readonly FunctionData[],
syntax: ILanguageSyntax,
language: ScriptingLanguage,
utilities?: SharedFunctionsParsingUtilities,
): ISharedFunctionCollection;
}

export const parseSharedFunctions: SharedFunctionsParser = (
functions: readonly FunctionData[],
syntax: ILanguageSyntax,
language: ScriptingLanguage,
utilities = DefaultUtilities,
) => {
const collection = new SharedFunctionCollection();
Expand All @@ -39,7 +37,7 @@ export const parseSharedFunctions: SharedFunctionsParser = (
}
ensureValidFunctions(functions);
return functions
.map((func) => parseFunction(func, syntax, utilities))
.map((func) => parseFunction(func, language, utilities))
.reduce((acc, func) => {
acc.addFunction(func);
return acc;
Expand All @@ -49,45 +47,49 @@ export const parseSharedFunctions: SharedFunctionsParser = (
const DefaultUtilities: SharedFunctionsParsingUtilities = {
wrapError: wrapErrorWithAdditionalContext,
parseParameter: parseFunctionParameter,
codeValidator: CodeValidator.instance,
codeValidator: validateCode,
createParameterCollection: createFunctionParameterCollection,
parseFunctionCalls,
};

interface SharedFunctionsParsingUtilities {
readonly wrapError: ErrorWithContextWrapper;
readonly parseParameter: FunctionParameterParser;
readonly codeValidator: ICodeValidator;
readonly codeValidator: CodeValidator;
readonly createParameterCollection: FunctionParameterCollectionFactory;
readonly parseFunctionCalls: FunctionCallsParser;
}

function parseFunction(
data: FunctionData,
syntax: ILanguageSyntax,
language: ScriptingLanguage,
utilities: SharedFunctionsParsingUtilities,
): ISharedFunction {
const { name } = data;
const parameters = parseParameters(data, utilities);
if (hasCode(data)) {
validateCode(data, syntax, utilities.codeValidator);
validateNonEmptyCode(data, language, utilities.codeValidator);
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
}
// Has call
const calls = utilities.parseFunctionCalls(data.call);
return createCallerFunction(name, parameters, calls);
}

function validateCode(
function validateNonEmptyCode(
data: CodeFunctionData,
syntax: ILanguageSyntax,
validator: ICodeValidator,
language: ScriptingLanguage,
validate: CodeValidator,
): void {
filterEmptyStrings([data.code, data.revertCode])
.forEach(
(code) => validator.throwIfInvalid(
(code) => validate(
code,
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
language,
[
CodeValidationRule.NoEmptyLines,
CodeValidationRule.NoDuplicatedLines,
],
),
);
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,86 +1,7 @@
import type { FunctionData, ScriptData, CallInstruction } from '@/application/collections/';
import type { ScriptData } from '@/application/collections/';
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
import { CodeValidator } from '@/application/Parser/Executable/Script/Validation/CodeValidator';
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import { createScriptCode, type ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
import { filterEmptyStrings } from '@/application/Common/Text/FilterEmptyStrings';
import { FunctionCallSequenceCompiler } from './Function/Call/Compiler/FunctionCallSequenceCompiler';
import { parseFunctionCalls } from './Function/Call/FunctionCallsParser';
import { parseSharedFunctions, type SharedFunctionsParser } from './Function/SharedFunctionsParser';
import type { CompiledCode } from './Function/Call/Compiler/CompiledCode';
import type { IScriptCompiler } from './IScriptCompiler';
import type { ISharedFunctionCollection } from './Function/ISharedFunctionCollection';
import type { FunctionCallCompiler } from './Function/Call/Compiler/FunctionCallCompiler';

interface ScriptCompilerUtilities {
readonly sharedFunctionsParser: SharedFunctionsParser;
readonly callCompiler: FunctionCallCompiler;
readonly codeValidator: ICodeValidator;
readonly wrapError: ErrorWithContextWrapper;
readonly scriptCodeFactory: ScriptCodeFactory;
}

const DefaultUtilities: ScriptCompilerUtilities = {
sharedFunctionsParser: parseSharedFunctions,
callCompiler: FunctionCallSequenceCompiler.instance,
codeValidator: CodeValidator.instance,
wrapError: wrapErrorWithAdditionalContext,
scriptCodeFactory: createScriptCode,
};

interface CategoryCollectionDataContext {
readonly functions: readonly FunctionData[];
readonly syntax: ILanguageSyntax;
}

export class ScriptCompiler implements IScriptCompiler {
private readonly functions: ISharedFunctionCollection;

constructor(
categoryContext: CategoryCollectionDataContext,
private readonly utilities: ScriptCompilerUtilities = DefaultUtilities,
) {
this.functions = this.utilities.sharedFunctionsParser(
categoryContext.functions,
categoryContext.syntax,
);
}

public canCompile(script: ScriptData): boolean {
return hasCall(script);
}

public compile(script: ScriptData): ScriptCode {
try {
if (!hasCall(script)) {
throw new Error('Script does include any calls.');
}
const calls = parseFunctionCalls(script.call);
const compiledCode = this.utilities.callCompiler.compileFunctionCalls(calls, this.functions);
validateCompiledCode(compiledCode, this.utilities.codeValidator);
return this.utilities.scriptCodeFactory(
compiledCode.code,
compiledCode.revertCode,
);
} catch (error) {
throw this.utilities.wrapError(error, `Failed to compile script: ${script.name}`);
}
}
}

function validateCompiledCode(compiledCode: CompiledCode, validator: ICodeValidator): void {
filterEmptyStrings([compiledCode.code, compiledCode.revertCode])
.forEach(
(code) => validator.throwIfInvalid(
code,
[new NoEmptyLines()],
),
);
}

function hasCall(data: ScriptData): data is ScriptData & CallInstruction {
return (data as CallInstruction).call !== undefined;
export interface ScriptCompiler {
canCompile(script: ScriptData): boolean;
compile(script: ScriptData): ScriptCode;
}
Loading

0 comments on commit dc5c873

Please sign in to comment.