Skip to content

Commit 2a4d75f

Browse files
authored
feat: parser result modifier (#9)
- add result modifier flow on `Parser` blueprint. - implement result modifier logic on `defaultParser`. - update `defaultParser` regex to support multiline source (tsc --pretty). - add `DefaultParserResult` type. - add new test case for parser result modifier.
1 parent 463755e commit 2a4d75f

File tree

5 files changed

+125
-15
lines changed

5 files changed

+125
-15
lines changed

__tests__/_errors.mock.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,4 +144,14 @@ index2.ts:1:1 - Unexpected error.
144144
${extendedDiagnostics}
145145
${verboseFooter}`;
146146

147-
export default { PRETTY, NOT_PRETTY };
147+
const MULTILINE_SOURCE = `
148+
index.ts(1,1): error TS1000: Unexpected error.
149+
150+
1 unexpected_error() as UnexpectedError<
151+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
152+
2 unexpected_error
153+
~~~~~~~~~~~~~~~~~~~~
154+
3 >;
155+
~~~`.trimStart();
156+
157+
export default { PRETTY, NOT_PRETTY, MULTILINE_SOURCE };

__tests__/src/blueprints/Parser.spec.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@ describe('blueprint > Parser', () => {
66
let matcher: RegExp;
77
let matcherKeys: string[] = [];
88
let inputModifier: jest.Mock;
9+
let resultModifier: jest.Mock;
910
let parser: Parser;
1011

1112
beforeEach(() => {
1213
input = '12';
1314
matcher = /^(\d)(\d)/g;
1415
matcherKeys = ['first', 'second'];
1516
inputModifier = jest.fn((input: string) => input);
16-
parser = new Parser(matcher, matcherKeys, inputModifier);
17+
resultModifier = jest.fn((result: object) => result);
18+
parser = new Parser(matcher, matcherKeys, inputModifier, resultModifier);
1719
});
1820

1921
describe('instantiation', () => {
@@ -51,5 +53,16 @@ describe('blueprint > Parser', () => {
5153
expect(inputModifier).toHaveBeenCalledTimes(1);
5254
expect(inputModifier).toHaveBeenLastCalledWith(input);
5355
});
56+
57+
it('should call resultModifier correctly', () => {
58+
expect(resultModifier).toHaveBeenCalledTimes(0);
59+
parser.parse(input);
60+
expect(resultModifier).toHaveBeenCalledTimes(1);
61+
expect(resultModifier).toHaveBeenLastCalledWith({
62+
_match: input,
63+
first: '1',
64+
second: '2'
65+
});
66+
});
5467
});
5568
});

__tests__/src/parsers/default-parser.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,32 @@ describe('parsers > defaultParser', () => {
2424
EXPECTED_RESULTS
2525
);
2626
});
27+
28+
it('should return multiline source correctly', () => {
29+
expect(defaultParser.parse(ERROR_MOCKS.MULTILINE_SOURCE)).toEqual([
30+
{
31+
_match: ERROR_MOCKS.MULTILINE_SOURCE,
32+
file: 'index.ts',
33+
errorCode: 'TS1000',
34+
column: '1',
35+
line: '1',
36+
message: 'Unexpected error.',
37+
source: [
38+
'1 unexpected_error() as UnexpectedError<',
39+
' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
40+
'2 unexpected_error',
41+
' ~~~~~~~~~~~~~~~~~~~~',
42+
'3 >;',
43+
' ~~~'
44+
].join('\n'),
45+
sourceClean: [
46+
'unexpected_error() as UnexpectedError<',
47+
' unexpected_error',
48+
'>;'
49+
].join('\n')
50+
}
51+
]);
52+
});
2753
});
2854

2955
describe('pretty format disabled', () => {

src/blueprints/Parser.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ export class Parser<MatcherKeys extends readonly string[] = string[]> {
77
#matcher: RegExp;
88
#matcherKeys: MatcherKeys;
99
#inputModifier?: (input: string) => string;
10+
#resultModifier?: (
11+
result: ParseResult<MatcherKeys>
12+
) => ParseResult<MatcherKeys>;
1013

1114
/**
1215
* Creates a new Parser instance.
@@ -19,14 +22,18 @@ export class Parser<MatcherKeys extends readonly string[] = string[]> {
1922
constructor(
2023
matcher: RegExp,
2124
matcherKeys: MatcherKeys,
22-
inputModifier?: (input: string) => string
25+
inputModifier?: (input: string) => string,
26+
resultModifier?: (
27+
result: ParseResult<MatcherKeys>
28+
) => ParseResult<MatcherKeys>
2329
) {
2430
if (!matcher.global) {
2531
throw new TypeError("argument 'matcher' must be a global regex.");
2632
}
2733
this.#matcher = matcher;
2834
this.#matcherKeys = matcherKeys;
2935
this.#inputModifier = inputModifier;
36+
this.#resultModifier = resultModifier;
3037
}
3138

3239
/**
@@ -62,7 +69,13 @@ export class Parser<MatcherKeys extends readonly string[] = string[]> {
6269
);
6370

6471
for (const matchError of matches) {
65-
results.push(this.#constructResult(matchError));
72+
let result = this.#constructResult(matchError);
73+
74+
if (this.#resultModifier) {
75+
result = this.#resultModifier({ ...result });
76+
}
77+
78+
results.push(result);
6679
}
6780

6881
return results;

src/parsers/default-parser.ts

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,69 @@
1-
import { Parser } from 'src/blueprints/Parser';
1+
import { type ParseResult, Parser } from 'src/blueprints/Parser';
2+
3+
const KEYS = [
4+
'file',
5+
'line',
6+
'column',
7+
'errorCode',
8+
'message',
9+
'source',
10+
'sourceClean'
11+
] as const;
212

313
/**
414
* Default `Parser` instance.
515
*/
616
export const defaultParser = new Parser(
7-
/^(?:(?:(.*?)[:(](\d+)[:,](\d+)[)]? ?[:-] ?)|(?:error ?))(?:error ?)?(TS\d+)?(?:(?:: )|(?: - )|(?: ))(.*(?:\r?\n {2,}.*)*)(?:(?:\r?\n){2,}(\d+\s+(.*)\r?\n\s+~+))?$/gm,
8-
[
9-
'file',
10-
'line',
11-
'column',
12-
'errorCode',
13-
'message',
14-
'source',
15-
'sourceClean'
16-
] as const,
17+
/^(?:(?:(.*?)[:(](\d+)[:,](\d+)[)]? ?[:-] ?)|(?:error ?))(?:error ?)?(TS\d+)?(?:(?:: )|(?: - )|(?: ))(.*(?:\r?\n {2,}.*)*)$(?:(?:\r?\n){2,}^((?:\d+\s+\S.*\r?\n^\s+~+$(?:\r?\n){0,1})*)){0,1}$/gm,
18+
KEYS,
1719
(input) => {
1820
// biome-ignore lint/suspicious/noControlCharactersInRegex: needed for removing colored text
1921
return input.replaceAll(/\x1b\[[0-9;]*m/g, '');
22+
},
23+
(result) => {
24+
result._match = result._match.trimEnd();
25+
26+
if (result.source) {
27+
result.source = result.source.trim();
28+
29+
const matches = Array.from(
30+
result.source.trim().matchAll(/^(?:\d+)(\s.*)$/gm)
31+
);
32+
33+
let minIndent: number;
34+
35+
const codeLines = matches.reduce<string[]>((acc, curr) => {
36+
if (curr?.[1]) {
37+
const indentLength = (curr[1].match(/^(\s+).*$/)?.[1] || '').length;
38+
if (minIndent === undefined) {
39+
minIndent = indentLength;
40+
} else {
41+
minIndent = indentLength < minIndent ? indentLength : minIndent;
42+
}
43+
acc.push(curr[1]);
44+
}
45+
return acc;
46+
}, []);
47+
48+
if (codeLines.length > 0) {
49+
result.sourceClean = codeLines.reduce((acc, curr, currIndex) => {
50+
let res = acc;
51+
res += curr.slice(minIndent);
52+
53+
if (currIndex !== codeLines.length - 1) {
54+
res += '\n';
55+
}
56+
57+
return res;
58+
}, '');
59+
}
60+
}
61+
62+
return result;
2063
}
2164
);
65+
66+
/**
67+
* Default `Parser` result type.
68+
*/
69+
export type DefaultParserResult = ParseResult<typeof KEYS>;

0 commit comments

Comments
 (0)