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

feat(eslint-plugin): [prefer-return-this-type] add a new rule #3228

Merged
merged 12 commits into from Jul 31, 2021

Conversation

Zzzen
Copy link
Contributor

@Zzzen Zzzen commented Mar 26, 2021

close #889
The implementation is pretty clear, it only warns if:

  1. the return type is written explicitly, and it is simple ( not union or intersection types )
  2. all code paths return this type

TODO:

  • doc
  • maybe support PropertyDeclaration initialized with functions like
class Foo {
  f = (): Foo => {
    return this;
  }
}

@typescript-eslint
Copy link
Contributor

typescript-eslint bot commented Mar 26, 2021

Thanks for the PR, @Zzzen!

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.

@codecov
Copy link

codecov bot commented Mar 26, 2021

Codecov Report

Merging #3228 (52df4db) into master (15f7184) will increase coverage by 0.87%.
The diff coverage is 91.54%.

@@            Coverage Diff             @@
##           master    #3228      +/-   ##
==========================================
+ Coverage   92.64%   93.51%   +0.87%     
==========================================
  Files         326      147     -179     
  Lines       11253     7848    -3405     
  Branches     3171     2485     -686     
==========================================
- Hits        10425     7339    -3086     
+ Misses        368      162     -206     
+ Partials      460      347     -113     
Flag Coverage Δ
unittest 93.51% <91.54%> (+0.87%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
packages/eslint-plugin/src/configs/all.ts 100.00% <ø> (ø)
...eslint-plugin/src/rules/prefer-return-this-type.ts 90.47% <90.47%> (ø)
packages/eslint-plugin/src/util/astUtils.ts 89.47% <100.00%> (+7.65%) ⬆️
...ckages/typescript-estree/src/jsx/xhtml-entities.ts
packages/scope-manager/src/lib/es2019.array.ts
packages/typescript-estree/src/ts-estree/index.ts
...es/experimental-utils/src/ts-eslint-scope/Scope.ts
packages/scope-manager/src/lib/esnext.array.ts
...ckages/scope-manager/src/lib/webworker.iterable.ts
packages/scope-manager/src/lib/dom.ts
... and 174 more

@bradzacher bradzacher added the enhancement: new plugin rule New rule request for eslint-plugin label Mar 26, 2021
@bradzacher bradzacher marked this pull request as draft Mar 28, 2021
@bradzacher bradzacher added the awaiting response Issues waiting for a reply from the OP or another party label Mar 28, 2021
@Zzzen Zzzen marked this pull request as ready for review Apr 5, 2021
@bradzacher bradzacher removed the awaiting response Issues waiting for a reply from the OP or another party label Apr 7, 2021
f2() {
return this;
}
f3(): Foo | undefined {
Copy link
Member

@bradzacher bradzacher Apr 10, 2021

Choose a reason for hiding this comment

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

why not this | undefined?

Copy link
Contributor Author

@Zzzen Zzzen Apr 19, 2021

Choose a reason for hiding this comment

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

👌

description:
'Enforce that `this` is used when only `this` type is returned',
category: 'Best Practices',
recommended: 'error',
Copy link
Member

@bradzacher bradzacher Apr 10, 2021

Choose a reason for hiding this comment

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

adding to the recommended set is a breaking change

Suggested change
recommended: 'error',
recommended: false,

'ClassBody > MethodDefinition'(node: TSESTree.MethodDefinition): void {
checkFunction(node.value, node.parent!.parent as ClassLikeDeclaration);
},
'ClassBody > ClassProperty'(node: TSESTree.ClassProperty): void {
if (
!(
node.value?.type === AST_NODE_TYPES.FunctionExpression ||
node.value?.type === AST_NODE_TYPES.ArrowFunctionExpression
)
) {
return;
}

checkFunction(node.value, node.parent!.parent as ClassLikeDeclaration);
},
Copy link
Member

@bradzacher bradzacher Apr 10, 2021

Choose a reason for hiding this comment

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

one easier way to do this is to use a stack to track your state.
It saves you doing manual traversal of the tree

interface Stack { classLike: ClassLikeDeclaration, methodLike: FunctionLike | null }
const stack: Stack[] = [];
function getCurrentStack(): Stack | undefined {
  return stack[stack.length - 1];
}

return {
  'ClassDeclaration, ClassExpression:enter'(node: ClassLikeDeclaration) {
    stack.push({ classLike: node, methodLike: null });
  },
  'ClassDeclaration, ClassExpression:exit'(node: ClassLikeDeclaration) {
    stack.pop();
  },
  'ClassBody > MethodDefinition:enter, ClassBody > ClassProperty > :matches(FunctionExpression, ArrowFunctionExpression).value:enter'(node: FunctionLike ) {
    const current = getCurrentStack();
    if (current != null) {
      current.methodLike = node;
    }
  },
  'ClassBody > MethodDefinition, ClassBody > ClassProperty > :matches(FunctionExpression, ArrowFunctionExpression).value:exit'(node: FunctionLike ) {
    const current = getCurrentStack();
    if (current != null) {
      current.methodLike = null;
    }
  },
  ReturnStatement(node) {
    const current = getCurrentStack();
    if (current != null && current.methodLike != null) {
      // check return and report
    }
  },
};

Copy link
Contributor Author

@Zzzen Zzzen Apr 11, 2021

Choose a reason for hiding this comment

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

Cool, I haven't seen this pattern before, will try it.

Copy link
Contributor Author

@Zzzen Zzzen Apr 19, 2021

Choose a reason for hiding this comment

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

This is more complicated than I have expected. We may have functions inside methods and need to track them too.

class Foo {
  bar() {
    const f = function() {
       return this;
    }
  }
}

Copy link
Member

@bradzacher bradzacher left a comment

a few quick comments - haven't reviewed SUPER deeply yet.


what about abstract methods? Do we want to check those signatures?
what about interfaces? Do we want to check those signatures?

@bradzacher bradzacher added the awaiting response Issues waiting for a reply from the OP or another party label Apr 10, 2021
Co-authored-by: Brad Zacher <brad.zacher@gmail.com>
@Zzzen
Copy link
Contributor Author

Zzzen commented Apr 14, 2021

what about abstract methods? Do we want to check those signatures?
what about interfaces? Do we want to check those signatures?

Showerthought: we don't need to check them because this doesn't make a difference in interfaces/methods. Eg. Playgroud

interface IAnimalFixed {
    move(): IAnimalFixed;
}

interface IAnimalPolymophic {
    move(): this;
}

class CatFixed implements IAnimalFixed {
    // return type will be checked by this rule
    move(): this {
        return this;
    }

    meow(): this {
        return this;
    }
}

class CatPolymophic implements IAnimalPolymophic {
    move(): this {
        return this;
    }

    meow(): this {
        return this;
    }
}

function useInterface(fixed: IAnimalFixed, polymophic: IAnimalPolymophic) {
    // return type: IAnimalFixed
    fixed.move();

    // return type: IAnimalFixed
    polymophic.move();
}

function useImplementation(fixed: CatFixed, polymophic: CatPolymophic) {
    // return type: CatFixed
    fixed.move();

    // return type: CatPolymophic
    polymophic.move();
}

@bradzacher bradzacher removed the awaiting response Issues waiting for a reply from the OP or another party label Apr 19, 2021
@@ -17,6 +17,7 @@ export = {
'@typescript-eslint/no-unsafe-member-access': 'error',
'@typescript-eslint/no-unsafe-return': 'error',
'@typescript-eslint/prefer-regexp-exec': 'error',
'@typescript-eslint/prefer-return-this-type': 'error',
Copy link
Member

@bradzacher bradzacher May 15, 2021

Choose a reason for hiding this comment

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

you need to regenerate the configs

@@ -163,6 +163,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int
| [`@typescript-eslint/prefer-readonly-parameter-types`](./docs/rules/prefer-readonly-parameter-types.md) | Requires that function parameters are typed as readonly to prevent accidental mutation of inputs | | | :thought_balloon: |
| [`@typescript-eslint/prefer-reduce-type-parameter`](./docs/rules/prefer-reduce-type-parameter.md) | Prefer using type parameter when calling `Array#reduce` instead of casting | | :wrench: | :thought_balloon: |
| [`@typescript-eslint/prefer-regexp-exec`](./docs/rules/prefer-regexp-exec.md) | Enforce that `RegExp#exec` is used instead of `String#match` if no global flag is provided | :white_check_mark: | :wrench: | :thought_balloon: |
| [`@typescript-eslint/prefer-return-this-type`](./docs/rules/prefer-return-this-type.md) | Enforce that `this` is used when only `this` type is returned | :heavy_check_mark: | :wrench: | :thought_balloon: |
Copy link
Member

@bradzacher bradzacher May 15, 2021

Choose a reason for hiding this comment

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

you need to regen the docs to remove the recommended tick

@bradzacher bradzacher added the awaiting response Issues waiting for a reply from the OP or another party label May 15, 2021
@Zzzen
Copy link
Contributor Author

Zzzen commented May 16, 2021

Why is CI not running?

@bradzacher bradzacher removed the awaiting response Issues waiting for a reply from the OP or another party label May 24, 2021
Copy link
Member

@bradzacher bradzacher left a comment

LGTM - this is enough coverage for a v1.
We can always add more cases later.
Thanks for your contribution!

@bradzacher bradzacher enabled auto-merge (squash) Jul 31, 2021
@bradzacher bradzacher added the 1 approval Feature PR that has an approval. label Jul 31, 2021
@bradzacher bradzacher disabled auto-merge Jul 31, 2021
@bradzacher bradzacher merged commit 5e1a615 into typescript-eslint:master Jul 31, 2021
10 of 11 checks passed
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 31, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
1 approval Feature PR that has an approval. enhancement: new plugin rule New rule request for eslint-plugin
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants