From cce2e926dec5098fb5d9d40e9eb296d1070db792 Mon Sep 17 00:00:00 2001 From: Oleksandr T Date: Fri, 6 Aug 2021 22:38:22 +0300 Subject: [PATCH] feat(45163): add QF to declare missing jsx attributes (#45179) --- src/compiler/diagnosticMessages.json | 8 ++ src/services/codefixes/fixAddMissingMember.ts | 86 +++++++++++++++---- .../fourslash/codeFixAddMissingAttributes1.ts | 21 +++++ .../fourslash/codeFixAddMissingAttributes2.ts | 21 +++++ .../fourslash/codeFixAddMissingAttributes3.ts | 23 +++++ .../fourslash/codeFixAddMissingAttributes4.ts | 24 ++++++ .../fourslash/codeFixAddMissingAttributes5.ts | 19 ++++ .../fourslash/codeFixAddMissingAttributes6.ts | 20 +++++ .../fourslash/codeFixAddMissingAttributes7.ts | 21 +++++ .../codeFixAddMissingAttributes_all.ts | 47 ++++++++++ 10 files changed, 275 insertions(+), 15 deletions(-) create mode 100644 tests/cases/fourslash/codeFixAddMissingAttributes1.ts create mode 100644 tests/cases/fourslash/codeFixAddMissingAttributes2.ts create mode 100644 tests/cases/fourslash/codeFixAddMissingAttributes3.ts create mode 100644 tests/cases/fourslash/codeFixAddMissingAttributes4.ts create mode 100644 tests/cases/fourslash/codeFixAddMissingAttributes5.ts create mode 100644 tests/cases/fourslash/codeFixAddMissingAttributes6.ts create mode 100644 tests/cases/fourslash/codeFixAddMissingAttributes7.ts create mode 100644 tests/cases/fourslash/codeFixAddMissingAttributes_all.ts diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index fcc8813a007..adda7231643 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -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", diff --git a/src/services/codefixes/fixAddMissingMember.ts b/src/services/codefixes/fixAddMissingMember.ts index 9101dafc287..e4bd2e43623 100644 --- a/src/services/codefixes/fixAddMissingMember.ts +++ b/src/services/codefixes/fixAddMissingMember.ts @@ -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))); + } } diff --git a/tests/cases/fourslash/codeFixAddMissingAttributes1.ts b/tests/cases/fourslash/codeFixAddMissingAttributes1.ts new file mode 100644 index 00000000000..4708fc84888 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingAttributes1.ts @@ -0,0 +1,21 @@ +/// + +// @jsx: preserve +// @filename: foo.tsx + +////interface P { +//// a: number; +//// b: string; +////} +//// +////const A = ({ a, b }: P) => +////
{a}{b}
; +//// +////const Bar = () => +//// [||] + +verify.codeFix({ + index: 0, + description: ts.Diagnostics.Add_missing_attributes.message, + newRangeContent: `` +}); diff --git a/tests/cases/fourslash/codeFixAddMissingAttributes2.ts b/tests/cases/fourslash/codeFixAddMissingAttributes2.ts new file mode 100644 index 00000000000..ccd8d778f6b --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingAttributes2.ts @@ -0,0 +1,21 @@ +/// + +// @jsx: preserve +// @filename: foo.tsx + +////interface P { +//// a: number; +//// b: string; +////} +//// +////const A = ({ a, b }: P) => +////
{a}{b}
; +//// +////const Bar = () => +//// [||] + +verify.codeFix({ + index: 0, + description: ts.Diagnostics.Add_missing_attributes.message, + newRangeContent: `` +}); diff --git a/tests/cases/fourslash/codeFixAddMissingAttributes3.ts b/tests/cases/fourslash/codeFixAddMissingAttributes3.ts new file mode 100644 index 00000000000..30f0ad06b45 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingAttributes3.ts @@ -0,0 +1,23 @@ +/// + +// @jsx: preserve +// @filename: foo.tsx + +////interface P { +//// a: number; +//// b: string; +//// c: number[]; +//// d: any; +////} +//// +////const A = ({ a, b, c, d }: P) => +////
{a}{b}{c}{d}
; +//// +////const Bar = () => +//// [||] + +verify.codeFix({ + index: 0, + description: ts.Diagnostics.Add_missing_attributes.message, + newRangeContent: `` +}); diff --git a/tests/cases/fourslash/codeFixAddMissingAttributes4.ts b/tests/cases/fourslash/codeFixAddMissingAttributes4.ts new file mode 100644 index 00000000000..0445ddc0de5 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingAttributes4.ts @@ -0,0 +1,24 @@ +/// + +// @jsx: preserve +// @filename: foo.tsx + +////interface P { +//// a: number; +//// b: string; +//// c: number[]; +//// d: any; +////} +//// +////const A = ({ a, b, c, d }: P) => +////
{a}{b}{c}{d}
; +//// +////const props = { a: 1, c: [] }; +////const Bar = () => +//// [||] + +verify.codeFix({ + index: 0, + description: ts.Diagnostics.Add_missing_attributes.message, + newRangeContent: `` +}); diff --git a/tests/cases/fourslash/codeFixAddMissingAttributes5.ts b/tests/cases/fourslash/codeFixAddMissingAttributes5.ts new file mode 100644 index 00000000000..4c6a3ebe169 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingAttributes5.ts @@ -0,0 +1,19 @@ +/// + +// @jsx: preserve +// @filename: foo.tsx + +////interface P { +//// a: number; +//// b: string; +//// c: number[]; +//// d: any; +////} +//// +////const A = ({ a, b, c, d }: P) => +////
{a}{b}{c}{d}
; +//// +////const Bar = () => +//// [||] + +verify.not.codeFixAvailable("fixMissingAttributes"); diff --git a/tests/cases/fourslash/codeFixAddMissingAttributes6.ts b/tests/cases/fourslash/codeFixAddMissingAttributes6.ts new file mode 100644 index 00000000000..65133c32014 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingAttributes6.ts @@ -0,0 +1,20 @@ +/// + +// @jsx: preserve +// @filename: foo.tsx + +////interface P { +//// a: number; +//// b: string; +//// c: number[]; +//// d: any; +////} +//// +////const A = ({ a, b, c, d }: P) => +////
{a}{b}{c}{d}
; +//// +////const props = { a: 1, b: "", c: [], d: undefined }; +////const Bar = () => +//// [||] + +verify.not.codeFixAvailable("fixMissingAttributes"); diff --git a/tests/cases/fourslash/codeFixAddMissingAttributes7.ts b/tests/cases/fourslash/codeFixAddMissingAttributes7.ts new file mode 100644 index 00000000000..107558742c0 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingAttributes7.ts @@ -0,0 +1,21 @@ +/// + +// @jsx: preserve +// @filename: foo.tsx + +////interface P { +//// a: number; +//// b?: string; +////} +//// +////const A = ({ a, b }: P) => +////
{a}{b}
; +//// +////const Bar = () => +//// [||] + +verify.codeFix({ + index: 0, + description: ts.Diagnostics.Add_missing_attributes.message, + newRangeContent: `` +}); diff --git a/tests/cases/fourslash/codeFixAddMissingAttributes_all.ts b/tests/cases/fourslash/codeFixAddMissingAttributes_all.ts new file mode 100644 index 00000000000..99ca5df4c45 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingAttributes_all.ts @@ -0,0 +1,47 @@ +/// + +// @jsx: preserve +// @filename: foo.tsx +////interface P { +//// a: number; +//// b: string; +//// c: number[]; +//// d: any; +////} +////const A = ({ a, b, c, d }: P) => +////
{a}{b}{c}{d}
; +////const props = { a: 1, b: "" }; +//// +////const C1 = () => +//// +////const C2 = () => +//// +////const C3 = () => +//// +////const C4 = () => +//// + +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) => +
{a}{b}{c}{d}
; +const props = { a: 1, b: "" }; + +const C1 = () => + +const C2 = () => + +const C3 = () => + +const C4 = () => + ` +});