Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
- Loading branch information
Showing
5 changed files
with
252 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
# Enforce that class methods utilize `this` (class-methods-use-this) | ||
|
||
If a class method does not use `this`, it can safely be made a static function. | ||
|
||
It's possible to have a class method which doesn't use `this`, such as: | ||
|
||
```js | ||
class A { | ||
constructor() { | ||
this.a = "hi"; | ||
} | ||
|
||
print() { | ||
console.log(this.a); | ||
} | ||
|
||
sayHi() { | ||
console.log("hi"); | ||
} | ||
} | ||
|
||
let aObj = new A(); | ||
a.sayHi(); // => "hi" | ||
``` | ||
|
||
In the example above, the `sayHi` method doesn't use `this`, so we can make it a static method: | ||
|
||
```js | ||
class A { | ||
constructor() { | ||
this.a = "hi"; | ||
} | ||
|
||
print() { | ||
console.log(this.a); | ||
} | ||
|
||
static sayHi() { | ||
console.log("hi"); | ||
} | ||
} | ||
|
||
A.sayHi(); // => "hi" | ||
``` | ||
|
||
## Rule Details | ||
|
||
This rule is aimed to flag class methods that do not use `this`. | ||
|
||
Examples of **incorrect** code for this rule: | ||
|
||
```js | ||
/*eslint class-methods-use-this: "error"*/ | ||
/*eslint-env es6*/ | ||
|
||
class A { | ||
foo() { | ||
console.log("Hello World"); /*error Expected 'this' to be used by class method 'foo'.*/ | ||
} | ||
} | ||
``` | ||
|
||
Examples of **correct** code for this rule: | ||
|
||
```js | ||
/*eslint class-methods-use-this: "error"*/ | ||
/*eslint-env es6*/ | ||
class A { | ||
foo() { | ||
this.bar = "Hello World"; // OK, this is used | ||
} | ||
} | ||
|
||
class A { | ||
constructor() { | ||
// OK. constructor is exempt | ||
} | ||
} | ||
|
||
class A { | ||
static foo() { | ||
// OK. static methods aren't expected to use this. | ||
} | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
/** | ||
* @fileoverview Rule to enforce that all class methods use 'this'. | ||
* @author Patrick Williams | ||
*/ | ||
|
||
"use strict"; | ||
|
||
//------------------------------------------------------------------------------ | ||
// Rule Definition | ||
//------------------------------------------------------------------------------ | ||
|
||
module.exports = { | ||
meta: { | ||
docs: { | ||
description: "enforce that class methods utilize `this`", | ||
category: "Best Practices", | ||
recommended: false | ||
}, | ||
schema: [] | ||
}, | ||
create(context) { | ||
const stack = []; | ||
|
||
/** | ||
* Initializes the current context to false and pushes it onto the stack. | ||
* These booleans represent whether 'this' has been used in the context. | ||
* @returns {void} | ||
* @private | ||
*/ | ||
function enterFunction() { | ||
stack.push(false); | ||
} | ||
|
||
/** | ||
* Check if the node is an instance method | ||
* @param {ASTNode} node - node to check | ||
* @returns {boolean} True if its an instance method | ||
* @private | ||
*/ | ||
function isInstanceMethod(node) { | ||
return !node.static && node.kind !== "constructor" && node.type === "MethodDefinition"; | ||
} | ||
|
||
/** | ||
* Checks if we are leaving a function that is a method, and reports if 'this' has not been used. | ||
* Static methods and the constructor are exempt. | ||
* Then pops the context off the stack. | ||
* @param {ASTNode} node - A function node that was entered. | ||
* @returns {void} | ||
* @private | ||
*/ | ||
function exitFunction(node) { | ||
const methodUsesThis = stack.pop(); | ||
|
||
if (isInstanceMethod(node.parent) && !methodUsesThis) { | ||
context.report(node, "Expected 'this' to be used by class method '" + node.parent.key.name + "'."); | ||
} | ||
} | ||
|
||
/** | ||
* Mark the current context as having used 'this'. | ||
* @returns {void} | ||
* @private | ||
*/ | ||
function markThisUsed() { | ||
if (stack.length) { | ||
stack[stack.length - 1] = true; | ||
} | ||
} | ||
|
||
return { | ||
FunctionDeclaration: enterFunction, | ||
"FunctionDeclaration:exit": exitFunction, | ||
FunctionExpression: enterFunction, | ||
"FunctionExpression:exit": exitFunction, | ||
ThisExpression: markThisUsed, | ||
Super: markThisUsed | ||
}; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
/** | ||
* @fileoverview Tests for class-methods-use-this rule. | ||
* @author Patrick Williams | ||
*/ | ||
|
||
"use strict"; | ||
|
||
//------------------------------------------------------------------------------ | ||
// Requirements | ||
//------------------------------------------------------------------------------ | ||
|
||
const rule = require("../../../lib/rules/class-methods-use-this"); | ||
const RuleTester = require("../../../lib/testers/rule-tester"); | ||
|
||
//------------------------------------------------------------------------------ | ||
// Tests | ||
//------------------------------------------------------------------------------ | ||
|
||
const ruleTester = new RuleTester(); | ||
|
||
ruleTester.run("class-methods-use-this", rule, { | ||
valid: [ | ||
{code: "class A { constructor() {} }", parserOptions: { ecmaVersion: 6 }}, | ||
{code: "class A { foo() {this} }", parserOptions: { ecmaVersion: 6 }}, | ||
{code: "class A { foo() {this.bar = 'bar';} }", parserOptions: { ecmaVersion: 6 }}, | ||
{code: "class A { foo() {bar(this);} }", parserOptions: { ecmaVersion: 6 }}, | ||
{code: "class A extends B { foo() {super.foo();} }", parserOptions: { ecmaVersion: 6 }}, | ||
{code: "class A { foo() { if(true) { return this; } } }", parserOptions: { ecmaVersion: 6 }}, | ||
{code: "class A { static foo() {} }", parserOptions: { ecmaVersion: 6 }}, | ||
{code: "({ a(){} });", parserOptions: { ecmaVersion: 6 }}, | ||
{code: "class A { foo() { () => this; } }", parserOptions: { ecmaVersion: 6 }}, | ||
{code: "({ a: function () {} });", parserOptions: { ecmaVersion: 6 }} | ||
], | ||
invalid: [ | ||
{ | ||
code: "class A { foo() {} }", | ||
parserOptions: { ecmaVersion: 6 }, | ||
errors: [ | ||
{type: "FunctionExpression", line: 1, column: 14, message: "Expected 'this' to be used by class method 'foo'."} | ||
] | ||
}, | ||
{ | ||
code: "class A { foo() {/**this**/} }", | ||
parserOptions: { ecmaVersion: 6 }, | ||
errors: [ | ||
{type: "FunctionExpression", line: 1, column: 14, message: "Expected 'this' to be used by class method 'foo'."} | ||
] | ||
}, | ||
{ | ||
code: "class A { foo() {var a = function () {this};} }", | ||
parserOptions: { ecmaVersion: 6 }, | ||
errors: [ | ||
{type: "FunctionExpression", line: 1, column: 14, message: "Expected 'this' to be used by class method 'foo'."} | ||
] | ||
}, | ||
{ | ||
code: "class A { foo() {var a = function () {var b = function(){this}};} }", | ||
parserOptions: { ecmaVersion: 6 }, | ||
errors: [ | ||
{type: "FunctionExpression", line: 1, column: 14, message: "Expected 'this' to be used by class method 'foo'."} | ||
] | ||
}, | ||
{ | ||
code: "class A { foo() {window.this} }", | ||
parserOptions: { ecmaVersion: 6 }, | ||
errors: [ | ||
{type: "FunctionExpression", line: 1, column: 14, message: "Expected 'this' to be used by class method 'foo'."} | ||
] | ||
}, | ||
{ | ||
code: "class A { foo() {that.this = 'this';} }", | ||
parserOptions: { ecmaVersion: 6 }, | ||
errors: [ | ||
{type: "FunctionExpression", line: 1, column: 14, message: "Expected 'this' to be used by class method 'foo'."} | ||
] | ||
}, | ||
{ | ||
code: "class A { foo() { () => undefined; } }", | ||
parserOptions: { ecmaVersion: 6 }, | ||
errors: [ | ||
{type: "FunctionExpression", line: 1, column: 14, message: "Expected 'this' to be used by class method 'foo'."} | ||
] | ||
} | ||
] | ||
}); |