lint-rules-alvin 1.0.5 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/eslint/configs/astro.js +2 -2
- package/eslint/configs/base.js +20 -2
- package/eslint/configs/custom.js +14 -10
- package/eslint/configs/importX.js +3 -1
- package/eslint/configs/importXTs.js +4 -2
- package/eslint/configs/json.js +7 -4
- package/eslint/configs/markdown.js +4 -1
- package/eslint/configs/reactHooks.js +9 -1
- package/eslint/configs/stylistic.js +7 -0
- package/eslint/configs/typescript.js +16 -5
- package/eslint/custom_rules/jsx-multiline-prop-newline.js +23 -4
- package/eslint/custom_rules/jsx-no-single-object-curly-newline.js +23 -3
- package/eslint/custom_rules/max-chain-per-line.js +162 -78
- package/eslint/custom_rules/multiline-array-accessor-newline.js +193 -0
- package/eslint/custom_rules/multiline-paren-newline.js +192 -14
- package/eslint/custom_rules/newline-between-imports.js +10 -4
- package/eslint/custom_rules/unnamed-imports-last.js +1 -3
- package/eslint/presets/astroReactTs.js +1 -1
- package/package.json +42 -7
- package/eslint/custom_rules/chain-first-on-newline.js +0 -174
package/eslint/configs/astro.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import eslintPluginAstro from 'eslint-plugin-astro';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* The
|
|
4
|
+
* The ESLint Astro config. Extends `configs['flat/base']` and configures all rules.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* @type {import('eslint').Linter.Config}
|
|
7
7
|
*/
|
|
8
8
|
export const astro = [
|
|
9
9
|
...eslintPluginAstro.configs['flat/base'],
|
package/eslint/configs/base.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { includeIgnoreFile } from '@eslint/compat';
|
|
3
|
+
import eslintJs from '@eslint/js';
|
|
3
4
|
import { globalIgnores } from 'eslint/config';
|
|
5
|
+
import globals from 'globals';
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
|
-
* The
|
|
8
|
+
* The ESLint base config. Includes base rules, ignore files and directives,
|
|
9
|
+
* and the recommended eslintJs rules.
|
|
10
|
+
*
|
|
11
|
+
* @type {import('eslint').Linter.Config}
|
|
7
12
|
*/
|
|
8
13
|
export const base = [
|
|
9
14
|
includeIgnoreFile(
|
|
@@ -15,5 +20,18 @@ export const base = [
|
|
|
15
20
|
globalIgnores([
|
|
16
21
|
'eslint.config.js',
|
|
17
22
|
'eslint.config.ts'
|
|
18
|
-
])
|
|
23
|
+
]),
|
|
24
|
+
{
|
|
25
|
+
name: 'base/env',
|
|
26
|
+
languageOptions: {
|
|
27
|
+
ecmaVersion: 2020,
|
|
28
|
+
sourceType: 'module',
|
|
29
|
+
globals: {
|
|
30
|
+
...globals.browser,
|
|
31
|
+
...globals.nodeBuiltin,
|
|
32
|
+
...globals.serviceworker
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
eslintJs.configs.recommended
|
|
19
37
|
];
|
package/eslint/configs/custom.js
CHANGED
|
@@ -3,9 +3,14 @@ import { unnamedImportsLastRule } from '../custom_rules/unnamed-imports-last.js'
|
|
|
3
3
|
import { jsxMultilinePropNewlineRule } from '../custom_rules/jsx-multiline-prop-newline.js';
|
|
4
4
|
import { jsxNoSingleObjectCurlyNewlineRule } from '../custom_rules/jsx-no-single-object-curly-newline.js';
|
|
5
5
|
import { maxChainPerLineRule } from '../custom_rules/max-chain-per-line.js';
|
|
6
|
-
import { chainFirstOnNewlineRule } from '../custom_rules/chain-first-on-newline.js';
|
|
7
6
|
import { multilineParenNewlineRule } from '../custom_rules/multiline-paren-newline.js';
|
|
7
|
+
import { multilineArrayAccessorNewlineRule } from '../custom_rules/multiline-array-accessor-newline.js';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Provides a config that creates a plugin and configures custom rules.
|
|
11
|
+
*
|
|
12
|
+
* @type {import('eslint').Linter.Config}
|
|
13
|
+
*/
|
|
9
14
|
export const custom = {
|
|
10
15
|
plugins: {
|
|
11
16
|
custom: {
|
|
@@ -15,8 +20,8 @@ export const custom = {
|
|
|
15
20
|
'jsx-multiline-prop-newline': jsxMultilinePropNewlineRule,
|
|
16
21
|
'jsx-no-single-object-curly-newline': jsxNoSingleObjectCurlyNewlineRule,
|
|
17
22
|
'max-chain-per-line': maxChainPerLineRule,
|
|
18
|
-
'
|
|
19
|
-
'multiline-
|
|
23
|
+
'multiline-paren-newline': multilineParenNewlineRule,
|
|
24
|
+
'multiline-array-accessor-newline': multilineArrayAccessorNewlineRule
|
|
20
25
|
}
|
|
21
26
|
}
|
|
22
27
|
},
|
|
@@ -30,16 +35,15 @@ export const custom = {
|
|
|
30
35
|
'custom/jsx-no-single-object-curly-newline': 'error',
|
|
31
36
|
'custom/max-chain-per-line': [
|
|
32
37
|
'error',
|
|
33
|
-
{
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
'require'
|
|
38
|
+
{
|
|
39
|
+
maxChain: 2,
|
|
40
|
+
enforceSingleLine: true
|
|
41
|
+
}
|
|
38
42
|
],
|
|
39
43
|
'custom/multiline-paren-newline': [
|
|
40
44
|
'error',
|
|
41
45
|
{ singleArgument: true }
|
|
42
|
-
]
|
|
46
|
+
],
|
|
47
|
+
'custom/multiline-array-accessor-newline': 'error'
|
|
43
48
|
}
|
|
44
49
|
};
|
|
45
|
-
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { importX as eslintPluginImportX } from 'eslint-plugin-import-x';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* The
|
|
4
|
+
* The ESLint import config. Configures all rules.
|
|
5
|
+
*
|
|
6
|
+
* @type {import('eslint').Linter.Config}
|
|
5
7
|
*/
|
|
6
8
|
export const importX = {
|
|
7
9
|
name: 'eslint-plugin-import-x',
|
|
@@ -2,9 +2,11 @@ import { importX as eslintPluginImportX } from 'eslint-plugin-import-x';
|
|
|
2
2
|
import { importX } from './index.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* The
|
|
5
|
+
* The ESLint import config with a TS resolver.
|
|
6
|
+
* Extends `../importX` and `flatConfigs.typescript` config.
|
|
7
|
+
*
|
|
8
|
+
* @type {import('eslint').Linter.Config}
|
|
6
9
|
*
|
|
7
|
-
* Extends `eslint-plugin-import-x`'s `typescript` flat config.
|
|
8
10
|
*/
|
|
9
11
|
export const importXTs = {
|
|
10
12
|
...importX,
|
package/eslint/configs/json.js
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import eslintPluginJsonc from 'eslint-plugin-jsonc';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* The
|
|
4
|
+
* The ESLint JSON config. Extends `flat/recommended-with-json`,
|
|
5
|
+
* `...with-jsonc` and `...with-json5` for the corresponding file types.
|
|
6
|
+
*
|
|
7
|
+
* @type {import('eslint').Linter.Config}
|
|
5
8
|
*/
|
|
6
9
|
export const json = [
|
|
7
|
-
{
|
|
10
|
+
{
|
|
8
11
|
name: 'eslint-plugin-json',
|
|
9
12
|
files: [ '**/*.{json}' ],
|
|
10
13
|
...eslintPluginJsonc.configs['flat/recommended-with-json']
|
|
11
14
|
},
|
|
12
|
-
{
|
|
15
|
+
{
|
|
13
16
|
name: 'eslint-plugin-jsonc',
|
|
14
17
|
files: [ '**/*.{jsonc}' ],
|
|
15
18
|
...eslintPluginJsonc.configs['flat/recommended-with-jsonc']
|
|
16
19
|
},
|
|
17
|
-
{
|
|
20
|
+
{
|
|
18
21
|
name: 'eslint-plugin-json5',
|
|
19
22
|
files: [ '**/*.{json5}' ],
|
|
20
23
|
...eslintPluginJsonc.configs['flat/recommended-with-json5']
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import eslintPluginMarkdown from 'eslint-plugin-markdown';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* The
|
|
4
|
+
* The ESLint markdown config. Extends `configs.recommended` only for `md` files.
|
|
5
|
+
*
|
|
6
|
+
* @type {import('eslint').Linter.Config}
|
|
7
|
+
*
|
|
5
8
|
*/
|
|
6
9
|
export const markdown = {
|
|
7
10
|
name: 'eslint-plugin-markdown',
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import eslintPluginReactHooks from 'eslint-plugin-react-hooks';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* The ESLint `react-hooks` config. Extends `configs.flat.recommended` only for `jsx` and `tsx` files.
|
|
5
|
+
*
|
|
6
|
+
* @type {import('eslint').Linter.Config}
|
|
7
|
+
*/
|
|
3
8
|
export const reactHooks = {
|
|
4
9
|
name: 'eslint-plugin-react-hooks',
|
|
5
10
|
files: [ '**/*.{jsx,tsx}' ],
|
|
6
|
-
...eslintPluginReactHooks
|
|
11
|
+
...eslintPluginReactHooks
|
|
12
|
+
.configs
|
|
13
|
+
.flat
|
|
14
|
+
.recommended
|
|
7
15
|
};
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
// eslint-disable-next-line import-x/no-deprecated, import-x/namespace, import-x/default
|
|
1
2
|
import eslintPluginStylistic from '@stylistic/eslint-plugin';
|
|
2
3
|
|
|
4
|
+
/**
|
|
5
|
+
* The ESLint `stylistic` config. Extends `configs.recommended` and overrides all rules.
|
|
6
|
+
*
|
|
7
|
+
* @type {import('eslint').Linter.Config}
|
|
8
|
+
*/
|
|
3
9
|
export const stylistic = {
|
|
4
10
|
name: 'eslint-plugin-stylistic',
|
|
5
11
|
...eslintPluginStylistic.configs.recommended,
|
|
@@ -309,6 +315,7 @@ export const stylistic = {
|
|
|
309
315
|
'all',
|
|
310
316
|
{
|
|
311
317
|
ignoreJSX: 'multi-line',
|
|
318
|
+
nestedBinaryExpressions: false,
|
|
312
319
|
ignoredNodes: [
|
|
313
320
|
|
|
314
321
|
// Arrow function ternaries
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import eslintTs from 'typescript-eslint';
|
|
1
|
+
import eslintTs from '@typescript-eslint/eslint-plugin';
|
|
2
|
+
import eslintTsParser from '@typescript-eslint/parser';
|
|
2
3
|
|
|
3
4
|
/**
|
|
5
|
+
* The ESLint `typescript` config. Extends `configs.base`,
|
|
6
|
+
* enables `languageOptions.parserOptions.projectService` and configures all rules,
|
|
7
|
+
* only for `ts`, `tsx`, `mts` and `cts` files.
|
|
8
|
+
*
|
|
4
9
|
* @type {import('eslint').Linter.Config}
|
|
5
10
|
*/
|
|
6
11
|
export const typescript = {
|
|
@@ -11,8 +16,12 @@ export const typescript = {
|
|
|
11
16
|
'**/*.mts',
|
|
12
17
|
'**/*.cts'
|
|
13
18
|
],
|
|
14
|
-
...eslintTs.configs
|
|
15
|
-
languageOptions: {
|
|
19
|
+
...eslintTs.configs['flat/base'],
|
|
20
|
+
languageOptions: {
|
|
21
|
+
parser: eslintTsParser,
|
|
22
|
+
parserOptions: { projectService: true },
|
|
23
|
+
sourceType: 'module'
|
|
24
|
+
},
|
|
16
25
|
rules: {
|
|
17
26
|
'@typescript-eslint/array-type': [
|
|
18
27
|
'error',
|
|
@@ -458,7 +467,6 @@ export const typescript = {
|
|
|
458
467
|
'always'
|
|
459
468
|
],
|
|
460
469
|
'@typescript-eslint/sort-type-constituents': 'off', // Deprecated in favor of sort-intersection-types, etc.
|
|
461
|
-
'@typescript-eslint/strict-boolean-expressions': 'off',
|
|
462
470
|
'@typescript-eslint/strict-boolean-expressions': [
|
|
463
471
|
'error',
|
|
464
472
|
{
|
|
@@ -498,7 +506,10 @@ export const typescript = {
|
|
|
498
506
|
ignoreOverloadsWithDifferentJSDoc: true
|
|
499
507
|
}
|
|
500
508
|
],
|
|
501
|
-
'@typescript-eslint/use-unknown-in-catch-callback-variable': 'off'
|
|
509
|
+
'@typescript-eslint/use-unknown-in-catch-callback-variable': 'off',
|
|
510
|
+
|
|
511
|
+
// Other base ESLint overrides
|
|
512
|
+
'no-undef': 'off'
|
|
502
513
|
|
|
503
514
|
}
|
|
504
515
|
};
|
|
@@ -20,7 +20,13 @@ export const jsxMultilinePropNewlineRule = {
|
|
|
20
20
|
|
|
21
21
|
function isMultiline(node) {
|
|
22
22
|
|
|
23
|
-
return node && node.loc && node
|
|
23
|
+
return node && node.loc && node
|
|
24
|
+
.loc
|
|
25
|
+
.start
|
|
26
|
+
.line < node
|
|
27
|
+
.loc
|
|
28
|
+
.end
|
|
29
|
+
.line;
|
|
24
30
|
|
|
25
31
|
}
|
|
26
32
|
|
|
@@ -63,7 +69,16 @@ export const jsxMultilinePropNewlineRule = {
|
|
|
63
69
|
const firstProp = node.attributes[0];
|
|
64
70
|
|
|
65
71
|
// Check if the tag name and the first prop are on the same line.
|
|
66
|
-
if (
|
|
72
|
+
if (
|
|
73
|
+
node
|
|
74
|
+
.name
|
|
75
|
+
.loc
|
|
76
|
+
.end
|
|
77
|
+
.line === firstProp
|
|
78
|
+
.loc
|
|
79
|
+
.start
|
|
80
|
+
.line
|
|
81
|
+
) {
|
|
67
82
|
|
|
68
83
|
context.report({
|
|
69
84
|
node: firstProp,
|
|
@@ -72,8 +87,12 @@ export const jsxMultilinePropNewlineRule = {
|
|
|
72
87
|
|
|
73
88
|
// Get indentation of the line with the opening tag.
|
|
74
89
|
const line = sourceCode
|
|
75
|
-
.getLines()
|
|
76
|
-
|
|
90
|
+
.getLines()[
|
|
91
|
+
node
|
|
92
|
+
.loc
|
|
93
|
+
.start
|
|
94
|
+
.line - 1
|
|
95
|
+
];
|
|
77
96
|
const baseIndentMatch = line.match(/^\s*/);
|
|
78
97
|
const baseIndent = baseIndentMatch
|
|
79
98
|
? baseIndentMatch[0]
|
|
@@ -31,7 +31,15 @@ export const jsxNoSingleObjectCurlyNewlineRule = {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// If the expression itself isn't multiline, there are no newlines to collapse.
|
|
34
|
-
if (
|
|
34
|
+
if (
|
|
35
|
+
expression
|
|
36
|
+
.loc
|
|
37
|
+
.start
|
|
38
|
+
.line === expression
|
|
39
|
+
.loc
|
|
40
|
+
.end
|
|
41
|
+
.line
|
|
42
|
+
) {
|
|
35
43
|
|
|
36
44
|
return;
|
|
37
45
|
|
|
@@ -42,8 +50,20 @@ export const jsxNoSingleObjectCurlyNewlineRule = {
|
|
|
42
50
|
const firstTokenInExpression = sourceCode.getFirstToken(expression);
|
|
43
51
|
const lastTokenInExpression = sourceCode.getLastToken(expression);
|
|
44
52
|
|
|
45
|
-
const hasNewlineBefore = openingBrace
|
|
46
|
-
|
|
53
|
+
const hasNewlineBefore = openingBrace
|
|
54
|
+
.loc
|
|
55
|
+
.end
|
|
56
|
+
.line < firstTokenInExpression
|
|
57
|
+
.loc
|
|
58
|
+
.start
|
|
59
|
+
.line;
|
|
60
|
+
const hasNewlineAfter = lastTokenInExpression
|
|
61
|
+
.loc
|
|
62
|
+
.end
|
|
63
|
+
.line < closingBrace
|
|
64
|
+
.loc
|
|
65
|
+
.start
|
|
66
|
+
.line;
|
|
47
67
|
|
|
48
68
|
if (hasNewlineBefore || hasNewlineAfter) {
|
|
49
69
|
|
|
@@ -5,8 +5,7 @@ export const maxChainPerLineRule = {
|
|
|
5
5
|
meta: {
|
|
6
6
|
type: 'layout',
|
|
7
7
|
docs: {
|
|
8
|
-
|
|
9
|
-
description: 'Require a newline after each item in a chain if it\'s too long and contains a call.',
|
|
8
|
+
description: 'Require a newline after each item in a chain if it\'s too long.',
|
|
10
9
|
category: 'Stylistic Issues'
|
|
11
10
|
},
|
|
12
11
|
fixable: 'whitespace',
|
|
@@ -16,102 +15,200 @@ export const maxChainPerLineRule = {
|
|
|
16
15
|
properties: {
|
|
17
16
|
maxChain: {
|
|
18
17
|
type: 'integer',
|
|
19
|
-
minimum: 1
|
|
18
|
+
minimum: 1,
|
|
19
|
+
default: 2
|
|
20
|
+
},
|
|
21
|
+
enforceSingleLine: {
|
|
22
|
+
type: 'boolean',
|
|
23
|
+
default: false
|
|
20
24
|
}
|
|
21
25
|
},
|
|
22
26
|
additionalProperties: false
|
|
23
27
|
}
|
|
24
28
|
],
|
|
25
|
-
messages: {
|
|
29
|
+
messages: {
|
|
30
|
+
expectedNewline: 'This call chain is too long and should be broken into multiple lines.',
|
|
31
|
+
noNewline: 'Unexpected newline in chain.'
|
|
32
|
+
}
|
|
26
33
|
},
|
|
27
34
|
create(context) {
|
|
28
35
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
const {
|
|
37
|
+
maxChain = 2, enforceSingleLine = false
|
|
38
|
+
} = context.options[0] || {};
|
|
39
|
+
const sourceCode = context.getSourceCode();
|
|
40
|
+
const processedChains = new Set();
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Traverses the AST from a given node upwards to find the outermost
|
|
44
|
+
* node of a continuous chain.
|
|
45
|
+
*/
|
|
46
|
+
function getOutermostChainNode(node) {
|
|
47
|
+
|
|
48
|
+
let outermostNode = node;
|
|
49
|
+
while (outermostNode.parent) {
|
|
32
50
|
|
|
33
|
-
|
|
51
|
+
const parent = outermostNode.parent;
|
|
52
|
+
if (
|
|
53
|
+
(parent.type === 'MemberExpression' && parent.object === outermostNode)
|
|
54
|
+
|| (parent.type === 'CallExpression' && parent.callee === outermostNode)
|
|
55
|
+
) {
|
|
34
56
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
57
|
+
outermostNode = parent;
|
|
58
|
+
|
|
59
|
+
} else {
|
|
60
|
+
|
|
61
|
+
break;
|
|
62
|
+
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
}
|
|
66
|
+
return outermostNode;
|
|
38
67
|
|
|
39
68
|
}
|
|
40
69
|
|
|
41
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Deconstructs a chain into an array of its "links".
|
|
72
|
+
* A "link" is defined as a non-computed property access (`.prop`).
|
|
73
|
+
* Any subsequent CallExpressions or computed accesses (`[key]`) are
|
|
74
|
+
* considered part of that same link.
|
|
75
|
+
* @param {import('estree').Node} node The outermost node of the chain.
|
|
76
|
+
* @returns {import('estree').MemberExpression[]} An array of MemberExpression nodes that start each link.
|
|
77
|
+
*/
|
|
78
|
+
function getChainLinks(node) {
|
|
42
79
|
|
|
43
|
-
const
|
|
44
|
-
let
|
|
45
|
-
let current = node;
|
|
80
|
+
const links = [];
|
|
81
|
+
let currentNode = node;
|
|
46
82
|
|
|
47
|
-
while (
|
|
83
|
+
while (currentNode.type === 'MemberExpression' || currentNode.type === 'CallExpression') {
|
|
48
84
|
|
|
49
|
-
if (
|
|
85
|
+
if (currentNode.type === 'CallExpression') {
|
|
86
|
+
|
|
87
|
+
// A call is part of the preceding link, so we just traverse through it.
|
|
88
|
+
currentNode = currentNode.callee;
|
|
89
|
+
continue;
|
|
90
|
+
|
|
91
|
+
}
|
|
50
92
|
|
|
51
|
-
|
|
52
|
-
|
|
93
|
+
// We have a MemberExpression.
|
|
94
|
+
if (currentNode.computed) {
|
|
53
95
|
|
|
54
|
-
|
|
96
|
+
// A computed property (`[key]`) is part of the preceding link.
|
|
97
|
+
currentNode = currentNode.object;
|
|
55
98
|
|
|
56
|
-
|
|
57
|
-
|
|
99
|
+
} else {
|
|
100
|
+
|
|
101
|
+
// A non-computed property (`.prop`) is a new link.
|
|
102
|
+
links.unshift(currentNode);
|
|
103
|
+
currentNode = currentNode.object;
|
|
58
104
|
|
|
59
105
|
}
|
|
60
106
|
|
|
61
107
|
}
|
|
62
|
-
|
|
63
|
-
const totalChainLength = members.length + 1;
|
|
108
|
+
return links;
|
|
64
109
|
|
|
65
|
-
|
|
66
|
-
while(isChainable(iter)) {
|
|
110
|
+
}
|
|
67
111
|
|
|
68
|
-
|
|
112
|
+
function checkChain(node) {
|
|
69
113
|
|
|
70
|
-
|
|
114
|
+
const outermostNode = getOutermostChainNode(node);
|
|
71
115
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
: iter.object;
|
|
116
|
+
if (processedChains.has(outermostNode)) {
|
|
117
|
+
|
|
118
|
+
return;
|
|
76
119
|
|
|
77
120
|
}
|
|
121
|
+
processedChains.add(outermostNode);
|
|
122
|
+
|
|
123
|
+
const links = getChainLinks(outermostNode);
|
|
124
|
+
const chainCount = links.length;
|
|
78
125
|
|
|
79
|
-
|
|
126
|
+
if (chainCount <= 1) {
|
|
80
127
|
|
|
81
|
-
|
|
128
|
+
return;
|
|
82
129
|
|
|
83
|
-
|
|
130
|
+
}
|
|
84
131
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
132
|
+
const baseIndent = sourceCode.lines[
|
|
133
|
+
outermostNode
|
|
134
|
+
.loc
|
|
135
|
+
.start
|
|
136
|
+
.line - 1
|
|
137
|
+
].match(/^\s*/)[0];
|
|
138
|
+
|
|
139
|
+
// --- Mode 1: Enforce newlines if chain is too long ---
|
|
140
|
+
if (chainCount > maxChain) {
|
|
141
|
+
|
|
142
|
+
for (const linkNode of links) {
|
|
143
|
+
|
|
144
|
+
// Find the dot separator for this link.
|
|
145
|
+
const separatorToken = sourceCode.getTokenBefore(
|
|
146
|
+
linkNode.property,
|
|
147
|
+
{ filter: (token) => token.value === '.' || token.value === '?.' }
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// We only care about the dot's position relative to the start of its own link.
|
|
151
|
+
const previousNode = linkNode.object;
|
|
152
|
+
if (
|
|
153
|
+
separatorToken
|
|
154
|
+
.loc
|
|
155
|
+
.start
|
|
156
|
+
.line === previousNode
|
|
157
|
+
.loc
|
|
158
|
+
.end
|
|
159
|
+
.line
|
|
160
|
+
) {
|
|
161
|
+
|
|
162
|
+
context.report({
|
|
163
|
+
node: linkNode.property,
|
|
164
|
+
loc: separatorToken.loc,
|
|
165
|
+
messageId: 'expectedNewline',
|
|
166
|
+
fix: (fixer) => fixer.insertTextBefore(
|
|
167
|
+
separatorToken,
|
|
168
|
+
`\n${baseIndent}`
|
|
169
|
+
)
|
|
170
|
+
});
|
|
89
171
|
|
|
90
|
-
|
|
91
|
-
members.forEach(
|
|
92
|
-
(memberNode) => {
|
|
172
|
+
}
|
|
93
173
|
|
|
94
|
-
|
|
95
|
-
const dotToken = sourceCode.getTokenBefore(propertyNode);
|
|
96
|
-
const rootLine = sourceCode
|
|
97
|
-
.getLines()
|
|
98
|
-
[root.loc.start.line - 1];
|
|
99
|
-
const indent = (rootLine
|
|
100
|
-
.match(/^\s*/)
|
|
101
|
-
[0] || '') + ' ';
|
|
102
|
-
fixes.push(
|
|
103
|
-
fixer.insertTextBefore(
|
|
104
|
-
dotToken,
|
|
105
|
-
'\n' + indent
|
|
106
|
-
)
|
|
107
|
-
);
|
|
174
|
+
}
|
|
108
175
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
176
|
+
} else if (enforceSingleLine) {
|
|
177
|
+
|
|
178
|
+
// --- Mode 2: Enforce single line if chain is short enough and option is enabled ---
|
|
179
|
+
|
|
180
|
+
for (const linkNode of links) {
|
|
181
|
+
|
|
182
|
+
const previousNode = linkNode.object;
|
|
183
|
+
const separatorToken = sourceCode.getTokenBefore(
|
|
184
|
+
linkNode.property,
|
|
185
|
+
{ filter: (token) => token.value === '.' || token.value === '?.' }
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
if (
|
|
189
|
+
separatorToken
|
|
190
|
+
.loc
|
|
191
|
+
.start
|
|
192
|
+
.line > previousNode
|
|
193
|
+
.loc
|
|
194
|
+
.end
|
|
195
|
+
.line
|
|
196
|
+
) {
|
|
197
|
+
|
|
198
|
+
context.report({
|
|
199
|
+
node: linkNode.property,
|
|
200
|
+
loc: separatorToken.loc,
|
|
201
|
+
messageId: 'noNewline',
|
|
202
|
+
fix: (fixer) => fixer.replaceTextRange(
|
|
203
|
+
[
|
|
204
|
+
previousNode.range[1],
|
|
205
|
+
separatorToken.range[0]
|
|
206
|
+
],
|
|
207
|
+
''
|
|
208
|
+
)
|
|
209
|
+
});
|
|
112
210
|
|
|
113
|
-
|
|
114
|
-
});
|
|
211
|
+
}
|
|
115
212
|
|
|
116
213
|
}
|
|
117
214
|
|
|
@@ -120,25 +217,12 @@ export const maxChainPerLineRule = {
|
|
|
120
217
|
}
|
|
121
218
|
|
|
122
219
|
return {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (node.parent.type === 'MemberExpression' && node.parent.object === node)
|
|
126
|
-
return;
|
|
127
|
-
if (node.parent.type === 'CallExpression' && node.parent.callee === node)
|
|
128
|
-
return;
|
|
129
|
-
check(node);
|
|
130
|
-
|
|
131
|
-
},
|
|
132
|
-
'CallExpression:exit'(node) {
|
|
133
|
-
|
|
134
|
-
if (node.parent.type === 'MemberExpression' && node.parent.object === node)
|
|
135
|
-
return;
|
|
136
|
-
if (node.parent.type === 'CallExpression' && node.parent.callee === node)
|
|
137
|
-
return;
|
|
220
|
+
MemberExpression(node) {
|
|
138
221
|
|
|
139
|
-
|
|
222
|
+
// Only start the check from non-computed MemberExpressions to avoid redundant checks.
|
|
223
|
+
if (!node.computed) {
|
|
140
224
|
|
|
141
|
-
|
|
225
|
+
checkChain(node);
|
|
142
226
|
|
|
143
227
|
}
|
|
144
228
|
|