Skip to content

Handle imports and exports in 'convert parameters to destructured object' #30475

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 36 commits into from
Mar 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
ecd4aca
add test for imported function
Mar 6, 2019
988626d
start to implement import references check
Mar 6, 2019
ef1d93d
fix imported function test
Mar 6, 2019
79027a0
skip alias when looking for symbol target
Mar 6, 2019
481fdff
recognize ES6 imports
Mar 6, 2019
9d09314
recognize some export syntax
Mar 7, 2019
9601d15
add tests for imports/exports
Mar 7, 2019
65e75fd
add test for imported function
Mar 6, 2019
f7a3001
start to implement import references check
Mar 6, 2019
902d0d1
fix imported function test
Mar 6, 2019
db3d161
skip alias when looking for symbol target
Mar 6, 2019
bd047a5
recognize ES6 imports
Mar 6, 2019
ec79b28
recognize some export syntax
Mar 7, 2019
0ac92a9
add tests for imports/exports
Mar 7, 2019
229886d
add test for imported function
Mar 6, 2019
a92659c
start to implement import references check
Mar 6, 2019
8b7d3e0
fix imported function test
Mar 6, 2019
b4f4375
recognize ES6 imports
Mar 6, 2019
ef19c85
recognize some export syntax
Mar 7, 2019
519cd8a
add mode import/export syntax cases
Mar 11, 2019
1fc427f
fix entryToFunctionCall to deal with new calls through property/eleme…
Mar 11, 2019
bd79bef
add more tests for imports/exports
Mar 11, 2019
1416153
allow function and class declarations that have no name but have a de…
Mar 15, 2019
fab5fd7
rename tests
Mar 15, 2019
7bb4a72
Merge branch 'namedParametersImportExport' of https://github.com/Micr…
Mar 15, 2019
1a3ce43
fix conflict
Mar 15, 2019
86431e6
fix tests
Mar 15, 2019
9e5f633
add test for nameless class
Mar 15, 2019
8ab900d
rename function
Mar 18, 2019
d0526c6
minor refactor
Mar 18, 2019
8f7a2f7
Merge branch 'master' into namedParametersImportExport
Mar 18, 2019
d1115cd
remove old tests
Mar 18, 2019
8c17d67
Merge branch 'master' into namedParametersImportExport
Mar 25, 2019
6d15398
delete old test
Mar 27, 2019
f191363
refactor as suggested
Mar 27, 2019
e96171b
use getContainingFunctionDeclaration
Mar 27, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1330,6 +1330,10 @@ namespace ts {
return findAncestor(node.parent, isFunctionLike);
}

export function getContainingFunctionDeclaration(node: Node): FunctionLikeDeclaration | undefined {
return findAncestor(node.parent, isFunctionLikeDeclaration);
}

