Skip to content

Commit 11eee2b

Browse files
authored
Slightly improve missing property errors (#28298)
* Slightly improve missing property errors * Add missing quote * Fix jsx case * Add related span * Fix crash (why can declarations be undefined) * Only skip top elaboration when no variant message is provided
1 parent eb21eb8 commit 11eee2b

File tree

237 files changed

+1494
-1856
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

237 files changed

+1494
-1856
lines changed

src/compiler/checker.ts

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,8 @@ namespace ts {
434434
const numberOrBigIntType = getUnionType([numberType, bigintType]);
435435

436436
const emptyObjectType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined);
437+
const emptyJsxObjectType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined);
438+
emptyJsxObjectType.objectFlags |= ObjectFlags.JsxAttributes;
437439

438440
const emptyTypeLiteralSymbol = createSymbol(SymbolFlags.TypeLiteral, InternalSymbolName.Type);
439441
emptyTypeLiteralSymbol.members = createSymbolTable();
@@ -11407,13 +11409,15 @@ namespace ts {
1140711409
): boolean {
1140811410

1140911411
let errorInfo: DiagnosticMessageChain | undefined;
11412+
let relatedInfo: [DiagnosticRelatedInformation, ...DiagnosticRelatedInformation[]] | undefined;
1141011413
let maybeKeys: string[];
1141111414
let sourceStack: Type[];
1141211415
let targetStack: Type[];
1141311416
let maybeCount = 0;
1141411417
let depth = 0;
1141511418
let expandingFlags = ExpandingFlags.None;
1141611419
let overflow = false;
11420+
let suppressNextError = false;
1141711421

1141811422
Debug.assert(relation !== identityRelation || !errorNode, "no error reporting in identity checking");
1141911423

@@ -11444,16 +11448,29 @@ namespace ts {
1144411448
}
1144511449

1144611450
const diag = createDiagnosticForNodeFromMessageChain(errorNode!, errorInfo, relatedInformation);
11451+
if (relatedInfo) {
11452+
addRelatedInfo(diag, ...relatedInfo);
11453+
}
1144711454
if (errorOutputContainer) {
1144811455
errorOutputContainer.error = diag;
1144911456
}
1145011457
diagnostics.add(diag); // TODO: GH#18217
1145111458
}
1145211459
return result !== Ternary.False;
1145311460

