From 19386505d43c25d40b02494f3a44f7a7fabe6348 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Thu, 31 May 2018 17:11:21 +0800 Subject: [PATCH 1/8] Impl getReferenceToReact isReactComponentClass isConvertableClass --- .../refactors/convertReactPureComponent.ts | 146 ++++++++++++++++++ src/services/tsconfig.json | 1 + 2 files changed, 147 insertions(+) create mode 100644 src/services/refactors/convertReactPureComponent.ts diff --git a/src/services/refactors/convertReactPureComponent.ts b/src/services/refactors/convertReactPureComponent.ts new file mode 100644 index 0000000000000..93c5ba88a2b5a --- /dev/null +++ b/src/services/refactors/convertReactPureComponent.ts @@ -0,0 +1,146 @@ +/** @internal */ +namespace ts.refactor.convertReactPureComponent { + const refactorName = "Convert React pure component"; + const actionNameComponentToSFC = "Covert React.Component to SFC"; + const actionNameSFCToPureComponent = "Covert React.SFC to PureComponent"; + + /** + * This refactor follows this rule. + * * Is optional. + * + * 1. [ ] Check if JSX Factory is React. If not, no actions will be provided + * 2. [ ] If the selection is a function declaration of type `React.SFC` + * [ ] Provide an action to convert it to `React.PureComponent` + * [ ] * Make a function with 1 to 2 arguments (treat as props & context) + * and explicit returnType T and T subtypeable to React.Element also convertable + * 3. [ ] If the selection is a subclass of `React.Component` or `React.PureComponent` + * [x] and if it satisifies `isConvertableClass()` + * [ ] Provide an action to convert it to `React.SFC` or `React.PureComponent` (if it is `React.Component`) + * 4. [x] Resolve React by `getReferenceToReact()` + * 5. [ ] * Also support `propTypes`, `contextTypes`, `defaultProps`, `displayName` (Only Class -> Function) + * 6. [ ] * Also support convert of reference to `this.context` (in both ways) + */ + + registerRefactor(refactorName, { + getAvailableActions(context: RefactorContext): ApplicableRefactorInfo[] | undefined { + return; + }, + getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined { + return; + } + }); + + function checkJSXFactoryIsReact() { + return true; + } + /** Get name binding to React, React.Component, React.PureComponent, React.SFC, React.StatelessComponent */ + function getReferenceToReact(sourcefile: SourceFile) { + const result: Record<"React" | "Component" | "PureComponent" | "SFC" | "StatelessComponent", string | undefined> = { + Component: undefined, + PureComponent: undefined, + React: undefined, + SFC: undefined, + StatelessComponent: undefined, + } as any; + let importClause: ImportClause; + sourcefile.forEachChild(c => { + if (!ts.isImportDeclaration(c)) { return; } + if (!c.importClause) { return; } + if ((c.moduleSpecifier as StringLiteral).text !== "react") { return; } + const { name, namedBindings } = c.importClause; + importClause = c.importClause; + const names: (keyof typeof result)[] = ["Component", "PureComponent", "SFC", "StatelessComponent"]; + // In case of: + // import * as ns from "mod" => name = undefined, namedBinding: NamespaceImport = { name: ns } + if (namedBindings && ts.isNamespaceImport(namedBindings)) { result.React = namedBindings.name.text; } + // import d from "mod" => name = d, namedBinding = undefined + else if (name) { result.React = name.text; } + // import d, * as ns from "mod" => name = d, namedBinding: NamespaceImport = { name: ns } + // ? No this case + // import { a, b as x } from "mod" => name = undefined, namedBinding: NamedImports = { elements: [{ name: a }, { name: x, propertyName: b}]} + if (namedBindings && ts.isNamedImports(namedBindings)) { + namedBindings.elements.forEach(e => { + if (e.propertyName) { + let p: keyof typeof result = e.propertyName.text as any; + if (names.indexOf(p) !== -1) { result[p] = e.name.text; } + } else { + let n: keyof typeof result = e.name.text as any; + if (names.indexOf(n) !== -1) { result[n] = n; } + } + }) + } + // import d, { a, b as x } from "mod" => name = d, namedBinding: NamedImports = { elements: [{ name: a }, { name: x, propertyName: b}]} + // ? No this case + }) + if (result.React) { + result.Component = result.Component || result.React + ".Component"; + result.PureComponent = result.PureComponent || result.React + ".PureComponent"; + result.SFC = result.SFC || result.React + ".SFC"; + result.StatelessComponent = result.StatelessComponent || result.StatelessComponent + ".StatelessComponent"; + } else if (!result.Component && !result.PureComponent && !result.React && !result.SFC && !result.StatelessComponent) { + return undefined; + } + return { ...result, importClause: importClause! }; + } + + /** Check if expression is a React Component Class, then get its name (opt) */ + function isReactComponentClass(expression: ClassDeclaration | ClassExpression, sourcefile: SourceFile) { + let is = false; + let name: string | undefined = ""; + let reference = getReferenceToReact(sourcefile)!; + let propsType: TypeNode | undefined; + if (expression.heritageClauses) { + if (expression.heritageClauses.some(x => + x.types.some(y => { + const text = y.getText(sourcefile).replace(/\s/g, ""); + if (text === reference.Component || text === reference.PureComponent) { + propsType = y.typeArguments && y.typeArguments[0]; + return true; + } + return false; + }) + )) { + is = true; + if (ts.isClassDeclaration(expression)) { + name = expression.name && expression.name.text; + } + } + } + if (is) return { name, propsType }; + return undefined; + } + function isReactSFCDeclaration(expression: FunctionDeclaration | FunctionExpression) { + return true; + } + /** + * 1. If have any property than `render`, return false + * 2. If have reference to `setState` or `state`, return false + */ + function isConvertableClass(expression: ClassDeclaration | ClassExpression, sourcefile: SourceFile) { + let render: MethodDeclaration; + let convertable = ts.every(expression.members, val => { + // * Should include `["render"]() {}` and `render = () => {}`, but that's crazy, no one code like this + if (ts.isMethodDeclaration(val) && val.modifiers) { + const isMethodNamedRender = ts.isIdentifier(val.name) && val.name.escapedText === "render" + const isStatic = val.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword); + if (isMethodNamedRender && !isStatic) { + render = val; + return true; + } + } + // TODO: Should include staic `propTypes`, `contextTypes`, `defaultProps`, `displayName`. Not now. + return false; + }) + // ? Now check reference to `state` or `setState` + if (convertable) { + // OK let's do this quick + if (render!.getText(sourcefile).match(/this\s+\.\s+(state|setState)/)) { + convertable = false; + } + } + return { convertable, render: render! }; + } + + function transformer() { } +} + diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index 237142fc5bff6..5a1ac735bb130 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -117,6 +117,7 @@ "refactors/extractSymbol.ts", "refactors/generateGetAccessorAndSetAccessor.ts", "refactors/moveToNewFile.ts", + "refactors/convertReactPureComponent.ts", "sourcemaps.ts", "services.ts", "breakpoints.ts", From d453f482ca4a3d3192d62e7985f077fbaa8c61cf Mon Sep 17 00:00:00 2001 From: Jack Works Date: Wed, 6 Jun 2018 14:56:31 +0800 Subject: [PATCH 2/8] Add refactors to tsconfig; Impl getAvailableActions; Impl checkJSXFactoryIsReact; Rewrite isConvertibleReactClassComponent --- src/harness/tsconfig.json | 1 + src/server/tsconfig.json | 284 +++++++++--------- src/server/tsconfig.library.json | 1 + .../refactors/convertReactPureComponent.ts | 175 +++++++---- .../refactorConvertReactPureComponent.ts | 29 ++ 5 files changed, 287 insertions(+), 203 deletions(-) create mode 100644 tests/cases/fourslash/refactorConvertReactPureComponent.ts diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index c401b40a339b5..5d22a455bbaa4 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -124,6 +124,7 @@ "../services/refactors/extractSymbol.ts", "../services/refactors/generateGetAccessorAndSetAccessor.ts", "../services/refactors/moveToNewFile.ts", + "../services/refactors/convertReactPureComponent.ts", "../services/sourcemaps.ts", "../services/services.ts", "../services/breakpoints.ts", diff --git a/src/server/tsconfig.json b/src/server/tsconfig.json index cb0485321ca51..c1945468ea04d 100644 --- a/src/server/tsconfig.json +++ b/src/server/tsconfig.json @@ -1,141 +1,143 @@ -{ - "extends": "../tsconfig-base", - "compilerOptions": { - "removeComments": true, - "outFile": "../../built/local/tsserver.js", - "preserveConstEnums": true, - "types": [ - "node" - ] - }, - "files": [ - "../compiler/types.ts", - "../compiler/performance.ts", - "../compiler/core.ts", - "../compiler/sys.ts", - "../compiler/diagnosticInformationMap.generated.ts", - "../compiler/scanner.ts", - "../compiler/utilities.ts", - "../compiler/parser.ts", - "../compiler/binder.ts", - "../compiler/symbolWalker.ts", - "../compiler/moduleNameResolver.ts", - "../compiler/checker.ts", - "../compiler/factory.ts", - "../compiler/visitor.ts", - "../compiler/transformers/utilities.ts", - "../compiler/transformers/destructuring.ts", - "../compiler/transformers/ts.ts", - "../compiler/transformers/es2017.ts", - "../compiler/transformers/esnext.ts", - "../compiler/transformers/jsx.ts", - "../compiler/transformers/es2016.ts", - "../compiler/transformers/es2015.ts", - "../compiler/transformers/es5.ts", - "../compiler/transformers/generators.ts", - "../compiler/transformers/module/module.ts", - "../compiler/transformers/module/system.ts", - "../compiler/transformers/module/es2015.ts", - "../compiler/transformers/declarations/diagnostics.ts", - "../compiler/transformers/declarations.ts", - "../compiler/transformer.ts", - "../compiler/sourcemap.ts", - "../compiler/comments.ts", - "../compiler/emitter.ts", - "../compiler/watchUtilities.ts", - "../compiler/program.ts", - "../compiler/builderState.ts", - "../compiler/builder.ts", - "../compiler/resolutionCache.ts", - "../compiler/moduleSpecifiers.ts", - "../compiler/watch.ts", - "../compiler/commandLineParser.ts", - - "../services/types.ts", - "../services/utilities.ts", - "../services/classifier.ts", - "../services/pathCompletions.ts", - "../services/completions.ts", - "../services/documentHighlights.ts", - "../services/documentRegistry.ts", - "../services/importTracker.ts", - "../services/findAllReferences.ts", - "../services/getEditsForFileRename.ts", - "../services/goToDefinition.ts", - "../services/jsDoc.ts", - "../services/semver.ts", - "../services/jsTyping.ts", - "../services/navigateTo.ts", - "../services/navigationBar.ts", - "../services/organizeImports.ts", - "../services/getEditsForFileRename.ts", - "../services/outliningElementsCollector.ts", - "../services/patternMatcher.ts", - "../services/preProcess.ts", - "../services/rename.ts", - "../services/signatureHelp.ts", - "../services/suggestionDiagnostics.ts", - "../services/symbolDisplay.ts", - "../services/transpile.ts", - "../services/formatting/formattingContext.ts", - "../services/formatting/formattingScanner.ts", - "../services/formatting/rule.ts", - "../services/formatting/rules.ts", - "../services/formatting/rulesMap.ts", - "../services/formatting/formatting.ts", - "../services/formatting/smartIndenter.ts", - "../services/textChanges.ts", - "../services/codeFixProvider.ts", - "../services/refactorProvider.ts", - "../services/codefixes/addMissingInvocationForDecorator.ts", - "../services/codefixes/annotateWithTypeFromJSDoc.ts", - "../services/codefixes/convertFunctionToEs6Class.ts", - "../services/codefixes/convertToEs6Module.ts", - "../services/codefixes/correctQualifiedNameToIndexedAccessType.ts", - "../services/codefixes/fixClassIncorrectlyImplementsInterface.ts", - "../services/codefixes/importFixes.ts", - "../services/codefixes/fixSpelling.ts", - "../services/codefixes/fixAddMissingMember.ts", - "../services/codefixes/fixCannotFindModule.ts", - "../services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts", - "../services/codefixes/fixClassSuperMustPrecedeThisAccess.ts", - "../services/codefixes/fixConstructorForDerivedNeedSuperCall.ts", - "../services/codefixes/fixExtendsInterfaceBecomesImplements.ts", - "../services/codefixes/fixForgottenThisPropertyAccess.ts", - "../services/codefixes/fixUnusedIdentifier.ts", - "../services/codefixes/fixUnreachableCode.ts", - "../services/codefixes/fixUnusedLabel.ts", - "../services/codefixes/fixJSDocTypes.ts", - "../services/codefixes/fixAwaitInSyncFunction.ts", - "../services/codefixes/disableJsDiagnostics.ts", - "../services/codefixes/helpers.ts", - "../services/codefixes/inferFromUsage.ts", - "../services/codefixes/fixInvalidImportSyntax.ts", - "../services/codefixes/fixStrictClassInitialization.ts", - "../services/codefixes/requireInTs.ts", - "../services/codefixes/useDefaultImport.ts", - "../services/codefixes/fixAddModuleReferTypeMissingTypeof.ts", - "../services/codefixes/convertToMappedObjectType.ts", - "../services/refactors/convertImport.ts", - "../services/refactors/extractSymbol.ts", - "../services/refactors/generateGetAccessorAndSetAccessor.ts", - "../services/refactors/moveToNewFile.ts", - "../services/sourcemaps.ts", - "../services/services.ts", - "../services/breakpoints.ts", - "../services/transform.ts", - "../services/shims.ts", - - "types.ts", - "shared.ts", - "utilities.ts", - "protocol.ts", - "scriptInfo.ts", - "typingsCache.ts", - "project.ts", - "editorServices.ts", - "session.ts", - "scriptVersionCache.ts", - "server.ts" - ] -} +{ + "extends": "../tsconfig-base", + "compilerOptions": { + "removeComments": true, + "outFile": "../../built/local/tsserver.js", + "preserveConstEnums": true, + "types": [ + "node" + ] + }, + "files": [ + "../compiler/types.ts", + "../compiler/performance.ts", + "../compiler/core.ts", + "../compiler/sys.ts", + "../compiler/diagnosticInformationMap.generated.ts", + "../compiler/scanner.ts", + "../compiler/utilities.ts", + "../compiler/parser.ts", + "../compiler/binder.ts", + "../compiler/symbolWalker.ts", + "../compiler/moduleNameResolver.ts", + "../compiler/checker.ts", + "../compiler/factory.ts", + "../compiler/visitor.ts", + "../compiler/transformers/utilities.ts", + "../compiler/transformers/destructuring.ts", + "../compiler/transformers/ts.ts", + "../compiler/transformers/es2017.ts", + "../compiler/transformers/esnext.ts", + "../compiler/transformers/jsx.ts", + "../compiler/transformers/es2016.ts", + "../compiler/transformers/es2015.ts", + "../compiler/transformers/es5.ts", + "../compiler/transformers/generators.ts", + "../compiler/transformers/module/module.ts", + "../compiler/transformers/module/system.ts", + "../compiler/transformers/module/es2015.ts", + "../compiler/transformers/declarations/diagnostics.ts", + "../compiler/transformers/declarations.ts", + "../compiler/transformer.ts", + "../compiler/sourcemap.ts", + "../compiler/comments.ts", + "../compiler/emitter.ts", + "../compiler/watchUtilities.ts", + "../compiler/program.ts", + "../compiler/builderState.ts", + "../compiler/builder.ts", + "../compiler/resolutionCache.ts", + "../compiler/watch.ts", + "../compiler/commandLineParser.ts", + + "../services/types.ts", + "../services/utilities.ts", + "../services/classifier.ts", + "../services/pathCompletions.ts", + "../services/completions.ts", + "../services/documentHighlights.ts", + "../services/documentRegistry.ts", + "../services/importTracker.ts", + "../services/findAllReferences.ts", + "../services/getEditsForFileRename.ts", + "../services/goToDefinition.ts", + "../services/jsDoc.ts", + "../services/semver.ts", + "../services/jsTyping.ts", + "../services/navigateTo.ts", + "../services/navigationBar.ts", + "../services/organizeImports.ts", + "../services/getEditsForFileRename.ts", + "../services/outliningElementsCollector.ts", + "../services/patternMatcher.ts", + "../services/preProcess.ts", + "../services/rename.ts", + "../services/signatureHelp.ts", + "../services/suggestionDiagnostics.ts", + "../services/symbolDisplay.ts", + "../services/transpile.ts", + "../services/formatting/formattingContext.ts", + "../services/formatting/formattingScanner.ts", + "../services/formatting/rule.ts", + "../services/formatting/rules.ts", + "../services/formatting/rulesMap.ts", + "../services/formatting/formatting.ts", + "../services/formatting/smartIndenter.ts", + "../services/textChanges.ts", + "../services/codeFixProvider.ts", + "../services/refactorProvider.ts", + "../services/codefixes/addMissingInvocationForDecorator.ts", + "../services/codefixes/annotateWithTypeFromJSDoc.ts", + "../services/codefixes/convertFunctionToEs6Class.ts", + "../services/codefixes/convertToEs6Module.ts", + "../services/codefixes/correctQualifiedNameToIndexedAccessType.ts", + "../services/codefixes/fixClassIncorrectlyImplementsInterface.ts", + "../services/codefixes/importFixes.ts", + "../services/codefixes/fixSpelling.ts", + "../services/codefixes/fixAddMissingMember.ts", + "../services/codefixes/fixCannotFindModule.ts", + "../services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts", + "../services/codefixes/fixClassSuperMustPrecedeThisAccess.ts", + "../services/codefixes/fixConstructorForDerivedNeedSuperCall.ts", + "../services/codefixes/fixExtendsInterfaceBecomesImplements.ts", + "../services/codefixes/fixForgottenThisPropertyAccess.ts", + "../services/codefixes/fixUnusedIdentifier.ts", + "../services/codefixes/fixUnreachableCode.ts", + "../services/codefixes/fixUnusedLabel.ts", + "../services/codefixes/fixJSDocTypes.ts", + "../services/codefixes/fixAwaitInSyncFunction.ts", + "../services/codefixes/disableJsDiagnostics.ts", + "../services/codefixes/helpers.ts", + "../services/codefixes/inferFromUsage.ts", + "../services/codefixes/fixInvalidImportSyntax.ts", + "../services/codefixes/fixStrictClassInitialization.ts", + "../services/codefixes/moduleSpecifiers.ts", + "../services/codefixes/requireInTs.ts", + "../services/codefixes/useDefaultImport.ts", + "../services/codefixes/fixAddModuleReferTypeMissingTypeof.ts", + "../services/codefixes/convertToMappedObjectType.ts", + "../services/refactors/convertImport.ts", + "../services/refactors/extractSymbol.ts", + "../services/refactors/generateGetAccessorAndSetAccessor.ts", + "../services/refactors/moveToNewFile.ts", + "../services/refactors/convertImport.ts", + "../services/refactors/convertReactPureComponent.ts", + "../services/sourcemaps.ts", + "../services/services.ts", + "../services/breakpoints.ts", + "../services/transform.ts", + "../services/shims.ts", + + "types.ts", + "shared.ts", + "utilities.ts", + "protocol.ts", + "scriptInfo.ts", + "typingsCache.ts", + "project.ts", + "editorServices.ts", + "session.ts", + "scriptVersionCache.ts", + "server.ts" + ] +} diff --git a/src/server/tsconfig.library.json b/src/server/tsconfig.library.json index 1adfe2a4bd043..4349913ae3589 100644 --- a/src/server/tsconfig.library.json +++ b/src/server/tsconfig.library.json @@ -126,6 +126,7 @@ "../services/refactors/extractSymbol.ts", "../services/refactors/generateGetAccessorAndSetAccessor.ts", "../services/refactors/moveToNewFile.ts", + "../services/refactors/convertReactPureComponent.ts", "../services/sourcemaps.ts", "../services/services.ts", "../services/breakpoints.ts", diff --git a/src/services/refactors/convertReactPureComponent.ts b/src/services/refactors/convertReactPureComponent.ts index 93c5ba88a2b5a..c3d24a96b409d 100644 --- a/src/services/refactors/convertReactPureComponent.ts +++ b/src/services/refactors/convertReactPureComponent.ts @@ -4,16 +4,24 @@ namespace ts.refactor.convertReactPureComponent { const actionNameComponentToSFC = "Covert React.Component to SFC"; const actionNameSFCToPureComponent = "Covert React.SFC to PureComponent"; + type SFCLikeDeclaration = + | FunctionDeclaration + | FunctionExpression + | ArrowFunction; + function isSFCLikeDeclaration(node: Node): node is SFCLikeDeclaration { + return isFunctionDeclaration(node) || isFunctionExpression(node) || isArrowFunction(node); + } + /** * This refactor follows this rule. * * Is optional. * - * 1. [ ] Check if JSX Factory is React. If not, no actions will be provided - * 2. [ ] If the selection is a function declaration of type `React.SFC` + * 1. [x] Check if JSX Factory is React. If not, no actions will be provided + * 2. [x] If the selection is a function declaration of type `React.SFC` * [ ] Provide an action to convert it to `React.PureComponent` * [ ] * Make a function with 1 to 2 arguments (treat as props & context) - * and explicit returnType T and T subtypeable to React.Element also convertable - * 3. [ ] If the selection is a subclass of `React.Component` or `React.PureComponent` + * and explicit returnType T and T subtypeable to React.Element also convertible + * 3. [x] If the selection is a subclass of `React.Component` or `React.PureComponent` * [x] and if it satisifies `isConvertableClass()` * [ ] Provide an action to convert it to `React.SFC` or `React.PureComponent` (if it is `React.Component`) * 4. [x] Resolve React by `getReferenceToReact()` @@ -23,16 +31,52 @@ namespace ts.refactor.convertReactPureComponent { registerRefactor(refactorName, { getAvailableActions(context: RefactorContext): ApplicableRefactorInfo[] | undefined { - return; + if (!checkJSXFactoryIsReact(context.host)) return; + if (!getReferenceToReact(context.file)) return; + const node = getSelectedNode(context); + if (!node) { return; } + + if (isConvertibleReactClassComponent(node, context.file)) { + // const description = Diagnostics.??? + const description = actionNameComponentToSFC; + return [{ + name: refactorName, + description, + actions: [{ description: description, name: actionNameComponentToSFC }] + }]; + } else if (isReactSFCDeclaration(node)) { + // const description = Diagnostics.??? + const description = actionNameSFCToPureComponent; + return [{ + name: refactorName, + description, + actions: [{ description: description, name: actionNameSFCToPureComponent }] + }]; + } }, getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined { + context.cancellationToken, actionName; return; } }); - function checkJSXFactoryIsReact() { - return true; + function checkJSXFactoryIsReact(host: LanguageServiceHost) { + const config = host.getCompilationSettings(); + if (config.jsxFactory === undefined) { if (config.jsx) return true } + // In vue, people use jsxFactory: "h" insteadof React.createElement + else { if (config.jsxFactory.match(/React/)) return true; } + return false; + } + + function getSelectedNode(context: RefactorContext): Node | false { + const { file } = context; + const span = getRefactorContextSpan(context); + const token = getTokenAtPosition(file, span.start, /*includeJsDocComment*/ false); + const component = getParentNodeInSpan(token, file, span); + if (!component) { return false; } + return component; } + /** Get name binding to React, React.Component, React.PureComponent, React.SFC, React.StatelessComponent */ function getReferenceToReact(sourcefile: SourceFile) { const result: Record<"React" | "Component" | "PureComponent" | "SFC" | "StatelessComponent", string | undefined> = { @@ -44,7 +88,7 @@ namespace ts.refactor.convertReactPureComponent { } as any; let importClause: ImportClause; sourcefile.forEachChild(c => { - if (!ts.isImportDeclaration(c)) { return; } + if (!isImportDeclaration(c)) { return; } if (!c.importClause) { return; } if ((c.moduleSpecifier as StringLiteral).text !== "react") { return; } const { name, namedBindings } = c.importClause; @@ -52,13 +96,13 @@ namespace ts.refactor.convertReactPureComponent { const names: (keyof typeof result)[] = ["Component", "PureComponent", "SFC", "StatelessComponent"]; // In case of: // import * as ns from "mod" => name = undefined, namedBinding: NamespaceImport = { name: ns } - if (namedBindings && ts.isNamespaceImport(namedBindings)) { result.React = namedBindings.name.text; } + if (namedBindings && isNamespaceImport(namedBindings)) { result.React = namedBindings.name.text; } // import d from "mod" => name = d, namedBinding = undefined else if (name) { result.React = name.text; } // import d, * as ns from "mod" => name = d, namedBinding: NamespaceImport = { name: ns } // ? No this case // import { a, b as x } from "mod" => name = undefined, namedBinding: NamedImports = { elements: [{ name: a }, { name: x, propertyName: b}]} - if (namedBindings && ts.isNamedImports(namedBindings)) { + if (namedBindings && isNamedImports(namedBindings)) { namedBindings.elements.forEach(e => { if (e.propertyName) { let p: keyof typeof result = e.propertyName.text as any; @@ -83,64 +127,71 @@ namespace ts.refactor.convertReactPureComponent { return { ...result, importClause: importClause! }; } - /** Check if expression is a React Component Class, then get its name (opt) */ - function isReactComponentClass(expression: ClassDeclaration | ClassExpression, sourcefile: SourceFile) { - let is = false; - let name: string | undefined = ""; - let reference = getReferenceToReact(sourcefile)!; - let propsType: TypeNode | undefined; - if (expression.heritageClauses) { - if (expression.heritageClauses.some(x => - x.types.some(y => { - const text = y.getText(sourcefile).replace(/\s/g, ""); - if (text === reference.Component || text === reference.PureComponent) { - propsType = y.typeArguments && y.typeArguments[0]; - return true; + function isReactSFCDeclaration(expression: Node) { + if (!isSFCLikeDeclaration(expression)) { return; } + return expression && false; + } + + function isConvertibleReactClassComponent(node: Node, sourcefile: SourceFile) { + if (!isClassLike(node)) return; + /** Check if expression is a React Component Class, then get its name and PropType */ + function isReactComponentClass(expression: ClassLikeDeclaration, sourcefile: SourceFile) { + let is = false; + let name: string | undefined = ""; + let reference = getReferenceToReact(sourcefile)!; + let propsType: TypeNode | undefined; + if (expression.heritageClauses) { + if (expression.heritageClauses.some(x => + x.types.some(y => { + const text = y.getText(sourcefile).replace(/\s/g, ""); + if (text === reference.Component || text === reference.PureComponent) { + propsType = y.typeArguments && y.typeArguments[0]; + return true; + } + return false; + }) + )) { + is = true; + if (isClassDeclaration(expression)) { + name = expression.name && expression.name.text; } - return false; - }) - )) { - is = true; - if (ts.isClassDeclaration(expression)) { - name = expression.name && expression.name.text; } } + if (is) return { name, propsType }; + return undefined; } - if (is) return { name, propsType }; - return undefined; - } - function isReactSFCDeclaration(expression: FunctionDeclaration | FunctionExpression) { - return true; - } - /** - * 1. If have any property than `render`, return false - * 2. If have reference to `setState` or `state`, return false - */ - function isConvertableClass(expression: ClassDeclaration | ClassExpression, sourcefile: SourceFile) { - let render: MethodDeclaration; - let convertable = ts.every(expression.members, val => { - // * Should include `["render"]() {}` and `render = () => {}`, but that's crazy, no one code like this - if (ts.isMethodDeclaration(val) && val.modifiers) { - const isMethodNamedRender = ts.isIdentifier(val.name) && val.name.escapedText === "render" - const isStatic = val.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword); - if (isMethodNamedRender && !isStatic) { - render = val; - return true; + const ircs = isReactComponentClass(node, sourcefile); + if (!ircs) return; + /** + * 1. If have any property than `render`, return false + * 2. If have reference to `setState` or `state`, return false + */ + function isConvertibleComponent(expression: ClassLikeDeclaration, sourcefile: SourceFile) { + let render: Block; + let convertable = every(expression.members, val => { + // * Should include `["render"]() {}` and `render = () => {}`, but that's crazy, no one code like this + if (isMethodDeclaration(val) && val.modifiers) { + const isMethodNamedRender = isIdentifier(val.name) && val.name.escapedText === "render" + const isStatic = val.modifiers.some(mod => mod.kind === SyntaxKind.StaticKeyword); + if (isMethodNamedRender && !isStatic) { + render = val.body!; + return true; + } + } + // TODO: Should include staic `propTypes`, `contextTypes`, `defaultProps`, `displayName`. Not now. + return false; + }) + // ? Now check reference to `state` or `setState` + if (convertable) { + // OK let's do this quick + if (render!.getText(sourcefile).match(/this\s+\.\s+(state|setState)/)) { + convertable = false; } } - // TODO: Should include staic `propTypes`, `contextTypes`, `defaultProps`, `displayName`. Not now. - return false; - }) - // ? Now check reference to `state` or `setState` - if (convertable) { - // OK let's do this quick - if (render!.getText(sourcefile).match(/this\s+\.\s+(state|setState)/)) { - convertable = false; - } + return { convertable, render: render! }; } - return { convertable, render: render! }; + const icc = isConvertibleComponent(node, sourcefile); + if (!icc.convertable) return; + return { name: ircs.name, propsType: ircs.propsType, render: icc.render }; } - - function transformer() { } } - diff --git a/tests/cases/fourslash/refactorConvertReactPureComponent.ts b/tests/cases/fourslash/refactorConvertReactPureComponent.ts new file mode 100644 index 0000000000000..cb9468de40548 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertReactPureComponent.ts @@ -0,0 +1,29 @@ +/// + +// @jsx: "react" + +//// import * as React from "react" +//// /*start*/class Ele extends React.Component

{ +//// render() { +//// return <> +//// {this.props.children} +////

+//// +//// } +//// }/*end*/ + +goTo.select("start", "end"); +edit.applyRefactor({ + refactorName: "Convert React pure component", + actionName: "Covert React.Component to SFC", + actionDescription: "Covert React.Component to SFC", + newContent: + `import * as React from "react" +const Ele: React.SFC

= (props) => { + return <> + {props.children} +

+ +} +`, +}); From ad18a1a6e58d775f9a99faa80b68964ace76c37e Mon Sep 17 00:00:00 2001 From: Jack Works Date: Thu, 7 Jun 2018 16:26:47 +0800 Subject: [PATCH 3/8] Impl isReactSFCDeclaration --- .../refactors/convertReactPureComponent.ts | 53 +++++++++++++++---- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/src/services/refactors/convertReactPureComponent.ts b/src/services/refactors/convertReactPureComponent.ts index c3d24a96b409d..66fda65a8a5e4 100644 --- a/src/services/refactors/convertReactPureComponent.ts +++ b/src/services/refactors/convertReactPureComponent.ts @@ -4,6 +4,8 @@ namespace ts.refactor.convertReactPureComponent { const actionNameComponentToSFC = "Covert React.Component to SFC"; const actionNameSFCToPureComponent = "Covert React.SFC to PureComponent"; + const removeEmpty = (s: string) => s.replace(/\s/g, ""); + type SFCLikeDeclaration = | FunctionDeclaration | FunctionExpression @@ -12,6 +14,12 @@ namespace ts.refactor.convertReactPureComponent { return isFunctionDeclaration(node) || isFunctionExpression(node) || isArrowFunction(node); } + type Component = { + name?: Identifier; + render: Block; + propsType?: TypeNode; + } + /** * This refactor follows this rule. * * Is optional. @@ -44,7 +52,7 @@ namespace ts.refactor.convertReactPureComponent { description, actions: [{ description: description, name: actionNameComponentToSFC }] }]; - } else if (isReactSFCDeclaration(node)) { + } else if (isReactSFCDeclaration(node, context.file)) { // const description = Diagnostics.??? const description = actionNameSFCToPureComponent; return [{ @@ -79,7 +87,7 @@ namespace ts.refactor.convertReactPureComponent { /** Get name binding to React, React.Component, React.PureComponent, React.SFC, React.StatelessComponent */ function getReferenceToReact(sourcefile: SourceFile) { - const result: Record<"React" | "Component" | "PureComponent" | "SFC" | "StatelessComponent", string | undefined> = { + const result: Record<"React" | "Component" | "PureComponent" | "SFC" | "StatelessComponent", string> = { Component: undefined, PureComponent: undefined, React: undefined, @@ -127,23 +135,48 @@ namespace ts.refactor.convertReactPureComponent { return { ...result, importClause: importClause! }; } - function isReactSFCDeclaration(expression: Node) { + function isReactSFCDeclaration(expression: Node, sourcefile: SourceFile): Component | undefined { if (!isSFCLikeDeclaration(expression)) { return; } - return expression && false; + if (expression.asteriskToken) { return; } + if (!expression.body) { return; } + + // TODO: Should also check the actual type insteadof only receive explicit typed + const typeNode = expression.type; + if (!typeNode) { return; } + const binding = getReferenceToReact(sourcefile)!; + const type = removeEmpty(typeNode.getFullText(sourcefile)); + if (type !== binding.SFC && type !== binding.Component) { return; } + + let propsType: TypeNode | undefined; + if (isTypeReferenceNode(typeNode)) { + if (typeNode.typeArguments) { propsType = typeNode.typeArguments[0]; } + } + + let render: Block; + if (isBlock(expression.body)) { + render = expression.body; + } else if (isExpression(expression.body!)) { + render = createBlock([createReturn(expression.body)]); + } + return { + propsType: propsType, + name: expression.name, + render: render!, + } } - function isConvertibleReactClassComponent(node: Node, sourcefile: SourceFile) { - if (!isClassLike(node)) return; + function isConvertibleReactClassComponent(node: Node, sourcefile: SourceFile): Component | undefined { + if (!isClassLike(node)) { return; } /** Check if expression is a React Component Class, then get its name and PropType */ function isReactComponentClass(expression: ClassLikeDeclaration, sourcefile: SourceFile) { let is = false; - let name: string | undefined = ""; + let name: Identifier | undefined; let reference = getReferenceToReact(sourcefile)!; let propsType: TypeNode | undefined; if (expression.heritageClauses) { if (expression.heritageClauses.some(x => x.types.some(y => { - const text = y.getText(sourcefile).replace(/\s/g, ""); + const text = removeEmpty(y.getText(sourcefile)); if (text === reference.Component || text === reference.PureComponent) { propsType = y.typeArguments && y.typeArguments[0]; return true; @@ -153,7 +186,7 @@ namespace ts.refactor.convertReactPureComponent { )) { is = true; if (isClassDeclaration(expression)) { - name = expression.name && expression.name.text; + name = expression.name; } } } @@ -184,7 +217,7 @@ namespace ts.refactor.convertReactPureComponent { // ? Now check reference to `state` or `setState` if (convertable) { // OK let's do this quick - if (render!.getText(sourcefile).match(/this\s+\.\s+(state|setState)/)) { + if (removeEmpty(render!.getText(sourcefile)).match(/this\.(state|setState)/)) { convertable = false; } } From 0b9f2f9163084fa8e642a3ff9d4e9e9f8bfc99aa Mon Sep 17 00:00:00 2001 From: Jack Works Date: Thu, 7 Jun 2018 16:54:41 +0800 Subject: [PATCH 4/8] Transformer for SFC->PureComponent --- .../refactors/convertReactPureComponent.ts | 52 ++++++++++++++----- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/src/services/refactors/convertReactPureComponent.ts b/src/services/refactors/convertReactPureComponent.ts index 66fda65a8a5e4..47fec5824cb94 100644 --- a/src/services/refactors/convertReactPureComponent.ts +++ b/src/services/refactors/convertReactPureComponent.ts @@ -18,6 +18,7 @@ namespace ts.refactor.convertReactPureComponent { name?: Identifier; render: Block; propsType?: TypeNode; + originNode: SFCLikeDeclaration | ClassLikeDeclaration; } /** @@ -63,7 +64,11 @@ namespace ts.refactor.convertReactPureComponent { } }, getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined { - context.cancellationToken, actionName; + Debug.assert(actionName === actionNameComponentToSFC || actionName === actionNameSFCToPureComponent); + if (actionName === actionNameSFCToPureComponent) { + const node = getSelectedNode(context) as Node; + return transformSFCtoComponent(isReactSFCDeclaration(node, context.file)!, context); + } return; } }); @@ -135,13 +140,13 @@ namespace ts.refactor.convertReactPureComponent { return { ...result, importClause: importClause! }; } - function isReactSFCDeclaration(expression: Node, sourcefile: SourceFile): Component | undefined { - if (!isSFCLikeDeclaration(expression)) { return; } - if (expression.asteriskToken) { return; } - if (!expression.body) { return; } + function isReactSFCDeclaration(node: Node, sourcefile: SourceFile): Component | undefined { + if (!isSFCLikeDeclaration(node)) { return; } + if (node.asteriskToken) { return; } + if (!node.body) { return; } // TODO: Should also check the actual type insteadof only receive explicit typed - const typeNode = expression.type; + const typeNode = node.type; if (!typeNode) { return; } const binding = getReferenceToReact(sourcefile)!; const type = removeEmpty(typeNode.getFullText(sourcefile)); @@ -153,15 +158,16 @@ namespace ts.refactor.convertReactPureComponent { } let render: Block; - if (isBlock(expression.body)) { - render = expression.body; - } else if (isExpression(expression.body!)) { - render = createBlock([createReturn(expression.body)]); + if (isBlock(node.body)) { + render = node.body; + } else if (isExpression(node.body!)) { + render = createBlock([createReturn(node.body)]); } return { propsType: propsType, - name: expression.name, + name: node.name, render: render!, + originNode: node, } } @@ -225,6 +231,28 @@ namespace ts.refactor.convertReactPureComponent { } const icc = isConvertibleComponent(node, sourcefile); if (!icc.convertable) return; - return { name: ircs.name, propsType: ircs.propsType, render: icc.render }; + return { name: ircs.name, propsType: ircs.propsType, render: icc.render, originNode: node }; + } + + function transformSFCtoComponent(component: Component, context: RefactorContext): RefactorEditInfo { + const changeTracker = textChanges.ChangeTracker.fromContext(context); + + const react = getReferenceToReact(context.file)!; + + const extendsClause = createHeritageClause(SyntaxKind.ExtendsKeyword, [ + createExpressionWithTypeArguments(component.propsType ? [component.propsType] : undefined, + createIdentifier(react.PureComponent)) + ]); + const renderMethod = createMethod(undefined, undefined, undefined, "render", + undefined, undefined, [], undefined, component.render); + + let newNode: ClassLikeDeclaration; + if (component.name) { + newNode = createClassDeclaration(undefined, undefined, component.name, undefined, [extendsClause], [renderMethod]); + } else { + newNode = createClassExpression(undefined, undefined, undefined, [extendsClause], [renderMethod]); + } + changeTracker.replaceNode(context.file, component.originNode, newNode); + return { edits: changeTracker.getChanges() }; } } From fcddd51ae6a6849d50dea62e927490b7ff69fa05 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Thu, 7 Jun 2018 17:52:08 +0800 Subject: [PATCH 5/8] Transform Component -> SFC --- .../refactors/convertReactPureComponent.ts | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/services/refactors/convertReactPureComponent.ts b/src/services/refactors/convertReactPureComponent.ts index 47fec5824cb94..4c397b2c8d1d7 100644 --- a/src/services/refactors/convertReactPureComponent.ts +++ b/src/services/refactors/convertReactPureComponent.ts @@ -65,9 +65,11 @@ namespace ts.refactor.convertReactPureComponent { }, getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined { Debug.assert(actionName === actionNameComponentToSFC || actionName === actionNameSFCToPureComponent); + const node = getSelectedNode(context) as Node; if (actionName === actionNameSFCToPureComponent) { - const node = getSelectedNode(context) as Node; return transformSFCtoComponent(isReactSFCDeclaration(node, context.file)!, context); + } else if (actionName === actionNameComponentToSFC) { + return transformComponentToSFC(isConvertibleReactClassComponent(node, context.file)!, context); } return; } @@ -241,7 +243,7 @@ namespace ts.refactor.convertReactPureComponent { const extendsClause = createHeritageClause(SyntaxKind.ExtendsKeyword, [ createExpressionWithTypeArguments(component.propsType ? [component.propsType] : undefined, - createIdentifier(react.PureComponent)) + createIdentifier(react.PureComponent)) ]); const renderMethod = createMethod(undefined, undefined, undefined, "render", undefined, undefined, [], undefined, component.render); @@ -253,6 +255,34 @@ namespace ts.refactor.convertReactPureComponent { newNode = createClassExpression(undefined, undefined, undefined, [extendsClause], [renderMethod]); } changeTracker.replaceNode(context.file, component.originNode, newNode); - return { edits: changeTracker.getChanges() }; + return { + edits: changeTracker.getChanges().map(x => ({ + ...x, textChanges: x.textChanges.map(y => ({ + ...y, newText: y.newText.replace(/props/g, "this.props") + })) + })) + }; + } + + function transformComponentToSFC(component: Component, context: RefactorContext): RefactorEditInfo { + const changeTracker = textChanges.ChangeTracker.fromContext(context); + + const react = getReferenceToReact(context.file)!; + + const newNode: FunctionDeclaration = createFunctionDeclaration( + undefined, undefined, undefined, component.name, undefined, + [createParameter(undefined, undefined, undefined, "props", undefined)], + createTypeReferenceNode(react.SFC, component.propsType ? [component.propsType] : undefined), + component.render + ); + + changeTracker.replaceNode(context.file, component.originNode, newNode); + return { + edits: changeTracker.getChanges().map(x => ({ + ...x, textChanges: x.textChanges.map(y => ({ + ...y, newText: y.newText.replace(/this\.props/g, "props") + })) + })) + }; } } From c47104017c8dc56f256ce477c1a6ee9078192c93 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Thu, 7 Jun 2018 18:20:08 +0800 Subject: [PATCH 6/8] Fix build error --- src/compiler/diagnosticMessages.json | 8 ++++++++ src/server/tsconfig.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index f2a10a08fdedc..8396ded8fb542 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -4313,5 +4313,13 @@ "Convert named imports to namespace import": { "category": "Message", "code": 95057 + }, + "Covert React.Component to SFC": { + "category": "Message", + "code": 95058 + }, + "Covert React.SFC to PureComponent": { + "category": "Message", + "code": 95059 } } diff --git a/src/server/tsconfig.json b/src/server/tsconfig.json index c1945468ea04d..90150449346c7 100644 --- a/src/server/tsconfig.json +++ b/src/server/tsconfig.json @@ -47,6 +47,7 @@ "../compiler/builderState.ts", "../compiler/builder.ts", "../compiler/resolutionCache.ts", + "../compiler/moduleSpecifiers.ts", "../compiler/watch.ts", "../compiler/commandLineParser.ts", @@ -111,7 +112,6 @@ "../services/codefixes/inferFromUsage.ts", "../services/codefixes/fixInvalidImportSyntax.ts", "../services/codefixes/fixStrictClassInitialization.ts", - "../services/codefixes/moduleSpecifiers.ts", "../services/codefixes/requireInTs.ts", "../services/codefixes/useDefaultImport.ts", "../services/codefixes/fixAddModuleReferTypeMissingTypeof.ts", From f5b619f917c21037beb83bc131e076c2a4631d6d Mon Sep 17 00:00:00 2001 From: Jack Works Date: Thu, 7 Jun 2018 22:35:43 +0800 Subject: [PATCH 7/8] Fix some logic error and tslint rule --- .../refactors/convertReactPureComponent.ts | 76 ++++++++++--------- .../refactorConvertReactPureComponent.ts | 58 +++++++------- 2 files changed, 70 insertions(+), 64 deletions(-) diff --git a/src/services/refactors/convertReactPureComponent.ts b/src/services/refactors/convertReactPureComponent.ts index 4c397b2c8d1d7..c4f02fb0276c9 100644 --- a/src/services/refactors/convertReactPureComponent.ts +++ b/src/services/refactors/convertReactPureComponent.ts @@ -14,7 +14,7 @@ namespace ts.refactor.convertReactPureComponent { return isFunctionDeclaration(node) || isFunctionExpression(node) || isArrowFunction(node); } - type Component = { + interface Component { name?: Identifier; render: Block; propsType?: TypeNode; @@ -40,7 +40,7 @@ namespace ts.refactor.convertReactPureComponent { registerRefactor(refactorName, { getAvailableActions(context: RefactorContext): ApplicableRefactorInfo[] | undefined { - if (!checkJSXFactoryIsReact(context.host)) return; + // if (!checkJSXFactoryIsReact(context.host)) return; if (!getReferenceToReact(context.file)) return; const node = getSelectedNode(context); if (!node) { return; } @@ -51,15 +51,16 @@ namespace ts.refactor.convertReactPureComponent { return [{ name: refactorName, description, - actions: [{ description: description, name: actionNameComponentToSFC }] + actions: [{ description, name: actionNameComponentToSFC }] }]; - } else if (isReactSFCDeclaration(node, context.file)) { + } + else if (isReactSFCDeclaration(node, context.file)) { // const description = Diagnostics.??? const description = actionNameSFCToPureComponent; return [{ name: refactorName, description, - actions: [{ description: description, name: actionNameSFCToPureComponent }] + actions: [{ description, name: actionNameSFCToPureComponent }] }]; } }, @@ -68,20 +69,21 @@ namespace ts.refactor.convertReactPureComponent { const node = getSelectedNode(context) as Node; if (actionName === actionNameSFCToPureComponent) { return transformSFCtoComponent(isReactSFCDeclaration(node, context.file)!, context); - } else if (actionName === actionNameComponentToSFC) { + } + else if (actionName === actionNameComponentToSFC) { return transformComponentToSFC(isConvertibleReactClassComponent(node, context.file)!, context); } return; } }); - function checkJSXFactoryIsReact(host: LanguageServiceHost) { - const config = host.getCompilationSettings(); - if (config.jsxFactory === undefined) { if (config.jsx) return true } - // In vue, people use jsxFactory: "h" insteadof React.createElement - else { if (config.jsxFactory.match(/React/)) return true; } - return false; - } + // function checkJSXFactoryIsReact(host: LanguageServiceHost) { + // const config = host.getCompilationSettings(); + // if (config.jsxFactory === undefined) { if (config.jsx) return true; } + // // In vue, people use jsxFactory: "h" insteadof React.createElement + // else { if (config.jsxFactory.match(/React/)) return true; } + // return false; + // } function getSelectedNode(context: RefactorContext): Node | false { const { file } = context; @@ -120,23 +122,25 @@ namespace ts.refactor.convertReactPureComponent { if (namedBindings && isNamedImports(namedBindings)) { namedBindings.elements.forEach(e => { if (e.propertyName) { - let p: keyof typeof result = e.propertyName.text as any; + const p: keyof typeof result = e.propertyName.text as any; if (names.indexOf(p) !== -1) { result[p] = e.name.text; } - } else { - let n: keyof typeof result = e.name.text as any; + } + else { + const n: keyof typeof result = e.name.text as any; if (names.indexOf(n) !== -1) { result[n] = n; } } - }) + }); } // import d, { a, b as x } from "mod" => name = d, namedBinding: NamedImports = { elements: [{ name: a }, { name: x, propertyName: b}]} // ? No this case - }) + }); if (result.React) { result.Component = result.Component || result.React + ".Component"; result.PureComponent = result.PureComponent || result.React + ".PureComponent"; result.SFC = result.SFC || result.React + ".SFC"; result.StatelessComponent = result.StatelessComponent || result.StatelessComponent + ".StatelessComponent"; - } else if (!result.Component && !result.PureComponent && !result.React && !result.SFC && !result.StatelessComponent) { + } + else if (!result.Component && !result.PureComponent && !result.React && !result.SFC && !result.StatelessComponent) { return undefined; } return { ...result, importClause: importClause! }; @@ -162,15 +166,16 @@ namespace ts.refactor.convertReactPureComponent { let render: Block; if (isBlock(node.body)) { render = node.body; - } else if (isExpression(node.body!)) { + } + else if (isExpression(node.body!)) { render = createBlock([createReturn(node.body)]); } return { - propsType: propsType, + propsType, name: node.name, render: render!, originNode: node, - } + }; } function isConvertibleReactClassComponent(node: Node, sourcefile: SourceFile): Component | undefined { @@ -179,12 +184,12 @@ namespace ts.refactor.convertReactPureComponent { function isReactComponentClass(expression: ClassLikeDeclaration, sourcefile: SourceFile) { let is = false; let name: Identifier | undefined; - let reference = getReferenceToReact(sourcefile)!; + const reference = getReferenceToReact(sourcefile)!; let propsType: TypeNode | undefined; if (expression.heritageClauses) { if (expression.heritageClauses.some(x => x.types.some(y => { - const text = removeEmpty(y.getText(sourcefile)); + const text = removeEmpty(y.expression.getText(sourcefile)); if (text === reference.Component || text === reference.PureComponent) { propsType = y.typeArguments && y.typeArguments[0]; return true; @@ -211,9 +216,9 @@ namespace ts.refactor.convertReactPureComponent { let render: Block; let convertable = every(expression.members, val => { // * Should include `["render"]() {}` and `render = () => {}`, but that's crazy, no one code like this - if (isMethodDeclaration(val) && val.modifiers) { - const isMethodNamedRender = isIdentifier(val.name) && val.name.escapedText === "render" - const isStatic = val.modifiers.some(mod => mod.kind === SyntaxKind.StaticKeyword); + if (isMethodDeclaration(val)) { + const isMethodNamedRender = isIdentifier(val.name) && val.name.escapedText === "render"; + const isStatic = val.modifiers && val.modifiers.some(mod => mod.kind === SyntaxKind.StaticKeyword); if (isMethodNamedRender && !isStatic) { render = val.body!; return true; @@ -221,7 +226,7 @@ namespace ts.refactor.convertReactPureComponent { } // TODO: Should include staic `propTypes`, `contextTypes`, `defaultProps`, `displayName`. Not now. return false; - }) + }); // ? Now check reference to `state` or `setState` if (convertable) { // OK let's do this quick @@ -245,14 +250,15 @@ namespace ts.refactor.convertReactPureComponent { createExpressionWithTypeArguments(component.propsType ? [component.propsType] : undefined, createIdentifier(react.PureComponent)) ]); - const renderMethod = createMethod(undefined, undefined, undefined, "render", - undefined, undefined, [], undefined, component.render); + const renderMethod = createMethod([], [], void 0, "render", + void 0, void 0, [], void 0, component.render); let newNode: ClassLikeDeclaration; if (component.name) { - newNode = createClassDeclaration(undefined, undefined, component.name, undefined, [extendsClause], [renderMethod]); - } else { - newNode = createClassExpression(undefined, undefined, undefined, [extendsClause], [renderMethod]); + newNode = createClassDeclaration(void 0, void 0, component.name, void 0, [extendsClause], [renderMethod]); + } + else { + newNode = createClassExpression(void 0, void 0, void 0, [extendsClause], [renderMethod]); } changeTracker.replaceNode(context.file, component.originNode, newNode); return { @@ -270,8 +276,8 @@ namespace ts.refactor.convertReactPureComponent { const react = getReferenceToReact(context.file)!; const newNode: FunctionDeclaration = createFunctionDeclaration( - undefined, undefined, undefined, component.name, undefined, - [createParameter(undefined, undefined, undefined, "props", undefined)], + void 0, void 0, void 0, component.name, void 0, + [createParameter(void 0, void 0, void 0, "props", void 0)], createTypeReferenceNode(react.SFC, component.propsType ? [component.propsType] : undefined), component.render ); diff --git a/tests/cases/fourslash/refactorConvertReactPureComponent.ts b/tests/cases/fourslash/refactorConvertReactPureComponent.ts index cb9468de40548..108e002dbc172 100644 --- a/tests/cases/fourslash/refactorConvertReactPureComponent.ts +++ b/tests/cases/fourslash/refactorConvertReactPureComponent.ts @@ -1,29 +1,29 @@ -/// - -// @jsx: "react" - -//// import * as React from "react" -//// /*start*/class Ele extends React.Component

{ -//// render() { -//// return <> -//// {this.props.children} -////

-//// -//// } -//// }/*end*/ - -goTo.select("start", "end"); -edit.applyRefactor({ - refactorName: "Convert React pure component", - actionName: "Covert React.Component to SFC", - actionDescription: "Covert React.Component to SFC", - newContent: - `import * as React from "react" -const Ele: React.SFC

= (props) => { - return <> - {props.children} -

- -} -`, -}); +/// + +//// import * as React from "react" +//// /*start*/ +//// class Ele extends React.Component

{ +//// render() { +//// return <> +//// {this.props.children} +////

+//// ; +//// } +//// } +///// /*end*/ + +goTo.select("start", "end"); +edit.applyRefactor({ + refactorName: "Convert React pure component", + actionName: "Covert React.Component to SFC", + actionDescription: "Covert React.Component to SFC", + newContent: + `import * as React from "react" +const EleQ: React.SFC

= (props) => { + return <> + {props.children} +

+ +} +`, +}); From 46e1c0aabd00bfe1602fd75599d78abf40cbe134 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Wed, 13 Jun 2018 15:22:10 +0800 Subject: [PATCH 8/8] Rewrite whole expect behavior after review --- src/compiler/diagnosticMessages.json | 4 +- .../refactors/convertReactPureComponent.ts | 111 +++++++++++++----- 2 files changed, 86 insertions(+), 29 deletions(-) diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 8396ded8fb542..28aa292729e03 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -4314,11 +4314,11 @@ "category": "Message", "code": 95057 }, - "Covert React.Component to SFC": { + "Convert component to a Stateless Functional Component (SFC)": { "category": "Message", "code": 95058 }, - "Covert React.SFC to PureComponent": { + "Convert Stateless Functional Component to a Pure component": { "category": "Message", "code": 95059 } diff --git a/src/services/refactors/convertReactPureComponent.ts b/src/services/refactors/convertReactPureComponent.ts index c4f02fb0276c9..ca3c48f3fe6ae 100644 --- a/src/services/refactors/convertReactPureComponent.ts +++ b/src/services/refactors/convertReactPureComponent.ts @@ -23,27 +23,86 @@ namespace ts.refactor.convertReactPureComponent { /** * This refactor follows this rule. - * * Is optional. + * 0. [ ] Continue if we are in a .jsx or .tsx file. + * 1. [ ] Get the current JSX config (reactNamespace, jsxFactory, jsx). + * Continue if there is a JSX provider + * ? It's better be React, ReactNative and Preact ? + * 2. [ ] Continue if the selection include a SFCLikeDeclaration | ClassLikeDeclaration + * [ ] and the declaration is the direct child of the current SourceFile + * [ ] and we can find the name (Class name, or variable name) in the declaration * - * 1. [x] Check if JSX Factory is React. If not, no actions will be provided - * 2. [x] If the selection is a function declaration of type `React.SFC` - * [ ] Provide an action to convert it to `React.PureComponent` - * [ ] * Make a function with 1 to 2 arguments (treat as props & context) - * and explicit returnType T and T subtypeable to React.Element also convertible - * 3. [x] If the selection is a subclass of `React.Component` or `React.PureComponent` - * [x] and if it satisifies `isConvertableClass()` - * [ ] Provide an action to convert it to `React.SFC` or `React.PureComponent` (if it is `React.Component`) - * 4. [x] Resolve React by `getReferenceToReact()` - * 5. [ ] * Also support `propTypes`, `contextTypes`, `defaultProps`, `displayName` (Only Class -> Function) - * 6. [ ] * Also support convert of reference to `this.context` (in both ways) + * For ClassLikeDeclaration detection: + * A. [ ] Continue if the declaration is a subclass of `PureComponent` or `Component` + * B. [ ] Continue if there is no reference to `this.state` or `this.setState` + * C. [ ] Continue if there is no any property more than listed below + * [ ] `render` + * [ ] static `propTypes` + * [ ] static `contextTypes` + * [ ] static `defaultProps` + * [ ] static `displayName` + * [ ] static `childContextTypes` + * D. [ ] Continue if the `render` method is not invalid + * E. [ ] Provide an action, Convert `PureComponent` or `Component` to SFC + * + * For SFCLikeDeclaration detection: + * a. [ ] Goto c, if the selection is a function declaration of type `React.SFC`. + * b. [ ] Continue if we guess the selection is an SFC + * In @types/react, ReactNode is = + * ReactElement | ReactText (number | string) // ReactChild + * | {} | ReactNode[] // ReactFragment + * | { key: Key | null; children: ReactNode; } // ReactPortal + * | string + * | number | boolean | null | undefined + * define type MeaningfulReactNode = + * ReactElement | MeaningfulReactNode[] | ReactPortal | string + * Guess as follow rules. + * [ ] i. Check all return path, make then an union undefined + * [ ] ii. Continue if parameters length < 3 (props?, context?) + * [ ] iii. Continue if all of member of U is compatiable with ReactNode + * [ ] iv. Continue if there is MeaningfulReactNode in U + * [ ] v. Continue if there is no reference to `this` + * [ ] vi. This is an SFC + * c. [ ] Continue if there is a body + * c. [ ] Provide an action, Convert SFC to `PureComponent` or `Component` + * + * For ClassLikeDeclaration transformation: + * A. [ ] Get the Class `C`, get the class name `Name` (by ClassDeclaration, or variable declaration) + * B. [ ] Collect the properties below + * [ ] `render` + * [ ] static `propTypes`? + * [ ] static `contextTypes`? + * [ ] static `defaultProps`? + * [ ] static `displayName`? + * [ ] static `childContextTypes`? + * C. [ ] Replace + * [ ] all `this.props` to `props` in `render`, + * [ ] all `this.context` to `context` in `render`, + * [ ] and make sure there is no name conflict in the current lexical scope + * [ ] if there is, generate a random name other than `props` and `context`? + * D. [ ] Create a FunctionDeclaration `F` named `Name`, with body `render` + * [ ] In .tsx file, add type annoation + * [ ] In .jsx file, add JSDoc Type annoation? + * E. [ ] For those static properties, add something like `Name`.propTypes = ... + * F. [ ] Replace `C` with `F` + * + * For SFCLikeDeclaration transformation: + * A. [ ] Get the SFC `F`, get the class name `Name` (by FunctionDeclaration, or variable declaration) + * B. [ ] Collect the properties below + * [ ] function body as `render` + * C. [ ] Replace + * [ ] first parameter to `this.props` in `render`, + * [ ] second parameter to `this.context` in `render`, + * D. [ ] Create a ClassDeclaration `C` named `Name` extends (React|Preact).PureComponent + * [ ] In .tsx file, add type arguments `T` if `F` is typed `(React|Preact).(Pure)?Component` + * E. [ ] Replace `F` with `C` */ registerRefactor(refactorName, { getAvailableActions(context: RefactorContext): ApplicableRefactorInfo[] | undefined { - // if (!checkJSXFactoryIsReact(context.host)) return; - if (!getReferenceToReact(context.file)) return; + // if (!checkJSXFactoryIsReact(context.host)) return undefined; + if (!getReferenceToReact(context.file)) return undefined; const node = getSelectedNode(context); - if (!node) { return; } + if (!node) { return undefined; } if (isConvertibleReactClassComponent(node, context.file)) { // const description = Diagnostics.??? @@ -73,7 +132,7 @@ namespace ts.refactor.convertReactPureComponent { else if (actionName === actionNameComponentToSFC) { return transformComponentToSFC(isConvertibleReactClassComponent(node, context.file)!, context); } - return; + return undefined; } }); @@ -105,9 +164,9 @@ namespace ts.refactor.convertReactPureComponent { } as any; let importClause: ImportClause; sourcefile.forEachChild(c => { - if (!isImportDeclaration(c)) { return; } - if (!c.importClause) { return; } - if ((c.moduleSpecifier as StringLiteral).text !== "react") { return; } + if (!isImportDeclaration(c)) { return undefined; } + if (!c.importClause) { return undefined; } + if ((c.moduleSpecifier as StringLiteral).text !== "react") { return undefined; } const { name, namedBindings } = c.importClause; importClause = c.importClause; const names: (keyof typeof result)[] = ["Component", "PureComponent", "SFC", "StatelessComponent"]; @@ -147,16 +206,14 @@ namespace ts.refactor.convertReactPureComponent { } function isReactSFCDeclaration(node: Node, sourcefile: SourceFile): Component | undefined { - if (!isSFCLikeDeclaration(node)) { return; } - if (node.asteriskToken) { return; } - if (!node.body) { return; } + if (!isSFCLikeDeclaration(node) || node.asteriskToken || !node.body) { return undefined; } // TODO: Should also check the actual type insteadof only receive explicit typed const typeNode = node.type; - if (!typeNode) { return; } + if (!typeNode) { return undefined; } const binding = getReferenceToReact(sourcefile)!; const type = removeEmpty(typeNode.getFullText(sourcefile)); - if (type !== binding.SFC && type !== binding.Component) { return; } + if (type !== binding.SFC && type !== binding.Component) { return undefined; } let propsType: TypeNode | undefined; if (isTypeReferenceNode(typeNode)) { @@ -179,7 +236,7 @@ namespace ts.refactor.convertReactPureComponent { } function isConvertibleReactClassComponent(node: Node, sourcefile: SourceFile): Component | undefined { - if (!isClassLike(node)) { return; } + if (!isClassLike(node)) { return undefined; } /** Check if expression is a React Component Class, then get its name and PropType */ function isReactComponentClass(expression: ClassLikeDeclaration, sourcefile: SourceFile) { let is = false; @@ -207,7 +264,7 @@ namespace ts.refactor.convertReactPureComponent { return undefined; } const ircs = isReactComponentClass(node, sourcefile); - if (!ircs) return; + if (!ircs) return undefined; /** * 1. If have any property than `render`, return false * 2. If have reference to `setState` or `state`, return false @@ -237,7 +294,7 @@ namespace ts.refactor.convertReactPureComponent { return { convertable, render: render! }; } const icc = isConvertibleComponent(node, sourcefile); - if (!icc.convertable) return; + if (!icc.convertable) return undefined; return { name: ircs.name, propsType: ircs.propsType, render: icc.render, originNode: node }; }