@kikiutils/eslint-config 9.2.0 → 9.3.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/dist/base.d.ts +2 -4
- package/dist/base.d.ts.map +1 -1
- package/dist/base.js +14 -2
- package/dist/base.js.map +1 -1
- package/dist/internals/plugins.d.ts +11 -0
- package/dist/internals/plugins.d.ts.map +1 -0
- package/dist/internals/plugins.js +15 -0
- package/dist/internals/plugins.js.map +1 -0
- package/dist/internals/rules/compact-iteration-layout.d.ts +5 -0
- package/dist/internals/rules/compact-iteration-layout.d.ts.map +1 -0
- package/dist/internals/rules/compact-iteration-layout.js +35 -0
- package/dist/internals/rules/compact-iteration-layout.js.map +1 -0
- package/dist/internals/rules/consistent-condition-layout.d.ts +5 -0
- package/dist/internals/rules/consistent-condition-layout.d.ts.map +1 -0
- package/dist/internals/rules/consistent-condition-layout.js +86 -0
- package/dist/internals/rules/consistent-condition-layout.js.map +1 -0
- package/dist/internals/rules/consistent-parameter-layout.d.ts +5 -0
- package/dist/internals/rules/consistent-parameter-layout.d.ts.map +1 -0
- package/dist/internals/rules/consistent-parameter-layout.js +91 -0
- package/dist/internals/rules/consistent-parameter-layout.js.map +1 -0
- package/dist/internals/rules/statement-padding.d.ts +5 -0
- package/dist/internals/rules/statement-padding.d.ts.map +1 -0
- package/dist/internals/rules/statement-padding.js +60 -0
- package/dist/internals/rules/statement-padding.js.map +1 -0
- package/dist/internals/utils.d.ts +6 -0
- package/dist/internals/utils.d.ts.map +1 -0
- package/dist/internals/utils.js +11 -0
- package/dist/internals/utils.js.map +1 -0
- package/dist/style.d.ts +1 -3
- package/dist/style.d.ts.map +1 -1
- package/dist/vue.d.ts +1 -3
- package/dist/vue.d.ts.map +1 -1
- package/dist/vue.js +2 -0
- package/dist/vue.js.map +1 -1
- package/package.json +51 -21
- package/src/base.ts +20 -2
- package/src/internals/plugins.ts +13 -0
- package/src/internals/rules/compact-iteration-layout.ts +57 -0
- package/src/internals/rules/consistent-condition-layout.ts +130 -0
- package/src/internals/rules/consistent-parameter-layout.ts +163 -0
- package/src/internals/rules/statement-padding.ts +86 -0
- package/src/internals/utils.ts +32 -0
- package/src/vue.ts +2 -0
package/dist/vue.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { customPlugins } from "./internals/plugins.js";
|
|
1
2
|
import { createBaseRules } from "./base.js";
|
|
2
3
|
import promise from "eslint-plugin-promise";
|
|
3
4
|
import kikiutilsEslintPluginVue from "@kikiutils/eslint-plugin-vue";
|
|
@@ -9,6 +10,7 @@ function createVueConfig(environment = "node") {
|
|
|
9
10
|
plugins: {
|
|
10
11
|
"@kikiutils/vue": kikiutilsEslintPluginVue,
|
|
11
12
|
"better-tailwindcss": betteTailwindcss,
|
|
13
|
+
"kikiutils": customPlugins,
|
|
12
14
|
promise
|
|
13
15
|
},
|
|
14
16
|
rules: {
|
package/dist/vue.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vue.js","names":[],"sources":["../src/vue.ts"],"sourcesContent":["import type { TypedFlatConfigItem } from '@antfu/eslint-config';\nimport kikiutilsEslintPluginVue from '@kikiutils/eslint-plugin-vue';\nimport betteTailwindcss from 'eslint-plugin-better-tailwindcss';\n// @ts-expect-error No declare file.\nimport promise from 'eslint-plugin-promise';\n\nimport { createBaseRules } from './base';\n\nexport function createVueConfig(environment: 'bun' | 'node' = 'node'): TypedFlatConfigItem {\n return {\n files: ['**/*.vue'],\n plugins: {\n '@kikiutils/vue': kikiutilsEslintPluginVue,\n 'better-tailwindcss': betteTailwindcss,\n promise,\n },\n rules: {\n ...createBaseRules(environment),\n '@kikiutils/vue/attributes-order': [\n 'error',\n {\n alphabetical: true,\n alphabeticalEnhanced: true,\n },\n ],\n '@kikiutils/vue/class-hex-color-case': 'error',\n 'better-tailwindcss/enforce-consistent-class-order': 'error',\n 'better-tailwindcss/enforce-consistent-important-position': 'error',\n 'better-tailwindcss/enforce-consistent-variable-syntax': 'error',\n 'better-tailwindcss/enforce-consistent-variant-order': 'error',\n 'better-tailwindcss/enforce-shorthand-classes': 'error',\n 'better-tailwindcss/no-deprecated-classes': 'warn',\n 'better-tailwindcss/no-duplicate-classes': 'error',\n 'better-tailwindcss/no-unnecessary-whitespace': 'error',\n 'style/max-len': 'off',\n 'vue/attribute-hyphenation': 'error',\n 'vue/attributes-order': 'off',\n 'vue/block-order': [\n 'error',\n {\n order: [\n 'template',\n 'script',\n 'style',\n ],\n },\n ],\n 'vue/component-api-style': 'error',\n 'vue/define-emits-declaration': 'error',\n 'vue/define-props-declaration': 'error',\n 'vue/enforce-style-attribute': 'error',\n 'vue/html-closing-bracket-newline': 'error',\n 'vue/html-closing-bracket-spacing': 'error',\n 'vue/html-indent': [\n 'error',\n 4,\n ],\n 'vue/max-attributes-per-line': [\n 'error',\n {\n multiline: 1,\n singleline: 1,\n },\n ],\n 'vue/max-len': [\n 'warn',\n {\n code: 120,\n comments: 120,\n template: 120,\n },\n ],\n 'vue/no-dupe-keys': 'error',\n 'vue/no-lone-template': 'error',\n 'vue/no-multi-spaces': 'error',\n 'vue/no-multiple-objects-in-class': 'error',\n 'vue/no-root-v-if': 'error',\n 'vue/no-template-target-blank': 'error',\n 'vue/no-v-html': 'error',\n 'vue/prefer-define-options': 'error',\n 'vue/prefer-separate-static-class': 'error',\n 'vue/require-explicit-emits': 'error',\n 'vue/require-typed-ref': 'error',\n },\n };\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"vue.js","names":[],"sources":["../src/vue.ts"],"sourcesContent":["import type { TypedFlatConfigItem } from '@antfu/eslint-config';\nimport kikiutilsEslintPluginVue from '@kikiutils/eslint-plugin-vue';\nimport betteTailwindcss from 'eslint-plugin-better-tailwindcss';\n// @ts-expect-error No declare file.\nimport promise from 'eslint-plugin-promise';\n\nimport { createBaseRules } from './base';\nimport { customPlugins } from './internals/plugins';\n\nexport function createVueConfig(environment: 'bun' | 'node' = 'node'): TypedFlatConfigItem {\n return {\n files: ['**/*.vue'],\n plugins: {\n '@kikiutils/vue': kikiutilsEslintPluginVue,\n 'better-tailwindcss': betteTailwindcss,\n 'kikiutils': customPlugins,\n promise,\n },\n rules: {\n ...createBaseRules(environment),\n '@kikiutils/vue/attributes-order': [\n 'error',\n {\n alphabetical: true,\n alphabeticalEnhanced: true,\n },\n ],\n '@kikiutils/vue/class-hex-color-case': 'error',\n 'better-tailwindcss/enforce-consistent-class-order': 'error',\n 'better-tailwindcss/enforce-consistent-important-position': 'error',\n 'better-tailwindcss/enforce-consistent-variable-syntax': 'error',\n 'better-tailwindcss/enforce-consistent-variant-order': 'error',\n 'better-tailwindcss/enforce-shorthand-classes': 'error',\n 'better-tailwindcss/no-deprecated-classes': 'warn',\n 'better-tailwindcss/no-duplicate-classes': 'error',\n 'better-tailwindcss/no-unnecessary-whitespace': 'error',\n 'style/max-len': 'off',\n 'vue/attribute-hyphenation': 'error',\n 'vue/attributes-order': 'off',\n 'vue/block-order': [\n 'error',\n {\n order: [\n 'template',\n 'script',\n 'style',\n ],\n },\n ],\n 'vue/component-api-style': 'error',\n 'vue/define-emits-declaration': 'error',\n 'vue/define-props-declaration': 'error',\n 'vue/enforce-style-attribute': 'error',\n 'vue/html-closing-bracket-newline': 'error',\n 'vue/html-closing-bracket-spacing': 'error',\n 'vue/html-indent': [\n 'error',\n 4,\n ],\n 'vue/max-attributes-per-line': [\n 'error',\n {\n multiline: 1,\n singleline: 1,\n },\n ],\n 'vue/max-len': [\n 'warn',\n {\n code: 120,\n comments: 120,\n template: 120,\n },\n ],\n 'vue/no-dupe-keys': 'error',\n 'vue/no-lone-template': 'error',\n 'vue/no-multi-spaces': 'error',\n 'vue/no-multiple-objects-in-class': 'error',\n 'vue/no-root-v-if': 'error',\n 'vue/no-template-target-blank': 'error',\n 'vue/no-v-html': 'error',\n 'vue/prefer-define-options': 'error',\n 'vue/prefer-separate-static-class': 'error',\n 'vue/require-explicit-emits': 'error',\n 'vue/require-typed-ref': 'error',\n },\n };\n}\n"],"mappings":";;;;;;AASA,SAAgB,gBAAgB,cAA8B,QAA6B;CACvF,OAAO;EACH,OAAO,CAAC,UAAU;EAClB,SAAS;GACL,kBAAkB;GAClB,sBAAsB;GACtB,aAAa;GACb;EACJ;EACA,OAAO;GACH,GAAG,gBAAgB,WAAW;GAC9B,mCAAmC,CAC/B,SACA;IACI,cAAc;IACd,sBAAsB;GAC1B,CACJ;GACA,uCAAuC;GACvC,qDAAqD;GACrD,4DAA4D;GAC5D,yDAAyD;GACzD,uDAAuD;GACvD,gDAAgD;GAChD,4CAA4C;GAC5C,2CAA2C;GAC3C,gDAAgD;GAChD,iBAAiB;GACjB,6BAA6B;GAC7B,wBAAwB;GACxB,mBAAmB,CACf,SACA,EACI,OAAO;IACH;IACA;IACA;GACJ,EACJ,CACJ;GACA,2BAA2B;GAC3B,gCAAgC;GAChC,gCAAgC;GAChC,+BAA+B;GAC/B,oCAAoC;GACpC,oCAAoC;GACpC,mBAAmB,CACf,SACA,CACJ;GACA,+BAA+B,CAC3B,SACA;IACI,WAAW;IACX,YAAY;GAChB,CACJ;GACA,eAAe,CACX,QACA;IACI,MAAM;IACN,UAAU;IACV,UAAU;GACd,CACJ;GACA,oBAAoB;GACpB,wBAAwB;GACxB,uBAAuB;GACvB,oCAAoC;GACpC,oBAAoB;GACpB,gCAAgC;GAChC,iBAAiB;GACjB,6BAA6B;GAC7B,oCAAoC;GACpC,8BAA8B;GAC9B,yBAAyB;EAC7B;CACJ;AACJ"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kikiutils/eslint-config",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "9.
|
|
4
|
+
"version": "9.3.0",
|
|
5
5
|
"description": "Using antfu/eslint-config as a base for eslint-config, slightly modified the settings and added additional plugins, mainly for my own use.",
|
|
6
6
|
"author": "kiki-kanri",
|
|
7
7
|
"license": "MIT",
|
|
@@ -12,6 +12,36 @@
|
|
|
12
12
|
"keywords": [],
|
|
13
13
|
"sideEffects": false,
|
|
14
14
|
"exports": {
|
|
15
|
+
"./internals/plugins": {
|
|
16
|
+
"types": null,
|
|
17
|
+
"import": null,
|
|
18
|
+
"require": null
|
|
19
|
+
},
|
|
20
|
+
"./internals/rules/compact-iteration-layout": {
|
|
21
|
+
"types": null,
|
|
22
|
+
"import": null,
|
|
23
|
+
"require": null
|
|
24
|
+
},
|
|
25
|
+
"./internals/rules/consistent-condition-layout": {
|
|
26
|
+
"types": null,
|
|
27
|
+
"import": null,
|
|
28
|
+
"require": null
|
|
29
|
+
},
|
|
30
|
+
"./internals/rules/consistent-parameter-layout": {
|
|
31
|
+
"types": null,
|
|
32
|
+
"import": null,
|
|
33
|
+
"require": null
|
|
34
|
+
},
|
|
35
|
+
"./internals/rules/statement-padding": {
|
|
36
|
+
"types": null,
|
|
37
|
+
"import": null,
|
|
38
|
+
"require": null
|
|
39
|
+
},
|
|
40
|
+
"./internals/utils": {
|
|
41
|
+
"types": null,
|
|
42
|
+
"import": null,
|
|
43
|
+
"require": null
|
|
44
|
+
},
|
|
15
45
|
"./package.json": "./package.json",
|
|
16
46
|
"./*": "./dist/*.js"
|
|
17
47
|
},
|
|
@@ -36,16 +66,16 @@
|
|
|
36
66
|
"typecheck": "tsc -b --noEmit"
|
|
37
67
|
},
|
|
38
68
|
"peerDependencies": {
|
|
39
|
-
"@antfu/eslint-config": "^9.
|
|
40
|
-
"@eslint/css": "^
|
|
41
|
-
"@html-eslint/parser": ">=0.
|
|
69
|
+
"@antfu/eslint-config": "^9.5.1",
|
|
70
|
+
"@eslint/css": "^2.0.0",
|
|
71
|
+
"@html-eslint/parser": ">=0.65.0",
|
|
42
72
|
"@kikiutils/eslint-plugin-vue": "^10.10.0",
|
|
43
73
|
"eslint": ">=9.0.0",
|
|
44
|
-
"eslint-plugin-better-tailwindcss": "^4.
|
|
45
|
-
"eslint-plugin-format": ">=
|
|
46
|
-
"tailwind-csstree": "^0.
|
|
47
|
-
"tailwindcss": ">=4.
|
|
48
|
-
"vue-eslint-parser": "^10.4.
|
|
74
|
+
"eslint-plugin-better-tailwindcss": "^4.7.0",
|
|
75
|
+
"eslint-plugin-format": ">=2.0.1",
|
|
76
|
+
"tailwind-csstree": "^0.4.0",
|
|
77
|
+
"tailwindcss": ">=4.3.3",
|
|
78
|
+
"vue-eslint-parser": "^10.4.1"
|
|
49
79
|
},
|
|
50
80
|
"peerDependenciesMeta": {
|
|
51
81
|
"@eslint/css": {
|
|
@@ -80,20 +110,20 @@
|
|
|
80
110
|
"eslint-plugin-promise": "^7.3.0"
|
|
81
111
|
},
|
|
82
112
|
"devDependencies": {
|
|
83
|
-
"@antfu/eslint-config": "^9.1
|
|
84
|
-
"@kikiutils/eslint-
|
|
85
|
-
"@kikiutils/tsconfigs": "^
|
|
86
|
-
"@types/node": "^26.
|
|
87
|
-
"@vitest/coverage-v8": "^
|
|
113
|
+
"@antfu/eslint-config": "^9.5.1",
|
|
114
|
+
"@kikiutils/eslint-config": "^9.2.1",
|
|
115
|
+
"@kikiutils/tsconfigs": "^7.0.0",
|
|
116
|
+
"@types/node": "^26.6.4",
|
|
117
|
+
"@vitest/coverage-v8": "^5.0.3",
|
|
88
118
|
"changelogen": "^0.6.2",
|
|
89
|
-
"eslint": "^10.
|
|
90
|
-
"eslint-plugin-better-tailwindcss": "^4.
|
|
119
|
+
"eslint": "^10.12.0",
|
|
120
|
+
"eslint-plugin-better-tailwindcss": "^4.7.0",
|
|
91
121
|
"eslint-plugin-format": "^2.0.1",
|
|
92
|
-
"knip": "^6.
|
|
93
|
-
"publint": "^0.3.
|
|
94
|
-
"tsdown": "^0.
|
|
122
|
+
"knip": "^6.39.0",
|
|
123
|
+
"publint": "^0.3.25",
|
|
124
|
+
"tsdown": "^0.23.0",
|
|
95
125
|
"typescript": "^6.0.3",
|
|
96
|
-
"unplugin-unused": "^0.
|
|
97
|
-
"vitest": "^
|
|
126
|
+
"unplugin-unused": "^0.6.0",
|
|
127
|
+
"vitest": "^5.0.3"
|
|
98
128
|
}
|
|
99
129
|
}
|
package/src/base.ts
CHANGED
|
@@ -5,11 +5,15 @@ import type {
|
|
|
5
5
|
// @ts-expect-error No declare file.
|
|
6
6
|
import promise from 'eslint-plugin-promise';
|
|
7
7
|
|
|
8
|
+
import { customPlugins } from './internals/plugins';
|
|
9
|
+
|
|
10
|
+
// Constants/Variables
|
|
8
11
|
const basePerfectionistSortOptions = {
|
|
9
12
|
ignoreCase: false,
|
|
10
13
|
type: 'natural',
|
|
11
14
|
} as const;
|
|
12
15
|
|
|
16
|
+
// Functions
|
|
13
17
|
export function createBaseConfigs(environment: 'bun' | 'node' = 'node'): TypedFlatConfigItem[] {
|
|
14
18
|
return [
|
|
15
19
|
// All files
|
|
@@ -30,7 +34,10 @@ export function createBaseConfigs(environment: 'bun' | 'node' = 'node'): TypedFl
|
|
|
30
34
|
// JavaScript
|
|
31
35
|
{
|
|
32
36
|
files: ['**/*.{cjs,js,mjs}'],
|
|
33
|
-
plugins: {
|
|
37
|
+
plugins: {
|
|
38
|
+
kikiutils: customPlugins,
|
|
39
|
+
promise,
|
|
40
|
+
},
|
|
34
41
|
rules: {
|
|
35
42
|
...createBaseRules(environment),
|
|
36
43
|
'node/prefer-global/process': [
|
|
@@ -42,7 +49,10 @@ export function createBaseConfigs(environment: 'bun' | 'node' = 'node'): TypedFl
|
|
|
42
49
|
// TypeScript
|
|
43
50
|
{
|
|
44
51
|
files: ['**/*.{ts,tsx}'],
|
|
45
|
-
plugins: {
|
|
52
|
+
plugins: {
|
|
53
|
+
kikiutils: customPlugins,
|
|
54
|
+
promise,
|
|
55
|
+
},
|
|
46
56
|
rules: {
|
|
47
57
|
...createBaseRules(environment),
|
|
48
58
|
'node/prefer-global/process': [
|
|
@@ -95,10 +105,18 @@ export function createBaseRules(environment: 'bun' | 'node' = 'node'): Rules {
|
|
|
95
105
|
'antfu/curly': 'off',
|
|
96
106
|
'antfu/if-newline': 'off',
|
|
97
107
|
'antfu/no-top-level-await': 'off',
|
|
108
|
+
'arrow-body-style': [
|
|
109
|
+
'error',
|
|
110
|
+
'as-needed',
|
|
111
|
+
],
|
|
98
112
|
'curly': [
|
|
99
113
|
'error',
|
|
100
114
|
'multi-line',
|
|
101
115
|
],
|
|
116
|
+
'kikiutils/compact-iteration-layout': 'error',
|
|
117
|
+
'kikiutils/consistent-condition-layout': 'error',
|
|
118
|
+
'kikiutils/consistent-parameter-layout': 'error',
|
|
119
|
+
'kikiutils/statement-padding': 'error',
|
|
102
120
|
'max-classes-per-file': [
|
|
103
121
|
'error',
|
|
104
122
|
1,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { compactIterationLayout } from './rules/compact-iteration-layout';
|
|
2
|
+
import { consistentConditionLayout } from './rules/consistent-condition-layout';
|
|
3
|
+
import { consistentParameterLayout } from './rules/consistent-parameter-layout';
|
|
4
|
+
import { statementPadding } from './rules/statement-padding';
|
|
5
|
+
|
|
6
|
+
export const customPlugins = {
|
|
7
|
+
rules: {
|
|
8
|
+
'compact-iteration-layout': compactIterationLayout,
|
|
9
|
+
'consistent-condition-layout': consistentConditionLayout,
|
|
10
|
+
'consistent-parameter-layout': consistentParameterLayout,
|
|
11
|
+
'statement-padding': statementPadding,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Rule } from 'eslint';
|
|
2
|
+
|
|
3
|
+
type IterationNode = Extract<Rule.Node, { type: 'ForInStatement' | 'ForOfStatement' }>;
|
|
4
|
+
|
|
5
|
+
export const compactIterationLayout: Rule.RuleModule = {
|
|
6
|
+
create(context) {
|
|
7
|
+
const { sourceCode } = context;
|
|
8
|
+
|
|
9
|
+
function checkParentheses(node: IterationNode): void {
|
|
10
|
+
const openingParen = sourceCode.getFirstToken(node, (token) => token.value === '(')!;
|
|
11
|
+
const closingParen = sourceCode.getTokenBefore(node.body)!;
|
|
12
|
+
const next = sourceCode.getTokenAfter(openingParen)!;
|
|
13
|
+
const previous = sourceCode.getTokenBefore(closingParen)!;
|
|
14
|
+
const ranges: Array<[number, number]> = [];
|
|
15
|
+
|
|
16
|
+
if (
|
|
17
|
+
openingParen.loc.end.line !== next.loc.start.line
|
|
18
|
+
&& !sourceCode.commentsExistBetween(openingParen, next)
|
|
19
|
+
) {
|
|
20
|
+
ranges.push([
|
|
21
|
+
openingParen.range[1],
|
|
22
|
+
next.range[0],
|
|
23
|
+
]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (
|
|
27
|
+
previous.loc.end.line !== closingParen.loc.start.line
|
|
28
|
+
&& !sourceCode.commentsExistBetween(previous, closingParen)
|
|
29
|
+
) {
|
|
30
|
+
ranges.push([
|
|
31
|
+
previous.range[1],
|
|
32
|
+
closingParen.range[0],
|
|
33
|
+
]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (ranges.length) {
|
|
37
|
+
context.report({
|
|
38
|
+
fix: (fixer) => ranges.map((range) => fixer.replaceTextRange(range, '')),
|
|
39
|
+
messageId: 'compact',
|
|
40
|
+
node,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
ForInStatement: checkParentheses,
|
|
47
|
+
ForOfStatement: checkParentheses,
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
meta: {
|
|
51
|
+
docs: { description: 'Keep for-in and for-of parentheses beside their header contents.' },
|
|
52
|
+
fixable: 'whitespace',
|
|
53
|
+
messages: { compact: 'Keep iteration parentheses on the same lines as their adjacent header contents.' },
|
|
54
|
+
schema: [],
|
|
55
|
+
type: 'layout',
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AST,
|
|
3
|
+
Rule,
|
|
4
|
+
} from 'eslint';
|
|
5
|
+
|
|
6
|
+
import type { TokenStore } from '../utils';
|
|
7
|
+
import { defineScriptAndTemplateVisitors } from '../utils';
|
|
8
|
+
|
|
9
|
+
type LogicalNode = Extract<Rule.Node, { type: 'LogicalExpression' }>;
|
|
10
|
+
|
|
11
|
+
export const consistentConditionLayout: Rule.RuleModule = {
|
|
12
|
+
create(context) {
|
|
13
|
+
function createVisitor(tokenStore: TokenStore): Rule.RuleListener {
|
|
14
|
+
const checkedOpeningOffsets = new Set<number>();
|
|
15
|
+
|
|
16
|
+
function collectOperators(node: LogicalNode['left'], operators: AST.Token[]): void {
|
|
17
|
+
if (node.type !== 'LogicalExpression' || (node.operator !== '&&' && node.operator !== '||')) return;
|
|
18
|
+
const operator = tokenStore.getTokensBetween(node.left, node.right)
|
|
19
|
+
.find((token) => token.type === 'Punctuator' && token.value === node.operator)!;
|
|
20
|
+
|
|
21
|
+
function collectChild(child: LogicalNode['left']): void {
|
|
22
|
+
const before = tokenStore.getTokenBefore(child);
|
|
23
|
+
const after = tokenStore.getTokenAfter(child);
|
|
24
|
+
if (
|
|
25
|
+
before?.value === '('
|
|
26
|
+
&& after?.value === ')'
|
|
27
|
+
&& before.range[0] >= node.range![0]
|
|
28
|
+
&& after.range[1] <= node.range![1]
|
|
29
|
+
) return;
|
|
30
|
+
|
|
31
|
+
collectOperators(child, operators);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
collectChild(node.left);
|
|
35
|
+
operators.push(operator);
|
|
36
|
+
collectChild(node.right);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function checkParentheses(node: LogicalNode['left']): void {
|
|
40
|
+
let openingParen = tokenStore.getTokenBefore(node);
|
|
41
|
+
let closingParen = tokenStore.getTokenAfter(node);
|
|
42
|
+
let operators: AST.Token[] | undefined;
|
|
43
|
+
const newlineOffsets: number[] = [];
|
|
44
|
+
while (openingParen?.value === '(' && closingParen?.value === ')') {
|
|
45
|
+
if (
|
|
46
|
+
!checkedOpeningOffsets.has(openingParen.range[0])
|
|
47
|
+
&& openingParen.loc.start.line !== closingParen.loc.end.line
|
|
48
|
+
) {
|
|
49
|
+
checkedOpeningOffsets.add(openingParen.range[0]);
|
|
50
|
+
if (!operators) {
|
|
51
|
+
operators = [];
|
|
52
|
+
collectOperators(node, operators);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const next = tokenStore.getTokenAfter(openingParen)!;
|
|
56
|
+
if (openingParen.loc.end.line === next.loc.start.line) {
|
|
57
|
+
newlineOffsets.push(openingParen.range[1]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const operator of operators) {
|
|
61
|
+
if (tokenStore.getTokenBefore(operator)!.loc.end.line === operator.loc.start.line) {
|
|
62
|
+
newlineOffsets.push(operator.range[0]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (tokenStore.getTokenBefore(closingParen)!.loc.end.line === closingParen.loc.start.line) {
|
|
67
|
+
newlineOffsets.push(closingParen.range[0]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
openingParen = tokenStore.getTokenBefore(openingParen);
|
|
72
|
+
closingParen = tokenStore.getTokenAfter(closingParen);
|
|
73
|
+
// An outer pair contains one grouped item, not the inner logical chain.
|
|
74
|
+
operators = [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (newlineOffsets.length) {
|
|
78
|
+
context.report({
|
|
79
|
+
fix: (fixer) => newlineOffsets.map((offset) => fixer.insertTextBeforeRange(
|
|
80
|
+
[
|
|
81
|
+
offset,
|
|
82
|
+
offset,
|
|
83
|
+
],
|
|
84
|
+
'\n',
|
|
85
|
+
)),
|
|
86
|
+
messageId: 'newline',
|
|
87
|
+
node,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
BinaryExpression: (node) => {
|
|
94
|
+
if (
|
|
95
|
+
[
|
|
96
|
+
'!=',
|
|
97
|
+
'!==',
|
|
98
|
+
'<',
|
|
99
|
+
'<=',
|
|
100
|
+
'==',
|
|
101
|
+
'===',
|
|
102
|
+
'>',
|
|
103
|
+
'>=',
|
|
104
|
+
'in',
|
|
105
|
+
'instanceof',
|
|
106
|
+
].includes(node.operator)
|
|
107
|
+
) checkParentheses(node);
|
|
108
|
+
},
|
|
109
|
+
DoWhileStatement: (node) => checkParentheses(node.test),
|
|
110
|
+
IfStatement: (node) => checkParentheses(node.test),
|
|
111
|
+
LogicalExpression: (node) => {
|
|
112
|
+
if (node.operator === '&&' || node.operator === '||') checkParentheses(node);
|
|
113
|
+
},
|
|
114
|
+
UnaryExpression: (node) => {
|
|
115
|
+
if (node.operator === '!') checkParentheses(node);
|
|
116
|
+
},
|
|
117
|
+
WhileStatement: (node) => checkParentheses(node.test),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return defineScriptAndTemplateVisitors(context.sourceCode, createVisitor);
|
|
122
|
+
},
|
|
123
|
+
meta: {
|
|
124
|
+
docs: { description: 'Expand multiline parenthesized conditions one logical item per line.' },
|
|
125
|
+
fixable: 'whitespace',
|
|
126
|
+
messages: { newline: 'Put each logical item and both parentheses on separate lines in a multiline condition.' },
|
|
127
|
+
schema: [],
|
|
128
|
+
type: 'layout',
|
|
129
|
+
},
|
|
130
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { Rule } from 'eslint';
|
|
2
|
+
|
|
3
|
+
import type { TokenStore } from '../utils';
|
|
4
|
+
import { defineScriptAndTemplateVisitors } from '../utils';
|
|
5
|
+
|
|
6
|
+
type CallNode = Extract<Rule.Node, { type: 'CallExpression' | 'NewExpression' }> & { typeArguments?: Rule.Node };
|
|
7
|
+
type FunctionNode = Extract<
|
|
8
|
+
Rule.Node,
|
|
9
|
+
{ type: 'ArrowFunctionExpression' | 'FunctionDeclaration' | 'FunctionExpression' }
|
|
10
|
+
>;
|
|
11
|
+
|
|
12
|
+
type ListItem = CallNode['arguments'][number] | FunctionNode['params'][number];
|
|
13
|
+
|
|
14
|
+
export const consistentParameterLayout: Rule.RuleModule = {
|
|
15
|
+
create(context) {
|
|
16
|
+
const isTestFile = /\.test\.[jt]sx?$/.test(context.filename);
|
|
17
|
+
|
|
18
|
+
function checkParameterLayout(
|
|
19
|
+
node: Rule.Node,
|
|
20
|
+
items: readonly ListItem[],
|
|
21
|
+
tokenStore: TokenStore,
|
|
22
|
+
isTemplate: boolean,
|
|
23
|
+
): void {
|
|
24
|
+
const first = items[0];
|
|
25
|
+
if (!first) return;
|
|
26
|
+
const isSingleParameter = items.length === 1;
|
|
27
|
+
|
|
28
|
+
// JSX argument layout belongs to the JSX formatting rules.
|
|
29
|
+
const firstType: string = first.type;
|
|
30
|
+
if (isSingleParameter && (firstType === 'JSXElement' || firstType === 'JSXFragment')) return;
|
|
31
|
+
let openingParen;
|
|
32
|
+
if (node.type === 'CallExpression' || node.type === 'NewExpression') {
|
|
33
|
+
const call = node as CallNode;
|
|
34
|
+
openingParen = tokenStore.getTokenAfter(
|
|
35
|
+
call.typeArguments ?? call.callee,
|
|
36
|
+
(token) => token.type === 'Punctuator' && token.value === '(',
|
|
37
|
+
);
|
|
38
|
+
} else {
|
|
39
|
+
openingParen = tokenStore.getTokenBefore(first);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!openingParen || openingParen.value !== '(') return;
|
|
43
|
+
// An unparenthesized arrow must not borrow its parent's delimiters.
|
|
44
|
+
if (openingParen.range[0] < node.range![0]) return;
|
|
45
|
+
const closingParen = node.type === 'CallExpression' || node.type === 'NewExpression'
|
|
46
|
+
? tokenStore.getLastToken(node)
|
|
47
|
+
: tokenStore.getTokenAfter(items.at(-1)!, (token) => token.value === ')');
|
|
48
|
+
|
|
49
|
+
const next = tokenStore.getTokenAfter(openingParen)!;
|
|
50
|
+
|
|
51
|
+
if (isSingleParameter) {
|
|
52
|
+
if (!closingParen || closingParen.value !== ')') return;
|
|
53
|
+
let previous = tokenStore.getTokenBefore(closingParen)!;
|
|
54
|
+
if (previous.value === ',') previous = tokenStore.getTokenBefore(previous)!;
|
|
55
|
+
if (
|
|
56
|
+
tokenStore.commentsExistBetween(openingParen, next)
|
|
57
|
+
|| tokenStore.commentsExistBetween(previous, closingParen)
|
|
58
|
+
) return;
|
|
59
|
+
|
|
60
|
+
const hasOpeningNewline = openingParen.loc.end.line !== next.loc.start.line;
|
|
61
|
+
const hasClosingNewline = tokenStore.getTokenBefore(closingParen)!.loc.end.line
|
|
62
|
+
!== closingParen.loc.start.line;
|
|
63
|
+
|
|
64
|
+
if (hasOpeningNewline === hasClosingNewline) return;
|
|
65
|
+
|
|
66
|
+
context.report({
|
|
67
|
+
fix: (fixer) => hasOpeningNewline
|
|
68
|
+
? fixer.insertTextBefore(closingParen, '\n')
|
|
69
|
+
: fixer.insertTextAfter(openingParen, '\n'),
|
|
70
|
+
messageId: 'singleBoundary',
|
|
71
|
+
node,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!closingParen || openingParen.loc.start.line === closingParen.loc.end.line) return;
|
|
78
|
+
|
|
79
|
+
if (isTemplate) {
|
|
80
|
+
// Script list formatters do not traverse Vue template expressions.
|
|
81
|
+
const newlineOffsets: number[] = [];
|
|
82
|
+
if (openingParen.loc.end.line === next.loc.start.line) newlineOffsets.push(openingParen.range[1]);
|
|
83
|
+
for (let index = 1; index < items.length; index++) {
|
|
84
|
+
const item = items[index]!;
|
|
85
|
+
const comma = tokenStore.getTokensBetween(items[index - 1]!, item)
|
|
86
|
+
.find((token) => token.value === ',');
|
|
87
|
+
|
|
88
|
+
if (comma && comma.loc.end.line === tokenStore.getTokenAfter(comma)!.loc.start.line) {
|
|
89
|
+
newlineOffsets.push(comma.range[1]);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (
|
|
94
|
+
closingParen.value === ')'
|
|
95
|
+
&& tokenStore.getTokenBefore(closingParen)!.loc.end.line === closingParen.loc.start.line
|
|
96
|
+
) newlineOffsets.push(closingParen.range[0]);
|
|
97
|
+
|
|
98
|
+
if (newlineOffsets.length) {
|
|
99
|
+
context.report({
|
|
100
|
+
fix: (fixer) => newlineOffsets.map((offset) => fixer.insertTextAfterRange(
|
|
101
|
+
[
|
|
102
|
+
offset,
|
|
103
|
+
offset,
|
|
104
|
+
],
|
|
105
|
+
'\n',
|
|
106
|
+
)),
|
|
107
|
+
messageId: 'newline',
|
|
108
|
+
node: first,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (openingParen.loc.end.line !== next.loc.start.line) return;
|
|
116
|
+
|
|
117
|
+
context.report({
|
|
118
|
+
fix: (fixer) => fixer.insertTextAfter(openingParen, '\n'),
|
|
119
|
+
messageId: 'newline',
|
|
120
|
+
node: first,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function createVisitor(tokenStore: TokenStore, isTemplate: boolean): Rule.RuleListener {
|
|
125
|
+
return {
|
|
126
|
+
ArrowFunctionExpression: (node) => checkParameterLayout(node, node.params, tokenStore, isTemplate),
|
|
127
|
+
CallExpression: (node) => {
|
|
128
|
+
const { callee } = node;
|
|
129
|
+
if (isTestFile) {
|
|
130
|
+
const target = callee.type === 'MemberExpression'
|
|
131
|
+
&& !callee.computed
|
|
132
|
+
&& callee.property.type === 'Identifier'
|
|
133
|
+
&& callee.property.name === 'concurrent'
|
|
134
|
+
? callee.object
|
|
135
|
+
: callee;
|
|
136
|
+
|
|
137
|
+
if (
|
|
138
|
+
target.type === 'Identifier'
|
|
139
|
+
&& (target.name === 'describe' || target.name === 'it')
|
|
140
|
+
) return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
checkParameterLayout(node, node.arguments, tokenStore, isTemplate);
|
|
144
|
+
},
|
|
145
|
+
FunctionDeclaration: (node) => checkParameterLayout(node, node.params, tokenStore, isTemplate),
|
|
146
|
+
FunctionExpression: (node) => checkParameterLayout(node, node.params, tokenStore, isTemplate),
|
|
147
|
+
NewExpression: (node) => checkParameterLayout(node, node.arguments, tokenStore, isTemplate),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return defineScriptAndTemplateVisitors(context.sourceCode, createVisitor);
|
|
152
|
+
},
|
|
153
|
+
meta: {
|
|
154
|
+
docs: { description: 'Preserve consistent single-parameter boundaries and expand multiline parameter lists.' },
|
|
155
|
+
fixable: 'whitespace',
|
|
156
|
+
messages: {
|
|
157
|
+
newline: 'Start a multiline parameter list on a new line.',
|
|
158
|
+
singleBoundary: 'Use matching newline boundaries around a single parameter.',
|
|
159
|
+
},
|
|
160
|
+
schema: [],
|
|
161
|
+
type: 'layout',
|
|
162
|
+
},
|
|
163
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Rule } from 'eslint';
|
|
2
|
+
|
|
3
|
+
type StatementNode = Extract<Rule.Node, { type: 'Program' }>['body'][number];
|
|
4
|
+
|
|
5
|
+
export const statementPadding: Rule.RuleModule = {
|
|
6
|
+
create(context) {
|
|
7
|
+
const { sourceCode } = context;
|
|
8
|
+
|
|
9
|
+
function checkStatements(statements: readonly StatementNode[]): void {
|
|
10
|
+
for (let index = 1; index < statements.length; index++) {
|
|
11
|
+
const previous = statements[index - 1]!;
|
|
12
|
+
const next = statements[index]!;
|
|
13
|
+
const lastToken = sourceCode.getLastToken(previous)!;
|
|
14
|
+
const firstToken = sourceCode.getFirstToken(next)!;
|
|
15
|
+
const closingLine = sourceCode.lines[lastToken.loc.end.line - 1]!;
|
|
16
|
+
const openingLine = sourceCode.lines[firstToken.loc.start.line - 1]!;
|
|
17
|
+
const hasDifferentIndent = /^\s*/.exec(closingLine)![0] !== /^\s*/.exec(openingLine)![0];
|
|
18
|
+
const hasStandaloneClosing = /^[)\]}]+;?$/.test(closingLine.slice(0, lastToken.loc.end.column).trim());
|
|
19
|
+
const blockNode = previous.type === 'ExportDefaultDeclaration'
|
|
20
|
+
|| previous.type === 'ExportNamedDeclaration'
|
|
21
|
+
? previous.declaration
|
|
22
|
+
: previous;
|
|
23
|
+
|
|
24
|
+
const hasBlockClosing = lastToken.value === '}' && blockNode && [
|
|
25
|
+
'BlockStatement',
|
|
26
|
+
'ClassDeclaration',
|
|
27
|
+
'ForInStatement',
|
|
28
|
+
'ForOfStatement',
|
|
29
|
+
'ForStatement',
|
|
30
|
+
'FunctionDeclaration',
|
|
31
|
+
'IfStatement',
|
|
32
|
+
'LabeledStatement',
|
|
33
|
+
'SwitchStatement',
|
|
34
|
+
'TryStatement',
|
|
35
|
+
'WhileStatement',
|
|
36
|
+
'WithStatement',
|
|
37
|
+
].includes(blockNode.type);
|
|
38
|
+
|
|
39
|
+
if (!hasDifferentIndent && !hasStandaloneClosing && !hasBlockClosing) continue;
|
|
40
|
+
const comments = sourceCode.getCommentsBefore(next);
|
|
41
|
+
if (
|
|
42
|
+
sourceCode
|
|
43
|
+
.lines
|
|
44
|
+
.slice(lastToken.loc.end.line, firstToken.loc.start.line - 1)
|
|
45
|
+
.some((line, index) => !line.trim() && !comments.some((comment) =>
|
|
46
|
+
comment.loc!.start.line <= lastToken.loc.end.line + index + 1
|
|
47
|
+
&& comment.loc!.end.line >= lastToken.loc.end.line + index + 1))
|
|
48
|
+
) continue;
|
|
49
|
+
|
|
50
|
+
// Keep leading comments attached to the next statement and trailing comments in place.
|
|
51
|
+
const boundary = comments
|
|
52
|
+
.find((comment) => comment.loc!.start.line > lastToken.loc.end.line) ?? firstToken;
|
|
53
|
+
|
|
54
|
+
const offset = boundary.loc!.start.line > lastToken.loc.end.line
|
|
55
|
+
? boundary.range![0] - boundary.loc!.start.column
|
|
56
|
+
: boundary.range![0];
|
|
57
|
+
|
|
58
|
+
context.report({
|
|
59
|
+
fix: (fixer) => fixer.insertTextBeforeRange(
|
|
60
|
+
[
|
|
61
|
+
offset,
|
|
62
|
+
offset,
|
|
63
|
+
],
|
|
64
|
+
boundary.loc!.start.line === lastToken.loc.end.line ? '\n\n' : '\n',
|
|
65
|
+
),
|
|
66
|
+
messageId: 'blankLine',
|
|
67
|
+
node: next,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
BlockStatement: (node) => checkStatements(node.body),
|
|
74
|
+
Program: (node) => checkStatements(node.body),
|
|
75
|
+
StaticBlock: (node) => checkStatements(node.body),
|
|
76
|
+
SwitchCase: (node) => checkStatements(node.consequent),
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
meta: {
|
|
80
|
+
docs: { description: 'Separate statements after closing lines, blocks, or indentation changes.' },
|
|
81
|
+
fixable: 'whitespace',
|
|
82
|
+
messages: { blankLine: 'Add a blank line between these complete statements.' },
|
|
83
|
+
schema: [],
|
|
84
|
+
type: 'layout',
|
|
85
|
+
},
|
|
86
|
+
};
|