Skip to content

Commit d2ad3ca

Browse files
authored
Improve diagnostics deduplication 2 (#58318)
1 parent 3358157 commit d2ad3ca

15 files changed

+310
-27
lines changed

src/compiler/checker.ts

+20-2
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ import {
251251
getAssignmentDeclarationKind,
252252
getAssignmentDeclarationPropertyAccessKind,
253253
getAssignmentTargetKind,
254+
getCanonicalDiagnostic,
254255
getCheckFlags,
255256
getClassExtendsHeritageElement,
256257
getClassLikeDeclarationOfSymbol,
@@ -3118,10 +3119,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
31183119
if (suggestion) {
31193120
const suggestionName = symbolToString(suggestion);
31203121
const isUncheckedJS = isUncheckedJSSuggestion(errorLocation, suggestion, /*excludeClasses*/ false);
3121-
const message = meaning === SymbolFlags.Namespace || nameArg && typeof nameArg !== "string" && nodeIsSynthesized(nameArg) ? Diagnostics.Cannot_find_namespace_0_Did_you_mean_1
3122+
const message = meaning === SymbolFlags.Namespace ||
3123+
nameArg && typeof nameArg !== "string" && nodeIsSynthesized(nameArg) ?
3124+
Diagnostics.Cannot_find_namespace_0_Did_you_mean_1
31223125
: isUncheckedJS ? Diagnostics.Could_not_find_name_0_Did_you_mean_1
31233126
: Diagnostics.Cannot_find_name_0_Did_you_mean_1;
31243127
const diagnostic = createError(errorLocation, message, diagnosticName(nameArg), suggestionName);
3128+
diagnostic.canonicalHead = getCanonicalDiagnostic(nameNotFoundMessage, diagnosticName(nameArg));
31253129
addErrorOrSuggestion(!isUncheckedJS, diagnostic);
31263130
if (suggestion.valueDeclaration) {
31273131
addRelatedInfo(
@@ -21697,9 +21701,23 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2169721701
else {
2169821702
errorInfo = elaborateNeverIntersection(errorInfo, originalTarget);
2169921703
}
21704+
// Used by, eg, missing property checking to replace the top-level message with a more informative one.
2170021705
if (!headMessage && maybeSuppress) {
21706+
// We suppress a call to `reportRelationError` or not depending on the state of the type checker, so
21707+
// we call `reportRelationError` here and then undo its effects to figure out what would be the diagnostic
21708+
// if we hadn't supress it, and save that as a canonical diagnostic for deduplication purposes.
21709+
const savedErrorState = captureErrorCalculationState();
21710+
reportRelationError(headMessage, source, target);
21711+
let canonical;
21712+
if (errorInfo && errorInfo !== savedErrorState.errorInfo) {
21713+
canonical = { code: errorInfo.code, messageText: errorInfo.messageText };
21714+
}
21715+
resetErrorInfo(savedErrorState);
21716+
if (canonical && errorInfo) {
21717+
errorInfo.canonicalHead = canonical;
21718+
}
21719+
2170121720
lastSkippedInfo = [source, target];
21702-
// Used by, eg, missing property checking to replace the top-level message with a more informative one
2170321721
return;
2170421722
}
2170521723
reportRelationError(headMessage, source, target);

src/compiler/types.ts

+17
Original file line numberDiff line numberDiff line change
@@ -7069,6 +7069,8 @@ export interface DiagnosticMessageChain {
70697069
next?: DiagnosticMessageChain[];
70707070
/** @internal */
70717071
repopulateInfo?: () => RepopulateDiagnosticChainInfo;
7072+
/** @internal */
7073+
canonicalHead?: CanonicalDiagnostic;
70727074
}
70737075

70747076
export interface Diagnostic extends DiagnosticRelatedInformation {
@@ -7079,6 +7081,21 @@ export interface Diagnostic extends DiagnosticRelatedInformation {
70797081
source?: string;
70807082
relatedInformation?: DiagnosticRelatedInformation[];
70817083
/** @internal */ skippedOn?: keyof CompilerOptions;
7084+
/**
7085+
* @internal
7086+
* Used for deduplication and comparison.
7087+
* Whenever it is possible for two diagnostics that report the same problem to be produced with
7088+
* different messages (e.g. "Cannot find name 'foo'" vs "Cannot find name 'foo'. Did you mean 'bar'?"),
7089+
* this property can be set to a canonical message,
7090+
* so that those two diagnostics are appropriately considered to be the same.
7091+
*/
7092+
canonicalHead?: CanonicalDiagnostic;
7093+
}
7094+
7095+
/** @internal */
7096+
export interface CanonicalDiagnostic {
7097+
code: number;
7098+
messageText: string;
70827099
}
70837100

70847101
/** @internal */

src/compiler/utilities.ts

+52-15
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
canHaveLocals,
4242
canHaveModifiers,
4343
type CanHaveModuleSpecifier,
44+
CanonicalDiagnostic,
4445
CaseBlock,
4546
CaseClause,
4647
CaseOrDefaultClause,
@@ -2178,6 +2179,7 @@ export function createFileDiagnosticFromMessageChain(file: SourceFile, start: nu
21782179
category: messageChain.category,
21792180
messageText: messageChain.next ? messageChain : messageChain.messageText,
21802181
relatedInformation,
2182+
canonicalHead: messageChain.canonicalHead,
21812183
};
21822184
}
21832185

@@ -2216,6 +2218,14 @@ export function createDiagnosticForRange(sourceFile: SourceFile, range: TextRang
22162218
};
22172219
}
22182220

2221+
/** @internal */
2222+
export function getCanonicalDiagnostic(message: DiagnosticMessage, ...args: string[]): CanonicalDiagnostic {
2223+
return {
2224+
code: message.code,
2225+
messageText: formatMessage(message, ...args),
2226+
};
2227+
}
2228+
22192229
/** @internal */
22202230
export function getSpanOfTokenAtPosition(sourceFile: SourceFile, pos: number): TextSpan {
22212231
const scanner = createScanner(sourceFile.languageVersion, /*skipTrivia*/ true, sourceFile.languageVariant, sourceFile.text, /*onError*/ undefined, pos);
@@ -8540,11 +8550,13 @@ export function compareDiagnostics(d1: Diagnostic, d2: Diagnostic): Comparison {
85408550

85418551
/** @internal */
85428552
export function compareDiagnosticsSkipRelatedInformation(d1: Diagnostic, d2: Diagnostic): Comparison {
8553+
const code1 = getDiagnosticCode(d1);
8554+
const code2 = getDiagnosticCode(d2);
85438555
return compareStringsCaseSensitive(getDiagnosticFilePath(d1), getDiagnosticFilePath(d2)) ||
85448556
compareValues(d1.start, d2.start) ||
85458557
compareValues(d1.length, d2.length) ||
8546-
compareValues(d1.code, d2.code) ||
8547-
compareMessageText(d1.messageText, d2.messageText) ||
8558+
compareValues(code1, code2) ||
8559+
compareMessageText(d1, d2) ||
85488560
Comparison.EqualTo;
85498561
}
85508562

@@ -8566,25 +8578,38 @@ function compareRelatedInformation(d1: Diagnostic, d2: Diagnostic): Comparison {
85668578
// An diagnostic message with more elaboration should be considered *less than* a diagnostic message
85678579
// with less elaboration that is otherwise similar.
85688580
function compareMessageText(
8569-
t1: string | Pick<DiagnosticMessageChain, "messageText" | "next">,
8570-
t2: string | Pick<DiagnosticMessageChain, "messageText" | "next">,
8581+
d1: Diagnostic,
8582+
d2: Diagnostic,
85718583
): Comparison {
8572-
if (typeof t1 === "string" && typeof t2 === "string") {
8573-
return compareStringsCaseSensitive(t1, t2);
8584+
let headMsg1 = getDiagnosticMessage(d1);
8585+
let headMsg2 = getDiagnosticMessage(d2);
8586+
if (typeof headMsg1 !== "string") {
8587+
headMsg1 = headMsg1.messageText;
85748588
}
8575-
8576-
if (typeof t1 === "string") {
8577-
t1 = { messageText: t1 };
8589+
if (typeof headMsg2 !== "string") {
8590+
headMsg2 = headMsg2.messageText;
85788591
}
8579-
if (typeof t2 === "string") {
8580-
t2 = { messageText: t2 };
8592+
const chain1 = typeof d1.messageText !== "string" ? d1.messageText.next : undefined;
8593+
const chain2 = typeof d2.messageText !== "string" ? d2.messageText.next : undefined;
8594+
8595+
let res = compareStringsCaseSensitive(headMsg1, headMsg2);
8596+
if (res) {
8597+
return res;
85818598
}
8582-
const res = compareStringsCaseSensitive(t1.messageText, t2.messageText);
8599+
8600+
res = compareMessageChain(chain1, chain2);
85838601
if (res) {
85848602
return res;
85858603
}
85868604

8587-
return compareMessageChain(t1.next, t2.next);
8605+
if (d1.canonicalHead && !d2.canonicalHead) {
8606+
return Comparison.LessThan;
8607+
}
8608+
if (d2.canonicalHead && !d1.canonicalHead) {
8609+
return Comparison.GreaterThan;
8610+
}
8611+
8612+
return Comparison.EqualTo;
85888613
}
85898614

85908615
// First compare by size of the message chain,
@@ -8659,11 +8684,23 @@ function compareMessageChainContent(
86598684

86608685
/** @internal */
86618686
export function diagnosticsEqualityComparer(d1: Diagnostic, d2: Diagnostic): boolean {
8687+
const code1 = getDiagnosticCode(d1);
8688+
const code2 = getDiagnosticCode(d2);
8689+
const msg1 = getDiagnosticMessage(d1);
8690+
const msg2 = getDiagnosticMessage(d2);
86628691
return compareStringsCaseSensitive(getDiagnosticFilePath(d1), getDiagnosticFilePath(d2)) === Comparison.EqualTo &&
86638692
compareValues(d1.start, d2.start) === Comparison.EqualTo &&
86648693
compareValues(d1.length, d2.length) === Comparison.EqualTo &&
8665-
compareValues(d1.code, d2.code) === Comparison.EqualTo &&
8666-
messageTextEqualityComparer(d1.messageText, d2.messageText);
8694+
compareValues(code1, code2) === Comparison.EqualTo &&
8695+
messageTextEqualityComparer(msg1, msg2);
8696+
}
8697+
8698+
function getDiagnosticCode(d: Diagnostic): number {
8699+
return d.canonicalHead?.code || d.code;
8700+
}
8701+
8702+
function getDiagnosticMessage(d: Diagnostic): string | DiagnosticMessageChain {
8703+
return d.canonicalHead?.messageText || d.messageText;
86678704
}
86688705

86698706
function messageTextEqualityComparer(m1: string | DiagnosticMessageChain, m2: string | DiagnosticMessageChain): boolean {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
duplicateErrorAssignability.ts(10,11): error TS2741: Property 'x' is missing in type 'B' but required in type 'A'.
2+
duplicateErrorAssignability.ts(12,5): error TS2538: Type 'B' cannot be used as an index type.
3+
4+
5+
==== duplicateErrorAssignability.ts (2 errors) ====
6+
interface A {
7+
x: number;
8+
}
9+
interface B {
10+
y: string;
11+
}
12+
13+
declare let b: B;
14+
declare let a: A;
15+
const x = a = b;
16+
~
17+
!!! error TS2741: Property 'x' is missing in type 'B' but required in type 'A'.
18+
!!! related TS2728 duplicateErrorAssignability.ts:2:5: 'x' is declared here.
19+
let obj: { 3: string } = { 3: "three" };
20+
obj[x];
21+
~
22+
!!! error TS2538: Type 'B' cannot be used as an index type.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//// [tests/cases/compiler/duplicateErrorAssignability.ts] ////
2+
3+
//// [duplicateErrorAssignability.ts]
4+
interface A {
5+
x: number;
6+
}
7+
interface B {
8+
y: string;
9+
}
10+
11+
declare let b: B;
12+
declare let a: A;
13+
const x = a = b;
14+
let obj: { 3: string } = { 3: "three" };
15+
obj[x];
16+
17+
//// [duplicateErrorAssignability.js]
18+
"use strict";
19+
var x = a = b;
20+
var obj = { 3: "three" };
21+
obj[x];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//// [tests/cases/compiler/duplicateErrorAssignability.ts] ////
2+
3+
=== duplicateErrorAssignability.ts ===
4+
interface A {
5+
>A : Symbol(A, Decl(duplicateErrorAssignability.ts, 0, 0))
6+
7+
x: number;
8+
>x : Symbol(A.x, Decl(duplicateErrorAssignability.ts, 0, 13))
9+
}
10+
interface B {
11+
>B : Symbol(B, Decl(duplicateErrorAssignability.ts, 2, 1))
12+
13+
y: string;
14+
>y : Symbol(B.y, Decl(duplicateErrorAssignability.ts, 3, 13))
15+
}
16+
17+
declare let b: B;
18+
>b : Symbol(b, Decl(duplicateErrorAssignability.ts, 7, 11))
19+
>B : Symbol(B, Decl(duplicateErrorAssignability.ts, 2, 1))
20+
21+
declare let a: A;
22+
>a : Symbol(a, Decl(duplicateErrorAssignability.ts, 8, 11))
23+
>A : Symbol(A, Decl(duplicateErrorAssignability.ts, 0, 0))
24+
25+
const x = a = b;
26+
>x : Symbol(x, Decl(duplicateErrorAssignability.ts, 9, 5))
27+
>a : Symbol(a, Decl(duplicateErrorAssignability.ts, 8, 11))
28+
>b : Symbol(b, Decl(duplicateErrorAssignability.ts, 7, 11))
29+
30+
let obj: { 3: string } = { 3: "three" };
31+
>obj : Symbol(obj, Decl(duplicateErrorAssignability.ts, 10, 3))
32+
>3 : Symbol(3, Decl(duplicateErrorAssignability.ts, 10, 10))
33+
>3 : Symbol(3, Decl(duplicateErrorAssignability.ts, 10, 26))
34+
35+
obj[x];
36+
>obj : Symbol(obj, Decl(duplicateErrorAssignability.ts, 10, 3))
37+
>x : Symbol(x, Decl(duplicateErrorAssignability.ts, 9, 5))
38+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//// [tests/cases/compiler/duplicateErrorAssignability.ts] ////
2+
3+
=== duplicateErrorAssignability.ts ===
4+
interface A {
5+
x: number;
6+
>x : number
7+
> : ^^^^^^
8+
}
9+
interface B {
10+
y: string;
11+
>y : string
12+
> : ^^^^^^
13+
}
14+
15+
declare let b: B;
16+
>b : B
17+
> : ^
18+
19+
declare let a: A;
20+
>a : A
21+
> : ^
22+
23+
const x = a = b;
24+
>x : B
25+
> : ^
26+
>a = b : B
27+
> : ^
28+
>a : A
29+
> : ^
30+
>b : B
31+
> : ^
32+
33+
let obj: { 3: string } = { 3: "three" };
34+
>obj : { 3: string; }
35+
> : ^^^^^ ^^^
36+
>3 : string
37+
> : ^^^^^^
38+
>{ 3: "three" } : { 3: string; }
39+
> : ^^^^^^^^^^^^^^
40+
>3 : string
41+
> : ^^^^^^
42+
>"three" : "three"
43+
> : ^^^^^^^
44+
45+
obj[x];
46+
>obj[x] : any
47+
> : ^^^
48+
>obj : { 3: string; }
49+
> : ^^^^^^^^^^^^^^
50+
>x : B
51+
> : ^
52+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
duplicateErrorNameNotFound.ts(4,5): error TS2552: Cannot find name 'RoomInterface'. Did you mean 'RoomInterfae'?
2+
3+
4+
==== duplicateErrorNameNotFound.ts (1 errors) ====
5+
type RoomInterfae = {};
6+
7+
export type {
8+
RoomInterface
9+
~~~~~~~~~~~~~
10+
!!! error TS2552: Cannot find name 'RoomInterface'. Did you mean 'RoomInterfae'?
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//// [tests/cases/compiler/duplicateErrorNameNotFound.ts] ////
2+
3+
//// [duplicateErrorNameNotFound.ts]
4+
type RoomInterfae = {};
5+
6+
export type {
7+
RoomInterface
8+
}
9+
10+
//// [duplicateErrorNameNotFound.js]
11+
"use strict";
12+
Object.defineProperty(exports, "__esModule", { value: true });
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//// [tests/cases/compiler/duplicateErrorNameNotFound.ts] ////
2+
3+
=== duplicateErrorNameNotFound.ts ===
4+
type RoomInterfae = {};
5+
>RoomInterfae : Symbol(RoomInterfae, Decl(duplicateErrorNameNotFound.ts, 0, 0))
6+
7+
export type {
8+
RoomInterface
9+
>RoomInterface : Symbol(RoomInterface, Decl(duplicateErrorNameNotFound.ts, 2, 13))
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//// [tests/cases/compiler/duplicateErrorNameNotFound.ts] ////
2+
3+
=== duplicateErrorNameNotFound.ts ===
4+
type RoomInterfae = {};
5+
>RoomInterfae : RoomInterfae
6+
> : ^^^^^^^^^^^^
7+
8+
export type {
9+
RoomInterface
10+
>RoomInterface : any
11+
> : ^^^
12+
}

tests/baselines/reference/mappedTypeRecursiveInference.types

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
Assignability cache: 5,000
55
Type Count: 10,000
66
Instantiation count: 250,000
7-
Symbol count: 100,000
7+
Symbol count: 250,000
88

99
=== mappedTypeRecursiveInference.ts ===
1010
interface A { a: A }

0 commit comments

Comments
 (0)