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:
undergroundwires
2024-05-25 13:55:30 +02:00
parent 7794846185
commit 4212c7b9e0
78 changed files with 3346 additions and 1268 deletions

View File

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