Skip to content

Commit 177d00b

Browse files
TheLarkInnCopilot
andcommitted
Add problem matchers over preserved external output
Recover diagnostics from raw external output for @rushstack/reporter (#5858). - Preserve raw stdout and stderr as ordered chunks and run matchers over an ANSI-normalized copy so the raw evidence and process status are never modified - Add a tool- and version-scoped problem matcher registry gated on default enablement, routing older Heft versions through the version predicate - Add runProblemMatchers, which reassembles lines split across chunks, links recovered diagnostics to the operation and source location, preserves unmatched text, and caps duplicate diagnostics - Cover ANSI stripping, scoping, recovery, split chunks, the duplicate cap, and a representative corpus with tests Assistant-model: GitHub Copilot Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 897dcf7e-e6e8-4a84-85ca-34b93fa29be3
1 parent b2a4820 commit 177d00b

10 files changed

Lines changed: 548 additions & 1 deletion

File tree

common/reviews/api/reporter.api.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,12 @@ export interface IFileReporterOptions {
390390
readonly retentionDays?: number;
391391
}
392392

393+
// @beta
394+
export interface IGetMatchersOptions {
395+
readonly includeDisabled?: boolean;
396+
readonly version?: string;
397+
}
398+
393399
// @beta
394400
export interface IInteractiveTerminal {
395401
readonly columns: number;
@@ -480,6 +486,34 @@ export interface IPlaintextReporterOptions {
480486
readonly write: (text: string) => void;
481487
}
482488

489+
// @beta
490+
export interface IProblemMatch {
491+
readonly code?: string;
492+
readonly column?: number;
493+
readonly file?: string;
494+
readonly line?: number;
495+
readonly message: string;
496+
}
497+
498+
// @beta
499+
export interface IProblemMatcher {
500+
readonly enabledByDefault: boolean;
501+
extract(match: RegExpMatchArray): IProblemMatch;
502+
matchesVersion?(version: string): boolean;
503+
readonly name: string;
504+
readonly pattern: RegExp;
505+
readonly severity: RushDiagnosticSeverity;
506+
readonly tool: string;
507+
}
508+
509+
// @beta
510+
export interface IProblemMatcherResult {
511+
readonly diagnostics: readonly IRushDiagnostic[];
512+
readonly matchedLineCount: number;
513+
readonly suppressedDuplicateCount: number;
514+
readonly unmatchedLineCount: number;
515+
}
516+
483517
// @beta
484518
export interface IRenderLiveRegionOptions {
485519
readonly color: IColorizer;
@@ -685,6 +719,11 @@ export interface IResolveExitStatusOptions {
685719
readonly signal?: NodeJS.Signals;
686720
}
687721

722+
// @beta
723+
export interface IRunProblemMatchersOptions {
724+
readonly maxDuplicates?: number;
725+
}
726+
688727
// @beta
689728
export interface IRushDiagnostic {
690729
readonly category: RushDiagnosticCategory;
@@ -945,6 +984,9 @@ export class NdjsonRecordTooLargeError extends Error {
945984
// @beta
946985
export function negotiateReporterHello(hello: IReporterHello, options: IReporterHandshakeOptions): IReporterHandshakeResult;
947986

987+
// @beta
988+
export function normalizeAnsi(text: string): string;
989+
948990
// @beta
949991
export class OldEngineOutputAdapter {
950992
constructor(options: IOldEngineOutputAdapterOptions);
@@ -993,6 +1035,12 @@ export type PlaintextVariant = 'detailed' | 'concise';
9931035
// @beta
9941036
export function planAutomaticReporters(selection: IReporterSelection): IAutomaticReporterPlan;
9951037

1038+
// @beta
1039+
export class ProblemMatcherRegistry {
1040+
getMatchers(tool: string, options?: IGetMatchersOptions): IProblemMatcher[];
1041+
register(matcher: IProblemMatcher): void;
1042+
}
1043+
9961044
// @beta
9971045
export function readBootstrapHandoffFileAsync(filePath: string): Promise<unknown[]>;
9981046

@@ -1096,6 +1144,9 @@ export function resolveReporterCompatibility(frontend: IReporterFrontendDescript
10961144
// @beta
10971145
export function resolveReporterSelection(input: IReporterSelectionInput): IReporterSelection;
10981146

1147+
// @beta
1148+
export function runProblemMatchers(events: readonly IReporterEventEnvelope<unknown>[], matchers: readonly IProblemMatcher[], options?: IRunProblemMatchersOptions): IProblemMatcherResult;
1149+
10991150
// @beta
11001151
export const RUSH_DIAGNOSTIC_CODE_DEFINITIONS: readonly IRushDiagnosticCodeDefinition[];
11011152

libraries/reporter/src/diagnostics/RushDiagnosticCodeRegistry.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@ export const RUSH_DIAGNOSTIC_CODE_DEFINITIONS: readonly IRushDiagnosticCodeDefin
110110
defaultSeverity: 'error',
111111
summaryKey: 'diagnostic.RUSH_OPERATION_FAILED.summary'
112112
},
113+
{
114+
code: 'RUSH_EXTERNAL_TOOL_PROBLEM',
115+
category: 'operation',
116+
defaultSeverity: 'error',
117+
summaryKey: 'diagnostic.RUSH_EXTERNAL_TOOL_PROBLEM.summary'
118+
},
113119
{
114120
code: 'RUSH_PROTOCOL_UPDATE_REQUIRED',
115121
category: 'environment',
@@ -162,6 +168,7 @@ export const RUSH_DIAGNOSTIC_TEMPLATES: { readonly [resourceKey: string]: string
162168
'diagnostic.RUSH_NETWORK_AUTH_UNAUTHORIZED.summary':
163169
'Authentication failed for the registry {registryUrl}.',
164170
'diagnostic.RUSH_OPERATION_FAILED.summary': 'The operation for {projectName} failed.',
171+
'diagnostic.RUSH_EXTERNAL_TOOL_PROBLEM.summary': 'The tool {tool} reported {code}: {message}',
165172
'diagnostic.RUSH_PROTOCOL_UPDATE_REQUIRED.summary':
166173
'A reporter protocol feature required by {producerVersion} is not supported by this Rush.',
167174
'diagnostic.RUSH_PROTOCOL_UPDATE_REQUIRED.detail':

libraries/reporter/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,13 @@ export { OperationStreamEmitter } from './scheduler/OperationStreamEmitter';
246246
export type { IExternalOutputChunk } from './scheduler/OperationOutputGrouping';
247247
export { iterateExternalOutput, regroupOperationOutput } from './scheduler/OperationOutputGrouping';
248248

249+
export { normalizeAnsi } from './matchers/AnsiNormalization';
250+
export type { IProblemMatch, IProblemMatcher } from './matchers/ProblemMatcher';
251+
export type { IGetMatchersOptions } from './matchers/ProblemMatcherRegistry';
252+
export { ProblemMatcherRegistry } from './matchers/ProblemMatcherRegistry';
253+
export type { IRunProblemMatchersOptions, IProblemMatcherResult } from './matchers/ProblemMatcherRunner';
254+
export { runProblemMatchers } from './matchers/ProblemMatcherRunner';
255+
249256
export type { IReporterEmitEventInput, IReporterEventSink } from './producers/IReporterEventSink';
250257
export type {
251258
ReporterMessageSeverity,
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
// eslint-disable-next-line no-control-regex
5+
const ANSI_ESCAPE_REGEXP: RegExp = /\u001b\[[0-9;]*[A-Za-z]/g;
6+
7+
/**
8+
* Removes ANSI escape sequences from text for problem matching.
9+
*
10+
* @remarks
11+
* Normalization is applied only to the copy that matchers process; the raw
12+
* output is preserved unchanged.
13+
*
14+
* @param text - the raw text
15+
*
16+
* @beta
17+
*/
18+
export function normalizeAnsi(text: string): string {
19+
return text.replace(ANSI_ESCAPE_REGEXP, '');
20+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
import type { RushDiagnosticSeverity } from '../diagnostics/IRushDiagnostic';
5+
6+
/**
7+
* A structured problem extracted from a matched output line.
8+
*
9+
* @beta
10+
*/
11+
export interface IProblemMatch {
12+
/**
13+
* The tool's own problem code, such as `TS1005`.
14+
*/
15+
readonly code?: string;
16+
17+
/**
18+
* The human-readable message.
19+
*/
20+
readonly message: string;
21+
22+
/**
23+
* The file the problem refers to.
24+
*/
25+
readonly file?: string;
26+
27+
/**
28+
* The 1-based line number.
29+
*/
30+
readonly line?: number;
31+
32+
/**
33+
* The 1-based column number.
34+
*/
35+
readonly column?: number;
36+
}
37+
38+
/**
39+
* A tool- and version-scoped problem matcher.
40+
*
41+
* @remarks
42+
* A matcher never modifies raw output or process status. It is enabled by
43+
* default only after high-confidence corpus tests pass.
44+
*
45+
* @beta
46+
*/
47+
export interface IProblemMatcher {
48+
/**
49+
* A unique matcher name.
50+
*/
51+
readonly name: string;
52+
53+
/**
54+
* The tool the matcher applies to, such as `tsc`.
55+
*/
56+
readonly tool: string;
57+
58+
/**
59+
* The severity of the produced diagnostic.
60+
*/
61+
readonly severity: RushDiagnosticSeverity;
62+
63+
/**
64+
* The per-line pattern.
65+
*/
66+
readonly pattern: RegExp;
67+
68+
/**
69+
* Whether the matcher is enabled in default runs. Requires corpus validation.
70+
*/
71+
readonly enabledByDefault: boolean;
72+
73+
/**
74+
* Returns whether the matcher applies to a tool version. When omitted, the
75+
* matcher applies to every version.
76+
*/
77+
matchesVersion?(version: string): boolean;
78+
79+
/**
80+
* Extracts the structured problem from a pattern match.
81+
*/
82+
extract(match: RegExpMatchArray): IProblemMatch;
83+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
import type { IProblemMatcher } from './ProblemMatcher';
5+
6+
/**
7+
* Options for {@link ProblemMatcherRegistry.getMatchers}.
8+
*
9+
* @beta
10+
*/
11+
export interface IGetMatchersOptions {
12+
/**
13+
* The tool version, used to scope matchers.
14+
*/
15+
readonly version?: string;
16+
17+
/**
18+
* Whether to include matchers that are not enabled by default.
19+
*/
20+
readonly includeDisabled?: boolean;
21+
}
22+
23+
/**
24+
* A registry of tool- and version-scoped problem matchers.
25+
*
26+
* @remarks
27+
* By default only matchers enabled after corpus validation are returned. Older
28+
* Heft versions are routed through this path by registering matchers whose
29+
* version predicate covers them.
30+
*
31+
* @beta
32+
*/
33+
export class ProblemMatcherRegistry {
34+
private readonly _matchers: IProblemMatcher[] = [];
35+
36+
/**
37+
* Registers a matcher.
38+
*/
39+
public register(matcher: IProblemMatcher): void {
40+
this._matchers.push(matcher);
41+
}
42+
43+
/**
44+
* Returns the matchers that apply to a tool and version.
45+
*/
46+
public getMatchers(tool: string, options: IGetMatchersOptions = {}): IProblemMatcher[] {
47+
return this._matchers.filter((matcher: IProblemMatcher) => {
48+
if (matcher.tool !== tool) {
49+
return false;
50+
}
51+
if (!options.includeDisabled && !matcher.enabledByDefault) {
52+
return false;
53+
}
54+
if (
55+
options.version !== undefined &&
56+
matcher.matchesVersion !== undefined &&
57+
!matcher.matchesVersion(options.version)
58+
) {
59+
return false;
60+
}
61+
return true;
62+
});
63+
}
64+
}

0 commit comments

Comments
 (0)