Skip to content
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

add refactor of extract type #30562

Merged
merged 22 commits into from May 7, 2019
Merged

add refactor of extract type #30562

merged 22 commits into from May 7, 2019

Conversation

Kingwl
Copy link
Contributor

@Kingwl Kingwl commented Mar 24, 2019

Fixes #23869

This pr implement a refactor to extract selected types into another type alias in ts or typedef jsdoc in js

  • The strategy is to look up the type node which is the biggest range and has been included in the selection and create a type alias node before the parent statement.
  • update the selected type node to reference the generated type.
  • the case of generic type argument will be collected and convert to type alias / type refernece
  • the infer type which part of a condition type and type pred will not be allowed to extract

follow things is WIP:

@Kingwl Kingwl changed the title add basically implement add basically extract type implement Mar 25, 2019
@Kingwl Kingwl marked this pull request as ready for review March 25, 2019 11:26
@Kingwl Kingwl changed the title add basically extract type implement add refactor of extract type Mar 25, 2019
src/services/refactors/extractType.ts Outdated Show resolved Hide resolved
src/services/refactors/extractType.ts Outdated Show resolved Hide resolved

function visitor(node: Node) {
if (isTypeReferenceNode(node)) {
if (isIdentifier(node.typeName)) {
Copy link
Member

@gabritto gabritto Mar 28, 2019

Choose a reason for hiding this comment

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

why is it not necessary to handle the case where node.typeName is not an identifier but is a qualified name?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could TypeParameter be a QualifiedName?
In checkAndCollectionTypeArguments we only iter the TypeParameter whitch defined in the statement

Copy link
Member

@gabritto gabritto Mar 29, 2019

Choose a reason for hiding this comment

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

i think in the common cases where you have to collect the type, the type name is an identifier and not a qualified name. but i'm really not sure, so that's why i asked.
update: came up with an example of a qualified name for the type query check. i think there could be unusual cases where this check can fail.

hasError = true;
return;
}
else if (isTypeQueryNode(node) && isIdentifier(node.exprName)) {
Copy link
Member

@gabritto gabritto Mar 28, 2019

Choose a reason for hiding this comment

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

same as above, why is it not necessary to handle the case where node.typeName is not an identifier but is a qualified name?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

usually, I think that QualifiedName is not including in current statement

Copy link
Contributor Author

Choose a reason for hiding this comment

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

And is not a TypeParameter and do not have TypeArguments

Copy link
Member

Choose a reason for hiding this comment

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

like before, i think common cases are covered by the identifier check, but just came up with the following code:

interface I { a: string, f: (this: O, b: number) => typeof this.a };

something bad happens if we extract typeof this.a

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Banned ThisTypeNode, QualityName and most left is ThisIdentifier

}
}
else if (isInferTypeNode(node)) {
const conditionalTypeNode = findAncestor(node, isConditionalTypeNode);
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 this might be wrong. we are assuming that the infer type node is only allowed in the extends clause of a conditional type. so we want to check if selection includes the conditional type node to whose extends clause the infer type node belongs. this conditional type node might not be the first conditional type ancestor we find because there could be nested conditional type nodes. for example:

type Crazy<T> = T extends [infer P, (infer R extends string ? string : never)] ? P & R : string;

we can apply the refactoring and extract (infer R extends string ? string : never) to obtain this:

type NewType = (infer R extends string ? string : never); // error: 'infer' declarations are only permitted in the 'extends' clause of a conditional type

type Crazy<T> = T extends [infer P, NewType] ? P & R : string;

which gives an error.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sorry, mis operation😅

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Find type node which is the first condition that contains selected infer type node in extends node

src/services/refactors/extractType.ts Outdated Show resolved Hide resolved
src/services/refactors/extractType.ts Outdated Show resolved Hide resolved
@Kingwl
Copy link
Contributor Author

Kingwl commented Apr 1, 2019

also this one: #29539

Copy link
Member

@andrewbranch andrewbranch left a comment

Choose a reason for hiding this comment

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

Hey @Kingwl, thanks so much for this! It's looking great! 🌟

I thought of two small additional cases I'd like to see, something along these lines:

// typeof other parameters within function signature?
function f(a: string, b: /*a*/typeof a/*b*/): /*a*/typeof b/*b*/ {
    return '';
}

// Where do lines get inserted?
// The exact structure here doesn't matter,
// just want to see something within a block body
// to have the behavior defined in tests.
function id<T>(x: T): T {
    return (() => {
        const s: /*a*/typeof x/*b*/ = x;
        return s;
    })();
}

At a quick glance I think these will already produce the correct behavior, just want to make sure. Thanks!

@Kingwl
Copy link
Contributor Author

Kingwl commented Apr 23, 2019

🆙

@Kingwl
Copy link
Contributor Author

Kingwl commented May 5, 2019

⬆️

Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

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

Looks really good. The only thing is that this is incorrectly refactored.

I also had a couple of questions and two small suggestions, although they are optional.

return rangeContainsStartEnd(r1, skipTrivia(file.text, node.pos), node.end);
}

function checkAndCollectionTypeArguments(checker: TypeChecker, selection: TypeNode, statement: Statement, file: SourceFile): TypeParameterDeclaration[] | undefined {
Copy link
Member

Choose a reason for hiding this comment

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

probably should be named collectTypeParameters ? I don't think you're really checking them, and it seems like they are intended to be type parameters in addition to arguments, so matching the type name TypeParameterDeclaration feels a little better.

}

function isStatementButNotBlock(n: Node): n is Statement {
return n && isStatement(n) && !isBlock(n);
Copy link
Member

Choose a reason for hiding this comment

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

in what cases will you encounter a block before a statement in findAncestor? That is, a block inside a statement?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes,

() => {
  {
   // here
  }
}

And i'm cannot remember sure why i do that🤷🏻‍♂️
Is this unnecessary?

Copy link
Member

Choose a reason for hiding this comment

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

Weird, I would have that that //here would have its own statement, but I guess not. In that case, it seems necessary.

}

function checkAndCollectionTypeArguments(checker: TypeChecker, selection: TypeNode, statement: Statement, file: SourceFile): TypeParameterDeclaration[] | undefined {
let hasError = false;
Copy link
Member

Choose a reason for hiding this comment

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

seems more elegant to turn this into a return value. It looks like the expected behaviour is to return undefined whenever hasError is true, so stopping the tree walk early should be more efficient.

}
}
else if (isInferTypeNode(node)) {
const conditionalTypeNode = findAncestor(node, n => isConditionalTypeNode(n) && rangeContainsSkipTrivia(n.extendsType, node, file));
Copy link
Member

Choose a reason for hiding this comment

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

if the selection contains only a syntactically-correct infer type, then I think this code should (1) not set hasError but also (2) not push any results into result. What happens if you get an empty result list?

Test cases 31-33 show that this works, but I don't understand why.

Copy link
Contributor Author

@Kingwl Kingwl May 7, 2019

Choose a reason for hiding this comment

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

That caused:

  1. find the selections by largest and topmost type node
  2. Infer type node contains only a TypeParameterDeclaration

in the following case:

type A = /*a*/Promise/*b*/

The selection is the TypeReference which have the same range as the Identifier``Promise.

In cases 31 and 32 were set the hasError flag.
And the case33:

type Item<T> = T extends (infer /*a*/P/*b*/)[] ? P : never

The selection is the TypeParameter(maybe, or an Identifier), that is a declaration rather a TypeNode. and we are already escaped before collectTypeParameters.

}
}
else {
if (isThisIdentifier(node.exprName.left) && !rangeContainsSkipTrivia(selection, node.parent, file)) {
Copy link
Member

Choose a reason for hiding this comment

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

you should skip this elsewhere too:

class C {
    m<T>(): /*a*/T | this | number/*b*/ {
        return {} as any
    }
}

currently produces type NewType<T> = T | this | number

@sandersn sandersn merged commit 714821f into microsoft:master May 7, 2019
@sandersn
Copy link
Member

sandersn commented May 7, 2019

@DanielRosenwasser you'll probably want to highlight this refactor in the release notes since it is the other half of the named-parameters refactor that @gabritto did.

@Kingwl Kingwl deleted the extrace_types branch May 7, 2019 16:18
@wclr
Copy link

wclr commented Jul 22, 2019

@Kingwl
Rename refactoring doesn't work for linked node_modules imports #31825

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Suggestion: Refactor extract to type alias/typedef
5 participants