Skip to content

Commit e6aedfd

Browse files
Merge pull request microsoft#37907 from Jack-Works/feat/class-to-classname
feat: add a codefix to fix class to className in react & add spelling suggest for JSX attributes
2 parents 0476a1a + 8dc4f7e commit e6aedfd

11 files changed

+314
-14
lines changed

src/compiler/checker.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,7 @@ namespace ts {
606606
getAllPossiblePropertiesOfTypes,
607607
getSuggestedSymbolForNonexistentProperty,
608608
getSuggestionForNonexistentProperty,
609+
getSuggestedSymbolForNonexistentJSXAttribute,
609610
getSuggestedSymbolForNonexistentSymbol: (location, name, meaning) => getSuggestedSymbolForNonexistentSymbol(location, escapeLeadingUnderscores(name), meaning),
610611
getSuggestionForNonexistentSymbol: (location, name, meaning) => getSuggestionForNonexistentSymbol(location, escapeLeadingUnderscores(name), meaning),
611612
getSuggestedSymbolForNonexistentModule,
@@ -16313,18 +16314,25 @@ namespace ts {
1631316314
if (isJsxAttributes(errorNode) || isJsxOpeningLikeElement(errorNode) || isJsxOpeningLikeElement(errorNode.parent)) {
1631416315
// JsxAttributes has an object-literal flag and undergo same type-assignablity check as normal object-literal.
1631516316
// However, using an object-literal error message will be very confusing to the users so we give different a message.
16316-
// TODO: Spelling suggestions for excess jsx attributes (needs new diagnostic messages)
1631716317
if (prop.valueDeclaration && isJsxAttribute(prop.valueDeclaration) && getSourceFileOfNode(errorNode) === getSourceFileOfNode(prop.valueDeclaration.name)) {
1631816318
// Note that extraneous children (as in `<NoChild>extra</NoChild>`) don't pass this check,
1631916319
// since `children` is a SyntaxKind.PropertySignature instead of a SyntaxKind.JsxAttribute.
1632016320
errorNode = prop.valueDeclaration.name;
1632116321
}
16322-
reportError(Diagnostics.Property_0_does_not_exist_on_type_1, symbolToString(prop), typeToString(errorTarget));
16322+
const propName = symbolToString(prop);
16323+
const suggestionSymbol = getSuggestedSymbolForNonexistentJSXAttribute(propName, errorTarget);
16324+
const suggestion = suggestionSymbol ? symbolToString(suggestionSymbol) : undefined;
16325+
if (suggestion) {
16326+
reportError(Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2, propName, typeToString(errorTarget), suggestion);
16327+
}
16328+
else {
16329+
reportError(Diagnostics.Property_0_does_not_exist_on_type_1, propName, typeToString(errorTarget));
16330+
}
1632316331
}
1632416332
else {
1632516333
// use the property's value declaration if the property is assigned inside the literal itself
1632616334
const objectLiteralDeclaration = source.symbol && firstOrUndefined(source.symbol.declarations);
16327-
let suggestion;
16335+
let suggestion: string | undefined;
1632816336
if (prop.valueDeclaration && findAncestor(prop.valueDeclaration, d => d === objectLiteralDeclaration) && getSourceFileOfNode(objectLiteralDeclaration) === getSourceFileOfNode(errorNode)) {
1632916337
const propDeclaration = prop.valueDeclaration as ObjectLiteralElementLike;
1633016338
Debug.assertNode(propDeclaration, isObjectLiteralElementLike);
@@ -24877,6 +24885,15 @@ namespace ts {
2487724885
return getSpellingSuggestionForName(isString(name) ? name : idText(name), getPropertiesOfType(containingType), SymbolFlags.Value);
2487824886
}
2487924887

24888+
function getSuggestedSymbolForNonexistentJSXAttribute(name: Identifier | PrivateIdentifier | string, containingType: Type): Symbol | undefined {
24889+
const strName = isString(name) ? name : idText(name);
24890+
const properties = getPropertiesOfType(containingType);
24891+
const jsxSpecific = strName === "for" ? find(properties, x => symbolName(x) === "htmlFor")
24892+
: strName === "class" ? find(properties, x => symbolName(x) === "className")
24893+
: undefined;
24894+
return jsxSpecific ?? getSpellingSuggestionForName(strName, properties, SymbolFlags.Value);
24895+
}
24896+
2488024897
function getSuggestionForNonexistentProperty(name: Identifier | PrivateIdentifier | string, containingType: Type): string | undefined {
2488124898
const suggestion = getSuggestedSymbolForNonexistentProperty(name, containingType);
2488224899
return suggestion && symbolName(suggestion);
@@ -28223,7 +28240,7 @@ namespace ts {
2822328240
error(expr, Diagnostics.The_operand_of_a_delete_operator_must_be_a_property_reference);
2822428241
return booleanType;
2822528242
}
28226-
if (expr.kind === SyntaxKind.PropertyAccessExpression && isPrivateIdentifier(expr.name)) {
28243+
if (isPropertyAccessExpression(expr) && isPrivateIdentifier(expr.name)) {
2822728244
error(expr, Diagnostics.The_operand_of_a_delete_operator_cannot_be_a_private_identifier);
2822828245
}
2822928246
const links = getNodeLinks(expr);

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4010,6 +4010,7 @@ namespace ts {
40104010
/* @internal */ tryGetMemberInModuleExportsAndProperties(memberName: string, moduleSymbol: Symbol): Symbol | undefined;
40114011
getApparentType(type: Type): Type;
40124012
/* @internal */ getSuggestedSymbolForNonexistentProperty(name: Identifier | PrivateIdentifier | string, containingType: Type): Symbol | undefined;
4013+
/* @internal */ getSuggestedSymbolForNonexistentJSXAttribute(name: Identifier | string, containingType: Type): Symbol | undefined;
40134014
/* @internal */ getSuggestionForNonexistentProperty(name: Identifier | PrivateIdentifier | string, containingType: Type): string | undefined;
40144015
/* @internal */ getSuggestedSymbolForNonexistentSymbol(location: Node, name: string, meaning: SymbolFlags): Symbol | undefined;
40154016
/* @internal */ getSuggestionForNonexistentSymbol(location: Node, name: string, meaning: SymbolFlags): string | undefined;

src/services/codefixes/fixSpelling.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@ namespace ts.codefix {
77
Diagnostics.Cannot_find_name_0_Did_you_mean_the_instance_member_this_0.code,
88
Diagnostics.Cannot_find_name_0_Did_you_mean_the_static_member_1_0.code,
99
Diagnostics.Module_0_has_no_exported_member_1_Did_you_mean_2.code,
10+
// for JSX class components
11+
Diagnostics.No_overload_matches_this_call.code,
12+
// for JSX FC
13+
Diagnostics.Type_0_is_not_assignable_to_type_1.code,
1014
];
1115
registerCodeFix({
1216
errorCodes,
1317
getCodeActions(context) {
14-
const { sourceFile } = context;
15-
const info = getInfo(sourceFile, context.span.start, context);
18+
const { sourceFile, errorCode } = context;
19+
const info = getInfo(sourceFile, context.span.start, context, errorCode);
1620
if (!info) return undefined;
1721
const { node, suggestedSymbol } = info;
1822
const { target } = context.host.getCompilationSettings();
@@ -21,18 +25,23 @@ namespace ts.codefix {
2125
},
2226
fixIds: [fixId],
2327
getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, diag) => {
24-
const info = getInfo(diag.file, diag.start, context);
28+
const info = getInfo(diag.file, diag.start, context, diag.code);
2529
const { target } = context.host.getCompilationSettings();
2630
if (info) doChange(changes, context.sourceFile, info.node, info.suggestedSymbol, target!);
2731
}),
2832
});
2933

30-
function getInfo(sourceFile: SourceFile, pos: number, context: CodeFixContextBase): { node: Node, suggestedSymbol: Symbol } | undefined {
34+
function getInfo(sourceFile: SourceFile, pos: number, context: CodeFixContextBase, errorCode: number): { node: Node, suggestedSymbol: Symbol } | undefined {
3135
// This is the identifier of the misspelled word. eg:
3236
// this.speling = 1;
3337
// ^^^^^^^
3438
const node = getTokenAtPosition(sourceFile, pos);
3539
const parent = node.parent;
40+
// Only fix spelling for No_overload_matches_this_call emitted on the React class component
41+
if ((
42+
errorCode === Diagnostics.No_overload_matches_this_call.code ||
43+
errorCode === Diagnostics.Type_0_is_not_assignable_to_type_1.code) &&
44+
!isJsxAttribute(parent)) return undefined;
3645
const checker = context.program.getTypeChecker();
3746

3847
let suggestedSymbol: Symbol | undefined;
@@ -52,6 +61,12 @@ namespace ts.codefix {
5261
suggestedSymbol = checker.getSuggestedSymbolForNonexistentModule(node, resolvedSourceFile.symbol);
5362
}
5463
}
64+
else if (isJsxAttribute(parent) && parent.name === node) {
65+
Debug.assertNode(node, isIdentifier, "Expected an identifier for JSX attribute");
66+
const tag = findAncestor(node, isJsxOpeningLikeElement)!;
67+
const props = checker.getContextualTypeForArgumentAtIndex(tag, 0);
68+
suggestedSymbol = checker.getSuggestedSymbolForNonexistentJSXAttribute(node, props!);
69+
}
5570
else {
5671
const meaning = getMeaningFromLocation(node);
5772
const name = getTextOfNode(node);
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
tests/cases/compiler/spellingSuggestionJSXAttribute.tsx(8,4): error TS2322: Type '{ class: string; }' is not assignable to type 'DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>'.
2+
Property 'class' does not exist on type 'DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>'. Did you mean 'className'?
3+
tests/cases/compiler/spellingSuggestionJSXAttribute.tsx(9,4): error TS2322: Type '{ for: string; }' is not assignable to type 'DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>'.
4+
Property 'for' does not exist on type 'DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>'.
5+
tests/cases/compiler/spellingSuggestionJSXAttribute.tsx(10,8): error TS2322: Type '{ for: string; }' is not assignable to type 'DetailedHTMLProps<LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>'.
6+
Property 'for' does not exist on type 'DetailedHTMLProps<LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>'. Did you mean 'htmlFor'?
7+
tests/cases/compiler/spellingSuggestionJSXAttribute.tsx(11,8): error TS2322: Type '{ for: string; class: string; }' is not assignable to type 'DetailedHTMLProps<LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>'.
8+
Property 'for' does not exist on type 'DetailedHTMLProps<LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>'. Did you mean 'htmlFor'?
9+
tests/cases/compiler/spellingSuggestionJSXAttribute.tsx(12,9): error TS2769: No overload matches this call.
10+
Overload 1 of 2, '(props: Readonly<{ className?: string; htmlFor?: string; }>): MyComp', gave the following error.
11+
Type '{ class: string; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<MyComp> & Readonly<{ children?: ReactNode; }> & Readonly<{ className?: string; htmlFor?: string; }>'.
12+
Property 'class' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<MyComp> & Readonly<{ children?: ReactNode; }> & Readonly<{ className?: string; htmlFor?: string; }>'. Did you mean 'className'?
13+
Overload 2 of 2, '(props: { className?: string; htmlFor?: string; }, context?: any): MyComp', gave the following error.
14+
Type '{ class: string; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<MyComp> & Readonly<{ children?: ReactNode; }> & Readonly<{ className?: string; htmlFor?: string; }>'.
15+
Property 'class' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<MyComp> & Readonly<{ children?: ReactNode; }> & Readonly<{ className?: string; htmlFor?: string; }>'. Did you mean 'className'?
16+
tests/cases/compiler/spellingSuggestionJSXAttribute.tsx(13,10): error TS2322: Type '{ class: string; }' is not assignable to type 'IntrinsicAttributes & { className?: string; htmlFor?: string; }'.
17+
Property 'class' does not exist on type 'IntrinsicAttributes & { className?: string; htmlFor?: string; }'. Did you mean 'className'?
18+
tests/cases/compiler/spellingSuggestionJSXAttribute.tsx(14,9): error TS2769: No overload matches this call.
19+
Overload 1 of 2, '(props: Readonly<{ className?: string; htmlFor?: string; }>): MyComp', gave the following error.
20+
Type '{ for: string; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<MyComp> & Readonly<{ children?: ReactNode; }> & Readonly<{ className?: string; htmlFor?: string; }>'.
21+
Property 'for' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<MyComp> & Readonly<{ children?: ReactNode; }> & Readonly<{ className?: string; htmlFor?: string; }>'. Did you mean 'htmlFor'?
22+
Overload 2 of 2, '(props: { className?: string; htmlFor?: string; }, context?: any): MyComp', gave the following error.
23+
Type '{ for: string; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<MyComp> & Readonly<{ children?: ReactNode; }> & Readonly<{ className?: string; htmlFor?: string; }>'.
24+
Property 'for' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<MyComp> & Readonly<{ children?: ReactNode; }> & Readonly<{ className?: string; htmlFor?: string; }>'. Did you mean 'htmlFor'?
25+
tests/cases/compiler/spellingSuggestionJSXAttribute.tsx(15,10): error TS2322: Type '{ for: string; }' is not assignable to type 'IntrinsicAttributes & { className?: string; htmlFor?: string; }'.
26+
Property 'for' does not exist on type 'IntrinsicAttributes & { className?: string; htmlFor?: string; }'. Did you mean 'htmlFor'?
27+
28+
29+
==== tests/cases/compiler/spellingSuggestionJSXAttribute.tsx (8 errors) ====
30+
/// <reference path="/.lib/react16.d.ts" />
31+
import * as React from "react";
32+
33+
function MyComp2(props: { className?: string, htmlFor?: string }) {
34+
return null!;
35+
}
36+
class MyComp extends React.Component<{ className?: string, htmlFor?: string }> { }
37+
<a class="" />;
38+
~~~~~
39+
!!! error TS2322: Type '{ class: string; }' is not assignable to type 'DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>'.
40+
!!! error TS2322: Property 'class' does not exist on type 'DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>'. Did you mean 'className'?
41+
<a for="" />; // should have no fix
42+
~~~
43+
!!! error TS2322: Type '{ for: string; }' is not assignable to type 'DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>'.
44+
!!! error TS2322: Property 'for' does not exist on type 'DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>'.
45+
<label for="" />;
46+
~~~
47+
!!! error TS2322: Type '{ for: string; }' is not assignable to type 'DetailedHTMLProps<LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>'.
48+
!!! error TS2322: Property 'for' does not exist on type 'DetailedHTMLProps<LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>'. Did you mean 'htmlFor'?
49+
<label for="" class="" />;
50+
~~~
51+
!!! error TS2322: Type '{ for: string; class: string; }' is not assignable to type 'DetailedHTMLProps<LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>'.
52+
!!! error TS2322: Property 'for' does not exist on type 'DetailedHTMLProps<LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>'. Did you mean 'htmlFor'?
53+
<MyComp class="" />;
54+
~~~~~
55+
!!! error TS2769: No overload matches this call.
56+
!!! error TS2769: Overload 1 of 2, '(props: Readonly<{ className?: string; htmlFor?: string; }>): MyComp', gave the following error.
57+
!!! error TS2769: Type '{ class: string; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<MyComp> & Readonly<{ children?: ReactNode; }> & Readonly<{ className?: string; htmlFor?: string; }>'.
58+
!!! error TS2769: Property 'class' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<MyComp> & Readonly<{ children?: ReactNode; }> & Readonly<{ className?: string; htmlFor?: string; }>'. Did you mean 'className'?
59+
!!! error TS2769: Overload 2 of 2, '(props: { className?: string; htmlFor?: string; }, context?: any): MyComp', gave the following error.
60+
!!! error TS2769: Type '{ class: string; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<MyComp> & Readonly<{ children?: ReactNode; }> & Readonly<{ className?: string; htmlFor?: string; }>'.
61+
!!! error TS2769: Property 'class' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<MyComp> & Readonly<{ children?: ReactNode; }> & Readonly<{ className?: string; htmlFor?: string; }>'. Did you mean 'className'?
62+
<MyComp2 class="" />;
63+
~~~~~
64+
!!! error TS2322: Type '{ class: string; }' is not assignable to type 'IntrinsicAttributes & { className?: string; htmlFor?: string; }'.
65+
!!! error TS2322: Property 'class' does not exist on type 'IntrinsicAttributes & { className?: string; htmlFor?: string; }'. Did you mean 'className'?
66+
<MyComp for="" />;
67+
~~~
68+
!!! error TS2769: No overload matches this call.
69+
!!! error TS2769: Overload 1 of 2, '(props: Readonly<{ className?: string; htmlFor?: string; }>): MyComp', gave the following error.
70+
!!! error TS2769: Type '{ for: string; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<MyComp> & Readonly<{ children?: ReactNode; }> & Readonly<{ className?: string; htmlFor?: string; }>'.
71+
!!! error TS2769: Property 'for' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<MyComp> & Readonly<{ children?: ReactNode; }> & Readonly<{ className?: string; htmlFor?: string; }>'. Did you mean 'htmlFor'?
72+
!!! error TS2769: Overload 2 of 2, '(props: { className?: string; htmlFor?: string; }, context?: any): MyComp', gave the following error.
73+
!!! error TS2769: Type '{ for: string; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<MyComp> & Readonly<{ children?: ReactNode; }> & Readonly<{ className?: string; htmlFor?: string; }>'.
74+
!!! error TS2769: Property 'for' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<MyComp> & Readonly<{ children?: ReactNode; }> & Readonly<{ className?: string; htmlFor?: string; }>'. Did you mean 'htmlFor'?
75+
<MyComp2 for="" />;
76+
~~~
77+
!!! error TS2322: Type '{ for: string; }' is not assignable to type 'IntrinsicAttributes & { className?: string; htmlFor?: string; }'.
78+
!!! error TS2322: Property 'for' does not exist on type 'IntrinsicAttributes & { className?: string; htmlFor?: string; }'. Did you mean 'htmlFor'?
79+

0 commit comments

Comments
 (0)