Improve context for errors thrown by compiler
This commit introduces a custom error object to provide additional context for errors throwing during parsing and compiling operations, improving troubleshooting. By integrating error context handling, the error messages become more informative and user-friendly, providing sequence of trace with context to aid in troubleshooting. Changes include: - Introduce custom error object that extends errors with contextual information. This replaces previous usages of `AggregateError` which is not displayed well by browsers when logged. - Improve parsing functions to encapsulate error context with more details. - Increase unit test coverage and refactor the related code to be more testable.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { isFunction } from '@/TypeHelpers';
|
||||
import { isFunction, type ConstructorArguments } from '@/TypeHelpers';
|
||||
|
||||
/*
|
||||
Provides a unified and resilient way to extend errors across platforms.
|
||||
@@ -12,8 +12,8 @@ import { isFunction } from '@/TypeHelpers';
|
||||
> https://web.archive.org/web/20230810014143/https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
|
||||
*/
|
||||
export abstract class CustomError extends Error {
|
||||
constructor(message?: string, options?: ErrorOptions) {
|
||||
super(message, options);
|
||||
constructor(...args: ConstructorArguments<typeof Error>) {
|
||||
super(...args);
|
||||
|
||||
fixPrototype(this, new.target.prototype);
|
||||
ensureStackTrace(this);
|
||||
|
||||
@@ -3,10 +3,12 @@ import type {
|
||||
} from '@/application/collections/';
|
||||
import { Script } from '@/domain/Script';
|
||||
import { Category } from '@/domain/Category';
|
||||
import { NodeValidator } from '@/application/Parser/NodeValidation/NodeValidator';
|
||||
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
|
||||
import { parseDocs } from './DocumentationParser';
|
||||
import { parseScript } from './Script/ScriptParser';
|
||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||
import type { ICategory } from '@/domain/ICategory';
|
||||
import { parseDocs, type DocsParser } from './DocumentationParser';
|
||||
import { parseScript, type ScriptParser } from './Script/ScriptParser';
|
||||
import { createNodeDataValidator, type NodeDataValidator, type NodeDataValidatorFactory } from './NodeValidation/NodeDataValidator';
|
||||
import { NodeDataType } from './NodeValidation/NodeDataType';
|
||||
import type { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
|
||||
|
||||
let categoryIdCounter = 0;
|
||||
@@ -14,96 +16,108 @@ let categoryIdCounter = 0;
|
||||
export function parseCategory(
|
||||
category: CategoryData,
|
||||
context: ICategoryCollectionParseContext,
|
||||
factory: CategoryFactoryType = CategoryFactory,
|
||||
utilities: CategoryParserUtilities = DefaultCategoryParserUtilities,
|
||||
): Category {
|
||||
return parseCategoryRecursively({
|
||||
categoryData: category,
|
||||
context,
|
||||
factory,
|
||||
utilities,
|
||||
});
|
||||
}
|
||||
|
||||
interface ICategoryParseContext {
|
||||
readonly categoryData: CategoryData,
|
||||
readonly context: ICategoryCollectionParseContext,
|
||||
readonly factory: CategoryFactoryType,
|
||||
readonly parentCategory?: CategoryData,
|
||||
interface CategoryParseContext {
|
||||
readonly categoryData: CategoryData;
|
||||
readonly context: ICategoryCollectionParseContext;
|
||||
readonly parentCategory?: CategoryData;
|
||||
readonly utilities: CategoryParserUtilities;
|
||||
}
|
||||
|
||||
function parseCategoryRecursively(context: ICategoryParseContext): Category | never {
|
||||
ensureValidCategory(context.categoryData, context.parentCategory);
|
||||
const children: ICategoryChildren = {
|
||||
subCategories: new Array<Category>(),
|
||||
subScripts: new Array<Script>(),
|
||||
function parseCategoryRecursively(
|
||||
context: CategoryParseContext,
|
||||
): Category | never {
|
||||
const validator = ensureValidCategory(context);
|
||||
const children: CategoryChildren = {
|
||||
subcategories: new Array<Category>(),
|
||||
subscripts: new Array<Script>(),
|
||||
};
|
||||
for (const data of context.categoryData.children) {
|
||||
parseNode({
|
||||
nodeData: data,
|
||||
children,
|
||||
parent: context.categoryData,
|
||||
factory: context.factory,
|
||||
utilities: context.utilities,
|
||||
context: context.context,
|
||||
});
|
||||
}
|
||||
try {
|
||||
return context.factory(
|
||||
/* id: */ categoryIdCounter++,
|
||||
/* name: */ context.categoryData.category,
|
||||
/* docs: */ parseDocs(context.categoryData),
|
||||
/* categories: */ children.subCategories,
|
||||
/* scripts: */ children.subScripts,
|
||||
return context.utilities.createCategory({
|
||||
id: categoryIdCounter++,
|
||||
name: context.categoryData.category,
|
||||
docs: context.utilities.parseDocs(context.categoryData),
|
||||
subcategories: children.subcategories,
|
||||
scripts: children.subscripts,
|
||||
});
|
||||
} catch (error) {
|
||||
throw context.utilities.wrapError(
|
||||
error,
|
||||
validator.createContextualErrorMessage('Failed to parse category.'),
|
||||
);
|
||||
} catch (err) {
|
||||
return new NodeValidator({
|
||||
type: NodeType.Category,
|
||||
selfNode: context.categoryData,
|
||||
parentNode: context.parentCategory,
|
||||
}).throw(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureValidCategory(category: CategoryData, parentCategory?: CategoryData) {
|
||||
new NodeValidator({
|
||||
type: NodeType.Category,
|
||||
selfNode: category,
|
||||
parentNode: parentCategory,
|
||||
})
|
||||
.assertDefined(category)
|
||||
.assertValidName(category.category)
|
||||
.assert(
|
||||
() => category.children.length > 0,
|
||||
`"${category.category}" has no children.`,
|
||||
);
|
||||
function ensureValidCategory(
|
||||
context: CategoryParseContext,
|
||||
): NodeDataValidator {
|
||||
const category = context.categoryData;
|
||||
const validator: NodeDataValidator = context.utilities.createValidator({
|
||||
type: NodeDataType.Category,
|
||||
selfNode: context.categoryData,
|
||||
parentNode: context.parentCategory,
|
||||
});
|
||||
validator.assertDefined(category);
|
||||
validator.assertValidName(category.category);
|
||||
validator.assert(
|
||||
() => Boolean(category.children) && category.children.length > 0,
|
||||
`"${category.category}" has no children.`,
|
||||
);
|
||||
return validator;
|
||||
}
|
||||
|
||||
interface ICategoryChildren {
|
||||
subCategories: Category[];
|
||||
subScripts: Script[];
|
||||
interface CategoryChildren {
|
||||
readonly subcategories: Category[];
|
||||
readonly subscripts: Script[];
|
||||
}
|
||||
|
||||
interface INodeParseContext {
|
||||
interface NodeParseContext {
|
||||
readonly nodeData: CategoryOrScriptData;
|
||||
readonly children: ICategoryChildren;
|
||||
readonly children: CategoryChildren;
|
||||
readonly parent: CategoryData;
|
||||
readonly factory: CategoryFactoryType;
|
||||
readonly context: ICategoryCollectionParseContext;
|
||||
|
||||
readonly utilities: CategoryParserUtilities;
|
||||
}
|
||||
function parseNode(context: INodeParseContext) {
|
||||
const validator = new NodeValidator({ selfNode: context.nodeData, parentNode: context.parent });
|
||||
|
||||
function parseNode(context: NodeParseContext) {
|
||||
const validator: NodeDataValidator = context.utilities.createValidator({
|
||||
selfNode: context.nodeData,
|
||||
parentNode: context.parent,
|
||||
});
|
||||
validator.assertDefined(context.nodeData);
|
||||
validator.assert(
|
||||
() => isCategory(context.nodeData) || isScript(context.nodeData),
|
||||
'Node is neither a category or a script.',
|
||||
);
|
||||
if (isCategory(context.nodeData)) {
|
||||
const subCategory = parseCategoryRecursively({
|
||||
categoryData: context.nodeData,
|
||||
context: context.context,
|
||||
factory: context.factory,
|
||||
parentCategory: context.parent,
|
||||
utilities: context.utilities,
|
||||
});
|
||||
context.children.subCategories.push(subCategory);
|
||||
} else if (isScript(context.nodeData)) {
|
||||
const script = parseScript(context.nodeData, context.context);
|
||||
context.children.subScripts.push(script);
|
||||
} else {
|
||||
validator.throw('Node is neither a category or a script.');
|
||||
context.children.subcategories.push(subCategory);
|
||||
} else { // A script
|
||||
const script = context.utilities.parseScript(context.nodeData, context.context);
|
||||
context.children.subscripts.push(script);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,11 +137,35 @@ function hasCall(data: unknown) {
|
||||
return hasProperty(data, 'call');
|
||||
}
|
||||
|
||||
function hasProperty(object: unknown, propertyName: string) {
|
||||
function hasProperty(
|
||||
object: unknown,
|
||||
propertyName: string,
|
||||
): object is NonNullable<object> {
|
||||
if (typeof object !== 'object') {
|
||||
return false;
|
||||
}
|
||||
if (object === null) { // `typeof object` is `null`
|
||||
return false;
|
||||
}
|
||||
return Object.prototype.hasOwnProperty.call(object, propertyName);
|
||||
}
|
||||
|
||||
export type CategoryFactoryType = (
|
||||
...parameters: ConstructorParameters<typeof Category>) => Category;
|
||||
export type CategoryFactory = (
|
||||
...parameters: ConstructorParameters<typeof Category>
|
||||
) => ICategory;
|
||||
|
||||
const CategoryFactory: CategoryFactoryType = (...parameters) => new Category(...parameters);
|
||||
interface CategoryParserUtilities {
|
||||
readonly createCategory: CategoryFactory;
|
||||
readonly wrapError: ErrorWithContextWrapper;
|
||||
readonly createValidator: NodeDataValidatorFactory;
|
||||
readonly parseScript: ScriptParser;
|
||||
readonly parseDocs: DocsParser;
|
||||
}
|
||||
|
||||
const DefaultCategoryParserUtilities: CategoryParserUtilities = {
|
||||
createCategory: (...parameters) => new Category(...parameters),
|
||||
wrapError: wrapErrorWithAdditionalContext,
|
||||
createValidator: createNodeDataValidator,
|
||||
parseScript,
|
||||
parseDocs,
|
||||
};
|
||||
|
||||
42
src/application/Parser/ContextualError.ts
Normal file
42
src/application/Parser/ContextualError.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { CustomError } from '@/application/Common/CustomError';
|
||||
|
||||
export interface ErrorWithContextWrapper {
|
||||
(
|
||||
error: Error,
|
||||
additionalContext: string,
|
||||
): Error;
|
||||
}
|
||||
|
||||
export const wrapErrorWithAdditionalContext: ErrorWithContextWrapper = (
|
||||
error: Error,
|
||||
additionalContext: string,
|
||||
) => {
|
||||
return (error instanceof ContextualError ? error : new ContextualError(error))
|
||||
.withAdditionalContext(additionalContext);
|
||||
};
|
||||
|
||||
/* AggregateError is similar but isn't well-serialized or displayed by browsers */
|
||||
class ContextualError extends CustomError {
|
||||
private readonly additionalContext = new Array<string>();
|
||||
|
||||
constructor(
|
||||
public readonly innerError: Error,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public withAdditionalContext(additionalContext: string): this {
|
||||
this.additionalContext.push(additionalContext);
|
||||
return this;
|
||||
}
|
||||
|
||||
public get message(): string { // toString() is not used when Chromium logs it on console
|
||||
return [
|
||||
'\n',
|
||||
this.innerError.message,
|
||||
'\n',
|
||||
'Additional context:',
|
||||
...this.additionalContext.map((context, index) => `${index + 1}: ${context}`),
|
||||
].join('\n');
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { DocumentableData, DocumentationData } from '@/application/collections/';
|
||||
import { isString, isArray } from '@/TypeHelpers';
|
||||
|
||||
export function parseDocs(documentable: DocumentableData): readonly string[] {
|
||||
export const parseDocs: DocsParser = (documentable) => {
|
||||
const { docs } = documentable;
|
||||
if (!docs) {
|
||||
return [];
|
||||
@@ -9,6 +9,12 @@ export function parseDocs(documentable: DocumentableData): readonly string[] {
|
||||
let result = new DocumentationContainer();
|
||||
result = addDocs(docs, result);
|
||||
return result.getAll();
|
||||
};
|
||||
|
||||
export interface DocsParser {
|
||||
(
|
||||
documentable: DocumentableData,
|
||||
): readonly string[];
|
||||
}
|
||||
|
||||
function addDocs(
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { CustomError } from '@/application/Common/CustomError';
|
||||
import { NodeType } from './NodeType';
|
||||
import type { NodeData } from './NodeData';
|
||||
|
||||
export class NodeDataError extends CustomError {
|
||||
constructor(message: string, public readonly context: INodeDataErrorContext) {
|
||||
super(createMessage(message, context));
|
||||
}
|
||||
}
|
||||
|
||||
export interface INodeDataErrorContext {
|
||||
readonly type?: NodeType;
|
||||
readonly selfNode: NodeData;
|
||||
readonly parentNode?: NodeData;
|
||||
}
|
||||
|
||||
function createMessage(errorMessage: string, context: INodeDataErrorContext) {
|
||||
let message = '';
|
||||
if (context.type !== undefined) {
|
||||
message += `${NodeType[context.type]}: `;
|
||||
}
|
||||
message += errorMessage;
|
||||
message += `\n${dump(context)}`;
|
||||
return message;
|
||||
}
|
||||
|
||||
function dump(context: INodeDataErrorContext): string {
|
||||
const printJson = (obj: unknown) => JSON.stringify(obj, undefined, 2);
|
||||
let output = `Self: ${printJson(context.selfNode)}`;
|
||||
if (context.parentNode) {
|
||||
output += `\nParent: ${printJson(context.parentNode)}`;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { CategoryData, ScriptData } from '@/application/collections/';
|
||||
import { NodeDataType } from './NodeDataType';
|
||||
import type { NodeData } from './NodeData';
|
||||
|
||||
export type NodeDataErrorContext = {
|
||||
readonly parentNode?: CategoryData;
|
||||
} & (CategoryNodeErrorContext | ScriptNodeErrorContext | UnknownNodeErrorContext);
|
||||
|
||||
export type CategoryNodeErrorContext = {
|
||||
readonly type: NodeDataType.Category;
|
||||
readonly selfNode: CategoryData;
|
||||
readonly parentNode?: CategoryData;
|
||||
};
|
||||
|
||||
export type ScriptNodeErrorContext = {
|
||||
readonly type: NodeDataType.Script;
|
||||
readonly selfNode: ScriptData;
|
||||
readonly parentNode?: CategoryData;
|
||||
};
|
||||
|
||||
export type UnknownNodeErrorContext = {
|
||||
readonly type?: undefined;
|
||||
readonly selfNode: NodeData;
|
||||
readonly parentNode?: CategoryData;
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NodeDataType } from './NodeDataType';
|
||||
import type { NodeDataErrorContext } from './NodeDataErrorContext';
|
||||
import type { NodeData } from './NodeData';
|
||||
|
||||
export interface NodeContextErrorMessageCreator {
|
||||
(
|
||||
errorMessage: string,
|
||||
context: NodeDataErrorContext,
|
||||
): string;
|
||||
}
|
||||
|
||||
export const createNodeContextErrorMessage: NodeContextErrorMessageCreator = (
|
||||
errorMessage,
|
||||
context,
|
||||
) => {
|
||||
let message = '';
|
||||
if (context.type !== undefined) {
|
||||
message += `${NodeDataType[context.type]}: `;
|
||||
}
|
||||
message += errorMessage;
|
||||
message += `\n${getErrorContextDetails(context)}`;
|
||||
return message;
|
||||
};
|
||||
|
||||
function getErrorContextDetails(context: NodeDataErrorContext): string {
|
||||
let output = `Self: ${printNodeDataAsJson(context.selfNode)}`;
|
||||
if (context.parentNode) {
|
||||
output += `\nParent: ${printNodeDataAsJson(context.parentNode)}`;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function printNodeDataAsJson(node: NodeData): string {
|
||||
return JSON.stringify(node, undefined, 2);
|
||||
}
|
||||
4
src/application/Parser/NodeValidation/NodeDataType.ts
Normal file
4
src/application/Parser/NodeValidation/NodeDataType.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum NodeDataType {
|
||||
Script,
|
||||
Category,
|
||||
}
|
||||
69
src/application/Parser/NodeValidation/NodeDataValidator.ts
Normal file
69
src/application/Parser/NodeValidation/NodeDataValidator.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { isString } from '@/TypeHelpers';
|
||||
import { type NodeDataErrorContext } from './NodeDataErrorContext';
|
||||
import { createNodeContextErrorMessage, type NodeContextErrorMessageCreator } from './NodeDataErrorContextMessage';
|
||||
import type { NodeData } from './NodeData';
|
||||
|
||||
export interface NodeDataValidatorFactory {
|
||||
(context: NodeDataErrorContext): NodeDataValidator;
|
||||
}
|
||||
|
||||
export interface NodeDataValidator {
|
||||
assertValidName(nameValue: string): void;
|
||||
assertDefined(
|
||||
node: NodeData | undefined,
|
||||
): asserts node is NonNullable<NodeData> & void;
|
||||
assert(
|
||||
validationPredicate: () => boolean,
|
||||
errorMessage: string,
|
||||
): asserts validationPredicate is (() => true);
|
||||
createContextualErrorMessage(errorMessage: string): string;
|
||||
}
|
||||
|
||||
export const createNodeDataValidator
|
||||
: NodeDataValidatorFactory = (context) => new ContextualNodeDataValidator(context);
|
||||
|
||||
export class ContextualNodeDataValidator implements NodeDataValidator {
|
||||
constructor(
|
||||
private readonly context: NodeDataErrorContext,
|
||||
private readonly createErrorMessage
|
||||
: NodeContextErrorMessageCreator = createNodeContextErrorMessage,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
public assertValidName(nameValue: string): void {
|
||||
this.assert(() => Boolean(nameValue), 'missing name');
|
||||
this.assert(
|
||||
() => isString(nameValue),
|
||||
`Name (${JSON.stringify(nameValue)}) is not a string but ${typeof nameValue}.`,
|
||||
);
|
||||
}
|
||||
|
||||
public assertDefined(
|
||||
node: NodeData,
|
||||
): asserts node is NonNullable<NodeData> {
|
||||
this.assert(
|
||||
() => node !== undefined && node !== null && Object.keys(node).length > 0,
|
||||
'missing node data',
|
||||
);
|
||||
}
|
||||
|
||||
public assert(
|
||||
validationPredicate: () => boolean,
|
||||
errorMessage: string,
|
||||
): asserts validationPredicate is (() => true) {
|
||||
if (!validationPredicate()) {
|
||||
this.throw(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public createContextualErrorMessage(errorMessage: string): string {
|
||||
return this.createErrorMessage(errorMessage, this.context);
|
||||
}
|
||||
|
||||
private throw(errorMessage: string): never {
|
||||
throw new Error(
|
||||
this.createContextualErrorMessage(errorMessage),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export enum NodeType {
|
||||
Script,
|
||||
Category,
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { isString } from '@/TypeHelpers';
|
||||
import { type INodeDataErrorContext, NodeDataError } from './NodeDataError';
|
||||
import type { NodeData } from './NodeData';
|
||||
|
||||
export class NodeValidator {
|
||||
constructor(private readonly context: INodeDataErrorContext) {
|
||||
|
||||
}
|
||||
|
||||
public assertValidName(nameValue: string) {
|
||||
return this
|
||||
.assert(
|
||||
() => Boolean(nameValue),
|
||||
'missing name',
|
||||
)
|
||||
.assert(
|
||||
() => isString(nameValue),
|
||||
`Name (${JSON.stringify(nameValue)}) is not a string but ${typeof nameValue}.`,
|
||||
);
|
||||
}
|
||||
|
||||
public assertDefined(node: NodeData) {
|
||||
return this.assert(
|
||||
() => node !== undefined && node !== null && Object.keys(node).length > 0,
|
||||
'missing node data',
|
||||
);
|
||||
}
|
||||
|
||||
public assert(validationPredicate: () => boolean, errorMessage: string) {
|
||||
if (!validationPredicate()) {
|
||||
this.throw(errorMessage);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public throw(errorMessage: string): never {
|
||||
throw new NodeDataError(errorMessage, this.context);
|
||||
}
|
||||
}
|
||||
@@ -7,15 +7,18 @@ import type { IReadOnlyFunctionParameterCollection } from '../../Function/Parame
|
||||
import type { IExpression } from './IExpression';
|
||||
|
||||
export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string;
|
||||
|
||||
export class Expression implements IExpression {
|
||||
public readonly parameters: IReadOnlyFunctionParameterCollection;
|
||||
|
||||
constructor(
|
||||
public readonly position: ExpressionPosition,
|
||||
public readonly evaluator: ExpressionEvaluator,
|
||||
parameters?: IReadOnlyFunctionParameterCollection,
|
||||
) {
|
||||
this.parameters = parameters ?? new FunctionParameterCollection();
|
||||
public readonly position: ExpressionPosition;
|
||||
|
||||
public readonly evaluator: ExpressionEvaluator;
|
||||
|
||||
constructor(parameters: ExpressionInitParameters) {
|
||||
this.parameters = parameters.parameters ?? new FunctionParameterCollection();
|
||||
this.evaluator = parameters.evaluator;
|
||||
this.position = parameters.position;
|
||||
}
|
||||
|
||||
public evaluate(context: IExpressionEvaluationContext): string {
|
||||
@@ -26,6 +29,12 @@ export class Expression implements IExpression {
|
||||
}
|
||||
}
|
||||
|
||||
export interface ExpressionInitParameters {
|
||||
readonly position: ExpressionPosition,
|
||||
readonly evaluator: ExpressionEvaluator,
|
||||
readonly parameters?: IReadOnlyFunctionParameterCollection,
|
||||
}
|
||||
|
||||
function validateThatAllRequiredParametersAreSatisfied(
|
||||
parameters: IReadOnlyFunctionParameterCollection,
|
||||
args: IReadOnlyFunctionCallArgumentCollection,
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { ExpressionPosition } from './ExpressionPosition';
|
||||
|
||||
export function createPositionFromRegexFullMatch(
|
||||
match: RegExpMatchArray,
|
||||
): ExpressionPosition {
|
||||
export interface ExpressionPositionFactory {
|
||||
(
|
||||
match: RegExpMatchArray,
|
||||
): ExpressionPosition
|
||||
}
|
||||
|
||||
export const createPositionFromRegexFullMatch
|
||||
: ExpressionPositionFactory = (match) => {
|
||||
const startPos = match.index;
|
||||
if (startPos === undefined) {
|
||||
throw new Error(`Regex match did not yield any results: ${JSON.stringify(match)}`);
|
||||
@@ -13,4 +18,4 @@ export function createPositionFromRegexFullMatch(
|
||||
}
|
||||
const endPos = startPos + fullMatch.length;
|
||||
return new ExpressionPosition(startPos, endPos);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,10 +3,10 @@ import { WithParser } from '../SyntaxParsers/WithParser';
|
||||
import type { IExpression } from '../Expression/IExpression';
|
||||
import type { IExpressionParser } from './IExpressionParser';
|
||||
|
||||
const Parsers = [
|
||||
const Parsers: readonly IExpressionParser[] = [
|
||||
new ParameterSubstitutionParser(),
|
||||
new WithParser(),
|
||||
];
|
||||
] as const;
|
||||
|
||||
export class CompositeExpressionParser implements IExpressionParser {
|
||||
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
|
||||
|
||||
@@ -1,53 +1,127 @@
|
||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||
import { Expression, type ExpressionEvaluator } from '../../Expression/Expression';
|
||||
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
|
||||
import { createPositionFromRegexFullMatch } from '../../Expression/ExpressionPositionFactory';
|
||||
import { createPositionFromRegexFullMatch, type ExpressionPositionFactory } from '../../Expression/ExpressionPositionFactory';
|
||||
import { createFunctionParameterCollection, type FunctionParameterCollectionFactory } from '../../../Function/Parameter/FunctionParameterCollectionFactory';
|
||||
import type { IExpressionParser } from '../IExpressionParser';
|
||||
import type { IExpression } from '../../Expression/IExpression';
|
||||
import type { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter';
|
||||
import type { IFunctionParameterCollection, IReadOnlyFunctionParameterCollection } from '../../../Function/Parameter/IFunctionParameterCollection';
|
||||
|
||||
export interface RegexParserUtilities {
|
||||
readonly wrapError: ErrorWithContextWrapper;
|
||||
readonly createPosition: ExpressionPositionFactory;
|
||||
readonly createExpression: ExpressionFactory;
|
||||
readonly createParameterCollection: FunctionParameterCollectionFactory;
|
||||
}
|
||||
|
||||
export abstract class RegexParser implements IExpressionParser {
|
||||
protected abstract readonly regex: RegExp;
|
||||
|
||||
public constructor(
|
||||
private readonly utilities: RegexParserUtilities = DefaultRegexParserUtilities,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
public findExpressions(code: string): IExpression[] {
|
||||
return Array.from(this.findRegexExpressions(code));
|
||||
}
|
||||
|
||||
protected abstract buildExpression(match: RegExpMatchArray): IPrimitiveExpression;
|
||||
protected abstract buildExpression(match: RegExpMatchArray): PrimitiveExpression;
|
||||
|
||||
private* findRegexExpressions(code: string): Iterable<IExpression> {
|
||||
if (!code) {
|
||||
throw new Error('missing code');
|
||||
throw new Error(
|
||||
this.buildErrorMessageWithContext({ errorMessage: 'missing code', code: 'EMPTY' }),
|
||||
);
|
||||
}
|
||||
const matches = code.matchAll(this.regex);
|
||||
const createErrorContext = (message: string): ErrorContext => ({ code, errorMessage: message });
|
||||
const matches = this.doOrRethrow(
|
||||
() => code.matchAll(this.regex),
|
||||
createErrorContext('Failed to match regex.'),
|
||||
);
|
||||
for (const match of matches) {
|
||||
const primitiveExpression = this.buildExpression(match);
|
||||
const position = this.doOrRethrow(() => createPositionFromRegexFullMatch(match), 'invalid script position', code);
|
||||
const parameters = createParameters(primitiveExpression);
|
||||
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
|
||||
const primitiveExpression = this.doOrRethrow(
|
||||
() => this.buildExpression(match),
|
||||
createErrorContext('Failed to build expression.'),
|
||||
);
|
||||
const position = this.doOrRethrow(
|
||||
() => this.utilities.createPosition(match),
|
||||
createErrorContext('Failed to create position.'),
|
||||
);
|
||||
const parameters = this.doOrRethrow(
|
||||
() => createParameters(
|
||||
primitiveExpression,
|
||||
this.utilities.createParameterCollection(),
|
||||
),
|
||||
createErrorContext('Failed to create parameters.'),
|
||||
);
|
||||
const expression = this.doOrRethrow(
|
||||
() => this.utilities.createExpression({
|
||||
position,
|
||||
evaluator: primitiveExpression.evaluator,
|
||||
parameters,
|
||||
}),
|
||||
createErrorContext('Failed to create expression.'),
|
||||
);
|
||||
yield expression;
|
||||
}
|
||||
}
|
||||
|
||||
private doOrRethrow<T>(action: () => T, errorText: string, code: string): T {
|
||||
private doOrRethrow<T>(
|
||||
action: () => T,
|
||||
context: ErrorContext,
|
||||
): T {
|
||||
try {
|
||||
return action();
|
||||
} catch (error) {
|
||||
throw new Error(`[${this.constructor.name}] ${errorText}: ${error.message}\nRegex: ${this.regex}\nCode: ${code}`);
|
||||
throw this.utilities.wrapError(
|
||||
error,
|
||||
this.buildErrorMessageWithContext(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private buildErrorMessageWithContext(context: ErrorContext): string {
|
||||
return [
|
||||
context.errorMessage,
|
||||
`Class name: ${this.constructor.name}`,
|
||||
`Regex pattern used: ${this.regex}`,
|
||||
`Code: ${context.code}`,
|
||||
].join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
interface ErrorContext {
|
||||
readonly errorMessage: string,
|
||||
readonly code: string,
|
||||
}
|
||||
|
||||
function createParameters(
|
||||
expression: IPrimitiveExpression,
|
||||
): FunctionParameterCollection {
|
||||
expression: PrimitiveExpression,
|
||||
parameterCollection: IFunctionParameterCollection,
|
||||
): IReadOnlyFunctionParameterCollection {
|
||||
return (expression.parameters || [])
|
||||
.reduce((parameters, parameter) => {
|
||||
parameters.addParameter(parameter);
|
||||
return parameters;
|
||||
}, new FunctionParameterCollection());
|
||||
}, parameterCollection);
|
||||
}
|
||||
|
||||
export interface IPrimitiveExpression {
|
||||
evaluator: ExpressionEvaluator;
|
||||
parameters?: readonly IFunctionParameter[];
|
||||
export interface PrimitiveExpression {
|
||||
readonly evaluator: ExpressionEvaluator;
|
||||
readonly parameters?: readonly IFunctionParameter[];
|
||||
}
|
||||
|
||||
export interface ExpressionFactory {
|
||||
(
|
||||
...args: ConstructorParameters<typeof Expression>
|
||||
): IExpression;
|
||||
}
|
||||
|
||||
const DefaultRegexParserUtilities: RegexParserUtilities = {
|
||||
wrapError: wrapErrorWithAdditionalContext,
|
||||
createPosition: createPositionFromRegexFullMatch,
|
||||
createExpression: (...args) => new Expression(...args),
|
||||
createParameterCollection: createFunctionParameterCollection,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||
import { RegexParser, type IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
||||
import { RegexParser, type PrimitiveExpression } from '../Parser/Regex/RegexParser';
|
||||
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
||||
|
||||
export class ParameterSubstitutionParser extends RegexParser {
|
||||
@@ -12,7 +12,7 @@ export class ParameterSubstitutionParser extends RegexParser {
|
||||
.expectExpressionEnd()
|
||||
.buildRegExp();
|
||||
|
||||
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
||||
protected buildExpression(match: RegExpMatchArray): PrimitiveExpression {
|
||||
const parameterName = match[1];
|
||||
const pipeline = match[2];
|
||||
return {
|
||||
|
||||
@@ -8,7 +8,7 @@ export class FunctionCallArgument implements IFunctionCallArgument {
|
||||
) {
|
||||
ensureValidParameterName(parameterName);
|
||||
if (!argumentValue) {
|
||||
throw new Error(`missing argument value for "${parameterName}"`);
|
||||
throw new Error(`Missing argument value for the parameter "${parameterName}".`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ function throwIfUnexpectedParametersExist(
|
||||
// eslint-disable-next-line prefer-template
|
||||
`Function "${functionName}" has unexpected parameter(s) provided: `
|
||||
+ `"${unexpectedParameters.join('", "')}"`
|
||||
+ '. Expected parameter(s): '
|
||||
+ (expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
|
||||
+ '.\nExpected parameter(s): '
|
||||
+ (expectedParameters.length ? `"${expectedParameters.join('", "')}".` : 'none'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,11 +6,14 @@ import type { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/
|
||||
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||
import type { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
||||
import { ParsedFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall';
|
||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||
import type { ArgumentCompiler } from './ArgumentCompiler';
|
||||
|
||||
export class NestedFunctionArgumentCompiler implements ArgumentCompiler {
|
||||
constructor(
|
||||
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
|
||||
private readonly wrapError: ErrorWithContextWrapper
|
||||
= wrapErrorWithAdditionalContext,
|
||||
) { }
|
||||
|
||||
public createCompiledNestedCall(
|
||||
@@ -22,18 +25,26 @@ export class NestedFunctionArgumentCompiler implements ArgumentCompiler {
|
||||
nestedFunction,
|
||||
parentFunction.args,
|
||||
context,
|
||||
this.expressionsCompiler,
|
||||
{
|
||||
expressionsCompiler: this.expressionsCompiler,
|
||||
wrapError: this.wrapError,
|
||||
},
|
||||
);
|
||||
const compiledCall = new ParsedFunctionCall(nestedFunction.functionName, compiledArgs);
|
||||
return compiledCall;
|
||||
}
|
||||
}
|
||||
|
||||
interface ArgumentCompilationUtilities {
|
||||
readonly expressionsCompiler: IExpressionsCompiler,
|
||||
readonly wrapError: ErrorWithContextWrapper;
|
||||
}
|
||||
|
||||
function compileNestedFunctionArguments(
|
||||
nestedFunction: FunctionCall,
|
||||
parentFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||
context: FunctionCallCompilationContext,
|
||||
expressionsCompiler: IExpressionsCompiler,
|
||||
utilities: ArgumentCompilationUtilities,
|
||||
): IReadOnlyFunctionCallArgumentCollection {
|
||||
const requiredParameterNames = context
|
||||
.allFunctions
|
||||
@@ -47,7 +58,7 @@ function compileNestedFunctionArguments(
|
||||
paramName,
|
||||
nestedFunction,
|
||||
parentFunctionArgs,
|
||||
expressionsCompiler,
|
||||
utilities,
|
||||
),
|
||||
}))
|
||||
// Filter out arguments with absent values
|
||||
@@ -89,13 +100,13 @@ function compileArgument(
|
||||
parameterName: string,
|
||||
nestedFunction: FunctionCall,
|
||||
parentFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||
expressionsCompiler: IExpressionsCompiler,
|
||||
utilities: ArgumentCompilationUtilities,
|
||||
): string {
|
||||
try {
|
||||
const { argumentValue: codeInArgument } = nestedFunction.args.getArgument(parameterName);
|
||||
return expressionsCompiler.compileExpressions(codeInArgument, parentFunctionArgs);
|
||||
} catch (err) {
|
||||
throw new AggregateError([err], `Error when compiling argument for "${parameterName}"`);
|
||||
return utilities.expressionsCompiler.compileExpressions(codeInArgument, parentFunctionArgs);
|
||||
} catch (error) {
|
||||
throw utilities.wrapError(error, `Error when compiling argument for "${parameterName}"`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import { type CallFunctionBody, FunctionBodyType, type ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||
import {
|
||||
type CallFunctionBody, FunctionBodyType,
|
||||
type ISharedFunction,
|
||||
} from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||
import type { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
||||
import type { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||
import { NestedFunctionArgumentCompiler } from './Argument/NestedFunctionArgumentCompiler';
|
||||
import type { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
|
||||
import type { ArgumentCompiler } from './Argument/ArgumentCompiler';
|
||||
|
||||
export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
|
||||
public constructor(
|
||||
private readonly argumentCompiler: ArgumentCompiler = new NestedFunctionArgumentCompiler(),
|
||||
private readonly argumentCompiler: ArgumentCompiler
|
||||
= new NestedFunctionArgumentCompiler(),
|
||||
private readonly wrapError: ErrorWithContextWrapper
|
||||
= wrapErrorWithAdditionalContext,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -29,8 +36,11 @@ export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
|
||||
const compiledNestedCall = context.singleCallCompiler
|
||||
.compileSingleCall(compiledParentCall, context);
|
||||
return compiledNestedCall;
|
||||
} catch (err) {
|
||||
throw new AggregateError([err], `Error with call to "${nestedCall.functionName}" function from "${callToFunction.functionName}" function`);
|
||||
} catch (error) {
|
||||
throw this.wrapError(
|
||||
error,
|
||||
`Failed to call '${nestedCall.functionName}' (callee function) from '${callToFunction.functionName}' (caller function).`,
|
||||
);
|
||||
}
|
||||
}).flat();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { FunctionParameterCollection } from './FunctionParameterCollection';
|
||||
import type { IFunctionParameterCollection } from './IFunctionParameterCollection';
|
||||
|
||||
export interface FunctionParameterCollectionFactory {
|
||||
(
|
||||
...args: ConstructorParameters<typeof FunctionParameterCollection>
|
||||
): IFunctionParameterCollection;
|
||||
}
|
||||
|
||||
export const createFunctionParameterCollection: FunctionParameterCollectionFactory = (...args) => {
|
||||
return new FunctionParameterCollection(...args);
|
||||
};
|
||||
@@ -15,7 +15,7 @@ export class SharedFunctionCollection implements ISharedFunctionCollection {
|
||||
if (!name) { throw Error('missing function name'); }
|
||||
const func = this.functionsByName.get(name);
|
||||
if (!func) {
|
||||
throw new Error(`called function is not defined "${name}"`);
|
||||
throw new Error(`Called function is not defined: "${name}"`);
|
||||
}
|
||||
return func;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
FunctionData, CodeInstruction, CodeFunctionData, CallFunctionData, CallInstruction,
|
||||
FunctionData, CodeInstruction, CodeFunctionData, CallFunctionData,
|
||||
CallInstruction, ParameterDefinitionData,
|
||||
} from '@/application/collections/';
|
||||
import type { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
|
||||
@@ -7,20 +8,30 @@ import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmp
|
||||
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines';
|
||||
import type { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||
import { isArray, isNullOrUndefined, isPlainObject } from '@/TypeHelpers';
|
||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||
import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
|
||||
import { SharedFunctionCollection } from './SharedFunctionCollection';
|
||||
import { FunctionParameter } from './Parameter/FunctionParameter';
|
||||
import { FunctionParameterCollection } from './Parameter/FunctionParameterCollection';
|
||||
import { parseFunctionCalls } from './Call/FunctionCallParser';
|
||||
import { createFunctionParameterCollection, type FunctionParameterCollectionFactory } from './Parameter/FunctionParameterCollectionFactory';
|
||||
import type { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||
import type { ISharedFunctionsParser } from './ISharedFunctionsParser';
|
||||
import type { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
|
||||
import type { ISharedFunction } from './ISharedFunction';
|
||||
|
||||
const DefaultSharedFunctionsParsingUtilities: SharedFunctionsParsingUtilities = {
|
||||
wrapError: wrapErrorWithAdditionalContext,
|
||||
createParameter: (...args) => new FunctionParameter(...args),
|
||||
codeValidator: CodeValidator.instance,
|
||||
createParameterCollection: createFunctionParameterCollection,
|
||||
};
|
||||
|
||||
export class SharedFunctionsParser implements ISharedFunctionsParser {
|
||||
public static readonly instance: ISharedFunctionsParser = new SharedFunctionsParser();
|
||||
|
||||
constructor(private readonly codeValidator: ICodeValidator = CodeValidator.instance) { }
|
||||
constructor(
|
||||
private readonly utilities = DefaultSharedFunctionsParsingUtilities,
|
||||
) { }
|
||||
|
||||
public parseFunctions(
|
||||
functions: readonly FunctionData[],
|
||||
@@ -32,7 +43,7 @@ export class SharedFunctionsParser implements ISharedFunctionsParser {
|
||||
}
|
||||
ensureValidFunctions(functions);
|
||||
return functions
|
||||
.map((func) => parseFunction(func, syntax, this.codeValidator))
|
||||
.map((func) => parseFunction(func, syntax, this.utilities))
|
||||
.reduce((acc, func) => {
|
||||
acc.addFunction(func);
|
||||
return acc;
|
||||
@@ -40,15 +51,26 @@ export class SharedFunctionsParser implements ISharedFunctionsParser {
|
||||
}
|
||||
}
|
||||
|
||||
interface SharedFunctionsParsingUtilities {
|
||||
readonly wrapError: ErrorWithContextWrapper;
|
||||
readonly createParameter: FunctionParameterFactory;
|
||||
readonly codeValidator: ICodeValidator;
|
||||
readonly createParameterCollection: FunctionParameterCollectionFactory;
|
||||
}
|
||||
|
||||
export type FunctionParameterFactory = (
|
||||
...args: ConstructorParameters<typeof FunctionParameter>
|
||||
) => FunctionParameter;
|
||||
|
||||
function parseFunction(
|
||||
data: FunctionData,
|
||||
syntax: ILanguageSyntax,
|
||||
validator: ICodeValidator,
|
||||
utilities: SharedFunctionsParsingUtilities,
|
||||
): ISharedFunction {
|
||||
const { name } = data;
|
||||
const parameters = parseParameters(data);
|
||||
const parameters = parseParameters(data, utilities);
|
||||
if (hasCode(data)) {
|
||||
validateCode(data, syntax, validator);
|
||||
validateCode(data, syntax, utilities.codeValidator);
|
||||
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
|
||||
}
|
||||
// Has call
|
||||
@@ -71,22 +93,38 @@ function validateCode(
|
||||
);
|
||||
}
|
||||
|
||||
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
|
||||
function parseParameters(
|
||||
data: FunctionData,
|
||||
utilities: SharedFunctionsParsingUtilities,
|
||||
): IReadOnlyFunctionParameterCollection {
|
||||
return (data.parameters || [])
|
||||
.map((parameter) => {
|
||||
try {
|
||||
return new FunctionParameter(
|
||||
parameter.name,
|
||||
parameter.optional || false,
|
||||
);
|
||||
} catch (err) {
|
||||
throw new Error(`"${data.name}": ${err.message}`);
|
||||
}
|
||||
})
|
||||
.map((parameter) => createFunctionParameter(
|
||||
data.name,
|
||||
parameter,
|
||||
utilities,
|
||||
))
|
||||
.reduce((parameters, parameter) => {
|
||||
parameters.addParameter(parameter);
|
||||
return parameters;
|
||||
}, new FunctionParameterCollection());
|
||||
}, utilities.createParameterCollection());
|
||||
}
|
||||
|
||||
function createFunctionParameter(
|
||||
functionName: string,
|
||||
parameterData: ParameterDefinitionData,
|
||||
utilities: SharedFunctionsParsingUtilities,
|
||||
): FunctionParameter {
|
||||
try {
|
||||
return utilities.createParameter(
|
||||
parameterData.name,
|
||||
parameterData.optional || false,
|
||||
);
|
||||
} catch (err) {
|
||||
throw utilities.wrapError(
|
||||
err,
|
||||
`Failed to create parameter: ${parameterData.name} for function "${functionName}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function hasCode(data: FunctionData): data is CodeFunctionData {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { FunctionData, ScriptData, CallInstruction } from '@/application/collections/';
|
||||
import type { IScriptCode } from '@/domain/IScriptCode';
|
||||
import { ScriptCode } from '@/domain/ScriptCode';
|
||||
import type { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
|
||||
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||
import type { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||
import { createScriptCode, type ScriptCodeFactory } from '@/domain/ScriptCodeFactory';
|
||||
import { SharedFunctionsParser } from './Function/SharedFunctionsParser';
|
||||
import { FunctionCallSequenceCompiler } from './Function/Call/Compiler/FunctionCallSequenceCompiler';
|
||||
import { parseFunctionCalls } from './Function/Call/FunctionCallParser';
|
||||
@@ -23,6 +24,8 @@ export class ScriptCompiler implements IScriptCompiler {
|
||||
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
||||
private readonly callCompiler: FunctionCallCompiler = FunctionCallSequenceCompiler.instance,
|
||||
private readonly codeValidator: ICodeValidator = CodeValidator.instance,
|
||||
private readonly wrapError: ErrorWithContextWrapper = wrapErrorWithAdditionalContext,
|
||||
private readonly scriptCodeFactory: ScriptCodeFactory = createScriptCode,
|
||||
) {
|
||||
this.functions = sharedFunctionsParser.parseFunctions(functions, syntax);
|
||||
}
|
||||
@@ -39,12 +42,12 @@ export class ScriptCompiler implements IScriptCompiler {
|
||||
const calls = parseFunctionCalls(script.call);
|
||||
const compiledCode = this.callCompiler.compileFunctionCalls(calls, this.functions);
|
||||
validateCompiledCode(compiledCode, this.codeValidator);
|
||||
return new ScriptCode(
|
||||
return this.scriptCodeFactory(
|
||||
compiledCode.code,
|
||||
compiledCode.revertCode,
|
||||
);
|
||||
} catch (error) {
|
||||
throw Error(`Script "${script.name}" ${error.message}`);
|
||||
throw this.wrapError(error, `Failed to compile script: ${script.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,37 +4,52 @@ import type { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syn
|
||||
import { Script } from '@/domain/Script';
|
||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||
import type { IScriptCode } from '@/domain/IScriptCode';
|
||||
import { ScriptCode } from '@/domain/ScriptCode';
|
||||
import type { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||
import { parseDocs } from '../DocumentationParser';
|
||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||
import type { ScriptCodeFactory } from '@/domain/ScriptCodeFactory';
|
||||
import { createScriptCode } from '@/domain/ScriptCodeFactory';
|
||||
import type { IScript } from '@/domain/IScript';
|
||||
import { parseDocs, type DocsParser } from '../DocumentationParser';
|
||||
import { createEnumParser, type IEnumParser } from '../../Common/Enum';
|
||||
import { NodeType } from '../NodeValidation/NodeType';
|
||||
import { NodeValidator } from '../NodeValidation/NodeValidator';
|
||||
import { NodeDataType } from '../NodeValidation/NodeDataType';
|
||||
import { createNodeDataValidator, type NodeDataValidator, type NodeDataValidatorFactory } from '../NodeValidation/NodeDataValidator';
|
||||
import { CodeValidator } from './Validation/CodeValidator';
|
||||
import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines';
|
||||
import type { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
||||
|
||||
export function parseScript(
|
||||
data: ScriptData,
|
||||
context: ICategoryCollectionParseContext,
|
||||
levelParser = createEnumParser(RecommendationLevel),
|
||||
scriptFactory: ScriptFactoryType = ScriptFactory,
|
||||
codeValidator: ICodeValidator = CodeValidator.instance,
|
||||
): Script {
|
||||
const validator = new NodeValidator({ type: NodeType.Script, selfNode: data });
|
||||
export interface ScriptParser {
|
||||
(
|
||||
data: ScriptData,
|
||||
context: ICategoryCollectionParseContext,
|
||||
utilities?: ScriptParserUtilities,
|
||||
): IScript;
|
||||
}
|
||||
|
||||
export const parseScript: ScriptParser = (
|
||||
data,
|
||||
context,
|
||||
utilities = DefaultScriptParserUtilities,
|
||||
) => {
|
||||
const validator = utilities.createValidator({
|
||||
type: NodeDataType.Script,
|
||||
selfNode: data,
|
||||
});
|
||||
validateScript(data, validator);
|
||||
try {
|
||||
const script = scriptFactory(
|
||||
/* name: */ data.name,
|
||||
/* code: */ parseCode(data, context, codeValidator),
|
||||
/* docs: */ parseDocs(data),
|
||||
/* level: */ parseLevel(data.recommend, levelParser),
|
||||
);
|
||||
const script = utilities.createScript({
|
||||
name: data.name,
|
||||
code: parseCode(data, context, utilities.codeValidator, utilities.createCode),
|
||||
docs: utilities.parseDocs(data),
|
||||
level: parseLevel(data.recommend, utilities.levelParser),
|
||||
});
|
||||
return script;
|
||||
} catch (err) {
|
||||
return validator.throw(err.message);
|
||||
} catch (error) {
|
||||
throw utilities.wrapError(
|
||||
error,
|
||||
validator.createContextualErrorMessage('Failed to parse script.'),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function parseLevel(
|
||||
level: string | undefined,
|
||||
@@ -50,18 +65,19 @@ function parseCode(
|
||||
script: ScriptData,
|
||||
context: ICategoryCollectionParseContext,
|
||||
codeValidator: ICodeValidator,
|
||||
createCode: ScriptCodeFactory,
|
||||
): IScriptCode {
|
||||
if (context.compiler.canCompile(script)) {
|
||||
return context.compiler.compile(script);
|
||||
}
|
||||
const codeScript = script as CodeScriptData; // Must be inline code if it cannot be compiled
|
||||
const code = new ScriptCode(codeScript.code, codeScript.revertCode);
|
||||
const code = createCode(codeScript.code, codeScript.revertCode);
|
||||
validateHardcodedCodeWithoutCalls(code, codeValidator, context.syntax);
|
||||
return code;
|
||||
}
|
||||
|
||||
function validateHardcodedCodeWithoutCalls(
|
||||
scriptCode: ScriptCode,
|
||||
scriptCode: IScriptCode,
|
||||
validator: ICodeValidator,
|
||||
syntax: ILanguageSyntax,
|
||||
) {
|
||||
@@ -77,25 +93,48 @@ function validateHardcodedCodeWithoutCalls(
|
||||
|
||||
function validateScript(
|
||||
script: ScriptData,
|
||||
validator: NodeValidator,
|
||||
validator: NodeDataValidator,
|
||||
): asserts script is NonNullable<ScriptData> {
|
||||
validator
|
||||
.assertDefined(script)
|
||||
.assertValidName(script.name)
|
||||
.assert(
|
||||
() => Boolean((script as CodeScriptData).code || (script as CallScriptData).call),
|
||||
'Neither "call" or "code" is defined.',
|
||||
)
|
||||
.assert(
|
||||
() => !((script as CodeScriptData).code && (script as CallScriptData).call),
|
||||
'Both "call" and "code" are defined.',
|
||||
)
|
||||
.assert(
|
||||
() => !((script as CodeScriptData).revertCode && (script as CallScriptData).call),
|
||||
'Both "call" and "revertCode" are defined.',
|
||||
);
|
||||
validator.assertDefined(script);
|
||||
validator.assertValidName(script.name);
|
||||
validator.assert(
|
||||
() => Boolean((script as CodeScriptData).code || (script as CallScriptData).call),
|
||||
'Neither "call" or "code" is defined.',
|
||||
);
|
||||
validator.assert(
|
||||
() => !((script as CodeScriptData).code && (script as CallScriptData).call),
|
||||
'Both "call" and "code" are defined.',
|
||||
);
|
||||
validator.assert(
|
||||
() => !((script as CodeScriptData).revertCode && (script as CallScriptData).call),
|
||||
'Both "call" and "revertCode" are defined.',
|
||||
);
|
||||
}
|
||||
|
||||
export type ScriptFactoryType = (...parameters: ConstructorParameters<typeof Script>) => Script;
|
||||
interface ScriptParserUtilities {
|
||||
readonly levelParser: IEnumParser<RecommendationLevel>;
|
||||
readonly createScript: ScriptFactory;
|
||||
readonly codeValidator: ICodeValidator;
|
||||
readonly wrapError: ErrorWithContextWrapper;
|
||||
readonly createValidator: NodeDataValidatorFactory;
|
||||
readonly createCode: ScriptCodeFactory;
|
||||
readonly parseDocs: DocsParser;
|
||||
}
|
||||
|
||||
const ScriptFactory: ScriptFactoryType = (...parameters) => new Script(...parameters);
|
||||
export type ScriptFactory = (
|
||||
...parameters: ConstructorParameters<typeof Script>
|
||||
) => IScript;
|
||||
|
||||
const createScript: ScriptFactory = (...parameters) => {
|
||||
return new Script(...parameters);
|
||||
};
|
||||
|
||||
const DefaultScriptParserUtilities: ScriptParserUtilities = {
|
||||
levelParser: createEnumParser(RecommendationLevel),
|
||||
createScript,
|
||||
codeValidator: CodeValidator.instance,
|
||||
wrapError: wrapErrorWithAdditionalContext,
|
||||
createValidator: createNodeDataValidator,
|
||||
createCode: createScriptCode,
|
||||
parseDocs,
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expres
|
||||
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
||||
import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection';
|
||||
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
|
||||
import type { IExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/IExpressionParser';
|
||||
import type { ICodeSubstituter } from './ICodeSubstituter';
|
||||
|
||||
export class CodeSubstituter implements ICodeSubstituter {
|
||||
@@ -29,7 +30,9 @@ export class CodeSubstituter implements ICodeSubstituter {
|
||||
}
|
||||
|
||||
function createSubstituteCompiler(): IExpressionsCompiler {
|
||||
const parsers = [new ParameterSubstitutionParser()];
|
||||
const parsers: readonly IExpressionParser[] = [
|
||||
new ParameterSubstitutionParser(),
|
||||
] as const;
|
||||
const parser = new CompositeExpressionParser(parsers);
|
||||
const expressionCompiler = new ExpressionsCompiler(parser);
|
||||
return expressionCompiler;
|
||||
|
||||
Reference in New Issue
Block a user