lint-rules-alvin 1.0.4 → 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.
@@ -1,9 +1,9 @@
1
1
  import eslintPluginAstro from 'eslint-plugin-astro';
2
2
 
3
3
  /**
4
- * The `ESLint` Astro config.
4
+ * The ESLint Astro config. Extends `configs['flat/base']` and configures all rules.
5
5
  *
6
- * Extends `eslint-plugin-astro`'s `flat/base` config.
6
+ * @type {import('eslint').Linter.Config}
7
7
  */
8
8
  export const astro = [
9
9
  ...eslintPluginAstro.configs['flat/base'],
@@ -1,22 +1,37 @@
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 `ESLint` base config. Includes base rules, ignore files and directives.
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
- includeIgnoreFile(path.join(
10
- process.cwd(),
11
- '.gitignore'
12
- )),
14
+ includeIgnoreFile(
15
+ path.join(
16
+ process.cwd(),
17
+ '.gitignore'
18
+ )
19
+ ),
13
20
  globalIgnores([
14
21
  'eslint.config.js',
15
22
  'eslint.config.ts'
16
23
  ]),
17
24
  {
18
- name: 'base',
19
- files: [ '**/*.{ts,tsx}' ],
20
- rules: { 'no-undef': 'off' }
21
- }
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
22
37
  ];
@@ -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
- 'chain-first-on-newline': chainFirstOnNewlineRule,
19
- 'multiline-paren-newline': multilineParenNewlineRule
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
- { maxChain: 2 }
34
- ],
35
- 'custom/chain-first-on-newline': [
36
- 'error',
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 `ESLint` import config.
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 `ESLint` import config with a TS resolver.
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,
@@ -1,20 +1,23 @@
1
1
  import eslintPluginJsonc from 'eslint-plugin-jsonc';
2
2
 
3
3
  /**
4
- * The `ESLint` JSON config. Supports JSON, JSONc an JSON5.
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
- { // eslint-plugin-json
10
+ {
8
11
  name: 'eslint-plugin-json',
9
12
  files: [ '**/*.{json}' ],
10
13
  ...eslintPluginJsonc.configs['flat/recommended-with-json']
11
14
  },
12
- { // eslint-plugin-jsonc
15
+ {
13
16
  name: 'eslint-plugin-jsonc',
14
17
  files: [ '**/*.{jsonc}' ],
15
18
  ...eslintPluginJsonc.configs['flat/recommended-with-jsonc']
16
19
  },
17
- { // eslint-plugin-json5
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 `ESLint` markdown config.
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,8 +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
- plugins: { 'react-hooks': eslintPluginReactHooks },
7
- extends: [ 'react-hooks/recommended' ]
11
+ ...eslintPluginReactHooks
12
+ .configs
13
+ .flat
14
+ .recommended
8
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.base,
15
- languageOptions: { parserOptions: { projectService: true } },
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.loc.start.line < node.loc.end.line;
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 (node.name.loc.end.line === firstProp.loc.start.line) {
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
- [node.loc.start.line - 1];
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 (expression.loc.start.line === expression.loc.end.line) {
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.loc.end.line < firstTokenInExpression.loc.start.line;
46
- const hasNewlineAfter = lastTokenInExpression.loc.end.line < closingBrace.loc.start.line;
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: { expectedNewline: 'This call chain is too long and should be broken into multiple lines.' }
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 options = context.options[0] || {};
30
- const maxChain = options.maxChain || 2;
31
- const sourceCode = context.sourceCode;
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
- function isChainable(node) {
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
- if (!node)
36
- return false;
37
- return node.type === 'MemberExpression' || node.type === 'CallExpression';
57
+ outermostNode = parent;
58
+
59
+ } else {
60
+
61
+ break;
62
+
63
+ }
64
+
65
+ }
66
+ return outermostNode;
38
67
 
39
68
  }
40
69
 
41
- function check(node) {
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 members = [];
44
- let callCount = 0;
45
- let current = node;
80
+ const links = [];
81
+ let currentNode = node;
46
82
 
47
- while (isChainable(current)) {
83
+ while (currentNode.type === 'MemberExpression' || currentNode.type === 'CallExpression') {
48
84
 
49
- if (current.type === 'CallExpression') {
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
- callCount++;
52
- current = current.callee;
93
+ // We have a MemberExpression.
94
+ if (currentNode.computed) {
53
95
 
54
- } else { // MemberExpression
96
+ // A computed property (`[key]`) is part of the preceding link.
97
+ currentNode = currentNode.object;
55
98
 
56
- members.unshift(current);
57
- current = current.object;
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
- const root = current;
63
- const totalChainLength = members.length + 1;
108
+ return links;
64
109
 
65
- let iter = node;
66
- while(isChainable(iter)) {
110
+ }
67
111
 
68
- if (iter.type === 'CallExpression') {
112
+ function checkChain(node) {
69
113
 
70
- callCount++;
114
+ const outermostNode = getOutermostChainNode(node);
71
115
 
72
- }
73
- iter = iter.type === 'CallExpression'
74
- ? iter.callee
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
- const isMultiline = root.loc.start.line !== node.loc.end.line;
126
+ if (chainCount <= 1) {
80
127
 
81
- if (callCount > 0) {
128
+ return;
82
129
 
83
- if (!isMultiline && totalChainLength > maxChain) {
130
+ }
84
131
 
85
- context.report({
86
- node: node,
87
- messageId: 'expectedNewline',
88
- fix(fixer) {
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
- const fixes = [];
91
- members.forEach(
92
- (memberNode) => {
172
+ }
93
173
 
94
- const propertyNode = memberNode.property;
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
- return fixes;
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
- 'MemberExpression:exit'(node) {
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
- if (isChainable(node.callee)) {
222
+ // Only start the check from non-computed MemberExpressions to avoid redundant checks.
223
+ if (!node.computed) {
140
224
 
141
- check(node);
225
+ checkChain(node);
142
226
 
143
227
  }
144
228