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

eslint-plugin: Add rule no-unused-vars-before-return #12828

Merged
merged 5 commits into from
Jan 30, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
384 changes: 207 additions & 177 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions packages/eslint-plugin/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

- The `esnext` and `recommended` rulesets now enforce [`object-shorthand`](https://eslint.org/docs/rules/object-shorthand)

### New Features

- New Rule: [`@wordpress/no-unused-vars-before-return`](https://github.com/WordPress/gutenberg/blob/master/packages/eslint-plugin/docs/rules/no-unused-vars-before-return.md)

## 1.0.0 (2018-12-12)

### New Features
Expand Down
12 changes: 9 additions & 3 deletions packages/eslint-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Install the module
npm install @wordpress/eslint-plugin --save-dev
```

### Usage
## Usage

To opt-in to the default configuration, extend your own project's `.eslintrc` file:

Expand All @@ -24,7 +24,7 @@ Refer to the [ESLint documentation on Shareable Configs](http://eslint.org/docs/

The `recommended` preset will include rules governing an ES2015+ environment, and includes rules from the [`eslint-plugin-jsx-a11y`](https://github.com/evcohen/eslint-plugin-jsx-a11y) and [`eslint-plugin-react`](https://github.com/yannickcr/eslint-plugin-react) projects.

#### Rulesets
### Rulesets

Alternatively, you can opt-in to only the more granular rulesets offered by the plugin. These include:

Expand All @@ -45,7 +45,13 @@ These rules can be used additively, so you could extend both `esnext` and `custo

The granular rulesets will not define any environment globals. As such, if they are required for your project, you will need to define them yourself.

#### Legacy
### Rules

Rule|Description
---|---
[no-unused-vars-before-return](docs/rules/no-unused-vars-before-return.md)|Disallow assigning variable values if unused before a return

### Legacy

If you are using WordPress' `.jshintrc` JSHint configuration and you would like to take the first step to migrate to an ESLint equivalent it is also possible to define your own project's `.eslintrc` file as:

Expand Down
4 changes: 4 additions & 0 deletions packages/eslint-plugin/configs/custom.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
module.exports = {
plugins: [
'@wordpress',
],
rules: {
'@wordpress/no-unused-vars-before-return': 'error',
'no-restricted-syntax': [
'error',
{
Expand Down
31 changes: 31 additions & 0 deletions packages/eslint-plugin/docs/rules/no-unused-vars-before-return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Avoid assigning variable values if unused before a return (no-unused-vars-before-return)

To avoid wastefully computing the result of a function call when assigning a variable value, the variable should be declared as late as possible. In practice, if a function includes an early return path, any variable declared prior to the `return` must be referenced at least once. Otherwise, in the condition which satisfies that return path, the declared variable is effectively considered dead code. This can have a performance impact when the logic performed in assigning the value is non-trivial.
Copy link
Member

Choose a reason for hiding this comment

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

Reads nicely 👍


## Rule details

The following patterns are considered warnings:

```js
function example( number ) {
const foo = doSomeCostlyOperation();
if ( number > 10 ) {
return number + 1;
}

return number + foo;
}
```

The following patterns are not considered warnings:

```js
function example( number ) {
if ( number > 10 ) {
return number + 1;
}

const foo = doSomeCostlyOperation();
return number + foo;
}
```
1 change: 1 addition & 0 deletions packages/eslint-plugin/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
module.exports = {
configs: require( './configs' ),
rules: require( './rules' ),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
gziolo marked this conversation as resolved.
Show resolved Hide resolved
* External dependencies
*/
import { RuleTester } from 'eslint';

/**
* Internal dependencies
*/
import rule from '../no-unused-vars-before-return';

const ruleTester = new RuleTester( {
parserOptions: {
ecmaVersion: 6,
},
} );

ruleTester.run( 'no-unused-vars-before-return', rule, {
valid: [
{
code: `
function example( number ) {
if ( number > 10 ) {
return number + 1;
}

const foo = doSomeCostlyOperation();
return number + foo;
}`,
},
],
invalid: [
{
code: `
function example( number ) {
const foo = doSomeCostlyOperation();
if ( number > 10 ) {
return number + 1;
}

return number + foo;
}`,
errors: [ { message: 'Declared variable `foo` is unused before a return path' } ],
},
],
} );
1 change: 1 addition & 0 deletions packages/eslint-plugin/rules/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require( 'requireindex' )( __dirname );
Copy link
Member

Choose a reason for hiding this comment

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

is it equivalent to re-exporting all files in the folder? just being curious :)

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 equivalent to something like:

module.exports = {
    foo: require( './foo' ),
    bar: require( './bar' ),
    baz: require( './baz' ),
};

55 changes: 55 additions & 0 deletions packages/eslint-plugin/rules/no-unused-vars-before-return.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
module.exports = {
meta: {
type: 'problem',
schema: [],
},
create( context ) {
return {
ReturnStatement( node ) {
let functionScope = context.getScope();
while ( functionScope.type !== 'function' && functionScope.upper ) {
functionScope = functionScope.upper;
}

if ( ! functionScope ) {
return;
}

for ( const variable of functionScope.variables ) {
const isAssignmentCandidate = variable.defs.some( ( def ) => {
return (
def.node.type === 'VariableDeclarator' &&
// Allow declarations which are not initialized.
def.node.init &&
// Target function calls as "expensive".
def.node.init.type === 'CallExpression' &&
// Allow unused if part of an object destructuring.
def.node.id.type !== 'ObjectPattern' &&
// Only target assignments preceding `return`.
def.node.end < node.end
);
} );

if ( ! isAssignmentCandidate ) {
continue;
}

// The first entry in `references` is the declaration
// itself, which can be ignored.
const isUsedBeforeReturn = variable.references.slice( 1 ).some( ( reference ) => {
return reference.identifier.end < node.end;
} );

if ( isUsedBeforeReturn ) {
continue;
}

context.report(
node,
`Declared variable \`${ variable.name }\` is unused before a return path`
Copy link
Member

Choose a reason for hiding this comment

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

I'm not quite sure that is going to be clear enough when printed on the console. Should it include more detailed info that explains that there is a return path where it is not used before it is even used? I doesn't read good but I hope it explain my concern. Anyway, I don't find it as blocker :)

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 played with the message a bit more. One thing which confused me is its placement on the return statement vs. the assignment itself, which I've found can be adjusted. Though there's also some value in having it on the actual return which flags it as offending 🤷‍♂️

Any thoughts on (attached to the variable declaration):

Variables should not be assigned until just prior its first reference. An early return statement may leave this variable unused.

Copy link
Member

Choose a reason for hiding this comment

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

This is much better. Let's proceed with it 👍

);
}
},
};
},
};
6 changes: 5 additions & 1 deletion packages/scripts/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
## 2.6.0 (Unreleased)
## 3.0.0 (Unreleased)

### Breaking Changes

- The bundled `eslint` dependency has been updated from requiring `^4.19.1` to requiring `^5.12.1`.
Copy link
Member

Choose a reason for hiding this comment

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

We should also bump Babel at the same time and apply some tweaks to the preset we ship in the same version bump. I will open another PR with changes proposed.


### New Features

Expand Down
2 changes: 1 addition & 1 deletion packages/scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"chalk": "^2.4.1",
"check-node-version": "^3.1.1",
"cross-spawn": "^5.1.0",
"eslint": "^4.19.1",
"eslint": "^5.12.1",
"jest": "^23.6.0",
"jest-puppeteer": "3.2.1",
"npm-package-json-lint": "^3.3.1",
Expand Down