Skip to content

Commit cce2e92

Browse files
authored
feat(45163): add QF to declare missing jsx attributes (#45179)
1 parent abfe5f0 commit cce2e92

10 files changed

+275
-15
lines changed

src/compiler/diagnosticMessages.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7110,6 +7110,14 @@
71107110
"category": "Message",
71117111
"code": 95166
71127112
},
7113+
"Add missing attributes": {
7114+
"category": "Message",
7115+
"code": 95167
7116+
},
7117+
"Add all missing attributes": {
7118+
"category": "Message",
7119+
"code": 95168
7120+
},
71137121

71147122
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
71157123
"category": "Error",

src/services/codefixes/fixAddMissingMember.ts

Lines changed: 71 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
namespace ts.codefix {
33
const fixMissingMember = "fixMissingMember";
44
const fixMissingProperties = "fixMissingProperties";
5+
const fixMissingAttributes = "fixMissingAttributes";
56
const fixMissingFunctionDeclaration = "fixMissingFunctionDeclaration";
67

78
const errorCodes = [
@@ -25,6 +26,10 @@ namespace ts.codefix {
2526
const changes = textChanges.ChangeTracker.with(context, t => addObjectLiteralProperties(t, context, info));
2627
return [createCodeFixAction(fixMissingProperties, changes, Diagnostics.Add_missing_properties, fixMissingProperties, Diagnostics.Add_all_missing_properties)];
2728
}
29+
if (info.kind === InfoKind.JsxAttributes) {
30+
const changes = textChanges.ChangeTracker.with(context, t => addJsxAttributes(t, context, info));
31+
return [createCodeFixAction(fixMissingAttributes, changes, Diagnostics.Add_missing_attributes, fixMissingAttributes, Diagnostics.Add_all_missing_attributes)];
32+
}
2833
if (info.kind === InfoKind.Function) {
2934
const changes = textChanges.ChangeTracker.with(context, t => addFunctionDeclaration(t, context, info));
3035
return [createCodeFixAction(fixMissingFunctionDeclaration, changes, [Diagnostics.Add_missing_function_declaration_0, info.token.text], fixMissingFunctionDeclaration, Diagnostics.Add_all_missing_function_declarations)];
@@ -35,7 +40,7 @@ namespace ts.codefix {
3540
}
3641
return concatenate(getActionsForMissingMethodDeclaration(context, info), getActionsForMissingMemberDeclaration(context, info));
3742
},
38-
fixIds: [fixMissingMember, fixMissingFunctionDeclaration, fixMissingProperties],
43+
fixIds: [fixMissingMember, fixMissingFunctionDeclaration, fixMissingProperties, fixMissingAttributes],
3944
getAllCodeActions: context => {
4045
const { program, fixId } = context;
4146
const checker = program.getTypeChecker();
@@ -49,15 +54,14 @@ namespace ts.codefix {
4954
return;
5055
}
5156

52-
if (fixId === fixMissingFunctionDeclaration) {
53-
if (info.kind === InfoKind.Function) {
54-
addFunctionDeclaration(changes, context, info);
55-
}
57+
if (fixId === fixMissingFunctionDeclaration && info.kind === InfoKind.Function) {
58+
addFunctionDeclaration(changes, context, info);
5659
}
57-
else if (fixId === fixMissingProperties) {
58-
if (info.kind === InfoKind.ObjectLiteral) {
59-
addObjectLiteralProperties(changes, context, info);
60-
}
60+
else if (fixId === fixMissingProperties && info.kind === InfoKind.ObjectLiteral) {
61+
addObjectLiteralProperties(changes, context, info);
62+
}
63+
else if (fixId === fixMissingAttributes && info.kind === InfoKind.JsxAttributes) {
64+
addJsxAttributes(changes, context, info);
6165
}
6266
else {
6367
if (info.kind === InfoKind.Enum) {
@@ -102,8 +106,8 @@ namespace ts.codefix {
102106
},
103107
});
104108

105-
const enum InfoKind { Enum, ClassOrInterface, Function, ObjectLiteral }
106-
type Info = EnumInfo | ClassOrInterfaceInfo | FunctionInfo | ObjectLiteralInfo;
109+
const enum InfoKind { Enum, ClassOrInterface, Function, ObjectLiteral, JsxAttributes }
110+
type Info = EnumInfo | ClassOrInterfaceInfo | FunctionInfo | ObjectLiteralInfo | JsxAttributesInfo;
107111

108112
interface EnumInfo {
109113
readonly kind: InfoKind.Enum;
@@ -137,6 +141,13 @@ namespace ts.codefix {
137141
readonly parentDeclaration: ObjectLiteralExpression;
138142
}
139143

144+
interface JsxAttributesInfo {
145+
readonly kind: InfoKind.JsxAttributes;
146+
readonly token: Identifier;
147+
readonly attributes: Symbol[];
148+
readonly parentDeclaration: JsxOpeningLikeElement;
149+
}
150+
140151
function getInfo(sourceFile: SourceFile, tokenPos: number, checker: TypeChecker, program: Program): Info | undefined {
141152
// The identifier of the missing property. eg:
142153
// this.missing = 1;
@@ -154,6 +165,13 @@ namespace ts.codefix {
154165
}
155166
}
156167

168+
if (isIdentifier(token) && isJsxOpeningLikeElement(token.parent)) {
169+
const attributes = getUnmatchedAttributes(checker, token.parent);
170+
if (length(attributes)) {
171+
return { kind: InfoKind.JsxAttributes, token, attributes, parentDeclaration: token.parent };
172+
}
173+
}
174+
157175
if (isIdentifier(token) && isCallExpression(parent)) {
158176
return { kind: InfoKind.Function, token, call: parent, sourceFile, modifierFlags: ModifierFlags.None, parentDeclaration: sourceFile };
159177
}
@@ -434,18 +452,33 @@ namespace ts.codefix {
434452
changes.insertNodeAtEndOfScope(info.sourceFile, info.parentDeclaration, functionDeclaration);
435453
}
436454

455+
function addJsxAttributes(changes: textChanges.ChangeTracker, context: CodeFixContextBase, info: JsxAttributesInfo) {
456+
const importAdder = createImportAdder(context.sourceFile, context.program, context.preferences, context.host);
457+
const quotePreference = getQuotePreference(context.sourceFile, context.preferences);
458+
const checker = context.program.getTypeChecker();
459+
const jsxAttributesNode = info.parentDeclaration.attributes;
460+
const hasSpreadAttribute = some(jsxAttributesNode.properties, isJsxSpreadAttribute);
461+
const attrs = map(info.attributes, attr => {
462+
const value = attr.valueDeclaration ? tryGetValueFromType(context, checker, importAdder, quotePreference, checker.getTypeAtLocation(attr.valueDeclaration)) : createUndefined();
463+
return factory.createJsxAttribute(factory.createIdentifier(attr.name), factory.createJsxExpression(/*dotDotDotToken*/ undefined, value));
464+
});
465+
const jsxAttributes = factory.createJsxAttributes(hasSpreadAttribute ? [...attrs, ...jsxAttributesNode.properties] : [...jsxAttributesNode.properties, ...attrs]);
466+
const options = { prefix: jsxAttributesNode.pos === jsxAttributesNode.end ? " " : undefined };
467+
changes.replaceNode(context.sourceFile, jsxAttributesNode, jsxAttributes, options);
468+
}
469+
437470
function addObjectLiteralProperties(changes: textChanges.ChangeTracker, context: CodeFixContextBase, info: ObjectLiteralInfo) {
438471
const importAdder = createImportAdder(context.sourceFile, context.program, context.preferences, context.host);
439472
const quotePreference = getQuotePreference(context.sourceFile, context.preferences);
440473
const checker = context.program.getTypeChecker();
441474
const props = map(info.properties, prop => {
442-
const initializer = prop.valueDeclaration ? tryGetInitializerValueFromType(context, checker, importAdder, quotePreference, checker.getTypeAtLocation(prop.valueDeclaration)) : createUndefined();
475+
const initializer = prop.valueDeclaration ? tryGetValueFromType(context, checker, importAdder, quotePreference, checker.getTypeAtLocation(prop.valueDeclaration)) : createUndefined();
443476
return factory.createPropertyAssignment(prop.name, initializer);
444477
});
445478
changes.replaceNode(context.sourceFile, info.parentDeclaration, factory.createObjectLiteralExpression([...info.parentDeclaration.properties, ...props], /*multiLine*/ true));
446479
}
447480

448-
function tryGetInitializerValueFromType(context: CodeFixContextBase, checker: TypeChecker, importAdder: ImportAdder, quotePreference: QuotePreference, type: Type): Expression {
481+
function tryGetValueFromType(context: CodeFixContextBase, checker: TypeChecker, importAdder: ImportAdder, quotePreference: QuotePreference, type: Type): Expression {
449482
if (type.flags & TypeFlags.AnyOrUnknown) {
450483
return createUndefined();
451484
}
@@ -482,15 +515,15 @@ namespace ts.codefix {
482515
return factory.createNull();
483516
}
484517
if (type.flags & TypeFlags.Union) {
485-
const expression = firstDefined((type as UnionType).types, t => tryGetInitializerValueFromType(context, checker, importAdder, quotePreference, t));
518+
const expression = firstDefined((type as UnionType).types, t => tryGetValueFromType(context, checker, importAdder, quotePreference, t));
486519
return expression ?? createUndefined();
487520
}
488521
if (checker.isArrayLikeType(type)) {
489522
return factory.createArrayLiteralExpression();
490523
}
491524
if (isObjectLiteralType(type)) {
492525
const props = map(checker.getPropertiesOfType(type), prop => {
493-
const initializer = prop.valueDeclaration ? tryGetInitializerValueFromType(context, checker, importAdder, quotePreference, checker.getTypeAtLocation(prop.valueDeclaration)) : createUndefined();
526+
const initializer = prop.valueDeclaration ? tryGetValueFromType(context, checker, importAdder, quotePreference, checker.getTypeAtLocation(prop.valueDeclaration)) : createUndefined();
494527
return factory.createPropertyAssignment(prop.name, initializer);
495528
});
496529
return factory.createObjectLiteralExpression(props, /*multiLine*/ true);
@@ -526,4 +559,27 @@ namespace ts.codefix {
526559
return (type.flags & TypeFlags.Object) &&
527560
((getObjectFlags(type) & ObjectFlags.ObjectLiteral) || (type.symbol && tryCast(singleOrUndefined(type.symbol.declarations), isTypeLiteralNode)));
528561
}
562+
563+
function getUnmatchedAttributes(checker: TypeChecker, source: JsxOpeningLikeElement) {
564+
const attrsType = checker.getContextualType(source.attributes);
565+
if (attrsType === undefined) return emptyArray;
566+
567+
const targetProps = attrsType.getProperties();
568+
if (!length(targetProps)) return emptyArray;
569+
570+
const seenNames = new Set<__String>();
571+
for (const sourceProp of source.attributes.properties) {
572+
if (isJsxAttribute(sourceProp)) {
573+
seenNames.add(sourceProp.name.escapedText);
574+
}
575+
if (isJsxSpreadAttribute(sourceProp)) {
576+
const type = checker.getTypeAtLocation(sourceProp.expression);
577+
for (const prop of type.getProperties()) {
578+
seenNames.add(prop.escapedName);
579+
}
580+
}
581+
}
582+
return filter(targetProps, targetProp =>
583+
!((targetProp.flags & SymbolFlags.Optional || getCheckFlags(targetProp) & CheckFlags.Partial) || seenNames.has(targetProp.escapedName)));
584+
}
529585
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @jsx: preserve
4+
// @filename: foo.tsx
5+
6+
////interface P {
7+
//// a: number;
8+
//// b: string;
9+
////}
10+
////
11+
////const A = ({ a, b }: P) =>
12+
//// <div>{a}{b}</div>;
13+
////
14+
////const Bar = () =>
15+
//// [|<A></A>|]
16+
17+
verify.codeFix({
18+
index: 0,
19+
description: ts.Diagnostics.Add_missing_attributes.message,
20+
newRangeContent: `<A a={0} b={""}></A>`
21+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @jsx: preserve
4+
// @filename: foo.tsx
5+
6+
////interface P {
7+
//// a: number;
8+
//// b: string;
9+
////}
10+
////
11+
////const A = ({ a, b }: P) =>
12+
//// <div>{a}{b}</div>;
13+
////
14+
////const Bar = () =>
15+
//// [|<A a={100}></A>|]
16+
17+
verify.codeFix({
18+
index: 0,
19+
description: ts.Diagnostics.Add_missing_attributes.message,
20+
newRangeContent: `<A a={100} b={""}></A>`
21+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @jsx: preserve
4+
// @filename: foo.tsx
5+
6+
////interface P {
7+
//// a: number;
8+
//// b: string;
9+
//// c: number[];
10+
//// d: any;
11+
////}
12+
////
13+
////const A = ({ a, b, c, d }: P) =>
14+
//// <div>{a}{b}{c}{d}</div>;
15+
////
16+
////const Bar = () =>
17+
//// [|<A {...{ a: 1, b: "" }}></A>|]
18+
19+
verify.codeFix({
20+
index: 0,
21+
description: ts.Diagnostics.Add_missing_attributes.message,
22+
newRangeContent: `<A c={[]} d={undefined} {...{ a: 1, b: "" }}></A>`
23+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @jsx: preserve
4+
// @filename: foo.tsx
5+
6+
////interface P {
7+
//// a: number;
8+
//// b: string;
9+
//// c: number[];
10+
//// d: any;
11+
////}
12+
////
13+
////const A = ({ a, b, c, d }: P) =>
14+
//// <div>{a}{b}{c}{d}</div>;
15+
////
16+
////const props = { a: 1, c: [] };
17+
////const Bar = () =>
18+
//// [|<A {...props}></A>|]
19+
20+
verify.codeFix({
21+
index: 0,
22+
description: ts.Diagnostics.Add_missing_attributes.message,
23+
newRangeContent: `<A b={""} d={undefined} {...props}></A>`
24+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @jsx: preserve
4+
// @filename: foo.tsx
5+
6+
////interface P {
7+
//// a: number;
8+
//// b: string;
9+
//// c: number[];
10+
//// d: any;
11+
////}
12+
////
13+
////const A = ({ a, b, c, d }: P) =>
14+
//// <div>{a}{b}{c}{d}</div>;
15+
////
16+
////const Bar = () =>
17+
//// [|<A a={100} b={""} c={[]} d={undefined}></A>|]
18+
19+
verify.not.codeFixAvailable("fixMissingAttributes");
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @jsx: preserve
4+
// @filename: foo.tsx
5+
6+
////interface P {
7+
//// a: number;
8+
//// b: string;
9+
//// c: number[];
10+
//// d: any;
11+
////}
12+
////
13+
////const A = ({ a, b, c, d }: P) =>
14+
//// <div>{a}{b}{c}{d}</div>;
15+
////
16+
////const props = { a: 1, b: "", c: [], d: undefined };
17+
////const Bar = () =>
18+
//// [|<A {...props}></A>|]
19+
20+
verify.not.codeFixAvailable("fixMissingAttributes");
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @jsx: preserve
4+
// @filename: foo.tsx
5+
6+
////interface P {
7+
//// a: number;
8+
//// b?: string;
9+
////}
10+
////
11+
////const A = ({ a, b }: P) =>
12+
//// <div>{a}{b}</div>;
13+
////
14+
////const Bar = () =>
15+
//// [|<A></A>|]
16+
17+
verify.codeFix({
18+
index: 0,
19+
description: ts.Diagnostics.Add_missing_attributes.message,
20+
newRangeContent: `<A a={0}></A>`
21+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @jsx: preserve
4+
// @filename: foo.tsx
5+
////interface P {
6+
//// a: number;
7+
//// b: string;
8+
//// c: number[];
9+
//// d: any;
10+
////}
11+
////const A = ({ a, b, c, d }: P) =>
12+
//// <div>{a}{b}{c}{d}</div>;
13+
////const props = { a: 1, b: "" };
14+
////
15+
////const C1 = () =>
16+
//// <A a={1} b={""}></A>
17+
////const C2 = () =>
18+
//// <A {...props}></A>
19+
////const C3 = () =>
20+
//// <A c={[]} {...props}></A>
21+
////const C4 = () =>
22+
//// <A></A>
23+
24+
goTo.file("foo.tsx");
25+
verify.codeFixAll({
26+
fixId: "fixMissingAttributes",
27+
fixAllDescription: ts.Diagnostics.Add_all_missing_attributes.message,
28+
newFileContent:
29+
`interface P {
30+
a: number;
31+
b: string;
32+
c: number[];
33+
d: any;
34+
}
35+
const A = ({ a, b, c, d }: P) =>
36+
<div>{a}{b}{c}{d}</div>;
37+
const props = { a: 1, b: "" };
38+
39+
const C1 = () =>
40+
<A a={1} b={""} c={[]} d={undefined}></A>
41+
const C2 = () =>
42+
<A c={[]} d={undefined} {...props}></A>
43+
const C3 = () =>
44+
<A d={undefined} c={[]} {...props}></A>
45+
const C4 = () =>
46+
<A a={0} b={""} c={[]} d={undefined}></A>`
47+
});

0 commit comments

Comments
 (0)