diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index caba37715d0a5..1ddd0c474a531 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -606,6 +606,7 @@ namespace ts { getAllPossiblePropertiesOfTypes, getSuggestedSymbolForNonexistentProperty, getSuggestionForNonexistentProperty, + getSuggestedSymbolForNonexistentJSXAttribute, getSuggestedSymbolForNonexistentSymbol: (location, name, meaning) => getSuggestedSymbolForNonexistentSymbol(location, escapeLeadingUnderscores(name), meaning), getSuggestionForNonexistentSymbol: (location, name, meaning) => getSuggestionForNonexistentSymbol(location, escapeLeadingUnderscores(name), meaning), getSuggestedSymbolForNonexistentModule, @@ -16313,18 +16314,25 @@ namespace ts { if (isJsxAttributes(errorNode) || isJsxOpeningLikeElement(errorNode) || isJsxOpeningLikeElement(errorNode.parent)) { // JsxAttributes has an object-literal flag and undergo same type-assignablity check as normal object-literal. // However, using an object-literal error message will be very confusing to the users so we give different a message. - // TODO: Spelling suggestions for excess jsx attributes (needs new diagnostic messages) if (prop.valueDeclaration && isJsxAttribute(prop.valueDeclaration) && getSourceFileOfNode(errorNode) === getSourceFileOfNode(prop.valueDeclaration.name)) { // Note that extraneous children (as in `extra`) don't pass this check, // since `children` is a SyntaxKind.PropertySignature instead of a SyntaxKind.JsxAttribute. errorNode = prop.valueDeclaration.name; } - reportError(Diagnostics.Property_0_does_not_exist_on_type_1, symbolToString(prop), typeToString(errorTarget)); + const propName = symbolToString(prop); + const suggestionSymbol = getSuggestedSymbolForNonexistentJSXAttribute(propName, errorTarget); + const suggestion = suggestionSymbol ? symbolToString(suggestionSymbol) : undefined; + if (suggestion) { + reportError(Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2, propName, typeToString(errorTarget), suggestion); + } + else { + reportError(Diagnostics.Property_0_does_not_exist_on_type_1, propName, typeToString(errorTarget)); + } } else { // use the property's value declaration if the property is assigned inside the literal itself const objectLiteralDeclaration = source.symbol && firstOrUndefined(source.symbol.declarations); - let suggestion; + let suggestion: string | undefined; if (prop.valueDeclaration && findAncestor(prop.valueDeclaration, d => d === objectLiteralDeclaration) && getSourceFileOfNode(objectLiteralDeclaration) === getSourceFileOfNode(errorNode)) { const propDeclaration = prop.valueDeclaration as ObjectLiteralElementLike; Debug.assertNode(propDeclaration, isObjectLiteralElementLike); @@ -24877,6 +24885,15 @@ namespace ts { return getSpellingSuggestionForName(isString(name) ? name : idText(name), getPropertiesOfType(containingType), SymbolFlags.Value); } + function getSuggestedSymbolForNonexistentJSXAttribute(name: Identifier | PrivateIdentifier | string, containingType: Type): Symbol | undefined { + const strName = isString(name) ? name : idText(name); + const properties = getPropertiesOfType(containingType); + const jsxSpecific = strName === "for" ? find(properties, x => symbolName(x) === "htmlFor") + : strName === "class" ? find(properties, x => symbolName(x) === "className") + : undefined; + return jsxSpecific ?? getSpellingSuggestionForName(strName, properties, SymbolFlags.Value); + } + function getSuggestionForNonexistentProperty(name: Identifier | PrivateIdentifier | string, containingType: Type): string | undefined { const suggestion = getSuggestedSymbolForNonexistentProperty(name, containingType); return suggestion && symbolName(suggestion); @@ -28223,7 +28240,7 @@ namespace ts { error(expr, Diagnostics.The_operand_of_a_delete_operator_must_be_a_property_reference); return booleanType; } - if (expr.kind === SyntaxKind.PropertyAccessExpression && isPrivateIdentifier(expr.name)) { + if (isPropertyAccessExpression(expr) && isPrivateIdentifier(expr.name)) { error(expr, Diagnostics.The_operand_of_a_delete_operator_cannot_be_a_private_identifier); } const links = getNodeLinks(expr); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index c9facd108a4c5..465af258f86da 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4010,6 +4010,7 @@ namespace ts { /* @internal */ tryGetMemberInModuleExportsAndProperties(memberName: string, moduleSymbol: Symbol): Symbol | undefined; getApparentType(type: Type): Type; /* @internal */ getSuggestedSymbolForNonexistentProperty(name: Identifier | PrivateIdentifier | string, containingType: Type): Symbol | undefined; + /* @internal */ getSuggestedSymbolForNonexistentJSXAttribute(name: Identifier | string, containingType: Type): Symbol | undefined; /* @internal */ getSuggestionForNonexistentProperty(name: Identifier | PrivateIdentifier | string, containingType: Type): string | undefined; /* @internal */ getSuggestedSymbolForNonexistentSymbol(location: Node, name: string, meaning: SymbolFlags): Symbol | undefined; /* @internal */ getSuggestionForNonexistentSymbol(location: Node, name: string, meaning: SymbolFlags): string | undefined; diff --git a/src/services/codefixes/fixSpelling.ts b/src/services/codefixes/fixSpelling.ts index b90c16c390826..0c63dc5ebfc95 100644 --- a/src/services/codefixes/fixSpelling.ts +++ b/src/services/codefixes/fixSpelling.ts @@ -7,12 +7,16 @@ namespace ts.codefix { Diagnostics.Cannot_find_name_0_Did_you_mean_the_instance_member_this_0.code, Diagnostics.Cannot_find_name_0_Did_you_mean_the_static_member_1_0.code, Diagnostics.Module_0_has_no_exported_member_1_Did_you_mean_2.code, + // for JSX class components + Diagnostics.No_overload_matches_this_call.code, + // for JSX FC + Diagnostics.Type_0_is_not_assignable_to_type_1.code, ]; registerCodeFix({ errorCodes, getCodeActions(context) { - const { sourceFile } = context; - const info = getInfo(sourceFile, context.span.start, context); + const { sourceFile, errorCode } = context; + const info = getInfo(sourceFile, context.span.start, context, errorCode); if (!info) return undefined; const { node, suggestedSymbol } = info; const { target } = context.host.getCompilationSettings(); @@ -21,18 +25,23 @@ namespace ts.codefix { }, fixIds: [fixId], getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, diag) => { - const info = getInfo(diag.file, diag.start, context); + const info = getInfo(diag.file, diag.start, context, diag.code); const { target } = context.host.getCompilationSettings(); if (info) doChange(changes, context.sourceFile, info.node, info.suggestedSymbol, target!); }), }); - function getInfo(sourceFile: SourceFile, pos: number, context: CodeFixContextBase): { node: Node, suggestedSymbol: Symbol } | undefined { + function getInfo(sourceFile: SourceFile, pos: number, context: CodeFixContextBase, errorCode: number): { node: Node, suggestedSymbol: Symbol } | undefined { // This is the identifier of the misspelled word. eg: // this.speling = 1; // ^^^^^^^ const node = getTokenAtPosition(sourceFile, pos); const parent = node.parent; + // Only fix spelling for No_overload_matches_this_call emitted on the React class component + if (( + errorCode === Diagnostics.No_overload_matches_this_call.code || + errorCode === Diagnostics.Type_0_is_not_assignable_to_type_1.code) && + !isJsxAttribute(parent)) return undefined; const checker = context.program.getTypeChecker(); let suggestedSymbol: Symbol | undefined; @@ -52,6 +61,12 @@ namespace ts.codefix { suggestedSymbol = checker.getSuggestedSymbolForNonexistentModule(node, resolvedSourceFile.symbol); } } + else if (isJsxAttribute(parent) && parent.name === node) { + Debug.assertNode(node, isIdentifier, "Expected an identifier for JSX attribute"); + const tag = findAncestor(node, isJsxOpeningLikeElement)!; + const props = checker.getContextualTypeForArgumentAtIndex(tag, 0); + suggestedSymbol = checker.getSuggestedSymbolForNonexistentJSXAttribute(node, props!); + } else { const meaning = getMeaningFromLocation(node); const name = getTextOfNode(node); diff --git a/tests/baselines/reference/spellingSuggestionJSXAttribute.errors.txt b/tests/baselines/reference/spellingSuggestionJSXAttribute.errors.txt new file mode 100644 index 0000000000000..cde21fb6a3861 --- /dev/null +++ b/tests/baselines/reference/spellingSuggestionJSXAttribute.errors.txt @@ -0,0 +1,79 @@ +tests/cases/compiler/spellingSuggestionJSXAttribute.tsx(8,4): error TS2322: Type '{ class: string; }' is not assignable to type 'DetailedHTMLProps, HTMLAnchorElement>'. + Property 'class' does not exist on type 'DetailedHTMLProps, HTMLAnchorElement>'. Did you mean 'className'? +tests/cases/compiler/spellingSuggestionJSXAttribute.tsx(9,4): error TS2322: Type '{ for: string; }' is not assignable to type 'DetailedHTMLProps, HTMLAnchorElement>'. + Property 'for' does not exist on type 'DetailedHTMLProps, HTMLAnchorElement>'. +tests/cases/compiler/spellingSuggestionJSXAttribute.tsx(10,8): error TS2322: Type '{ for: string; }' is not assignable to type 'DetailedHTMLProps, HTMLLabelElement>'. + Property 'for' does not exist on type 'DetailedHTMLProps, HTMLLabelElement>'. Did you mean 'htmlFor'? +tests/cases/compiler/spellingSuggestionJSXAttribute.tsx(11,8): error TS2322: Type '{ for: string; class: string; }' is not assignable to type 'DetailedHTMLProps, HTMLLabelElement>'. + Property 'for' does not exist on type 'DetailedHTMLProps, HTMLLabelElement>'. Did you mean 'htmlFor'? +tests/cases/compiler/spellingSuggestionJSXAttribute.tsx(12,9): error TS2769: No overload matches this call. + Overload 1 of 2, '(props: Readonly<{ className?: string; htmlFor?: string; }>): MyComp', gave the following error. + Type '{ class: string; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes & Readonly<{ children?: ReactNode; }> & Readonly<{ className?: string; htmlFor?: string; }>'. + Property 'class' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes & Readonly<{ children?: ReactNode; }> & Readonly<{ className?: string; htmlFor?: string; }>'. Did you mean 'className'? + Overload 2 of 2, '(props: { className?: string; htmlFor?: string; }, context?: any): MyComp', gave the following error. + Type '{ class: string; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes & Readonly<{ children?: ReactNode; }> & Readonly<{ className?: string; htmlFor?: string; }>'. + Property 'class' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes & Readonly<{ children?: ReactNode; }> & Readonly<{ className?: string; htmlFor?: string; }>'. Did you mean 'className'? +tests/cases/compiler/spellingSuggestionJSXAttribute.tsx(13,10): error TS2322: Type '{ class: string; }' is not assignable to type 'IntrinsicAttributes & { className?: string; htmlFor?: string; }'. + Property 'class' does not exist on type 'IntrinsicAttributes & { className?: string; htmlFor?: string; }'. Did you mean 'className'? +tests/cases/compiler/spellingSuggestionJSXAttribute.tsx(14,9): error TS2769: No overload matches this call. + Overload 1 of 2, '(props: Readonly<{ className?: string; htmlFor?: string; }>): MyComp', gave the following error. + Type '{ for: string; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes & Readonly<{ children?: ReactNode; }> & Readonly<{ className?: string; htmlFor?: string; }>'. + Property 'for' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes & Readonly<{ children?: ReactNode; }> & Readonly<{ className?: string; htmlFor?: string; }>'. Did you mean 'htmlFor'? + Overload 2 of 2, '(props: { className?: string; htmlFor?: string; }, context?: any): MyComp', gave the following error. + Type '{ for: string; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes & Readonly<{ children?: ReactNode; }> & Readonly<{ className?: string; htmlFor?: string; }>'. + Property 'for' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes & Readonly<{ children?: ReactNode; }> & Readonly<{ className?: string; htmlFor?: string; }>'. Did you mean 'htmlFor'? +tests/cases/compiler/spellingSuggestionJSXAttribute.tsx(15,10): error TS2322: Type '{ for: string; }' is not assignable to type 'IntrinsicAttributes & { className?: string; htmlFor?: string; }'. + Property 'for' does not exist on type 'IntrinsicAttributes & { className?: string; htmlFor?: string; }'. Did you mean 'htmlFor'? + + +==== tests/cases/compiler/spellingSuggestionJSXAttribute.tsx (8 errors) ==== + /// + import * as React from "react"; + + function MyComp2(props: { className?: string, htmlFor?: string }) { + return null!; + } + class MyComp extends React.Component<{ className?: string, htmlFor?: string }> { } + ; + ~~~~~ +!!! error TS2322: Type '{ class: string; }' is not assignable to type 'DetailedHTMLProps, HTMLAnchorElement>'. +!!! error TS2322: Property 'class' does not exist on type 'DetailedHTMLProps, HTMLAnchorElement>'. Did you mean 'className'? + ; // should have no fix + ~~~ +!!! error TS2322: Type '{ for: string; }' is not assignable to type 'DetailedHTMLProps, HTMLAnchorElement>'. +!!! error TS2322: Property 'for' does not exist on type 'DetailedHTMLProps, HTMLAnchorElement>'. + ; +; // should have no fix +; +>a : Symbol(JSX.IntrinsicElements.a, Decl(react16.d.ts, 2390, 41)) +>class : Symbol(class, Decl(spellingSuggestionJSXAttribute.tsx, 7, 2)) + +; // should have no fix +>a : Symbol(JSX.IntrinsicElements.a, Decl(react16.d.ts, 2390, 41)) +>for : Symbol(for, Decl(spellingSuggestionJSXAttribute.tsx, 8, 2)) + +; +> : JSX.Element +>a : any +>class : string + +; // should have no fix +> : JSX.Element +>a : any +>for : string + +; +; // should have no fix +