Skip to content

fix(eslint-plugin): [prefer-function-type] handle this type #1783

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

Closed
wants to merge 3 commits into from

Conversation

anikethsaha
Copy link
Contributor

fixes #1756

  • added a no check when return type is this for interface declarations
  • added tests

@typescript-eslint
Copy link
Contributor

Thanks for the PR, @anikethsaha!

typescript-eslint is a 100% community driven project, and we are incredibly grateful that you are contributing to that community.

The core maintainers work on this in their personal time, so please understand that it may not be possible for them to review your work immediately.

Thanks again!


🙏 Please, if you or your company is finding typescript-eslint valuable, help us sustain the project by sponsoring it transparently on https://opencollective.com/typescript-eslint. As a thank you, your profile/company logo will be added to our main README which receives thousands of unique visitors per day.

Copy link
Member

@bradzacher bradzacher left a comment

Choose a reason for hiding this comment

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

What about cases like

interface Interface {(): this | string}
interface Interface {(): Array<this>}
interface Interface {(arg: this): void}
interface Interface {(arg: this | string): void}
interface Interface {(arg: Array<this>): void}

In addition to the other comment, this PR should handle all cases where a callable interface uses this this type.

@bradzacher bradzacher added the bug Something isn't working label Mar 23, 2020
@anikethsaha
Copy link
Contributor Author

interface Interface {(): this | string} // valid 
interface Interface {(): Array<this>} // not sure, I will add a test for this to check 
interface Interface {(arg: this): void} // it will be invalid !
interface Interface {(arg: this | string): void}  // it will be invalid !
interface Interface {(arg: Array<this>): void}  // it will be invalid !
interface Interface {(arg: this): void} // it will be invalid !
interface Interface {(arg: this | string): void}  // it will be invalid !
interface Interface {(arg: Array<this>): void}  // it will be invalid !

Should I add support for parameters as well ? or they should be kept as invalid ?

@bradzacher
Copy link
Member

With this fix, we will want to update the fixer so it does not output semantically invalid code.
One of the core rules in eslint is that fixers should not knowingly break the build.
In typescript, this is not always possible, because some changes can have a butterfly-effect types-wise - we allow some level of "unsafety" in the fixers.

But in this case it's pretty clear-cut: a type declaration cannot reference the this type. So no matter the position, it should not fix it.

@anikethsaha
Copy link
Contributor Author

But in this case it's pretty clear-cut: a type declaration cannot reference the this type. So no matter the position, it should not fix it.

so, what I understood, we have to disable the fixer for this related code ?

@bradzacher
Copy link
Member

There are essentially two options here.

When the rule detects an interface that use the this type, it can either:

  1. completely ignore it. This is the "simplest" solution, but also kinda sucks because it can lead to stylistic inconsistencies in the codebase.
  2. conditionally ignore it, gated by an option. This would probably be my preferred option, as it allows people to keep their codebase consistent.
    • the fixer would need to be removed for the specific case.
    • A new message ID would probably make sense as well, because the message could suggest converting to a recursive type:
      interface Foo { (): this } ====> type Foo = () => Foo.
    • The rule could provide a "suggestion" fixer to do this conversion for the user.
      • Suggestion fixers are like normal fixers, except they can only be applied manually via the IDE. They're a way to provide a fix for an error, but force the user to make a decision.

@anikethsaha
Copy link
Contributor Author

ok, that make sense,

just one query though,

  • A new message ID would probably make sense as well, because the message could suggest converting to a recursive type:
    interface Foo { (): this } ====> type Foo = () => Foo.

interface Foo { () : this | string } ?

@bradzacher
Copy link
Member

whenever this is encountered in the type, it can be replaced with the name of the type.

interface Interface1a {(): this}
type Interface1b = {(): Interface1b}

interface Interface2a {(): this | string}
type Interface2b = {(): Interface2b | string}

interface Interface3a {(): Array<this>}
type Interface3b = {(): Array<Interface3b>}

interface Interface4a {(arg: this): void}
type Interface4b = {(arg: Interface4b): void}

interface Interface5a {(arg: this | string): void}
type Interface5b = {(arg: Interface5b | string): void}

interface Interface6a {(arg: Array<this>): void}
type Interface6b = {(arg: Array<Interface6b>): void}

repl

@anikethsaha
Copy link
Contributor Author

ok cool, I will change it

