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:
@@ -229,7 +229,11 @@ class ExpressionBuilder {
|
||||
}
|
||||
|
||||
public build() {
|
||||
return new Expression(this.position, this.evaluator, this.parameters);
|
||||
return new Expression({
|
||||
position: this.position,
|
||||
evaluator: this.evaluator,
|
||||
parameters: this.parameters,
|
||||
});
|
||||
}
|
||||
|
||||
private evaluator: ExpressionEvaluator = () => `[${ExpressionBuilder.name}] evaluated-expression`;
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createPositionFromRegexFullMatch } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPositionFactory';
|
||||
import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPosition';
|
||||
import { itIsTransientFactory } from '@tests/unit/shared/TestCases/TransientFactoryTests';
|
||||
|
||||
describe('ExpressionPositionFactory', () => {
|
||||
describe('createPositionFromRegexFullMatch', () => {
|
||||
it(`creates ${ExpressionPosition.name} instance`, () => {
|
||||
describe('it is a transient factory', () => {
|
||||
// arrange
|
||||
const expectedType = ExpressionPosition;
|
||||
const fakeMatch = createRegexMatch({
|
||||
fullMatch: 'matched string',
|
||||
matchIndex: 5,
|
||||
});
|
||||
const fakeMatch = createRegexMatch();
|
||||
// act
|
||||
const position = createPositionFromRegexFullMatch(fakeMatch);
|
||||
const create = () => createPositionFromRegexFullMatch(fakeMatch);
|
||||
// assert
|
||||
expect(position).to.be.instanceOf(expectedType);
|
||||
itIsTransientFactory({
|
||||
getter: create,
|
||||
expectedType: ExpressionPosition,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a position with the correct start position', () => {
|
||||
// arrange
|
||||
const expectedStartPosition = 5;
|
||||
@@ -63,10 +62,8 @@ describe('ExpressionPositionFactory', () => {
|
||||
describe('invalid values', () => {
|
||||
it('throws an error if match.index is undefined', () => {
|
||||
// arrange
|
||||
const fakeMatch = createRegexMatch({
|
||||
fullMatch: 'matched string',
|
||||
matchIndex: undefined,
|
||||
});
|
||||
const fakeMatch = createRegexMatch();
|
||||
fakeMatch.index = undefined;
|
||||
const expectedError = `Regex match did not yield any results: ${JSON.stringify(fakeMatch)}`;
|
||||
// act
|
||||
const act = () => createPositionFromRegexFullMatch(fakeMatch);
|
||||
@@ -94,9 +91,9 @@ function createRegexMatch(options?: {
|
||||
readonly capturingGroups?: readonly string[],
|
||||
readonly matchIndex?: number,
|
||||
}): RegExpMatchArray {
|
||||
const fullMatch = options?.fullMatch ?? 'fake match';
|
||||
const fullMatch = options?.fullMatch ?? 'default fake match';
|
||||
const capturingGroups = options?.capturingGroups ?? [];
|
||||
const fakeMatch: RegExpMatchArray = [fullMatch, ...capturingGroups];
|
||||
fakeMatch.index = options?.matchIndex;
|
||||
fakeMatch.index = options?.matchIndex ?? 0;
|
||||
return fakeMatch;
|
||||
}
|
||||
@@ -1,168 +1,438 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { ExpressionEvaluator } from '@/application/Parser/Script/Compiler/Expressions/Expression/Expression';
|
||||
import { type IPrimitiveExpression, RegexParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/Regex/RegexParser';
|
||||
import type {
|
||||
ExpressionEvaluator, ExpressionInitParameters,
|
||||
} from '@/application/Parser/Script/Compiler/Expressions/Expression/Expression';
|
||||
import {
|
||||
type PrimitiveExpression, RegexParser, type ExpressionFactory, type RegexParserUtilities,
|
||||
} from '@/application/Parser/Script/Compiler/Expressions/Parser/Regex/RegexParser';
|
||||
import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPosition';
|
||||
import { FunctionParameterStub } from '@tests/unit/shared/Stubs/FunctionParameterStub';
|
||||
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
|
||||
import { itThrowsContextualError } from '@tests/unit/application/Parser/ContextualErrorTester';
|
||||
import { ExpressionStub } from '@tests/unit/shared/Stubs/ExpressionStub';
|
||||
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
|
||||
import type { IExpression } from '@/application/Parser/Script/Compiler/Expressions/Expression/IExpression';
|
||||
import { FunctionParameterStub } from '@tests/unit/shared/Stubs/FunctionParameterStub';
|
||||
import type { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/IFunctionParameterCollection';
|
||||
import type { ExpressionPositionFactory } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPositionFactory';
|
||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||
import { indentText } from '@tests/shared/Text';
|
||||
import type { FunctionParameterCollectionFactory } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollectionFactory';
|
||||
|
||||
describe('RegexParser', () => {
|
||||
describe('findExpressions', () => {
|
||||
describe('throws when code is absent', () => {
|
||||
itEachAbsentStringValue((absentValue) => {
|
||||
describe('error handling', () => {
|
||||
describe('throws when code is absent', () => {
|
||||
itEachAbsentStringValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing code';
|
||||
const sut = new RegexParserConcrete({
|
||||
regex: /unimportant/,
|
||||
});
|
||||
// act
|
||||
const act = () => sut.findExpressions(absentValue);
|
||||
// assert
|
||||
const errorMessage = collectExceptionMessage(act);
|
||||
expect(errorMessage).to.include(expectedError);
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
});
|
||||
describe('rethrows regex match errors', () => {
|
||||
// arrange
|
||||
const expectedError = 'missing code';
|
||||
const sut = new RegexParserConcrete(/unimportant/);
|
||||
// act
|
||||
const act = () => sut.findExpressions(absentValue);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
const expectedMatchError = new TypeError('String.prototype.matchAll called with a non-global RegExp argument');
|
||||
const expectedMessage = 'Failed to match regex.';
|
||||
const expectedCodeInMessage = 'unimportant code content';
|
||||
const expectedRegexInMessage = /failing-regex-because-it-is-non-global/;
|
||||
const expectedErrorMessage = buildRethrowErrorMessage({
|
||||
message: expectedMessage,
|
||||
code: expectedCodeInMessage,
|
||||
regex: expectedRegexInMessage,
|
||||
});
|
||||
itThrowsContextualError({
|
||||
// act
|
||||
throwingAction: (wrapError) => {
|
||||
const sut = new RegexParserConcrete(
|
||||
{
|
||||
regex: expectedRegexInMessage,
|
||||
utilities: {
|
||||
wrapError,
|
||||
},
|
||||
},
|
||||
);
|
||||
sut.findExpressions(expectedCodeInMessage);
|
||||
},
|
||||
// assert
|
||||
expectedContextMessage: expectedErrorMessage,
|
||||
expectedWrappedError: expectedMatchError,
|
||||
});
|
||||
});
|
||||
describe('rethrows expression building errors', () => {
|
||||
// arrange
|
||||
const expectedMessage = 'Failed to build expression.';
|
||||
const expectedInnerError = new Error('Expected error from building expression');
|
||||
const {
|
||||
code: expectedCodeInMessage,
|
||||
regex: expectedRegexInMessage,
|
||||
} = createCodeAndRegexMatchingOnce();
|
||||
const throwingExpressionBuilder = () => {
|
||||
throw expectedInnerError;
|
||||
};
|
||||
const expectedErrorMessage = buildRethrowErrorMessage({
|
||||
message: expectedMessage,
|
||||
code: expectedCodeInMessage,
|
||||
regex: expectedRegexInMessage,
|
||||
});
|
||||
itThrowsContextualError({
|
||||
// act
|
||||
throwingAction: (wrapError) => {
|
||||
const sut = new RegexParserConcrete(
|
||||
{
|
||||
regex: expectedRegexInMessage,
|
||||
builder: throwingExpressionBuilder,
|
||||
utilities: {
|
||||
wrapError,
|
||||
},
|
||||
},
|
||||
);
|
||||
sut.findExpressions(expectedCodeInMessage);
|
||||
},
|
||||
// assert
|
||||
expectedWrappedError: expectedInnerError,
|
||||
expectedContextMessage: expectedErrorMessage,
|
||||
});
|
||||
});
|
||||
describe('rethrows position creation errors', () => {
|
||||
// arrange
|
||||
const expectedMessage = 'Failed to create position.';
|
||||
const expectedInnerError = new Error('Expected error from position factory');
|
||||
const {
|
||||
code: expectedCodeInMessage,
|
||||
regex: expectedRegexInMessage,
|
||||
} = createCodeAndRegexMatchingOnce();
|
||||
const throwingPositionFactory = () => {
|
||||
throw expectedInnerError;
|
||||
};
|
||||
const expectedErrorMessage = buildRethrowErrorMessage({
|
||||
message: expectedMessage,
|
||||
code: expectedCodeInMessage,
|
||||
regex: expectedRegexInMessage,
|
||||
});
|
||||
itThrowsContextualError({
|
||||
// act
|
||||
throwingAction: (wrapError) => {
|
||||
const sut = new RegexParserConcrete(
|
||||
{
|
||||
regex: expectedRegexInMessage,
|
||||
utilities: {
|
||||
createPosition: throwingPositionFactory,
|
||||
wrapError,
|
||||
},
|
||||
},
|
||||
);
|
||||
sut.findExpressions(expectedCodeInMessage);
|
||||
},
|
||||
// assert
|
||||
expectedWrappedError: expectedInnerError,
|
||||
expectedContextMessage: expectedErrorMessage,
|
||||
});
|
||||
});
|
||||
describe('rethrows parameter creation errors', () => {
|
||||
// arrange
|
||||
const expectedMessage = 'Failed to create parameters.';
|
||||
const expectedInnerError = new Error('Expected error from parameter collection factory');
|
||||
const {
|
||||
code: expectedCodeInMessage,
|
||||
regex: expectedRegexInMessage,
|
||||
} = createCodeAndRegexMatchingOnce();
|
||||
const throwingParameterCollectionFactory = () => {
|
||||
throw expectedInnerError;
|
||||
};
|
||||
const expectedErrorMessage = buildRethrowErrorMessage({
|
||||
message: expectedMessage,
|
||||
code: expectedCodeInMessage,
|
||||
regex: expectedRegexInMessage,
|
||||
});
|
||||
itThrowsContextualError({
|
||||
// act
|
||||
throwingAction: (wrapError) => {
|
||||
const sut = new RegexParserConcrete(
|
||||
{
|
||||
regex: expectedRegexInMessage,
|
||||
utilities: {
|
||||
createParameterCollection: throwingParameterCollectionFactory,
|
||||
wrapError,
|
||||
},
|
||||
},
|
||||
);
|
||||
sut.findExpressions(expectedCodeInMessage);
|
||||
},
|
||||
// assert
|
||||
expectedWrappedError: expectedInnerError,
|
||||
expectedContextMessage: expectedErrorMessage,
|
||||
});
|
||||
});
|
||||
describe('rethrows expression creation errors', () => {
|
||||
// arrange
|
||||
const expectedMessage = 'Failed to create expression.';
|
||||
const expectedInnerError = new Error('Expected error from expression factory');
|
||||
const {
|
||||
code: expectedCodeInMessage,
|
||||
regex: expectedRegexInMessage,
|
||||
} = createCodeAndRegexMatchingOnce();
|
||||
const throwingExpressionFactory = () => {
|
||||
throw expectedInnerError;
|
||||
};
|
||||
const expectedErrorMessage = buildRethrowErrorMessage({
|
||||
message: expectedMessage,
|
||||
code: expectedCodeInMessage,
|
||||
regex: expectedRegexInMessage,
|
||||
});
|
||||
itThrowsContextualError({
|
||||
// act
|
||||
throwingAction: (wrapError) => {
|
||||
const sut = new RegexParserConcrete(
|
||||
{
|
||||
regex: expectedRegexInMessage,
|
||||
utilities: {
|
||||
createExpression: throwingExpressionFactory,
|
||||
wrapError,
|
||||
},
|
||||
},
|
||||
);
|
||||
sut.findExpressions(expectedCodeInMessage);
|
||||
},
|
||||
// assert
|
||||
expectedWrappedError: expectedInnerError,
|
||||
expectedContextMessage: expectedErrorMessage,
|
||||
});
|
||||
});
|
||||
});
|
||||
it('throws when position is invalid', () => {
|
||||
describe('handles matched regex correctly', () => {
|
||||
// arrange
|
||||
const regexMatchingEmpty = /^/gm; /* expressions cannot be empty */
|
||||
const code = 'unimportant';
|
||||
const expectedErrorParts = [
|
||||
`[${RegexParserConcrete.constructor.name}]`,
|
||||
'invalid script position',
|
||||
`Regex: ${regexMatchingEmpty}`,
|
||||
`Code: ${code}`,
|
||||
];
|
||||
const sut = new RegexParserConcrete(regexMatchingEmpty);
|
||||
// act
|
||||
let errorMessage: string | undefined;
|
||||
try {
|
||||
sut.findExpressions(code);
|
||||
} catch (err) {
|
||||
errorMessage = err.message;
|
||||
}
|
||||
// assert
|
||||
expectExists(errorMessage);
|
||||
const error = errorMessage; // workaround for ts(18048): possibly 'undefined'
|
||||
expect(
|
||||
expectedErrorParts.every((part) => error.includes(part)),
|
||||
`Expected parts: ${expectedErrorParts.join(', ')}`
|
||||
+ `Actual error: ${errorMessage}`,
|
||||
);
|
||||
});
|
||||
describe('matches regex as expected', () => {
|
||||
// arrange
|
||||
const testCases = [
|
||||
const testScenarios: readonly {
|
||||
readonly description: string;
|
||||
readonly regex: RegExp;
|
||||
readonly code: string;
|
||||
}[] = [
|
||||
{
|
||||
name: 'returns no result when regex does not match',
|
||||
description: 'non-matching regex',
|
||||
regex: /hello/g,
|
||||
code: 'world',
|
||||
},
|
||||
{
|
||||
name: 'returns expected when regex matches single',
|
||||
description: 'single regex match',
|
||||
regex: /hello/g,
|
||||
code: 'hello world',
|
||||
},
|
||||
{
|
||||
name: 'returns expected when regex matches multiple',
|
||||
description: 'multiple regex matches',
|
||||
regex: /l/g,
|
||||
code: 'hello world',
|
||||
},
|
||||
];
|
||||
for (const testCase of testCases) {
|
||||
it(testCase.name, () => {
|
||||
const expected = Array.from(testCase.code.matchAll(testCase.regex));
|
||||
const matches = new Array<RegExpMatchArray>();
|
||||
const builder = (m: RegExpMatchArray): IPrimitiveExpression => {
|
||||
matches.push(m);
|
||||
return mockPrimitiveExpression();
|
||||
};
|
||||
const sut = new RegexParserConcrete(testCase.regex, builder);
|
||||
// act
|
||||
const expressions = sut.findExpressions(testCase.code);
|
||||
// assert
|
||||
expect(expressions).to.have.lengthOf(matches.length);
|
||||
expect(matches).to.deep.equal(expected);
|
||||
testScenarios.forEach(({
|
||||
description, code, regex,
|
||||
}) => {
|
||||
describe(description, () => {
|
||||
it('generates expressions for all matches', () => {
|
||||
// arrange
|
||||
const expectedTotalExpressions = Array.from(code.matchAll(regex)).length;
|
||||
const sut = new RegexParserConcrete({
|
||||
regex,
|
||||
});
|
||||
// act
|
||||
const expressions = sut.findExpressions(code);
|
||||
// assert
|
||||
const actualTotalExpressions = expressions.length;
|
||||
expect(actualTotalExpressions).to.equal(
|
||||
expectedTotalExpressions,
|
||||
formatAssertionMessage([
|
||||
`Expected ${actualTotalExpressions} expressions due to ${expectedTotalExpressions} matches`,
|
||||
`Expressions:\n${indentText(JSON.stringify(expressions, undefined, 2))}`,
|
||||
]),
|
||||
);
|
||||
});
|
||||
it('builds primitive expressions for each match', () => {
|
||||
const expected = Array.from(code.matchAll(regex));
|
||||
const matches = new Array<RegExpMatchArray>();
|
||||
const builder = (m: RegExpMatchArray): PrimitiveExpression => {
|
||||
matches.push(m);
|
||||
return createPrimitiveExpressionStub();
|
||||
};
|
||||
const sut = new RegexParserConcrete({
|
||||
regex,
|
||||
builder,
|
||||
});
|
||||
// act
|
||||
sut.findExpressions(code);
|
||||
// assert
|
||||
expect(matches).to.deep.equal(expected);
|
||||
});
|
||||
it('sets positions correctly from matches', () => {
|
||||
// arrange
|
||||
const expectedMatches = [...code.matchAll(regex)];
|
||||
const { createExpression, getInitParameters } = createExpressionFactorySpy();
|
||||
const serializeRegexMatch = (match: RegExpMatchArray) => `[startPos:${match?.index ?? 'none'},length:${match?.[0]?.length ?? 'none'}]`;
|
||||
const positionsForMatches = new Map<string, ExpressionPosition>(expectedMatches.map(
|
||||
(expectedMatch) => [serializeRegexMatch(expectedMatch), new ExpressionPosition(1, 4)],
|
||||
));
|
||||
const createPositionMock: ExpressionPositionFactory = (match) => {
|
||||
const position = positionsForMatches.get(serializeRegexMatch(match));
|
||||
return position ?? new ExpressionPosition(66, 666);
|
||||
};
|
||||
const sut = new RegexParserConcrete({
|
||||
regex,
|
||||
utilities: {
|
||||
createExpression,
|
||||
createPosition: createPositionMock,
|
||||
},
|
||||
});
|
||||
// act
|
||||
const expressions = sut.findExpressions(code);
|
||||
// assert
|
||||
const expectedPositions = [...positionsForMatches.values()];
|
||||
const actualPositions = expressions.map((e) => getInitParameters(e)?.position);
|
||||
expect(actualPositions).to.deep.equal(expectedPositions, formatAssertionMessage([
|
||||
'Actual positions do not match the expected positions.',
|
||||
`Expected total positions: ${expectedPositions.length} (due to ${expectedMatches.length} regex matches)`,
|
||||
`Actual total positions: ${actualPositions.length}`,
|
||||
`Expected positions:\n${indentText(JSON.stringify(expectedPositions, undefined, 2))}`,
|
||||
`Actual positions:\n${indentText(JSON.stringify(actualPositions, undefined, 2))}`,
|
||||
]));
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
it('sets evaluator as expected', () => {
|
||||
// arrange
|
||||
const expected = getEvaluatorStub();
|
||||
const regex = /hello/g;
|
||||
const code = 'hello';
|
||||
const builder = (): IPrimitiveExpression => ({
|
||||
evaluator: expected,
|
||||
});
|
||||
const sut = new RegexParserConcrete(regex, builder);
|
||||
});
|
||||
it('sets evaluator correctly from expression', () => {
|
||||
// arrange
|
||||
const { createExpression, getInitParameters } = createExpressionFactorySpy();
|
||||
const expectedEvaluate = createEvaluatorStub();
|
||||
const { code, regex } = createCodeAndRegexMatchingOnce();
|
||||
const builder = (): PrimitiveExpression => ({
|
||||
evaluator: expectedEvaluate,
|
||||
});
|
||||
const sut = new RegexParserConcrete({
|
||||
regex,
|
||||
builder,
|
||||
utilities: {
|
||||
createExpression,
|
||||
},
|
||||
});
|
||||
// act
|
||||
const expressions = sut.findExpressions(code);
|
||||
// assert
|
||||
expect(expressions).to.have.lengthOf(1);
|
||||
expect(expressions[0].evaluate === expected);
|
||||
const actualEvaluate = getInitParameters(expressions[0])?.evaluator;
|
||||
expect(actualEvaluate).to.equal(expectedEvaluate);
|
||||
});
|
||||
it('sets parameters as expected', () => {
|
||||
it('sets parameters correctly from expression', () => {
|
||||
// arrange
|
||||
const expected = [
|
||||
new FunctionParameterStub().withName('parameter1').withOptionality(true),
|
||||
new FunctionParameterStub().withName('parameter2').withOptionality(false),
|
||||
const expectedParameters: IReadOnlyFunctionParameterCollection['all'] = [
|
||||
new FunctionParameterStub().withName('parameter1').withOptional(true),
|
||||
new FunctionParameterStub().withName('parameter2').withOptional(false),
|
||||
];
|
||||
const regex = /hello/g;
|
||||
const code = 'hello';
|
||||
const builder = (): IPrimitiveExpression => ({
|
||||
evaluator: getEvaluatorStub(),
|
||||
parameters: expected,
|
||||
const builder = (): PrimitiveExpression => ({
|
||||
evaluator: createEvaluatorStub(),
|
||||
parameters: expectedParameters,
|
||||
});
|
||||
const parameterCollection = new FunctionParameterCollectionStub();
|
||||
const parameterCollectionFactoryStub
|
||||
: FunctionParameterCollectionFactory = () => parameterCollection;
|
||||
const { createExpression, getInitParameters } = createExpressionFactorySpy();
|
||||
const sut = new RegexParserConcrete({
|
||||
regex,
|
||||
builder,
|
||||
utilities: {
|
||||
createExpression,
|
||||
createParameterCollection: parameterCollectionFactoryStub,
|
||||
},
|
||||
});
|
||||
const sut = new RegexParserConcrete(regex, builder);
|
||||
// act
|
||||
const expressions = sut.findExpressions(code);
|
||||
// assert
|
||||
expect(expressions).to.have.lengthOf(1);
|
||||
expect(expressions[0].parameters.all).to.deep.equal(expected);
|
||||
});
|
||||
it('sets expected position', () => {
|
||||
// arrange
|
||||
const code = 'mate date in state is fate';
|
||||
const regex = /ate/g;
|
||||
const expected = [
|
||||
new ExpressionPosition(1, 4),
|
||||
new ExpressionPosition(6, 9),
|
||||
new ExpressionPosition(15, 18),
|
||||
new ExpressionPosition(23, 26),
|
||||
];
|
||||
const sut = new RegexParserConcrete(regex);
|
||||
// act
|
||||
const expressions = sut.findExpressions(code);
|
||||
// assert
|
||||
const actual = expressions.map((e) => e.position);
|
||||
expect(actual).to.deep.equal(expected);
|
||||
const actualParameters = getInitParameters(expressions[0])?.parameters;
|
||||
expect(actualParameters).to.equal(parameterCollection);
|
||||
expect(actualParameters?.all).to.deep.equal(expectedParameters);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function mockBuilder(): (match: RegExpMatchArray) => IPrimitiveExpression {
|
||||
return () => ({
|
||||
evaluator: getEvaluatorStub(),
|
||||
});
|
||||
}
|
||||
function getEvaluatorStub(): ExpressionEvaluator {
|
||||
return () => `[${getEvaluatorStub.name}] evaluated code`;
|
||||
function buildRethrowErrorMessage(
|
||||
expectedContext: {
|
||||
readonly message: string;
|
||||
readonly regex: RegExp;
|
||||
readonly code: string;
|
||||
},
|
||||
): string {
|
||||
return [
|
||||
expectedContext.message,
|
||||
`Class name: ${RegexParserConcrete.name}`,
|
||||
`Regex pattern used: ${expectedContext.regex}`,
|
||||
`Code: ${expectedContext.code}`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function mockPrimitiveExpression(): IPrimitiveExpression {
|
||||
function createExpressionFactorySpy() {
|
||||
const createdExpressions = new Map<IExpression, ExpressionInitParameters>();
|
||||
const createExpression: ExpressionFactory = (parameters) => {
|
||||
const expression = new ExpressionStub();
|
||||
createdExpressions.set(expression, parameters);
|
||||
return expression;
|
||||
};
|
||||
return {
|
||||
evaluator: getEvaluatorStub(),
|
||||
createExpression,
|
||||
getInitParameters: (expression) => createdExpressions.get(expression),
|
||||
};
|
||||
}
|
||||
|
||||
function createBuilderStub(): (match: RegExpMatchArray) => PrimitiveExpression {
|
||||
return () => ({
|
||||
evaluator: createEvaluatorStub(),
|
||||
});
|
||||
}
|
||||
function createEvaluatorStub(): ExpressionEvaluator {
|
||||
return () => `[${createEvaluatorStub.name}] evaluated code`;
|
||||
}
|
||||
|
||||
function createPrimitiveExpressionStub(): PrimitiveExpression {
|
||||
return {
|
||||
evaluator: createEvaluatorStub(),
|
||||
};
|
||||
}
|
||||
|
||||
function createCodeAndRegexMatchingOnce() {
|
||||
const code = 'expected code in context';
|
||||
const regex = /code/g;
|
||||
return { code, regex };
|
||||
}
|
||||
|
||||
class RegexParserConcrete extends RegexParser {
|
||||
private readonly builder: RegexParser['buildExpression'];
|
||||
|
||||
protected regex: RegExp;
|
||||
|
||||
public constructor(
|
||||
regex: RegExp,
|
||||
private readonly builder = mockBuilder(),
|
||||
) {
|
||||
super();
|
||||
this.regex = regex;
|
||||
public constructor(parameters?: {
|
||||
regex?: RegExp,
|
||||
builder?: RegexParser['buildExpression'],
|
||||
utilities?: Partial<RegexParserUtilities>,
|
||||
}) {
|
||||
super({
|
||||
wrapError: parameters?.utilities?.wrapError
|
||||
?? (() => new Error(`[${RegexParserConcrete}] wrapped error`)),
|
||||
createPosition: parameters?.utilities?.createPosition
|
||||
?? (() => new ExpressionPosition(0, 5)),
|
||||
createExpression: parameters?.utilities?.createExpression
|
||||
?? (() => new ExpressionStub()),
|
||||
createParameterCollection: parameters?.utilities?.createParameterCollection
|
||||
?? (() => new FunctionParameterCollectionStub()),
|
||||
});
|
||||
this.builder = parameters?.builder ?? createBuilderStub();
|
||||
this.regex = parameters?.regex ?? /unimportant/g;
|
||||
}
|
||||
|
||||
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
||||
protected buildExpression(match: RegExpMatchArray): PrimitiveExpression {
|
||||
return this.builder(match);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user