11454-
function reportError(message: DiagnosticMessage, arg0?: string, arg1?: string, arg2?: string): void {
11461+
function reportError(message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): void {
1145511462
Debug.assert(!!errorNode);
11456-
errorInfo = chainDiagnosticMessages(errorInfo, message, arg0, arg1, arg2);
11463+
errorInfo = chainDiagnosticMessages(errorInfo, message, arg0, arg1, arg2, arg3);
11464+
}
11465+
11466+
function associateRelatedInfo(info: DiagnosticRelatedInformation) {
11467+
Debug.assert(!!errorInfo);
11468+
if (!relatedInfo) {
11469+
relatedInfo = [info];
11470+
}
11471+
else {
11472+
relatedInfo.push(info);
11473+
}
1145711474
}
1145811475

1145911476
function reportRelationError(message: DiagnosticMessage | undefined, source: Type, target: Type) {
@@ -11661,13 +11678,15 @@ namespace ts {
1166111678
}
1166211679

1166311680
if (!result && reportErrors) {
11681+
const maybeSuppress = suppressNextError;
11682+
suppressNextError = false;
1166411683
if (source.flags & TypeFlags.Object && target.flags & TypeFlags.Primitive) {
1166511684
tryElaborateErrorsForPrimitivesAndObjects(source, target);
1166611685
}
1166711686
else if (source.symbol && source.flags & TypeFlags.Object && globalObjectType === source) {
1166811687
reportError(Diagnostics.The_Object_type_is_assignable_to_very_few_other_types_Did_you_mean_to_use_the_any_type_instead);
1166911688
}
11670-
else if (getObjectFlags(source) & ObjectFlags.JsxAttributes && target.flags & TypeFlags.Intersection) {
11689+
else if (isComparingJsxAttributes && target.flags & TypeFlags.Intersection) {
1167111690
const targetTypes = (target as IntersectionType).types;
1167211691
const intrinsicAttributes = getJsxType(JsxNames.IntrinsicAttributes, errorNode);
1167311692
const intrinsicClassAttributes = getJsxType(JsxNames.IntrinsicClassAttributes, errorNode);
@@ -11677,6 +11696,10 @@ namespace ts {
1167711696
return result;
1167811697
}
1167911698
}
11699+
if (!headMessage && maybeSuppress) {
11700+
// Used by, eg, missing property checking to replace the top-level message with a more informative one
11701+
return result;
11702+
}
1168011703
reportRelationError(headMessage, source, target);
1168111704
}
1168211705
return result;
@@ -12310,7 +12333,24 @@ namespace ts {
1231012333
const unmatchedProperty = getUnmatchedProperty(source, target, requireOptionalProperties);
1231112334
if (unmatchedProperty) {
1231212335
if (reportErrors) {
12313-
reportError(Diagnostics.Property_0_is_missing_in_type_1, symbolToString(unmatchedProperty), typeToString(source));
12336+
const props = arrayFrom(getUnmatchedProperties(source, target, requireOptionalProperties));
12337+
if (!headMessage || (headMessage.code !== Diagnostics.Class_0_incorrectly_implements_interface_1.code &&
12338+
headMessage.code !== Diagnostics.Class_0_incorrectly_implements_class_1_Did_you_mean_to_extend_1_and_inherit_its_members_as_a_subclass.code)) {
12339+
suppressNextError = true; // Retain top-level error for interface implementing issues, otherwise omit it
12340+
}
12341+
if (props.length === 1) {
12342+
const propName = symbolToString(unmatchedProperty);
12343+
reportError(Diagnostics.Property_0_is_missing_in_type_1_but_required_in_type_2, propName, typeToString(source), typeToString(target));
12344+
if (length(unmatchedProperty.declarations)) {
12345+
associateRelatedInfo(createDiagnosticForNode(unmatchedProperty.declarations[0], Diagnostics._0_is_declared_here, propName));
12346+
}
12347+
}
12348+
else if (props.length > 5) { // arbitrary cutoff for too-long list form
12349+
reportError(Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2_and_3_more, typeToString(source), typeToString(target), map(props.slice(0, 4), p => symbolToString(p)).join(", "), props.length - 4);
12350+
}
12351+
else {
12352+
reportError(Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2, typeToString(source), typeToString(target), map(props, p => symbolToString(p)).join(", "));
12353+
}
1231412354
}
1231512355
return Ternary.False;
1231612356
}
@@ -13701,17 +13741,20 @@ namespace ts {
1370113741
return getTypeFromInference(inference);
1370213742
}
1370313743

13704-
function getUnmatchedProperty(source: Type, target: Type, requireOptionalProperties: boolean) {
13744+
function* getUnmatchedProperties(source: Type, target: Type, requireOptionalProperties: boolean) {
1370513745
const properties = target.flags & TypeFlags.Intersection ? getPropertiesOfUnionOrIntersectionType(<IntersectionType>target) : getPropertiesOfObjectType(target);
1370613746
for (const targetProp of properties) {
1370713747
if (requireOptionalProperties || !(targetProp.flags & SymbolFlags.Optional)) {
1370813748
const sourceProp = getPropertyOfType(source, targetProp.escapedName);
1370913749
if (!sourceProp) {
13710-
return targetProp;
13750+
yield targetProp;
1371113751
}
1371213752
}
1371313753
}
13714-
return undefined;
13754+
}
13755+
13756+
function getUnmatchedProperty(source: Type, target: Type, requireOptionalProperties: boolean): Symbol | undefined {
13757+
return getUnmatchedProperties(source, target, requireOptionalProperties).next().value;
1371513758
}
1371613759

1371713760
function tupleTypesDefinitelyUnrelated(source: TupleTypeReference, target: TupleTypeReference) {
@@ -17914,7 +17957,7 @@ namespace ts {
1791417957
function createJsxAttributesTypeFromAttributesProperty(openingLikeElement: JsxOpeningLikeElement, checkMode: CheckMode | undefined) {
1791517958
const attributes = openingLikeElement.attributes;
1791617959
let attributesTable = createSymbolTable();
17917-
let spread: Type = emptyObjectType;
17960+
let spread: Type = emptyJsxObjectType;
1791817961
let hasSpreadAnyType = false;
1791917962
let typeToIntersect: Type | undefined;
1792017963
let explicitlySpecifyChildrenAttribute = false;
@@ -17998,10 +18041,10 @@ namespace ts {
1799818041
if (hasSpreadAnyType) {
1799918042
return anyType;
1800018043
}
18001-
if (typeToIntersect && spread !== emptyObjectType) {
18044+
if (typeToIntersect && spread !== emptyJsxObjectType) {
1800218045
return getIntersectionType([typeToIntersect, spread]);
1800318046
}
18004-
return typeToIntersect || (spread === emptyObjectType ? createJsxAttributesType() : spread);
18047+
return typeToIntersect || (spread === emptyJsxObjectType ? createJsxAttributesType() : spread);
1800518048

1800618049
/**
1800718050
* Create anonymous type from given attributes symbol table.

src/compiler/core.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1268,9 +1268,9 @@ namespace ts {
12681268
}
12691269

12701270
/** Shims `Array.from`. */
1271-
export function arrayFrom<T, U>(iterator: Iterator<T>, map: (t: T) => U): U[];
1272-
export function arrayFrom<T>(iterator: Iterator<T>): T[];
1273-
export function arrayFrom(iterator: Iterator<any>, map?: (t: any) => any): any[] {
1271+
export function arrayFrom<T, U>(iterator: Iterator<T> | IterableIterator<T>, map: (t: T) => U): U[];
1272+
export function arrayFrom<T>(iterator: Iterator<T> | IterableIterator<T>): T[];
1273+
export function arrayFrom(iterator: Iterator<any> | IterableIterator<any>, map?: (t: any) => any): any[] {
12741274
const result: any[] = [];
12751275
for (let { value, done } = iterator.next(); !done; { value, done } = iterator.next()) {
12761276
result.push(map ? map(value) : value);

src/compiler/diagnosticMessages.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2513,6 +2513,18 @@
25132513
"category": "Message",
25142514
"code": 2738
25152515
},
2516+
"Type '{0}' is missing the following properties from type '{1}': {2}": {
2517+
"category": "Error",
2518+
"code": 2739
2519+
},
2520+
"Type '{0}' is missing the following properties from type '{1}': {2}, and {3} more.": {
2521+
"category": "Error",
2522+
"code": 2740
2523+
},
2524+
"Property '{0}' is missing in type '{1}' but required in type '{2}'.": {
2525+
"category": "Error",
2526+
"code": 2741
2527+
},
25162528

25172529
"Import declaration '{0}' is using private name '{1}'.": {
25182530
"category": "Error",

src/compiler/utilities.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6980,8 +6980,8 @@ namespace ts {
69806980
getSourceMapSourceConstructor: () => <any>SourceMapSource,
69816981
};
69826982

6983-
export function formatStringFromArgs(text: string, args: ArrayLike<string>, baseIndex = 0): string {
6984-
return text.replace(/{(\d+)}/g, (_match, index: string) => Debug.assertDefined(args[+index + baseIndex]));
6983+
export function formatStringFromArgs(text: string, args: ArrayLike<string | number>, baseIndex = 0): string {
6984+
return text.replace(/{(\d+)}/g, (_match, index: string) => "" + Debug.assertDefined(args[+index + baseIndex]));
69856985
}
69866986

69876987
export let localizedDiagnosticMessages: MapLike<string> | undefined;
@@ -7060,7 +7060,7 @@ namespace ts {
70607060
};
70617061
}
70627062

7063-
export function chainDiagnosticMessages(details: DiagnosticMessageChain | undefined, message: DiagnosticMessage, ...args: (string | undefined)[]): DiagnosticMessageChain;
7063+
export function chainDiagnosticMessages(details: DiagnosticMessageChain | undefined, message: DiagnosticMessage, ...args: (string | number | undefined)[]): DiagnosticMessageChain;
70647064
export function chainDiagnosticMessages(details: DiagnosticMessageChain | undefined, message: DiagnosticMessage): DiagnosticMessageChain {
70657065
let text = getLocaleSpecificMessage(message);
70667066

src/services/codefixes/fixAddMissingMember.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ namespace ts.codefix {
44
const errorCodes = [
55
Diagnostics.Property_0_does_not_exist_on_type_1.code,
66
Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2.code,
7+
Diagnostics.Property_0_is_missing_in_type_1_but_required_in_type_2.code,
8+
Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2.code,
9+
Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2_and_3_more.code
710
];
811
const fixId = "addMissingMember";
912
registerCodeFix({

tests/baselines/reference/argumentExpressionContextualTyping.errors.txt

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
tests/cases/conformance/expressions/contextualTyping/argumentExpressionContextualTyping.ts(16,5): error TS2345: Argument of type '(string | number | boolean)[]' is not assignable to parameter of type '[string, number, boolean]'.
2-
Property '0' is missing in type '(string | number | boolean)[]'.
2+
Type '(string | number | boolean)[]' is missing the following properties from type '[string, number, boolean]': 0, 1, 2
33
tests/cases/conformance/expressions/contextualTyping/argumentExpressionContextualTyping.ts(17,5): error TS2345: Argument of type '[string, number, true, ...(string | number | boolean)[]]' is not assignable to parameter of type '[string, number, boolean]'.
44
Types of property 'length' are incompatible.
55
Type 'number' is not assignable to type '3'.
66
tests/cases/conformance/expressions/contextualTyping/argumentExpressionContextualTyping.ts(18,5): error TS2345: Argument of type '{ x: (string | number)[]; y: { c: boolean; d: string; e: number; }; }' is not assignable to parameter of type '{ x: [any, any]; y: { c: any; d: any; e: any; }; }'.
77
Types of property 'x' are incompatible.
8-
Type '(string | number)[]' is not assignable to type '[any, any]'.
9-
Property '0' is missing in type '(string | number)[]'.
8+
Type '(string | number)[]' is missing the following properties from type '[any, any]': 0, 1
109

1110

1211
==== tests/cases/conformance/expressions/contextualTyping/argumentExpressionContextualTyping.ts (3 errors) ====
@@ -28,7 +27,7 @@ tests/cases/conformance/expressions/contextualTyping/argumentExpressionContextua
2827
baz(array); // Error
2928
~~~~~
3029
!!! error TS2345: Argument of type '(string | number | boolean)[]' is not assignable to parameter of type '[string, number, boolean]'.
31-
!!! error TS2345: Property '0' is missing in type '(string | number | boolean)[]'.
30+
!!! error TS2345: Type '(string | number | boolean)[]' is missing the following properties from type '[string, number, boolean]': 0, 1, 2
3231
baz(["string", 1, true, ...array]); // Error
3332
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3433
!!! error TS2345: Argument of type '[string, number, true, ...(string | number | boolean)[]]' is not assignable to parameter of type '[string, number, boolean]'.
@@ -38,5 +37,4 @@ tests/cases/conformance/expressions/contextualTyping/argumentExpressionContextua
3837
~
3938
!!! error TS2345: Argument of type '{ x: (string | number)[]; y: { c: boolean; d: string; e: number; }; }' is not assignable to parameter of type '{ x: [any, any]; y: { c: any; d: any; e: any; }; }'.
4039
!!! error TS2345: Types of property 'x' are incompatible.
41-
!!! error TS2345: Type '(string | number)[]' is not assignable to type '[any, any]'.
42-
!!! error TS2345: Property '0' is missing in type '(string | number)[]'.
40+
!!! error TS2345: Type '(string | number)[]' is missing the following properties from type '[any, any]': 0, 1

0 commit comments

Comments
 (0)