Skip to content

Commit

Permalink
Update: add "variables" option to no-use-before-define (fixes #7111) (#…
Browse files Browse the repository at this point in the history
…7948)

* Update: add "variables" option to no-use-before-define (fixes #7111)

* Clarify the default behavior
  • Loading branch information
not-an-aardvark committed Jan 27, 2017
1 parent 09546a4 commit c5066ce
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 31 deletions.
28 changes: 28 additions & 0 deletions docs/rules/no-use-before-define.md
Expand Up @@ -77,6 +77,11 @@ function g() {
Otherwise, ignores those references if the declaration is in upper function scopes.
Class declarations are not hoisted, so it might be danger.
Default is `true`.
* `variables` (`boolean`) -
This flag determines whether or not the rule checks variable declarations in upper scopes.
If this is `true`, the rule warns every reference to a variable before the variable declaration.
Otherwise, the rule ignores a reference if the declaration is in an upper scope, while still reporting the reference if it's in the same scope as the declaration.
Default is `true`.

This rule accepts `"nofunc"` string as a option.
`"nofunc"` is the same as `{ "functions": false, "classes": true }`.
Expand Down Expand Up @@ -118,3 +123,26 @@ function foo() {
class A {
}
```

### variables

Examples of **incorrect** code for the `{ "variables": false }` option:

```js
/*eslint no-use-before-define: ["error", { "variables": false }]*/

console.log(foo);
var foo = 1;
```

Examples of **correct** code for the `{ "variables": false }` option:

```js
/*eslint no-use-before-define: ["error", { "variables": false }]*/

function baz() {
console.log(foo);
}

var foo = 1;
```
62 changes: 33 additions & 29 deletions lib/rules/no-use-before-define.js
Expand Up @@ -21,22 +21,17 @@ const FOR_IN_OF_TYPE = /^For(?:In|Of)Statement$/;
function parseOptions(options) {
let functions = true;
let classes = true;
let variables = true;

if (typeof options === "string") {
functions = (options !== "nofunc");
} else if (typeof options === "object" && options !== null) {
functions = options.functions !== false;
classes = options.classes !== false;
variables = options.variables !== false;
}

return { functions, classes };
}

/**
* @returns {boolean} `false`.
*/
function alwaysFalse() {
return false;
return { functions, classes, variables };
}

/**
Expand Down Expand Up @@ -64,14 +59,16 @@ function isOuterClass(variable, reference) {
}

/**
* Checks whether or not a given variable is a function declaration or a class declaration in an upper function scope.
*
* @param {escope.Variable} variable - A variable to check.
* @param {escope.Reference} reference - A reference to check.
* @returns {boolean} `true` if the variable is a function declaration or a class declaration.
*/
function isFunctionOrOuterClass(variable, reference) {
return isFunction(variable, reference) || isOuterClass(variable, reference);
* Checks whether or not a given variable is a variable declaration in an upper function scope.
* @param {escope.Variable} variable - A variable to check.
* @param {escope.Reference} reference - A reference to check.
* @returns {boolean} `true` if the variable is a variable declaration.
*/
function isOuterVariable(variable, reference) {
return (
variable.defs[0].type === "Variable" &&
variable.scope.variableScope !== reference.from.variableScope
);
}

/**
Expand Down Expand Up @@ -155,7 +152,8 @@ module.exports = {
type: "object",
properties: {
functions: { type: "boolean" },
classes: { type: "boolean" }
classes: { type: "boolean" },
variables: { type: "boolean" }
},
additionalProperties: false
}
Expand All @@ -167,17 +165,23 @@ module.exports = {
create(context) {
const options = parseOptions(context.options[0]);

// Defines a function which checks whether or not a reference is allowed according to the option.
let isAllowed;

if (options.functions && options.classes) {
isAllowed = alwaysFalse;
} else if (options.functions) {
isAllowed = isOuterClass;
} else if (options.classes) {
isAllowed = isFunction;
} else {
isAllowed = isFunctionOrOuterClass;
/**
* Determines whether a given use-before-define case should be reportedaccording to the options.
* @param {escope.Variable} variable The variable that gets used before being defined
* @param {escope.Reference} reference The reference to the variable
* @returns {boolean} `true` if the usage should be reported
*/
function isForbidden(variable, reference) {
if (isFunction(variable)) {
return options.functions;
}
if (isOuterClass(variable, reference)) {
return options.classes;
}
if (isOuterVariable(variable, reference)) {
return options.variables;
}
return true;
}

/**
Expand All @@ -200,7 +204,7 @@ module.exports = {
!variable ||
variable.identifiers.length === 0 ||
(variable.identifiers[0].range[1] < reference.identifier.range[1] && !isInInitializer(variable, reference)) ||
isAllowed(variable, reference)
!isForbidden(variable, reference)
) {
return;
}
Expand Down
27 changes: 25 additions & 2 deletions tests/lib/rules/no-use-before-define.js
Expand Up @@ -45,7 +45,18 @@ ruleTester.run("no-use-before-define", rule, {
// object style options
{ code: "a(); function a() { alert(arguments); }", options: [{ functions: false }] },
{ code: "\"use strict\"; { a(); function a() {} }", options: [{ functions: false }], parserOptions: { ecmaVersion: 6 } },
{ code: "function foo() { new A(); } class A {};", options: [{ classes: false }], parserOptions: { ecmaVersion: 6 } }
{ code: "function foo() { new A(); } class A {};", options: [{ classes: false }], parserOptions: { ecmaVersion: 6 } },

// "variables" option
{
code: "function foo() { bar; } var bar;",
options: [{ variables: false }]
},
{
code: "var foo = () => bar; var bar;",
parserOptions: { ecmaVersion: 6 },
options: [{ variables: false }]
}
],
invalid: [
{ code: "a++; var a=19;", parserOptions: { sourceType: "module" }, errors: [{ message: "'a' was used before it was defined.", type: "Identifier" }] },
Expand Down Expand Up @@ -91,6 +102,18 @@ ruleTester.run("no-use-before-define", rule, {
{ code: "var {a = 0} = a;", parserOptions: { ecmaVersion: 6 }, errors: [{ message: "'a' was used before it was defined.", type: "Identifier" }] },
{ code: "var [a = 0] = a;", parserOptions: { ecmaVersion: 6 }, errors: [{ message: "'a' was used before it was defined.", type: "Identifier" }] },
{ code: "for (var a in a) {}", errors: [{ message: "'a' was used before it was defined.", type: "Identifier" }] },
{ code: "for (var a of a) {}", parserOptions: { ecmaVersion: 6 }, errors: [{ message: "'a' was used before it was defined.", type: "Identifier" }] }
{ code: "for (var a of a) {}", parserOptions: { ecmaVersion: 6 }, errors: [{ message: "'a' was used before it was defined.", type: "Identifier" }] },

// "variables" option
{
code: "function foo() { bar; var bar = 1; } var bar;",
options: [{ variables: false }],
errors: [{ message: "'bar' was used before it was defined.", type: "Identifier" }]
},
{
code: "foo; var foo;",
options: [{ variables: false }],
errors: [{ message: "'foo' was used before it was defined.", type: "Identifier" }]
}
]
});

0 comments on commit c5066ce

Please sign in to comment.