mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-02-05 08:11:30 -06:00
feat(45163): add QF to declare missing jsx attributes (#45179)
This commit is contained in:
parent
abfe5f0be7
commit
cce2e926de
@ -7110,6 +7110,14 @@
|
||||
"category": "Message",
|
||||
"code": 95166
|
||||
},
|
||||
"Add missing attributes": {
|
||||
"category": "Message",
|
||||
"code": 95167
|
||||
},
|
||||
"Add all missing attributes": {
|
||||
"category": "Message",
|
||||
"code": 95168
|
||||
},
|
||||
|
||||
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
|
||||
"category": "Error",
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
namespace ts.codefix {
|
||||
const fixMissingMember = "fixMissingMember";
|
||||
const fixMissingProperties = "fixMissingProperties";
|
||||
const fixMissingAttributes = "fixMissingAttributes";
|
||||
const fixMissingFunctionDeclaration = "fixMissingFunctionDeclaration";
|
||||
|
||||
const errorCodes = [
|
||||
@ -25,6 +26,10 @@ namespace ts.codefix {
|
||||
const changes = textChanges.ChangeTracker.with(context, t => addObjectLiteralProperties(t, context, info));
|
||||
return [createCodeFixAction(fixMissingProperties, changes, Diagnostics.Add_missing_properties, fixMissingProperties, Diagnostics.Add_all_missing_properties)];
|
||||
}
|
||||
if (info.kind === InfoKind.JsxAttributes) {
|
||||
const changes = textChanges.ChangeTracker.with(context, t => addJsxAttributes(t, context, info));
|
||||
return [createCodeFixAction(fixMissingAttributes, changes, Diagnostics.Add_missing_attributes, fixMissingAttributes, Diagnostics.Add_all_missing_attributes)];
|
||||
}
|
||||
if (info.kind === InfoKind.Function) {
|
||||
const changes = textChanges.ChangeTracker.with(context, t => addFunctionDeclaration(t, context, info));
|
||||
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 {
|
||||
}
|
||||
return concatenate(getActionsForMissingMethodDeclaration(context, info), getActionsForMissingMemberDeclaration(context, info));
|
||||
},
|
||||
fixIds: [fixMissingMember, fixMissingFunctionDeclaration, fixMissingProperties],
|
||||
fixIds: [fixMissingMember, fixMissingFunctionDeclaration, fixMissingProperties, fixMissingAttributes],
|
||||
getAllCodeActions: context => {
|
||||
const { program, fixId } = context;
|
||||
const checker = program.getTypeChecker();
|
||||
@ -49,15 +54,14 @@ namespace ts.codefix {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fixId === fixMissingFunctionDeclaration) {
|
||||
if (info.kind === InfoKind.Function) {
|
||||
addFunctionDeclaration(changes, context, info);
|
||||
}
|
||||
if (fixId === fixMissingFunctionDeclaration && info.kind === InfoKind.Function) {
|
||||
addFunctionDeclaration(changes, context, info);
|
||||
}
|
||||
else if (fixId === fixMissingProperties) {
|
||||
if (info.kind === InfoKind.ObjectLiteral) {
|
||||
addObjectLiteralProperties(changes, context, info);
|
||||
}
|
||||
else if (fixId === fixMissingProperties && info.kind === InfoKind.ObjectLiteral) {
|
||||
addObjectLiteralProperties(changes, context, info);
|
||||
}
|
||||
else if (fixId === fixMissingAttributes && info.kind === InfoKind.JsxAttributes) {
|
||||
addJsxAttributes(changes, context, info);
|
||||
}
|
||||
else {
|
||||
if (info.kind === InfoKind.Enum) {
|
||||
@ -102,8 +106,8 @@ namespace ts.codefix {
|
||||
},
|
||||
});
|
||||
|
||||
const enum InfoKind { Enum, ClassOrInterface, Function, ObjectLiteral }
|
||||
type Info = EnumInfo | ClassOrInterfaceInfo | FunctionInfo | ObjectLiteralInfo;
|
||||
const enum InfoKind { Enum, ClassOrInterface, Function, ObjectLiteral, JsxAttributes }
|
||||
type Info = EnumInfo | ClassOrInterfaceInfo | FunctionInfo | ObjectLiteralInfo | JsxAttributesInfo;
|
||||
|
||||
interface EnumInfo {
|
||||
readonly kind: InfoKind.Enum;
|
||||
@ -137,6 +141,13 @@ namespace ts.codefix {
|
||||
readonly parentDeclaration: ObjectLiteralExpression;
|
||||
}
|
||||
|
||||
interface JsxAttributesInfo {
|
||||
readonly kind: InfoKind.JsxAttributes;
|
||||
readonly token: Identifier;
|
||||
readonly attributes: Symbol[];
|
||||
readonly parentDeclaration: JsxOpeningLikeElement;
|
||||
}
|
||||
|
||||
function getInfo(sourceFile: SourceFile, tokenPos: number, checker: TypeChecker, program: Program): Info | undefined {
|
||||
// The identifier of the missing property. eg:
|
||||
// this.missing = 1;
|
||||
@ -154,6 +165,13 @@ namespace ts.codefix {
|
||||
}
|
||||
}
|
||||
|
||||
if (isIdentifier(token) && isJsxOpeningLikeElement(token.parent)) {
|
||||
const attributes = getUnmatchedAttributes(checker, token.parent);
|
||||
if (length(attributes)) {
|
||||
return { kind: InfoKind.JsxAttributes, token, attributes, parentDeclaration: token.parent };
|
||||
}
|
||||
}
|
||||
|
||||
if (isIdentifier(token) && isCallExpression(parent)) {
|
||||
return { kind: InfoKind.Function, token, call: parent, sourceFile, modifierFlags: ModifierFlags.None, parentDeclaration: sourceFile };
|
||||
}
|
||||
@ -434,18 +452,33 @@ namespace ts.codefix {
|
||||
changes.insertNodeAtEndOfScope(info.sourceFile, info.parentDeclaration, functionDeclaration);
|
||||
}
|
||||
|
||||
function addJsxAttributes(changes: textChanges.ChangeTracker, context: CodeFixContextBase, info: JsxAttributesInfo) {
|
||||
const importAdder = createImportAdder(context.sourceFile, context.program, context.preferences, context.host);
|
||||
const quotePreference = getQuotePreference(context.sourceFile, context.preferences);
|
||||
const checker = context.program.getTypeChecker();
|
||||
const jsxAttributesNode = info.parentDeclaration.attributes;
|
||||
const hasSpreadAttribute = some(jsxAttributesNode.properties, isJsxSpreadAttribute);
|
||||
const attrs = map(info.attributes, attr => {
|
||||
const value = attr.valueDeclaration ? tryGetValueFromType(context, checker, importAdder, quotePreference, checker.getTypeAtLocation(attr.valueDeclaration)) : createUndefined();
|
||||
return factory.createJsxAttribute(factory.createIdentifier(attr.name), factory.createJsxExpression(/*dotDotDotToken*/ undefined, value));
|
||||
});
|
||||
const jsxAttributes = factory.createJsxAttributes(hasSpreadAttribute ? [...attrs, ...jsxAttributesNode.properties] : [...jsxAttributesNode.properties, ...attrs]);
|
||||
const options = { prefix: jsxAttributesNode.pos === jsxAttributesNode.end ? " " : undefined };
|
||||
changes.replaceNode(context.sourceFile, jsxAttributesNode, jsxAttributes, options);
|
||||
}
|
||||
|
||||
function addObjectLiteralProperties(changes: textChanges.ChangeTracker, context: CodeFixContextBase, info: ObjectLiteralInfo) {
|
||||
const importAdder = createImportAdder(context.sourceFile, context.program, context.preferences, context.host);
|
||||
const quotePreference = getQuotePreference(context.sourceFile, context.preferences);
|
||||
const checker = context.program.getTypeChecker();
|
||||
const props = map(info.properties, prop => {
|
||||
const initializer = prop.valueDeclaration ? tryGetInitializerValueFromType(context, checker, importAdder, quotePreference, checker.getTypeAtLocation(prop.valueDeclaration)) : createUndefined();
|
||||
const initializer = prop.valueDeclaration ? tryGetValueFromType(context, checker, importAdder, quotePreference, checker.getTypeAtLocation(prop.valueDeclaration)) : createUndefined();
|
||||
return factory.createPropertyAssignment(prop.name, initializer);
|
||||
});
|
||||
changes.replaceNode(context.sourceFile, info.parentDeclaration, factory.createObjectLiteralExpression([...info.parentDeclaration.properties, ...props], /*multiLine*/ true));
|
||||
}
|
||||
|
||||
function tryGetInitializerValueFromType(context: CodeFixContextBase, checker: TypeChecker, importAdder: ImportAdder, quotePreference: QuotePreference, type: Type): Expression {
|
||||
function tryGetValueFromType(context: CodeFixContextBase, checker: TypeChecker, importAdder: ImportAdder, quotePreference: QuotePreference, type: Type): Expression {
|
||||
if (type.flags & TypeFlags.AnyOrUnknown) {
|
||||
return createUndefined();
|
||||
}
|
||||
@ -482,7 +515,7 @@ namespace ts.codefix {
|
||||
return factory.createNull();
|
||||
}
|
||||
if (type.flags & TypeFlags.Union) {
|
||||
const expression = firstDefined((type as UnionType).types, t => tryGetInitializerValueFromType(context, checker, importAdder, quotePreference, t));
|
||||
const expression = firstDefined((type as UnionType).types, t => tryGetValueFromType(context, checker, importAdder, quotePreference, t));
|
||||
return expression ?? createUndefined();
|
||||
}
|
||||
if (checker.isArrayLikeType(type)) {
|
||||
@ -490,7 +523,7 @@ namespace ts.codefix {
|
||||
}
|
||||
if (isObjectLiteralType(type)) {
|
||||
const props = map(checker.getPropertiesOfType(type), prop => {
|
||||
const initializer = prop.valueDeclaration ? tryGetInitializerValueFromType(context, checker, importAdder, quotePreference, checker.getTypeAtLocation(prop.valueDeclaration)) : createUndefined();
|
||||
const initializer = prop.valueDeclaration ? tryGetValueFromType(context, checker, importAdder, quotePreference, checker.getTypeAtLocation(prop.valueDeclaration)) : createUndefined();
|
||||
return factory.createPropertyAssignment(prop.name, initializer);
|
||||
});
|
||||
return factory.createObjectLiteralExpression(props, /*multiLine*/ true);
|
||||
@ -526,4 +559,27 @@ namespace ts.codefix {
|
||||
return (type.flags & TypeFlags.Object) &&
|
||||
((getObjectFlags(type) & ObjectFlags.ObjectLiteral) || (type.symbol && tryCast(singleOrUndefined(type.symbol.declarations), isTypeLiteralNode)));
|
||||
}
|
||||
|
||||
function getUnmatchedAttributes(checker: TypeChecker, source: JsxOpeningLikeElement) {
|
||||
const attrsType = checker.getContextualType(source.attributes);
|
||||
if (attrsType === undefined) return emptyArray;
|
||||
|
||||
const targetProps = attrsType.getProperties();
|
||||
if (!length(targetProps)) return emptyArray;
|
||||
|
||||
const seenNames = new Set<__String>();
|
||||
for (const sourceProp of source.attributes.properties) {
|
||||
if (isJsxAttribute(sourceProp)) {
|
||||
seenNames.add(sourceProp.name.escapedText);
|
||||
}
|
||||
if (isJsxSpreadAttribute(sourceProp)) {
|
||||
const type = checker.getTypeAtLocation(sourceProp.expression);
|
||||
for (const prop of type.getProperties()) {
|
||||
seenNames.add(prop.escapedName);
|
||||
}
|
||||
}
|
||||
}
|
||||
return filter(targetProps, targetProp =>
|
||||
!((targetProp.flags & SymbolFlags.Optional || getCheckFlags(targetProp) & CheckFlags.Partial) || seenNames.has(targetProp.escapedName)));
|
||||
}
|
||||
}
|
||||
|
||||
21
tests/cases/fourslash/codeFixAddMissingAttributes1.ts
Normal file
21
tests/cases/fourslash/codeFixAddMissingAttributes1.ts
Normal file
@ -0,0 +1,21 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
// @jsx: preserve
|
||||
// @filename: foo.tsx
|
||||
|
||||
////interface P {
|
||||
//// a: number;
|
||||
//// b: string;
|
||||
////}
|
||||
////
|
||||
////const A = ({ a, b }: P) =>
|
||||
//// <div>{a}{b}</div>;
|
||||
////
|
||||
////const Bar = () =>
|
||||
//// [|<A></A>|]
|
||||
|
||||
verify.codeFix({
|
||||
index: 0,
|
||||
description: ts.Diagnostics.Add_missing_attributes.message,
|
||||
newRangeContent: `<A a={0} b={""}></A>`
|
||||
});
|
||||
21
tests/cases/fourslash/codeFixAddMissingAttributes2.ts
Normal file
21
tests/cases/fourslash/codeFixAddMissingAttributes2.ts
Normal file
@ -0,0 +1,21 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
// @jsx: preserve
|
||||
// @filename: foo.tsx
|
||||
|
||||
////interface P {
|
||||
//// a: number;
|
||||
//// b: string;
|
||||
////}
|
||||
////
|
||||
////const A = ({ a, b }: P) =>
|
||||
//// <div>{a}{b}</div>;
|
||||
////
|
||||
////const Bar = () =>
|
||||
//// [|<A a={100}></A>|]
|
||||
|
||||
verify.codeFix({
|
||||
index: 0,
|
||||
description: ts.Diagnostics.Add_missing_attributes.message,
|
||||
newRangeContent: `<A a={100} b={""}></A>`
|
||||
});
|
||||
23
tests/cases/fourslash/codeFixAddMissingAttributes3.ts
Normal file
23
tests/cases/fourslash/codeFixAddMissingAttributes3.ts
Normal file
@ -0,0 +1,23 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
// @jsx: preserve
|
||||
// @filename: foo.tsx
|
||||
|
||||
////interface P {
|
||||
//// a: number;
|
||||
//// b: string;
|
||||
//// c: number[];
|
||||
//// d: any;
|
||||
////}
|
||||
////
|
||||
////const A = ({ a, b, c, d }: P) =>
|
||||
//// <div>{a}{b}{c}{d}</div>;
|
||||
////
|
||||
////const Bar = () =>
|
||||
//// [|<A {...{ a: 1, b: "" }}></A>|]
|
||||
|
||||
verify.codeFix({
|
||||
index: 0,
|
||||
description: ts.Diagnostics.Add_missing_attributes.message,
|
||||
newRangeContent: `<A c={[]} d={undefined} {...{ a: 1, b: "" }}></A>`
|
||||
});
|
||||
24
tests/cases/fourslash/codeFixAddMissingAttributes4.ts
Normal file
24
tests/cases/fourslash/codeFixAddMissingAttributes4.ts
Normal file
@ -0,0 +1,24 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
// @jsx: preserve
|
||||
// @filename: foo.tsx
|
||||
|
||||
////interface P {
|
||||
//// a: number;
|
||||
//// b: string;
|
||||
//// c: number[];
|
||||
//// d: any;
|
||||
////}
|
||||
////
|
||||
////const A = ({ a, b, c, d }: P) =>
|
||||
//// <div>{a}{b}{c}{d}</div>;
|
||||
////
|
||||
////const props = { a: 1, c: [] };
|
||||
////const Bar = () =>
|
||||
//// [|<A {...props}></A>|]
|
||||
|
||||
verify.codeFix({
|
||||
index: 0,
|
||||
description: ts.Diagnostics.Add_missing_attributes.message,
|
||||
newRangeContent: `<A b={""} d={undefined} {...props}></A>`
|
||||
});
|
||||
19
tests/cases/fourslash/codeFixAddMissingAttributes5.ts
Normal file
19
tests/cases/fourslash/codeFixAddMissingAttributes5.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
// @jsx: preserve
|
||||
// @filename: foo.tsx
|
||||
|
||||
////interface P {
|
||||
//// a: number;
|
||||
//// b: string;
|
||||
//// c: number[];
|
||||
//// d: any;
|
||||
////}
|
||||
////
|
||||
////const A = ({ a, b, c, d }: P) =>
|
||||
//// <div>{a}{b}{c}{d}</div>;
|
||||
////
|
||||
////const Bar = () =>
|
||||
//// [|<A a={100} b={""} c={[]} d={undefined}></A>|]
|
||||
|
||||
verify.not.codeFixAvailable("fixMissingAttributes");
|
||||
20
tests/cases/fourslash/codeFixAddMissingAttributes6.ts
Normal file
20
tests/cases/fourslash/codeFixAddMissingAttributes6.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
// @jsx: preserve
|
||||
// @filename: foo.tsx
|
||||
|
||||
////interface P {
|
||||
//// a: number;
|
||||
//// b: string;
|
||||
//// c: number[];
|
||||
//// d: any;
|
||||
////}
|
||||
////
|
||||
////const A = ({ a, b, c, d }: P) =>
|
||||
//// <div>{a}{b}{c}{d}</div>;
|
||||
////
|
||||
////const props = { a: 1, b: "", c: [], d: undefined };
|
||||
////const Bar = () =>
|
||||
//// [|<A {...props}></A>|]
|
||||
|
||||
verify.not.codeFixAvailable("fixMissingAttributes");
|
||||
21
tests/cases/fourslash/codeFixAddMissingAttributes7.ts
Normal file
21
tests/cases/fourslash/codeFixAddMissingAttributes7.ts
Normal file
@ -0,0 +1,21 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
// @jsx: preserve
|
||||
// @filename: foo.tsx
|
||||
|
||||
////interface P {
|
||||
//// a: number;
|
||||
//// b?: string;
|
||||
////}
|
||||
////
|
||||
////const A = ({ a, b }: P) =>
|
||||
//// <div>{a}{b}</div>;
|
||||
////
|
||||
////const Bar = () =>
|
||||
//// [|<A></A>|]
|
||||
|
||||
verify.codeFix({
|
||||
index: 0,
|
||||
description: ts.Diagnostics.Add_missing_attributes.message,
|
||||
newRangeContent: `<A a={0}></A>`
|
||||
});
|
||||
47
tests/cases/fourslash/codeFixAddMissingAttributes_all.ts
Normal file
47
tests/cases/fourslash/codeFixAddMissingAttributes_all.ts
Normal file
@ -0,0 +1,47 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
// @jsx: preserve
|
||||
// @filename: foo.tsx
|
||||
////interface P {
|
||||
//// a: number;
|
||||
//// b: string;
|
||||
//// c: number[];
|
||||
//// d: any;
|
||||
////}
|
||||
////const A = ({ a, b, c, d }: P) =>
|
||||
//// <div>{a}{b}{c}{d}</div>;
|
||||
////const props = { a: 1, b: "" };
|
||||
////
|
||||
////const C1 = () =>
|
||||
//// <A a={1} b={""}></A>
|
||||
////const C2 = () =>
|
||||
//// <A {...props}></A>
|
||||
////const C3 = () =>
|
||||
//// <A c={[]} {...props}></A>
|
||||
////const C4 = () =>
|
||||
//// <A></A>
|
||||
|
||||
goTo.file("foo.tsx");
|
||||
verify.codeFixAll({
|
||||
fixId: "fixMissingAttributes",
|
||||
fixAllDescription: ts.Diagnostics.Add_all_missing_attributes.message,
|
||||
newFileContent:
|
||||
`interface P {
|
||||
a: number;
|
||||
b: string;
|
||||
c: number[];
|
||||
d: any;
|
||||
}
|
||||
const A = ({ a, b, c, d }: P) =>
|
||||
<div>{a}{b}{c}{d}</div>;
|
||||
const props = { a: 1, b: "" };
|
||||
|
||||
const C1 = () =>
|
||||
<A a={1} b={""} c={[]} d={undefined}></A>
|
||||
const C2 = () =>
|
||||
<A c={[]} d={undefined} {...props}></A>
|
||||
const C3 = () =>
|
||||
<A d={undefined} c={[]} {...props}></A>
|
||||
const C4 = () =>
|
||||
<A a={0} b={""} c={[]} d={undefined}></A>`
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user