)
26 | }
27 | ```
28 |
29 | ```js
30 | render() {
31 | const props = this.props;
32 |
33 | return (
34 |
{props.text}
35 |
)
36 | }
37 | ```
38 |
--------------------------------------------------------------------------------
/docs/no-void-map.md:
--------------------------------------------------------------------------------
1 | # Forces to assign array.map to variable
2 |
3 | You have not to leave array.map without variable or property (bad example). Here you can to assign it to variable if you need new array of elements from old array (first good example ) or continue to work with new array with other property (second good example). Look carefully at examples.
4 |
5 | ## Rule Details
6 |
7 | The following pattern is considered a warning:
8 |
9 | ```js
10 | users.map(user=> user.status = "ACTIVE");
11 | ```
12 |
13 | The following pattern is not considered a warning:
14 |
15 | ```js
16 | var users = [{id: 1}, {id: 2}, {id: 3}];
17 |
18 | const usersIds = users.map(user => user.id);
19 |
20 | ```
21 |
22 | ```js
23 | var users = [{id: 1}, {id: 2}, {id: 3}];
24 |
25 | users.map(user => user.id).forEach( id => { console.log(id) } );
26 |
27 | ```
28 |
--------------------------------------------------------------------------------
/lib/rules/no-filter-instead-of-find.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | create(context) {
3 | return {
4 | MemberExpression(node) {
5 | const property = node.property;
6 | const isPropertyZero = property.type === 'Literal' && property.value === 0;
7 | const isFilterExpression = node.object.type === 'CallExpression' && node.object.callee
8 | && node.object.callee.property && node.object.callee.property.name === 'filter';
9 |
10 | if (isFilterExpression && isPropertyZero) {
11 | context.report({
12 | node,
13 | message: 'Do not use \'filter\' to find one element, use find method instead'
14 | });
15 | }
16 | }
17 | };
18 | }
19 | };
20 |
21 | module.exports.schema = [];
22 |
--------------------------------------------------------------------------------
/lib/rules/force-native-methods.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | module.exports = function (context) {
4 | return {
5 | CallExpression(node) {
6 | const callee = node.callee;
7 | const objectName = callee.name || (callee.object && callee.object.name);
8 | const notAllowedMethods = ['find', 'findIndex', 'indexOf', 'each', 'every', 'filter', 'includes', 'map',
9 | 'reduce', 'toLower', 'toUpper', 'trim', 'keys'];
10 |
11 | if ((objectName === '_' || objectName === 'lodash' || objectName === 'underscore')
12 | && callee.property && notAllowedMethods.indexOf(callee.property.name) !== -1) {
13 | context.report({
14 | node,
15 | message: 'Do not use lodash methods, use native instead'
16 | });
17 | }
18 | }
19 | };
20 | };
21 |
22 | module.exports.schema = [];
23 |
--------------------------------------------------------------------------------
/docs/force-native-methods.md:
--------------------------------------------------------------------------------
1 | # Forces the use of native methods instead of lodash/underscore
2 |
3 | Many of the array functionality present in libraries like [lodash.com](lodash) or [underscorejs.org](underscore) are available on the Array prototype.
4 |
5 | ## Rule Details
6 |
7 | The following patterns are considered warnings:
8 |
9 | ```js
10 | import _ from 'lodash';
11 |
12 | function getEvenNumbers(numbers) {
13 | return _.filter(numbers, num => num % 2 === 0);
14 | }
15 | ```
16 |
17 | ``` js
18 | import _ from 'lodash';
19 |
20 | function increment(numbers) {
21 | return _.map(numbers, num => num + 1);
22 | }
23 | ```
24 |
25 | The following patterns are not considered warnings:
26 |
27 | ```js
28 | function getEvenNumbers(numbers) {
29 | return numbers.filter(num => num % 2 === 0);
30 | }
31 | ```
32 |
33 | ``` js
34 | function increment(numbers) {
35 | return numbers.map(num => num + 1);
36 | }
37 | ```
38 |
--------------------------------------------------------------------------------
/docs/no-window.md:
--------------------------------------------------------------------------------
1 | # Prohibits the usage of `window` global
2 |
3 | Addng variables to the global scope can cause unintended consequences.
4 |
5 | ## Rule Details
6 |
7 | The following pattern is considered a warning:
8 |
9 | ```js
10 | function setDetail(detail) {
11 | window.detail = detail;
12 | }
13 |
14 | function getDetail() {
15 | return window.detail;
16 | }
17 | ```
18 |
19 | The following pattern is not considered a warning:
20 |
21 | ```js
22 | let __detail;
23 |
24 | function setDetail(detail) {
25 | __detail = detail;
26 | }
27 |
28 | function getDetail() {
29 | return __detail;
30 | }
31 | ```
32 |
33 | ## Rule Options
34 |
35 | This rule can take one argument to exclude some properties calls.
36 |
37 | ```
38 | ...
39 | "more/no-window": [, { exclude: }]
40 | ...
41 | ```
42 |
43 | * `enabled`: for enabling the rule. 0=off, 1=warn, 2=error.
44 | * `exclude`: optional array of methods.
45 |
46 | The default configuration is:
47 |
48 | ```js
49 | {
50 | exclude: [
51 | 'postMessage',
52 | 'open',
53 | 'addEventListener',
54 | 'removeEventListener'
55 | ]
56 | }
57 | ```
58 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 WebbyLab
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/lib/rules/no-hardcoded-password.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = context => {
3 | function inspectHardcodedPassword(node, value) {
4 | const isHardcoded = value.toLowerCase().includes('passw');
5 |
6 | if (isHardcoded) {
7 | return context.report({
8 | node,
9 | message : 'Do not use hardcoded password'
10 | });
11 | }
12 | }
13 |
14 | return {
15 | VariableDeclarator({ id, init }) {
16 | if (id && id.name && init && init.type === 'Literal' ) {
17 | return inspectHardcodedPassword(id, id.name);
18 | }
19 | },
20 |
21 | Property({ key, value }) {
22 | if (!key || !value || value.type !== 'Literal') return;
23 |
24 | if (key.type === 'Identifier') {
25 | return inspectHardcodedPassword(key, key.name);
26 | }
27 |
28 | if (key.type === 'Literal') {
29 | return inspectHardcodedPassword(key, key.value);
30 | }
31 |
32 | if (key.type === 'TemplateLiteral') {
33 | return key.quasis.some(element => inspectHardcodedPassword(key, element.value.raw));
34 | }
35 | }
36 | };
37 | };
38 |
39 | module.exports.schema = [];
--------------------------------------------------------------------------------
/lib/rules/prefer-includes.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | module.exports = function (context) {
4 | return {
5 | BinaryExpression(node) {
6 | const left = node.left || {};
7 | const right = node.right || {};
8 | const operator = node.operator;
9 |
10 | const isIndexOfCall = left.callee
11 | && (left.callee.type === 'MemberExpression')
12 | && (left.callee.property.name === 'indexOf');
13 |
14 | if (!isIndexOfCall) {
15 | return;
16 | }
17 |
18 | const compareWithMinusOne = right.operator === '-' && right.argument && right.argument.value === 1;
19 | const lessThanZero = operator === '<' && right.value === 0;
20 | const moreOrEqualThanZero = operator === '>=' && right.value === 0;
21 |
22 | const isIndexOfEqualToMinusOne = compareWithMinusOne
23 | || lessThanZero
24 | || moreOrEqualThanZero;
25 |
26 | if (isIndexOfEqualToMinusOne) {
27 | context.report({
28 | node,
29 | message: 'Do not use indexOf, instead use includes'
30 | });
31 | }
32 | }
33 | };
34 | };
35 |
36 | module.exports.schema = [];
37 |
--------------------------------------------------------------------------------
/tests/lib/rules/prefer-includes.js:
--------------------------------------------------------------------------------
1 | const { RuleTester } = require('eslint/lib/rule-tester')
2 | const rule = require('../../../lib/rules/prefer-includes');
3 |
4 | const ruleTester = new RuleTester();
5 |
6 | ruleTester.run('prefer-includes', rule, {
7 | valid: [
8 | 'arr.indexOf(2) === 2',
9 | 'arr.indexOf(2) === 0',
10 | 'indexOf(2) >= 0',
11 | 'indexOf(2) < 0',
12 | 'indexOf(2) === -1',
13 | '[1, 2, 3, 4].includes(2)',
14 | // Expressions below used to cause ESLint parser bugs
15 | 'foo() + "bar"',
16 | 'obj.field += sum - some.value',
17 | 'parseInt(obj.field, 10) > 5 ? \'abc\' : obj.field'
18 | ],
19 | invalid: [
20 | {
21 | code: 'arr.indexOf(2) >= 0',
22 | errors: [ { message: 'Do not use indexOf, instead use includes' } ]
23 | },
24 | {
25 | code: 'arr.indexOf(2) === -1',
26 | errors: [ { message: 'Do not use indexOf, instead use includes' } ]
27 | },
28 | {
29 | code: 'arr.indexOf(2) < 0',
30 | errors: [ { message: 'Do not use indexOf, instead use includes' } ]
31 | },
32 | {
33 | code: '[1, 2, 3, 4].indexOf(2) === -1',
34 | errors: [ { message: 'Do not use indexOf, instead use includes' } ]
35 | }
36 | ]
37 | });
38 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | module.exports = {
4 | rules: {
5 | 'no-void-map': require('./lib/rules/no-void-map.js'),
6 | 'no-c-like-loops': require('./lib/rules/no-c-like-loops.js'),
7 | 'prefer-includes': require('./lib/rules/prefer-includes.js'),
8 | 'no-then': require('./lib/rules/no-then'),
9 | 'no-window': require('./lib/rules/no-window'),
10 | 'no-numeric-endings-for-variables': require('./lib/rules/no-numeric-endings-for-variables'),
11 | 'force-native-methods': require('./lib/rules/force-native-methods'),
12 | 'no-duplicated-chains': require('./lib/rules/no-duplicated-chains'),
13 | 'classbody-starts-with-newline': require('./lib/rules/classbody-starts-with-newline'),
14 | 'no-filter-instead-of-find': require('./lib/rules/no-filter-instead-of-find'),
15 | 'no-hardcoded-password': require('./lib/rules/no-hardcoded-password'),
16 | 'no-hardcoded-configuration-data': require('./lib/rules/no-hardcoded-configuration-data')
17 | },
18 | configs: {
19 | recommended: {
20 | rules: {
21 | 'more/no-void-map': 2,
22 | 'more/no-c-like-loops': 2,
23 | 'more/prefer-includes': 2,
24 | 'more/no-then': 2,
25 | 'more/no-window': 2,
26 | 'more/no-numeric-endings-for-variables': 2,
27 | 'more/force-native-methods': 2,
28 | 'more/no-duplicated-chains': 2,
29 | 'more/classbody-starts-with-newline': [2, 'never'],
30 | 'more/no-filter-instead-of-find': 2,
31 | 'more/no-hardcoded-password': 2,
32 | 'more/no-hardcoded-configuration-data': 2
33 | }
34 | }
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/docs/no-hardcoded-configuration-data.md:
--------------------------------------------------------------------------------
1 | # Prohibits using hardcoded configuration data.
2 |
3 | Configuration data should be passed via configuraion mechanism to provide multi-environment support.
4 |
5 | ## Rule Details
6 |
7 | The following pattern is considered a warning:
8 |
9 | ```js
10 | const ip = '192.168.1.1';
11 | const uuid = 'b0d4ce5d-2757-4699-948c-cfa72ba94f86';
12 | const token = 'AEYGF7K0DM1X';
13 | const domain = 'domain.com'
14 |
15 | const object = {
16 | ip : '192.168.1.1',
17 | uuid : 'b0d4ce5d-2757-4699-948c-cfa72ba94f86',
18 | token : 'AEYGF7K0DM1X',
19 | domain : 'domain.com'
20 | };
21 | ```
22 |
23 | The following pattern is not considered a warning:
24 |
25 | ```js
26 | const ip = config.ip;
27 | const uuid = config.uuid;
28 | const token = config.token;
29 | const domain = config.domain
30 |
31 | const object = {
32 | ip: config.ip,
33 | uuid: config.uuid,
34 | token: config.token,
35 | domain: config.domain
36 | };
37 | ```
38 |
39 | ## Rule Options
40 |
41 | This rule can take arguments to forbid and exclude some configuration data.
42 |
43 | ```
44 | "more/no-hardcoded-configuration-data": [
45 | ,
46 | {
47 | forbidContaining : ,
48 | excludeContaining :
49 | }
50 | ]
51 | ```
52 |
53 | * `enabled` : for enabling the rule. 0=off, 1=warn, 2=error.
54 | * `forbidContaining` : optional array of stings for forbid.
55 | * `excludeContaining` : optional array of stings for exlude.
56 |
57 | The default configuration is:
58 |
59 | ```
60 | {
61 | forbidContaining : ["ipAddress", "UUID", "alphanumericToken", "domainName"],
62 | excludeContaining : ['facebook', 'google', 'yandex']
63 | }
64 | ```
--------------------------------------------------------------------------------
/lib/rules/no-window.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = function (context) {
3 | const DEFAULT = ['postMessage', 'open', 'addEventListener', 'removeEventListener'];
4 | const exclude = context.options[0] && context.options[0].exclude || DEFAULT;
5 |
6 | function report(node) {
7 | context.report({
8 | node,
9 | message: 'Avoid using window'
10 | });
11 | }
12 |
13 | function isWindow(name) {
14 | return name === 'window';
15 | }
16 |
17 | function checkExclude(name) {
18 | return exclude.indexOf(name) === -1;
19 | }
20 |
21 | return {
22 | MemberExpression(node) {
23 | if (!node.object) return;
24 |
25 | const objectName = node.object.name || (node.object.expression && node.object.expression.name);
26 |
27 | if (!isWindow(objectName)) return;
28 |
29 | const propName = node.property.name;
30 |
31 | if (checkExclude(propName)) {
32 | report(node);
33 | }
34 | },
35 |
36 | VariableDeclarator(node) {
37 | if (!node.init) return;
38 |
39 | const initName = node.init.name;
40 |
41 | if (!isWindow(initName)) return;
42 |
43 | for (const property of node.id.properties) {
44 | const propName = property.key.name;
45 |
46 | if (checkExclude(propName)) {
47 | report(node.init);
48 | }
49 | }
50 | }
51 |
52 | };
53 | };
54 |
55 | module.exports.schema = [ {
56 | type: 'object',
57 | properties: {
58 | exclude: {
59 | type: 'array',
60 | items: {
61 | type: 'string'
62 | }
63 | }
64 | },
65 | additionalProperties: false
66 | } ];
67 |
--------------------------------------------------------------------------------
/lib/rules/classbody-starts-with-newline.js:
--------------------------------------------------------------------------------
1 | // No empty string after class definition (before varructor) (with fixer)
2 |
3 | module.exports = function (context) {
4 | const sourceCode = context.getSourceCode();
5 |
6 | const mode = context.options[0] || 'never';
7 |
8 | function checkForNewLine(node) {
9 | const nextNode = node.body[0];
10 |
11 | if (!nextNode) {
12 | return;
13 | }
14 |
15 | if (nextNode.type !== 'ClassProperty' && nextNode.type !== 'MethodDefinition') {
16 | return;
17 | }
18 |
19 | const nextLineNum = node.loc.start.line + 1;
20 | let bodyStartsWithNewLine = nextNode.loc.start.line > nextLineNum;
21 |
22 |
23 | const comments = sourceCode.getComments(nextNode).leading;
24 |
25 | if (comments.length && comments[0].loc.start.line === nextLineNum) {
26 | // It is not a new line it is a comment
27 | bodyStartsWithNewLine = false;
28 | }
29 |
30 |
31 | if (mode === 'never' && bodyStartsWithNewLine) {
32 | context.report({
33 | node,
34 | fix: fixer => {
35 | return fixer.replaceTextRange([node.start + 1, nextNode.start - nextNode.loc.start.column], '\n');
36 | },
37 | message: 'Do not start class body with a newline'
38 | });
39 | } else if (mode === 'always' && !bodyStartsWithNewLine) {
40 | context.report({
41 | node,
42 | loc: {
43 | start: { line: nextLineNum },
44 | end: { line: nextLineNum }
45 | },
46 | fix: fixer => fixer.insertTextAfterRange([0, node.start + 1], '\n'),
47 | message: 'Start class body with a newline'
48 | });
49 | }
50 | }
51 |
52 | return {
53 | ClassBody: checkForNewLine
54 | };
55 | };
56 |
57 | module.exports.schema = [
58 | {
59 | enum: ['never', 'always']
60 | }
61 | ];
62 |
--------------------------------------------------------------------------------
/lib/rules/no-hardcoded-configuration-data.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = context => {
3 | const REGEX = {
4 | ipAddress : /((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/i,
5 | UUID : /[0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{12}$/i,
6 | alphanumericToken : /(?=.*[a-z])(?=.*[0-9])(?=[a-z0-9]{12,})/i,
7 | domainName : /(?!:\/\/)([a-zA-Z0-9-_]+\.)*[a-zA-Z0-9][a-zA-Z0-9-_]+\.[a-zA-Z]{2,11}?/
8 | };
9 |
10 | const DEFAULT_FORBID = [ 'ipAddress', 'UUID', 'alphanumericToken', 'domainName' ];
11 | const DEFAULT_EXCLUDE = [ 'facebook', 'google', 'yandex' ];
12 |
13 | const forbid = context.options[0] && context.options[0].forbidContaining || DEFAULT_FORBID;
14 | const exclude = context.options[0] && context.options[0].excludeContaining || DEFAULT_EXCLUDE;
15 |
16 | function inspect(regex, node, value) {
17 | if (typeof value !== 'string') return;
18 |
19 | if (exclude.some(name => value.toLowerCase().includes(name.toLowerCase()))) return;
20 |
21 | if (REGEX[regex].test(value)) {
22 | return context.report({
23 | node,
24 | message : 'Do not use hardcoded configuration data'
25 | });
26 | }
27 | }
28 |
29 | return {
30 | VariableDeclarator({ init }) {
31 | if (init && init.type === 'Literal') {
32 | return forbid.some(rule => inspect(rule, init, init.value));
33 | }
34 | },
35 |
36 | Property({ value }) {
37 | if (value && value.type === 'Literal') {
38 | return forbid.some(rule => inspect(rule, value, value.value));
39 | }
40 | }
41 | };
42 | };
43 |
44 | module.exports.schema = [ {
45 | type : 'object',
46 | properties : {
47 | forbidContaining : {
48 | type : 'array',
49 | items : {
50 | type : 'string'
51 | }
52 | },
53 | excludeContaining : {
54 | type : 'array',
55 | items : {
56 | type : 'string'
57 | }
58 | }
59 | },
60 | additionalProperties : false
61 | } ];
62 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # eslint-plugin-more - extra rules for Eslint
2 |
3 | [](https://npmjs.org/package/eslint-plugin-more)
4 |
5 | Eslint is a very great tool and it already has tons of rules. Eslint allows us to reduce amount of time required for code review. But, of course, eslint cannot cover all the issues. So, we do in the following way, if during code review we see, that we comment on thing that is possible to check automatically, then we create new eslint rule.
6 |
7 | Some of this rules will go to upstream after proving them in our codebase.
8 |
9 | # Installation
10 |
11 | Install [ESLint](https://www.github.com/eslint/eslint) either locally or globally.
12 |
13 | ```sh
14 | $ npm install eslint
15 | ```
16 |
17 | If you installed `ESLint` globally, you have to install this plugin globally too. Otherwise, install it locally.
18 |
19 | ```sh
20 | $ npm install eslint-plugin-more
21 | ```
22 |
23 | # Configuration
24 |
25 | Add `plugins` section and specify ESLint-plugin-more as a plugin.
26 |
27 | ```json
28 | {
29 | "plugins": [
30 | "more"
31 | ]
32 | }
33 | ```
34 |
35 | Finally, enable all of the rules that you would like to use. For example:
36 |
37 | ```json
38 | "rules": {
39 | "more/no-void-map": 2,
40 | "more/no-c-like-loops": 2,
41 | "more/prefer-includes": 2,
42 | "more/no-then": 2,
43 | "more/no-window": 2,
44 | "more/no-numeric-endings-for-variables": 2,
45 | "more/no-filter-instead-of-find": 2,
46 | "more/force-native-methods": 2,
47 | "more/no-duplicated-chains": 2,
48 | "more/classbody-starts-with-newline": [2, 'never'],
49 | "more/no-hardcoded-password": 2,
50 | "more/no-hardcoded-configuration-data": 2
51 | }
52 | ```
53 |
54 | # Supported rules
55 | * [no-void-map](docs/no-void-map.md): Prohibits the use of array.map without variable or property
56 | * [no-c-like-loops](docs/no-c-like-loops.md): Prohibits the use of 'For loop' with ++ or +=
57 | * [prefer-includes](docs/prefer-includes.md): Prohibits the use of comparison array.indexOf() == -1 and ask to use 'includes' instead
58 | * [no-then](docs/no-then.md): Forces the use of async / await instead of then
59 | * [no-window](docs/no-window.md): Prohibits the usage of `window` global
60 | * [force-native-methods](docs/force-native-methods.md): - Forces the use of native methods instead of lodash/underscore
61 | * [no-filter-instead-of-find](docs/no-filter-instead-of-find.md): - Prohibits using Array.prototype.filter to find one element and asks to use 'find' instead.
62 | * [no-numeric-endings-for-variables](docs/no-numeric-endings-for-variables.md): - Prohibits the use of variables that end in numerics.
63 | * [no-duplicated-chains](docs/no-duplicated-chains.md): - Prohibits the duplication of long chains like `this.props.user.name`
64 | * [classbody-starts-with-newline](docs/classbody-starts-with-newline.md) - Prohibits an empty line at the beggining of a class body
65 | * [no-hardcoded-password](docs/no-hardcoded-password.md) - Prohibits using hardcoded passwords.
66 | * [no-hardcoded-configuration-data](docs/no-hardcoded-configuration-data.md) - Prohibits using hardcoded configuration data.
67 |
68 | ## Author
69 | WebbyLab (https://webbylab.com)
70 |
--------------------------------------------------------------------------------
/lib/rules/no-duplicated-chains.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | module.exports = function (context) {
4 | const longChains = [];
5 | let isInsideCallExpression = false;
6 |
7 | // to avoid subexpressions output
8 | let memberExpressionsDepth = 0;
9 |
10 | function startFunction() {
11 | longChains.push({});
12 | }
13 |
14 | function endFunction() {
15 | longChains.pop();
16 | }
17 |
18 | function incrementChainCount(chain) {
19 | if (longChains.length) {
20 | if (longChains[longChains.length - 1][chain]) {
21 | longChains[longChains.length - 1][chain]++;
22 | } else {
23 | longChains[longChains.length - 1][chain] = 1;
24 | }
25 |
26 | return longChains[longChains.length - 1][chain];
27 | }
28 |
29 | return 0;
30 | }
31 |
32 | function pauseChecking() {
33 | isInsideCallExpression = true;
34 | }
35 |
36 | function resumeChecking() {
37 | isInsideCallExpression = false;
38 | }
39 |
40 | function checkLongChainForDuplication(node) {
41 | memberExpressionsDepth++;
42 |
43 | if (memberExpressionsDepth > 1
44 | || isInsideCallExpression
45 | || !node.object
46 | || node.object.type !== 'MemberExpression'
47 | || node.computed
48 | ) {
49 | return;
50 | }
51 |
52 |
53 | const pathParts = [];
54 | let parent = node;
55 | let depth = 0;
56 |
57 | while (parent) {
58 | if (parent.computed) {
59 | break;
60 | }
61 |
62 | if (parent.type === 'ThisExpression') {
63 | pathParts.push('this');
64 | } else if (parent.type === 'MemberExpression') {
65 | pathParts.push(parent.property.name);
66 | } else if (parent.type === 'Identifier') {
67 | pathParts.push(parent.name);
68 | } else {
69 | break;
70 | }
71 |
72 | depth++;
73 | parent = parent.object;
74 | }
75 |
76 |
77 | if (depth <= 2) {
78 | return;
79 | }
80 |
81 | const path = pathParts.reverse().join('.');
82 | const chainCount = incrementChainCount(path);
83 |
84 | if (chainCount >= 2) {
85 | context.report({
86 | node,
87 | message: `Do not duplicate long chains. Assign "${ path }" to a variable or destruct it.`
88 | });
89 | }
90 | }
91 |
92 | return {
93 | 'FunctionDeclaration': startFunction,
94 | 'FunctionExpression': startFunction,
95 | 'ArrowFunctionExpression': startFunction,
96 | 'FunctionDeclaration:exit': endFunction,
97 | 'FunctionExpression:exit': endFunction,
98 | 'ArrowFunctionExpression:exit': endFunction,
99 | 'MemberExpression': checkLongChainForDuplication,
100 | 'MemberExpression:exit'() {
101 | memberExpressionsDepth--;
102 | },
103 | 'CallExpression': pauseChecking,
104 | 'CallExpression:exit': resumeChecking
105 |
106 | };
107 | };
108 |
109 | module.exports.schema = [];
110 |
--------------------------------------------------------------------------------