export function getContainingClass(node: Node): ClassLikeDeclaration | undefined {
return findAncestor(node.parent, isClassLike);
}
Expand Down
120 changes: 81 additions & 39 deletions src/services/refactors/convertParamsToDestructuredObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ namespace ts.refactor.convertParamsToDestructuredObject {
function groupReferences(referenceEntries: ReadonlyArray<FindAllReferences.Entry>): GroupedReferences {
const classReferences: ClassReferences = { accessExpressions: [], typeUsages: [] };
const groupedReferences: GroupedReferences = { functionCalls: [], declarations: [], classReferences, valid: true };
const functionSymbols = map(functionNames, checker.getSymbolAtLocation);
const classSymbols = map(classNames, checker.getSymbolAtLocation);
const functionSymbols = map(functionNames, getSymbolTargetAtLocation);
const classSymbols = map(classNames, getSymbolTargetAtLocation);
const isConstructor = isConstructorDeclaration(functionDeclaration);

for (const entry of referenceEntries) {
Expand All @@ -111,7 +111,11 @@ namespace ts.refactor.convertParamsToDestructuredObject {
So we need to add a special case for this because when calling a constructor of a class through one of its subclasses,
the symbols are going to be different.
*/
if (contains(functionSymbols, checker.getSymbolAtLocation(entry.node), symbolComparer) || isNewExpressionTarget(entry.node)) {
if (contains(functionSymbols, getSymbolTargetAtLocation(entry.node)) || isNewExpressionTarget(entry.node)) {
const importOrExportReference = entryToImportOrExport(entry);
if (importOrExportReference) {
continue;
}
const decl = entryToDeclaration(entry);
if (decl) {
groupedReferences.declarations.push(decl);
Expand All @@ -125,7 +129,12 @@ namespace ts.refactor.convertParamsToDestructuredObject {
}
}
// if the refactored function is a constructor, we must also check if the references to its class are valid
if (isConstructor && contains(classSymbols, checker.getSymbolAtLocation(entry.node), symbolComparer)) {
if (isConstructor && contains(classSymbols, getSymbolTargetAtLocation(entry.node))) {
const importOrExportReference = entryToImportOrExport(entry);
if (importOrExportReference) {
continue;
}

const decl = entryToDeclaration(entry);
if (decl) {
groupedReferences.declarations.push(decl);
Expand Down Expand Up @@ -153,10 +162,27 @@ namespace ts.refactor.convertParamsToDestructuredObject {

return groupedReferences;
}

function getSymbolTargetAtLocation(node: Node) {
const symbol = checker.getSymbolAtLocation(node);
return symbol && getSymbolTarget(symbol, checker);
}
}

function symbolComparer(a: Symbol, b: Symbol): boolean {
return getSymbolTarget(a) === getSymbolTarget(b);
function entryToImportOrExport(entry: FindAllReferences.NodeEntry): Node | undefined {
const node = entry.node;

if (isImportSpecifier(node.parent)
|| isImportClause(node.parent)
|| isImportEqualsDeclaration(node.parent)
|| isNamespaceImport(node.parent)) {
return node;
}

if (isExportSpecifier(node.parent) || isExportAssignment(node.parent)) {
return node;
}
return undefined;
}

function entryToDeclaration(entry: FindAllReferences.NodeEntry): Node | undefined {
Expand All @@ -171,37 +197,31 @@ namespace ts.refactor.convertParamsToDestructuredObject {
const functionReference = entry.node;
const parent = functionReference.parent;
switch (parent.kind) {
// Function call (foo(...) or super(...))
// foo(...) or super(...) or new Foo(...)
case SyntaxKind.CallExpression:
const callExpression = tryCast(parent, isCallExpression);
if (callExpression && callExpression.expression === functionReference) {
return callExpression;
}
break;
// Constructor call (new Foo(...))
case SyntaxKind.NewExpression:
const newExpression = tryCast(parent, isNewExpression);
if (newExpression && newExpression.expression === functionReference) {
return newExpression;
const callOrNewExpression = tryCast(parent, isCallOrNewExpression);
if (callOrNewExpression && callOrNewExpression.expression === functionReference) {
return callOrNewExpression;
}
break;
// Method call (x.foo(...))
// x.foo(...)
case SyntaxKind.PropertyAccessExpression:
const propertyAccessExpression = tryCast(parent, isPropertyAccessExpression);
if (propertyAccessExpression && propertyAccessExpression.parent && propertyAccessExpression.name === functionReference) {
const callExpression = tryCast(propertyAccessExpression.parent, isCallExpression);
if (callExpression && callExpression.expression === propertyAccessExpression) {
return callExpression;
const callOrNewExpression = tryCast(propertyAccessExpression.parent, isCallOrNewExpression);
if (callOrNewExpression && callOrNewExpression.expression === propertyAccessExpression) {
return callOrNewExpression;
}
}
break;
// Method call (x["foo"](...))
// x["foo"](...)
case SyntaxKind.ElementAccessExpression:
const elementAccessExpression = tryCast(parent, isElementAccessExpression);
if (elementAccessExpression && elementAccessExpression.parent && elementAccessExpression.argumentExpression === functionReference) {
const callExpression = tryCast(elementAccessExpression.parent, isCallExpression);
if (callExpression && callExpression.expression === elementAccessExpression) {
return callExpression;
const callOrNewExpression = tryCast(elementAccessExpression.parent, isCallOrNewExpression);
if (callOrNewExpression && callOrNewExpression.expression === elementAccessExpression) {
return callOrNewExpression;
}
}
break;
Expand Down Expand Up @@ -244,7 +264,7 @@ namespace ts.refactor.convertParamsToDestructuredObject {

function getFunctionDeclarationAtPosition(file: SourceFile, startPosition: number, checker: TypeChecker): ValidFunctionDeclaration | undefined {
const node = getTouchingToken(file, startPosition);
const functionDeclaration = getContainingFunction(node);
const functionDeclaration = getContainingFunctionDeclaration(node);

// don't offer refactor on top-level JSDoc
if (isTopLevelJSDoc(node)) return undefined;
Expand All @@ -267,25 +287,21 @@ namespace ts.refactor.convertParamsToDestructuredObject {
}

function isValidFunctionDeclaration(
functionDeclaration: SignatureDeclaration,
functionDeclaration: FunctionLikeDeclaration,
checker: TypeChecker): functionDeclaration is ValidFunctionDeclaration {
if (!isValidParameterNodeArray(functionDeclaration.parameters, checker)) return false;
switch (functionDeclaration.kind) {
case SyntaxKind.FunctionDeclaration:
return hasNameOrDefault(functionDeclaration) && isSingleImplementation(functionDeclaration, checker);
case SyntaxKind.MethodDeclaration:
return !!functionDeclaration.name
&& !!functionDeclaration.body
&& !checker.isImplementationOfOverload(functionDeclaration);
return isSingleImplementation(functionDeclaration, checker);
case SyntaxKind.Constructor:
if (isClassDeclaration(functionDeclaration.parent)) {
return !!functionDeclaration.body
&& !!functionDeclaration.parent.name
&& !checker.isImplementationOfOverload(functionDeclaration);
return hasNameOrDefault(functionDeclaration.parent) && isSingleImplementation(functionDeclaration, checker);
}
else {
return isValidVariableDeclaration(functionDeclaration.parent.parent)
&& !!functionDeclaration.body
&& !checker.isImplementationOfOverload(functionDeclaration);
&& isSingleImplementation(functionDeclaration, checker);
}
case SyntaxKind.FunctionExpression:
case SyntaxKind.ArrowFunction:
Expand All @@ -294,6 +310,18 @@ namespace ts.refactor.convertParamsToDestructuredObject {
return false;
}

function isSingleImplementation(functionDeclaration: FunctionLikeDeclaration, checker: TypeChecker): boolean {
return !!functionDeclaration.body && !checker.isImplementationOfOverload(functionDeclaration);
}

function hasNameOrDefault(functionOrClassDeclaration: FunctionDeclaration | ClassDeclaration): boolean {
if (!functionOrClassDeclaration.name) {
const defaultKeyword = findModifier(functionOrClassDeclaration, SyntaxKind.DefaultKeyword);
return !!defaultKeyword;
}
return true;
}

function isValidParameterNodeArray(
parameters: NodeArray<ParameterDeclaration>,
checker: TypeChecker): parameters is ValidParameterNodeArray {
Expand Down Expand Up @@ -488,11 +516,17 @@ namespace ts.refactor.convertParamsToDestructuredObject {
return getTextOfIdentifierOrLiteral(paramDeclaration.name);
}

function getClassNames(constructorDeclaration: ValidConstructor): Identifier[] {
function getClassNames(constructorDeclaration: ValidConstructor): (Identifier | Modifier)[] {
switch (constructorDeclaration.parent.kind) {
case SyntaxKind.ClassDeclaration:
const classDeclaration = constructorDeclaration.parent;
return [classDeclaration.name];
if (classDeclaration.name) return [classDeclaration.name];
// If the class declaration doesn't have a name, it should have a default modifier.
// We validated this in `isValidFunctionDeclaration` through `hasNameOrDefault`
const defaultModifier = Debug.assertDefined(
findModifier(classDeclaration, SyntaxKind.DefaultKeyword),
"Nameless class declaration should be a default export");
return [defaultModifier];
case SyntaxKind.ClassExpression:
const classExpression = constructorDeclaration.parent;
const variableDeclaration = constructorDeclaration.parent.parent;
Expand All @@ -505,10 +539,19 @@ namespace ts.refactor.convertParamsToDestructuredObject {
function getFunctionNames(functionDeclaration: ValidFunctionDeclaration): Node[] {
switch (functionDeclaration.kind) {
case SyntaxKind.FunctionDeclaration:
if (functionDeclaration.name) return [functionDeclaration.name];
// If the function declaration doesn't have a name, it should have a default modifier.
// We validated this in `isValidFunctionDeclaration` through `hasNameOrDefault`
const defaultModifier = Debug.assertDefined(
findModifier(functionDeclaration, SyntaxKind.DefaultKeyword),
"Nameless function declaration should be a default export");
return [defaultModifier];
case SyntaxKind.MethodDeclaration:
return [functionDeclaration.name];
case SyntaxKind.Constructor:
const ctrKeyword = findChildOfKind(functionDeclaration, SyntaxKind.ConstructorKeyword, functionDeclaration.getSourceFile())!;
const ctrKeyword = Debug.assertDefined(
findChildOfKind(functionDeclaration, SyntaxKind.ConstructorKeyword, functionDeclaration.getSourceFile()),
"Constructor declaration should have constructor keyword");
if (functionDeclaration.parent.kind === SyntaxKind.ClassExpression) {
const variableDeclaration = functionDeclaration.parent.parent;
return [variableDeclaration.name, ctrKeyword];
Expand All @@ -532,13 +575,12 @@ namespace ts.refactor.convertParamsToDestructuredObject {
}

interface ValidConstructor extends ConstructorDeclaration {
parent: (ClassDeclaration & { name: Identifier }) | (ClassExpression & { parent: ValidVariableDeclaration });
parent: ClassDeclaration | (ClassExpression & { parent: ValidVariableDeclaration });
parameters: NodeArray<ValidParameterDeclaration>;
body: FunctionBody;
}

interface ValidFunction extends FunctionDeclaration {
name: Identifier;
parameters: NodeArray<ValidParameterDeclaration>;
body: FunctionBody;
}
Expand Down
15 changes: 12 additions & 3 deletions src/services/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1664,10 +1664,15 @@ namespace ts {
return ensureScriptKind(fileName, host && host.getScriptKind && host.getScriptKind(fileName));
}

export function getSymbolTarget(symbol: Symbol): Symbol {
export function getSymbolTarget(symbol: Symbol, checker: TypeChecker): Symbol {
let next: Symbol = symbol;
while (isTransientSymbol(next) && next.target) {
next = next.target;
while (isAliasSymbol(next) || (isTransientSymbol(next) && next.target)) {
if (isTransientSymbol(next) && next.target) {
next = next.target;
}
else {
next = skipAlias(next, checker);
}
}
return next;
}
Expand All @@ -1676,6 +1681,10 @@ namespace ts {
return (symbol.flags & SymbolFlags.Transient) !== 0;
}

function isAliasSymbol(symbol: Symbol): boolean {
return (symbol.flags & SymbolFlags.Alias) !== 0;
}

export function getUniqueSymbolId(symbol: Symbol, checker: TypeChecker) {
return getSymbolId(skipAlias(symbol, checker));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
////}

goTo.select("a", "b");
verify.not.refactorAvailable("Convert to named parameters");
verify.not.refactorAvailable("Convert parameters to destructured object");

goTo.select("c", "d");
verify.not.refactorAvailable("Convert to named parameters");
verify.not.refactorAvailable("Convert parameters to destructured object");

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/// <reference path='fourslash.ts' />

// @Filename: f.ts
////export function f(/*a*/a: number, b: string/*b*/): string {
//// return b;
////}

// @Filename: a.ts
////import { f as g } from "./f";
////g(4, "b");

goTo.select("a", "b");
edit.applyRefactor({
refactorName: "Convert parameters to destructured object",
actionName: "Convert parameters to destructured object",
actionDescription: "Convert parameters to destructured object",
newContent: `export function f({ a, b }: { a: number; b: string; }): string {
return b;
}`
});

goTo.file("a.ts");
verify.currentFileContentIs(`import { f as g } from "./f";
g({ a: 4, b: "b" });`)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/// <reference path='fourslash.ts' />

// @Filename: f.ts
////export default function f(/*a*/a: number, b: string/*b*/): string {
//// return b;
////}

// @Filename: a.ts
////import g from "./f";
////g(4, "b");

goTo.select("a", "b");
edit.applyRefactor({
refactorName: "Convert parameters to destructured object",
actionName: "Convert parameters to destructured object",
actionDescription: "Convert parameters to destructured object",
newContent: `export default function f({ a, b }: { a: number; b: string; }): string {
return b;
}`
});

goTo.file("a.ts");
verify.currentFileContentIs(`import g from "./f";
g({ a: 4, b: "b" });`)
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/// <reference path='fourslash.ts' />

// @Filename: f.ts
////function foo(/*a*/a: string, b: string/*b*/) { }
////export = foo;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can also export = function(.... Is that case supported?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

("No" is a reasonable answer - I mostly want to know if the case was considered.)


In reply to: 269694751 [](ancestors = 269694751)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not supported, but I didn't considered it, that's a good point. The refactoring is not offered in this case because this is a function expression (or an arrow function) that is in an export assignment and we only offer it if the function expression is assigned to a const variable.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i've briefly thought about supporting this case and it would require some changes to be able to call findAllReferences, so i think it's better to consider supporting this on another PR later.


// @Filename: a.ts
////import bar = require("./f");
////bar("a", "b");


goTo.select("a", "b");
edit.applyRefactor({
refactorName: "Convert parameters to destructured object",
actionName: "Convert parameters to destructured object",
actionDescription: "Convert parameters to destructured object",
newContent: `function foo({ a, b }: { a: string; b: string; }) { }
export = foo;`
});

goTo.file("a.ts");
verify.currentFileContentIs(`import bar = require("./f");
bar({ a: "a", b: "b" });`)
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/// <reference path='fourslash.ts' />

// @Filename: f.ts
////export { foo as default };
////function /*a*/foo/*b*/(a: number, b: number) {
//// return a + b;
////}

// @Filename: a.ts
////import bar from "./f";
////bar(1, 2);


goTo.select("a", "b");
edit.applyRefactor({
refactorName: "Convert parameters to destructured object",
actionName: "Convert parameters to destructured object",
actionDescription: "Convert parameters to destructured object",
newContent: `export { foo as default };
function foo({ a, b }: { a: number; b: number; }) {
return a + b;
}`
});

goTo.file("a.ts");
verify.currentFileContentIs(`import bar from "./f";
bar({ a: 1, b: 2 });`)
Loading