@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.
Files changed (43) hide show
  1. package/dist/base.d.ts +2 -4
  2. package/dist/base.d.ts.map +1 -1
  3. package/dist/base.js +14 -2
  4. package/dist/base.js.map +1 -1
  5. package/dist/internals/plugins.d.ts +11 -0
  6. package/dist/internals/plugins.d.ts.map +1 -0
  7. package/dist/internals/plugins.js +15 -0
  8. package/dist/internals/plugins.js.map +1 -0
  9. package/dist/internals/rules/compact-iteration-layout.d.ts +5 -0
  10. package/dist/internals/rules/compact-iteration-layout.d.ts.map +1 -0
  11. package/dist/internals/rules/compact-iteration-layout.js +35 -0
  12. package/dist/internals/rules/compact-iteration-layout.js.map +1 -0
  13. package/dist/internals/rules/consistent-condition-layout.d.ts +5 -0
  14. package/dist/internals/rules/consistent-condition-layout.d.ts.map +1 -0
  15. package/dist/internals/rules/consistent-condition-layout.js +86 -0
  16. package/dist/internals/rules/consistent-condition-layout.js.map +1 -0
  17. package/dist/internals/rules/consistent-parameter-layout.d.ts +5 -0
  18. package/dist/internals/rules/consistent-parameter-layout.d.ts.map +1 -0
  19. package/dist/internals/rules/consistent-parameter-layout.js +91 -0
  20. package/dist/internals/rules/consistent-parameter-layout.js.map +1 -0
  21. package/dist/internals/rules/statement-padding.d.ts +5 -0
  22. package/dist/internals/rules/statement-padding.d.ts.map +1 -0
  23. package/dist/internals/rules/statement-padding.js +60 -0
  24. package/dist/internals/rules/statement-padding.js.map +1 -0
  25. package/dist/internals/utils.d.ts +6 -0
  26. package/dist/internals/utils.d.ts.map +1 -0
  27. package/dist/internals/utils.js +11 -0
  28. package/dist/internals/utils.js.map +1 -0
  29. package/dist/style.d.ts +1 -3
  30. package/dist/style.d.ts.map +1 -1
  31. package/dist/vue.d.ts +1 -3
  32. package/dist/vue.d.ts.map +1 -1
  33. package/dist/vue.js +2 -0
  34. package/dist/vue.js.map +1 -1
  35. package/package.json +51 -21
  36. package/src/base.ts +20 -2
  37. package/src/internals/plugins.ts +13 -0
  38. package/src/internals/rules/compact-iteration-layout.ts +57 -0
  39. package/src/internals/rules/consistent-condition-layout.ts +130 -0
  40. package/src/internals/rules/consistent-parameter-layout.ts +163 -0
  41. package/src/internals/rules/statement-padding.ts +86 -0
  42. package/src/internals/utils.ts +32 -0
  43. 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":";;;;;AAQA,SAAgB,gBAAgB,cAA8B,QAA6B;CACvF,OAAO;EACH,OAAO,CAAC,UAAU;EAClB,SAAS;GACL,kBAAkB;GAClB,sBAAsB;GACtB;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"}
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.2.0",
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.0.0",
40
- "@eslint/css": "^1.1.0",
41
- "@html-eslint/parser": ">=0.61.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.4.0",
45
- "eslint-plugin-format": ">=1.0.0",
46
- "tailwind-csstree": "^0.3.1",
47
- "tailwindcss": ">=4.0.0",
48
- "vue-eslint-parser": "^10.4.0"
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.0",
84
- "@kikiutils/eslint-plugin-vue": "^10.10.0",
85
- "@kikiutils/tsconfigs": "^6.0.0",
86
- "@types/node": "^26.1.0",
87
- "@vitest/coverage-v8": "^4.1.9",
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.6.0",
90
- "eslint-plugin-better-tailwindcss": "^4.6.0",
119
+ "eslint": "^10.12.0",
120
+ "eslint-plugin-better-tailwindcss": "^4.7.0",
91
121
  "eslint-plugin-format": "^2.0.1",
92
- "knip": "^6.24.0",
93
- "publint": "^0.3.21",
94
- "tsdown": "^0.22.3",
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.5.7",
97
- "vitest": "^4.1.9"
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: { promise },
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: { promise },
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
+ };