Skip to content

Commit 9fe25ca

Browse files
committed
feat: add a codefix to fix class to className in react
1 parent 59ad375 commit 9fe25ca

8 files changed

+170
-0
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/* @internal */
2+
namespace ts.codefix {
3+
const fixID = "fixReactClassNameAndHTMLFor";
4+
const errorCodes = [Diagnostics.Type_0_is_not_assignable_to_type_1.code];
5+
registerCodeFix({
6+
errorCodes,
7+
getCodeActions: context => {
8+
const { jsx } = context.program.getCompilerOptions();
9+
if (jsx !== JsxEmit.React) {
10+
return undefined;
11+
}
12+
const { sourceFile, span } = context;
13+
const node = getTokenAtPosition(sourceFile, span.start);
14+
if (!shouldFix(node)) return undefined;
15+
const changes = textChanges.ChangeTracker.with(context, t => doChange(t, sourceFile, node));
16+
return [createCodeFixAction(fixID, changes, [Diagnostics.Did_you_mean_0, getCorrectName(node)], fixID, Diagnostics.Fix_all_detected_spelling_errors)];
17+
},
18+
fixIds: [fixID],
19+
getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, diag) => {
20+
const node = getTokenAtPosition(context.sourceFile, diag.start);
21+
if (!shouldFix(node)) return;
22+
doChange(changes, context.sourceFile, node);
23+
}),
24+
});
25+
26+
function doChange(changeTracker: textChanges.ChangeTracker, sf: SourceFile, node: Identifier) {
27+
changeTracker.replaceNode(sf, node, createIdentifier(getCorrectName(node)));
28+
}
29+
30+
function getCorrectName(node: Identifier) {
31+
const text = node.text;
32+
if (text === "class") return "className";
33+
if (text === "for") return "htmlFor";
34+
return Debug.fail();
35+
}
36+
37+
function shouldFix(node: Node): node is Identifier {
38+
// <label for="content">, the error is on the `for`
39+
if (!isIdentifier(node)) return false;
40+
41+
const id = node.text;
42+
// Only fix class => className and for => htmlFor
43+
if (id !== "for" && id !== "class") return false;
44+
45+
if (!isJsxAttribute(node.parent)) return false;
46+
const parent = node.parent.parent.parent;
47+
if (!isIdentifier(parent.tagName)) return false;
48+
const tagName = parent.tagName.text;
49+
// html for only appear on label tag
50+
if (tagName !== "label" && id === "for") return false;
51+
const firstChar = tagName[0];
52+
// Only fix for html elements. Non-lowercase elements are React Elements
53+
if (firstChar.toLowerCase() !== firstChar) return false;
54+
55+
// See https://reactjs.org/docs/web-components.html#using-web-components-in-react
56+
// "One common confusion is that Web Components use “class” instead of “className”."
57+
if (tagName.indexOf("-") !== -1 && id === "class") return false;
58+
return true;
59+
}
60+
}

src/services/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
"codefixes/fixPropertyOverrideAccessor.ts",
8989
"codefixes/inferFromUsage.ts",
9090
"codefixes/fixReturnTypeInAsyncFunction.ts",
91+
"codefixes/fixReactClassNameAndHTMLFor.ts",
9192
"codefixes/disableJsDiagnostics.ts",
9293
"codefixes/helpers.ts",
9394
"codefixes/generateAccessors.ts",
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @jsx: react
4+
// @Filename: /a.tsx
5+
////declare namespace JSX {
6+
//// interface Element {}
7+
//// interface IntrinsicElements {
8+
//// div: {
9+
//// className?: string
10+
//// }
11+
//// }
12+
////}
13+
////[|const div = <div class="a" />|]
14+
15+
verify.rangeAfterCodeFix(`const div = <div className="a" />`, /*includeWhiteSpace*/false, /*errorCode*/ 2322, /*index*/ 0);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @jsx: react
4+
// @Filename: /a.tsx
5+
////declare namespace JSX {
6+
//// interface Element {}
7+
//// interface IntrinsicElements {
8+
//// label: {
9+
//// htmlFor?: string
10+
//// }
11+
//// }
12+
////}
13+
////[|<label for="a" />|]
14+
15+
verify.rangeAfterCodeFix(`<label htmlFor="a" />`, /*includeWhiteSpace*/false, /*errorCode*/ 2322, /*index*/ 0);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @jsx: react
4+
// @Filename: /a.tsx
5+
////<div for="a" />;
6+
////declare namespace JSX {
7+
//// interface Element {}
8+
//// interface IntrinsicElements {
9+
//// div: {
10+
//// className?: string
11+
//// }
12+
//// 'my-tag': { className?: string }
13+
//// }
14+
////}
15+
16+
verify.not.codeFixAvailable("fixReactClassNameAndHTMLFor");
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @jsx: react
4+
// @Filename: /a.tsx
5+
////function MyComponent(props: {className?: string}) {}
6+
////<MyComponent class="a" />;
7+
////declare namespace JSX {
8+
//// interface Element {}
9+
//// interface IntrinsicElements {
10+
//// div: {
11+
//// className?: string
12+
//// }
13+
//// 'my-tag': { className?: string }
14+
//// }
15+
////}
16+
17+
verify.not.codeFixAvailable("fixReactClassNameAndHTMLFor");
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @jsx: react
4+
// @Filename: /a.tsx
5+
////<my-tag class="a" />;
6+
////declare namespace JSX {
7+
//// interface Element {}
8+
//// interface IntrinsicElements {
9+
//// div: {
10+
//// className?: string
11+
//// }
12+
//// 'my-tag': { className?: string }
13+
//// }
14+
////}
15+
16+
verify.not.codeFixAvailable("fixReactClassNameAndHTMLFor");
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @jsx: react
4+
// @Filename: /a.tsx
5+
////declare namespace JSX {
6+
////interface Element {}
7+
////interface IntrinsicElements {
8+
////div: { className?: string }
9+
////label: { htmlFor?: string }
10+
////}}
11+
////<div class="a" />;
12+
////<label for="a" />;
13+
////<div class="a" />;
14+
////<div class="a" id="a" />;
15+
////<div id="b" />;
16+
17+
verify.codeFixAll({
18+
newFileContent: `declare namespace JSX {
19+
interface Element {}
20+
interface IntrinsicElements {
21+
div: { className?: string }
22+
label: { htmlFor?: string }
23+
}}
24+
<div className="a" />;
25+
<label htmlFor="a" />;
26+
<div className="a" />;
27+
<div className="a" id="a" />;
28+
<div id="b" />;`, fixId: "fixReactClassNameAndHTMLFor",
29+
fixAllDescription: "Fix all detected spelling errors"
30+
});

0 commit comments

Comments
 (0)