@bradzacher bradzacher changed the title fix: no check for this return type (fix #1756) fix(eslint-plugin): [prefer-function-type] handle this type Mar 24, 2020
@bradzacher bradzacher added the awaiting response Issues waiting for a reply from the OP or another party label Mar 24, 2020
output: `type Interface = () => Interface`,
},
{
code: 'interface Interface {(): Object<this>}',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bradzacher this ( generic types ) will fail as the latest commit is still WIP.

@anikethsaha
Copy link
Contributor Author

@bradzacher the latest commit is still WIP

As per the review I made changes in the fixer.

  • Can you please check if you are fine with how its being checked ( getReturnType function and its being implemented ),
  • for changing the message ID with, we need to return it from the renderSuggestion method as well cause there only we are doing the call to the recursion thing. Can you suggest the id or message
  • There are few overlapping issue while comparing which is why I think the CI is failing, should I turn off the rule there ?
    image
  • If you have a better suggestions to implement this, it would be awesome. Like if there is any utility
    function to replace the string in a better way for the type like here , here

Comment on lines +48 to +50
if (node.type === AST_NODE_TYPES.TSThisType) {
return node.range;
}
Copy link
Member

Choose a reason for hiding this comment

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

There are few overlapping issue while comparing which is why I think the CI is failing, should I turn off the rule there ?

This is failing the typecheck because it can never be true.
Your argument is of type TSESTree.TypeAnnotation, which is a container node.

See my other comment for a suggestion of how to structure the function.

Comment on lines +41 to +63
function getReturnType(
node: TSESTree.TSTypeAnnotation,
): Array<number> | null {
if (!possibleReturnThisTypeHolders.has(node.type)) {
return null;
}

if (node.type === AST_NODE_TYPES.TSThisType) {
return node.range;
}

if (node.type === AST_NODE_TYPES.TSTypeAnnotation) {
if (node.typeAnnotation.type === AST_NODE_TYPES.TSThisType) {
return node.typeAnnotation.range;
}
}

if (node.type === AST_NODE_TYPES.TSFunctionType) {
return getReturnType(node?.returnType);
}

return null;
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
function getReturnType(
node: TSESTree.TSTypeAnnotation,
): Array<number> | null {
if (!possibleReturnThisTypeHolders.has(node.type)) {
return null;
}
if (node.type === AST_NODE_TYPES.TSThisType) {
return node.range;
}
if (node.type === AST_NODE_TYPES.TSTypeAnnotation) {
if (node.typeAnnotation.type === AST_NODE_TYPES.TSThisType) {
return node.typeAnnotation.range;
}
}
if (node.type === AST_NODE_TYPES.TSFunctionType) {
return getReturnType(node?.returnType);
}
return null;
}
function getReturnType(
node: TSESTree.TSTypeAnnotation,
): Array<number> | null {
if (node.typeAnnotation.type === AST_NODE_TYPES.TSThisType) {
return node.range;
}
if (node.typeAnnotation.type === AST_NODE_TYPES.TSFunctionType) {
return getReturnType(node?.returnType);
}
// etc
return null;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried this

    function getReturnType(
      node: TSESTree.TSTypeAnnotation,
    ): Array<number> | null {
      if (!possibleReturnThisTypeHolders.has(node.type)) {
        return null;
      }

      if (node.typeAnnotation.type === AST_NODE_TYPES.TSThisType) {
        return node.range;
      }

      if (node.typeAnnotation.type === AST_NODE_TYPES.TSTypeAnnotation) { // <--- type error
        if (node.typeAnnotation.type === AST_NODE_TYPES.TSThisType) {  // <--- type error
          return node.typeAnnotation.range;
        }
      }

      if (node.typeAnnotation.type === AST_NODE_TYPES.TSFunctionType) {
        return getReturnType(node?.returnType);
      }

      return null;
    }

Still no luck, even tests are failing cause of this !

@bradzacher
Copy link
Member

Okay. So looking at this, it's going to be a bit of a pain to recurse through all of the various permutations of valid positions for the TSThisType.

A better way to do this would be something like the following rough code.
The idea is that we could use ESLint selectors to do the heavy lifting for us.

let tsThisTypes: TSESTree.TSThisType[] | null;

{
  'TSInterfaceDeclaration:enter'(): void {
    tsThisTypes = [];
  },
  'TSInterfaceDeclaration:exit'(node: TSESTree.TSInterfaceDeclaration): void {
    if (!hasOneSupertype(node) && node.body.body.length === 1) {
      checkMember(node.body.body[0], node);
    }
    tsThisTypes = null;
  },

  'TSTypeLiteral[members.length = 1]:enter'(): void {
    tsThisTypes = [];

  },
  'TSTypeLiteral[members.length = 1]:exit'(node: TSESTree.TSTypeLiteral): void {
    checkMember(node.members[0], node);
    tsThisTypes = null;
  },

  TSThisType(node): void {
    tsThisTypes?.push(node);
  },
};

// then in the fixer for interface
for (const thisType of (tsThisTypes ?? [])) {
  fixer.replaceText(thisType, interfaceName);
}

// in the reporter for TSTypeLiteral
if (tsThisTypes && tsThisTypes.length > 0) {
  // cannot handle this types because we don't know the name of the type literal
  // so we can't generate a recursive type
  return;
}

@bradzacher bradzacher added the stale PRs or Issues that are at risk of being or have been closed due to inactivity for a prolonged period label Jun 24, 2020
@bradzacher bradzacher closed this Jun 24, 2020
@anikethsaha
Copy link
Contributor Author

@bradzacher I am sorry as I couldn't able to complete this PR.

I may look at this in the future if no one is taking.

Thanks

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jul 25, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
awaiting response Issues waiting for a reply from the OP or another party bug Something isn't working stale PRs or Issues that are at risk of being or have been closed due to inactivity for a prolonged period
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[prefer-function-type] Quick fixing an interface using this causes tsc error
2 participants