diff --git a/__tests__/_errors.mock.ts b/__tests__/_errors.mock.ts index 90a4f51..780c84e 100644 --- a/__tests__/_errors.mock.ts +++ b/__tests__/_errors.mock.ts @@ -144,4 +144,14 @@ index2.ts:1:1 - Unexpected error. ${extendedDiagnostics} ${verboseFooter}`; -export default { PRETTY, NOT_PRETTY }; +const MULTILINE_SOURCE = ` +index.ts(1,1): error TS1000: Unexpected error. + +1 unexpected_error() as UnexpectedError< + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +2 unexpected_error + ~~~~~~~~~~~~~~~~~~~~ +3 >; + ~~~`.trimStart(); + +export default { PRETTY, NOT_PRETTY, MULTILINE_SOURCE }; diff --git a/__tests__/src/blueprints/Parser.spec.ts b/__tests__/src/blueprints/Parser.spec.ts index 5320fd7..e6526e3 100644 --- a/__tests__/src/blueprints/Parser.spec.ts +++ b/__tests__/src/blueprints/Parser.spec.ts @@ -6,6 +6,7 @@ describe('blueprint > Parser', () => { let matcher: RegExp; let matcherKeys: string[] = []; let inputModifier: jest.Mock; + let resultModifier: jest.Mock; let parser: Parser; beforeEach(() => { @@ -13,7 +14,8 @@ describe('blueprint > Parser', () => { matcher = /^(\d)(\d)/g; matcherKeys = ['first', 'second']; inputModifier = jest.fn((input: string) => input); - parser = new Parser(matcher, matcherKeys, inputModifier); + resultModifier = jest.fn((result: object) => result); + parser = new Parser(matcher, matcherKeys, inputModifier, resultModifier); }); describe('instantiation', () => { @@ -51,5 +53,16 @@ describe('blueprint > Parser', () => { expect(inputModifier).toHaveBeenCalledTimes(1); expect(inputModifier).toHaveBeenLastCalledWith(input); }); + + it('should call resultModifier correctly', () => { + expect(resultModifier).toHaveBeenCalledTimes(0); + parser.parse(input); + expect(resultModifier).toHaveBeenCalledTimes(1); + expect(resultModifier).toHaveBeenLastCalledWith({ + _match: input, + first: '1', + second: '2' + }); + }); }); }); diff --git a/__tests__/src/parsers/default-parser.spec.ts b/__tests__/src/parsers/default-parser.spec.ts index 39dee14..7ad11d0 100644 --- a/__tests__/src/parsers/default-parser.spec.ts +++ b/__tests__/src/parsers/default-parser.spec.ts @@ -24,6 +24,32 @@ describe('parsers > defaultParser', () => { EXPECTED_RESULTS ); }); + + it('should return multiline source correctly', () => { + expect(defaultParser.parse(ERROR_MOCKS.MULTILINE_SOURCE)).toEqual([ + { + _match: ERROR_MOCKS.MULTILINE_SOURCE, + file: 'index.ts', + errorCode: 'TS1000', + column: '1', + line: '1', + message: 'Unexpected error.', + source: [ + '1 unexpected_error() as UnexpectedError<', + ' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~', + '2 unexpected_error', + ' ~~~~~~~~~~~~~~~~~~~~', + '3 >;', + ' ~~~' + ].join('\n'), + sourceClean: [ + 'unexpected_error() as UnexpectedError<', + ' unexpected_error', + '>;' + ].join('\n') + } + ]); + }); }); describe('pretty format disabled', () => { diff --git a/src/blueprints/Parser.ts b/src/blueprints/Parser.ts index db1c29e..4f54315 100644 --- a/src/blueprints/Parser.ts +++ b/src/blueprints/Parser.ts @@ -7,6 +7,9 @@ export class Parser { #matcher: RegExp; #matcherKeys: MatcherKeys; #inputModifier?: (input: string) => string; + #resultModifier?: ( + result: ParseResult + ) => ParseResult; /** * Creates a new Parser instance. @@ -19,7 +22,10 @@ export class Parser { constructor( matcher: RegExp, matcherKeys: MatcherKeys, - inputModifier?: (input: string) => string + inputModifier?: (input: string) => string, + resultModifier?: ( + result: ParseResult + ) => ParseResult ) { if (!matcher.global) { throw new TypeError("argument 'matcher' must be a global regex."); @@ -27,6 +33,7 @@ export class Parser { this.#matcher = matcher; this.#matcherKeys = matcherKeys; this.#inputModifier = inputModifier; + this.#resultModifier = resultModifier; } /** @@ -62,7 +69,13 @@ export class Parser { ); for (const matchError of matches) { - results.push(this.#constructResult(matchError)); + let result = this.#constructResult(matchError); + + if (this.#resultModifier) { + result = this.#resultModifier({ ...result }); + } + + results.push(result); } return results; diff --git a/src/parsers/default-parser.ts b/src/parsers/default-parser.ts index 5dd9295..030918a 100644 --- a/src/parsers/default-parser.ts +++ b/src/parsers/default-parser.ts @@ -1,21 +1,69 @@ -import { Parser } from 'src/blueprints/Parser'; +import { type ParseResult, Parser } from 'src/blueprints/Parser'; + +const KEYS = [ + 'file', + 'line', + 'column', + 'errorCode', + 'message', + 'source', + 'sourceClean' +] as const; /** * Default `Parser` instance. */ export const defaultParser = new Parser( - /^(?:(?:(.*?)[:(](\d+)[:,](\d+)[)]? ?[:-] ?)|(?:error ?))(?:error ?)?(TS\d+)?(?:(?:: )|(?: - )|(?: ))(.*(?:\r?\n {2,}.*)*)(?:(?:\r?\n){2,}(\d+\s+(.*)\r?\n\s+~+))?$/gm, - [ - 'file', - 'line', - 'column', - 'errorCode', - 'message', - 'source', - 'sourceClean' - ] as const, + /^(?:(?:(.*?)[:(](\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, + KEYS, (input) => { // biome-ignore lint/suspicious/noControlCharactersInRegex: needed for removing colored text return input.replaceAll(/\x1b\[[0-9;]*m/g, ''); + }, + (result) => { + result._match = result._match.trimEnd(); + + if (result.source) { + result.source = result.source.trim(); + + const matches = Array.from( + result.source.trim().matchAll(/^(?:\d+)(\s.*)$/gm) + ); + + let minIndent: number; + + const codeLines = matches.reduce((acc, curr) => { + if (curr?.[1]) { + const indentLength = (curr[1].match(/^(\s+).*$/)?.[1] || '').length; + if (minIndent === undefined) { + minIndent = indentLength; + } else { + minIndent = indentLength < minIndent ? indentLength : minIndent; + } + acc.push(curr[1]); + } + return acc; + }, []); + + if (codeLines.length > 0) { + result.sourceClean = codeLines.reduce((acc, curr, currIndex) => { + let res = acc; + res += curr.slice(minIndent); + + if (currIndex !== codeLines.length - 1) { + res += '\n'; + } + + return res; + }, ''); + } + } + + return result; } ); + +/** + * Default `Parser` result type. + */ +export type DefaultParserResult = ParseResult;