From 5b947e6526eaa00260ba3e9ef835ac515fbb3e48 Mon Sep 17 00:00:00 2001 From: Oleksandr T Date: Thu, 17 Feb 2022 21:30:52 +0200 Subject: [PATCH] feat(47619): add support for extracting jsx string literal attribute to a constant (#47624) --- src/services/refactors/extractSymbol.ts | 16 ++++++--- .../fourslash/extract-const_jsxAttribute1.ts | 28 +++++++++++++++ .../fourslash/extract-const_jsxAttribute2.ts | 28 +++++++++++++++ .../fourslash/extract-const_jsxAttribute3.ts | 35 +++++++++++++++++++ 4 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 tests/cases/fourslash/extract-const_jsxAttribute1.ts create mode 100644 tests/cases/fourslash/extract-const_jsxAttribute2.ts create mode 100644 tests/cases/fourslash/extract-const_jsxAttribute3.ts diff --git a/src/services/refactors/extractSymbol.ts b/src/services/refactors/extractSymbol.ts index 9f0392f1014..37310c041e8 100644 --- a/src/services/refactors/extractSymbol.ts +++ b/src/services/refactors/extractSymbol.ts @@ -433,7 +433,7 @@ namespace ts.refactor.extractSymbol { // For understanding how skipTrivia functioned: Debug.assert(!positionIsSynthesized(nodeToCheck.pos), "This failure could trigger https://github.com/Microsoft/TypeScript/issues/20809 (2)"); - if (!isStatement(nodeToCheck) && !(isExpressionNode(nodeToCheck) && isExtractableExpression(nodeToCheck))) { + if (!isStatement(nodeToCheck) && !(isExpressionNode(nodeToCheck) && isExtractableExpression(nodeToCheck)) && !isStringLiteralJsxAttribute(nodeToCheck)) { return [createDiagnosticForNode(nodeToCheck, Messages.statementOrExpressionExpected)]; } @@ -625,13 +625,16 @@ namespace ts.refactor.extractSymbol { if (isStatement(node)) { return [node]; } - else if (isExpressionNode(node)) { + if (isExpressionNode(node)) { // If our selection is the expression in an ExpressionStatement, expand // the selection to include the enclosing Statement (this stops us // from trying to care about the return value of the extracted function // and eliminates double semicolon insertion in certain scenarios) return isExpressionStatement(node.parent) ? [node.parent] : node as Expression; } + if (isStringLiteralJsxAttribute(node)) { + return node; + } return undefined; } @@ -1649,7 +1652,7 @@ namespace ts.refactor.extractSymbol { // Unfortunately, this code takes advantage of the knowledge that the generated method // will use the contextual type of an expression as the return type of the extracted // method (and will therefore "use" all the types involved). - if (inGenericContext && !isReadonlyArray(targetRange.range)) { + if (inGenericContext && !isReadonlyArray(targetRange.range) && !isJsxAttribute(targetRange.range)) { const contextualType = checker.getContextualType(targetRange.range)!; // TODO: GH#18217 recordTypeParameterUsages(contextualType); } @@ -1997,6 +2000,11 @@ namespace ts.refactor.extractSymbol { } function isInJSXContent(node: Node) { - return (isJsxElement(node) || isJsxSelfClosingElement(node) || isJsxFragment(node)) && (isJsxElement(node.parent) || isJsxFragment(node.parent)); + return isStringLiteralJsxAttribute(node) || + (isJsxElement(node) || isJsxSelfClosingElement(node) || isJsxFragment(node)) && (isJsxElement(node.parent) || isJsxFragment(node.parent)); + } + + function isStringLiteralJsxAttribute(node: Node): node is StringLiteral { + return isStringLiteral(node) && node.parent && isJsxAttribute(node.parent); } } diff --git a/tests/cases/fourslash/extract-const_jsxAttribute1.ts b/tests/cases/fourslash/extract-const_jsxAttribute1.ts new file mode 100644 index 00000000000..f1962afece1 --- /dev/null +++ b/tests/cases/fourslash/extract-const_jsxAttribute1.ts @@ -0,0 +1,28 @@ +/// + +// @jsx: preserve +// @filename: a.tsx +////function Foo() { +//// return ( +////
+//// ; +////
+//// ); +////} + +goTo.file("a.tsx"); +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Extract Symbol", + actionName: "constant_scope_1", + actionDescription: "Extract to constant in global scope", + newContent: +`const /*RENAME*/newLocal = "string"; +function Foo() { + return ( +
+ ; +
+ ); +}` +}); diff --git a/tests/cases/fourslash/extract-const_jsxAttribute2.ts b/tests/cases/fourslash/extract-const_jsxAttribute2.ts new file mode 100644 index 00000000000..f5f247c8d67 --- /dev/null +++ b/tests/cases/fourslash/extract-const_jsxAttribute2.ts @@ -0,0 +1,28 @@ +/// + +// @jsx: preserve +// @filename: a.tsx +////function Foo() { +//// return ( +////
+//// ; +////
+//// ); +////} + +goTo.file("a.tsx"); +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Extract Symbol", + actionName: "constant_scope_0", + actionDescription: "Extract to constant in enclosing scope", + newContent: +`function Foo() { + const /*RENAME*/newLocal = "string"; + return ( +
+ ; +
+ ); +}` +}); diff --git a/tests/cases/fourslash/extract-const_jsxAttribute3.ts b/tests/cases/fourslash/extract-const_jsxAttribute3.ts new file mode 100644 index 00000000000..d3ab80428b3 --- /dev/null +++ b/tests/cases/fourslash/extract-const_jsxAttribute3.ts @@ -0,0 +1,35 @@ +/// + +// @jsx: preserve +// @filename: a.tsx +////declare var React: any; +////class Foo extends React.Component<{}, {}> { +//// render() { +//// return ( +////
+//// ; +////
+//// ); +//// } +////} + +goTo.file("a.tsx"); +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Extract Symbol", + actionName: "constant_scope_1", + actionDescription: "Extract to readonly field in class 'Foo'", + newContent: +`declare var React: any; +class Foo extends React.Component<{}, {}> { + private readonly newProperty = "string"; + + render() { + return ( +
+ ; +
+ ); + } +}` +});