@noxickon/codex 2.0.1 → 3.0.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/README.md +6 -18
- package/eslint/base.config.js +45 -4
- package/eslint/blank-lines.js +24 -0
- package/eslint/blank-lines.test.js +27 -0
- package/package.json +6 -22
- package/eslint/css.config.js +0 -28
- package/eslint/html.config.js +0 -26
- package/eslint/plugin.js +0 -15
- package/eslint/rules/blank-line-spacing-css.js +0 -51
- package/eslint/rules/blank-line-spacing-html.js +0 -38
- package/eslint/rules/blank-line-spacing.js +0 -254
- package/eslint/rules/blank-line-spacing.test.js +0 -272
- package/eslint/rules/shared.js +0 -86
- package/eslint/vue.config.js +0 -37
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ npm install --save-dev \
|
|
|
19
19
|
eslint \
|
|
20
20
|
prettier \
|
|
21
21
|
@eslint/js \
|
|
22
|
+
@stylistic/eslint-plugin \
|
|
22
23
|
typescript-eslint \
|
|
23
24
|
globals \
|
|
24
25
|
eslint-config-prettier \
|
|
@@ -56,6 +57,7 @@ npm install --save-dev \
|
|
|
56
57
|
eslint \
|
|
57
58
|
prettier \
|
|
58
59
|
@eslint/js \
|
|
60
|
+
@stylistic/eslint-plugin \
|
|
59
61
|
typescript-eslint \
|
|
60
62
|
globals \
|
|
61
63
|
eslint-config-prettier \
|
|
@@ -120,25 +122,11 @@ export default createReactConfig({
|
|
|
120
122
|
});
|
|
121
123
|
```
|
|
122
124
|
|
|
123
|
-
##### Blank-line spacing
|
|
125
|
+
##### Blank-line spacing:
|
|
124
126
|
|
|
125
|
-
|
|
127
|
+
Blank lines are enforced via [ESLint Stylistic](https://eslint.style) — `@stylistic/padding-line-between-statements` and `@stylistic/lines-between-class-members`. They are included in the base, React, and Node configs for `.ts` / `.tsx` / `.js` / `.jsx` files — no setup needed. Collapsing of multiple blank lines and trimming of block edges is left to Prettier.
|
|
126
128
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
###### eslint.config.js
|
|
130
|
-
|
|
131
|
-
```
|
|
132
|
-
import { createReactConfig } from '@noxickon/codex/eslint/react';
|
|
133
|
-
import { createCssConfig } from '@noxickon/codex/eslint/css';
|
|
134
|
-
import { createHtmlConfig } from '@noxickon/codex/eslint/html';
|
|
135
|
-
|
|
136
|
-
export default [
|
|
137
|
-
...createReactConfig(),
|
|
138
|
-
...createCssConfig(),
|
|
139
|
-
...createHtmlConfig(),
|
|
140
|
-
];
|
|
141
|
-
```
|
|
129
|
+
CSS and HTML blank-line spacing is owned entirely by Prettier (no ESLint config required).
|
|
142
130
|
|
|
143
131
|
### Prettier
|
|
144
132
|
|
|
@@ -194,7 +182,7 @@ export default createPrettierConfig({
|
|
|
194
182
|
- Perfectionist (Auto-sorting)
|
|
195
183
|
- Unused Imports
|
|
196
184
|
- Sort Destructure Keys
|
|
197
|
-
-
|
|
185
|
+
- ESLint Stylistic (Blank-line spacing — `padding-line-between-statements`, `lines-between-class-members`)
|
|
198
186
|
- ESLint Config Prettier (Disables ESLint rules that conflict with Prettier)
|
|
199
187
|
|
|
200
188
|
**React (additional):**
|
package/eslint/base.config.js
CHANGED
|
@@ -19,7 +19,7 @@ import unusedImports from 'eslint-plugin-unused-imports';
|
|
|
19
19
|
import globals from 'globals';
|
|
20
20
|
import tseslint from 'typescript-eslint';
|
|
21
21
|
|
|
22
|
-
import {
|
|
22
|
+
import { blankLineRules, stylisticPlugin } from './blank-lines.js';
|
|
23
23
|
|
|
24
24
|
export function createBaseConfig(options = {}) {
|
|
25
25
|
const { tailwindEntryPoint = './src/tailwind.css', enableTailwind = true } = options;
|
|
@@ -56,15 +56,15 @@ export function createBaseConfig(options = {}) {
|
|
|
56
56
|
globals: globals.browser,
|
|
57
57
|
},
|
|
58
58
|
plugins: {
|
|
59
|
-
'@
|
|
59
|
+
'@stylistic': stylisticPlugin,
|
|
60
60
|
perfectionist,
|
|
61
61
|
'sort-destructure-keys': sortDestructureKeys,
|
|
62
62
|
unicorn,
|
|
63
63
|
'unused-imports': unusedImports,
|
|
64
64
|
},
|
|
65
65
|
rules: {
|
|
66
|
-
// Blank-line spacing -
|
|
67
|
-
|
|
66
|
+
// Blank-line spacing - type-based padding (Stylistic); Prettier owns collapsing
|
|
67
|
+
...blankLineRules,
|
|
68
68
|
|
|
69
69
|
// Unicorn - modern JS/TS best practices
|
|
70
70
|
...unicorn.configs.recommended.rules,
|
|
@@ -145,6 +145,10 @@ export function createBaseConfig(options = {}) {
|
|
|
145
145
|
'unicorn/import-style': 'off',
|
|
146
146
|
'unicorn/no-useless-undefined': 'off',
|
|
147
147
|
|
|
148
|
+
// globalThis.setTimeout is intentional: it pins the DOM return type
|
|
149
|
+
// (number) instead of leaking NodeJS.Timeout into shipped library types.
|
|
150
|
+
'unicorn/no-unnecessary-global-this': 'off',
|
|
151
|
+
|
|
148
152
|
// Perfectionist - auto-sorting
|
|
149
153
|
'perfectionist/sort-interfaces': ['error'],
|
|
150
154
|
'perfectionist/sort-object-types': ['error'],
|
|
@@ -209,6 +213,43 @@ export function createBaseConfig(options = {}) {
|
|
|
209
213
|
],
|
|
210
214
|
},
|
|
211
215
|
},
|
|
216
|
+
{
|
|
217
|
+
// Stories embed literal JSX in docs `source.code` strings and define
|
|
218
|
+
// local example render helpers, which fight several unicorn rules.
|
|
219
|
+
files: ['**/*.stories.{ts,tsx,js,jsx}'],
|
|
220
|
+
rules: {
|
|
221
|
+
// {onClick} in a code:`...` string is documentation, not interpolation.
|
|
222
|
+
'unicorn/no-incorrect-template-string-interpolation': 'off',
|
|
223
|
+
// Example render helpers are intentionally local to the story.
|
|
224
|
+
'unicorn/consistent-function-scoping': 'off',
|
|
225
|
+
// Stories mock backends by assigning globalThis.fetch and friends.
|
|
226
|
+
'unicorn/no-global-object-property-assignment': 'off',
|
|
227
|
+
// Fixtures read members off `new` directly (new AbortController().signal).
|
|
228
|
+
'unicorn/no-unreadable-new-expression': 'off',
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
// Test files and test infra fight several unicorn rules with standard
|
|
233
|
+
// testing patterns (DOM queries, global mocks, prototype patches).
|
|
234
|
+
files: [
|
|
235
|
+
'**/*.{test,spec}.{ts,tsx,js,jsx}',
|
|
236
|
+
'**/{test,tests,__tests__}/**/*.{ts,tsx,js,jsx}',
|
|
237
|
+
],
|
|
238
|
+
rules: {
|
|
239
|
+
// Simple element queries; :scope is behaviorally identical here.
|
|
240
|
+
'unicorn/prefer-scoped-selector': 'off',
|
|
241
|
+
// Local helpers inside describe blocks are intentional for locality.
|
|
242
|
+
'unicorn/consistent-function-scoping': 'off',
|
|
243
|
+
// Mocking browser globals (globalThis.ResizeObserver = ...) is standard.
|
|
244
|
+
'unicorn/no-global-object-property-assignment': 'off',
|
|
245
|
+
// Prototype mocks (HTMLElement.prototype.x = function () { this... }).
|
|
246
|
+
'unicorn/no-this-outside-of-class': 'off',
|
|
247
|
+
// Fixtures read members off `new` directly (new AbortController().signal).
|
|
248
|
+
'unicorn/no-unreadable-new-expression': 'off',
|
|
249
|
+
// Tracking call order with () => arr.push(x) ignores the return value.
|
|
250
|
+
'unicorn/no-return-array-push': 'off',
|
|
251
|
+
},
|
|
252
|
+
},
|
|
212
253
|
];
|
|
213
254
|
|
|
214
255
|
// Add Tailwind config if enabled
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared blank-line spacing via ESLint Stylistic.
|
|
3
|
+
* Type-based padding rules (the community standard); Prettier owns blank
|
|
4
|
+
* collapsing and block-edge trimming, so those are intentionally not set here.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import stylistic from '@stylistic/eslint-plugin';
|
|
8
|
+
|
|
9
|
+
export const stylisticPlugin = stylistic;
|
|
10
|
+
|
|
11
|
+
export const blankLineRules = {
|
|
12
|
+
'@stylistic/padding-line-between-statements': [
|
|
13
|
+
'warn',
|
|
14
|
+
{ blankLine: 'always', prev: '*', next: 'return' },
|
|
15
|
+
{ blankLine: 'always', prev: 'directive', next: '*' },
|
|
16
|
+
{ blankLine: 'any', prev: 'directive', next: 'directive' },
|
|
17
|
+
{ blankLine: 'always', prev: ['case', 'default'], next: '*' },
|
|
18
|
+
{ blankLine: 'always', prev: 'block-like', next: '*' },
|
|
19
|
+
{ blankLine: 'always', prev: '*', next: 'block-like' },
|
|
20
|
+
{ blankLine: 'always', prev: ['function', 'class'], next: '*' },
|
|
21
|
+
{ blankLine: 'always', prev: '*', next: ['function', 'class'] },
|
|
22
|
+
],
|
|
23
|
+
'@stylistic/lines-between-class-members': ['warn', 'always', { exceptAfterSingleLine: true }],
|
|
24
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { blankLineRules, stylisticPlugin } from './blank-lines.js';
|
|
5
|
+
|
|
6
|
+
describe('blank-lines', () => {
|
|
7
|
+
it('wires only Stylistic rules', () => {
|
|
8
|
+
const keys = Object.keys(blankLineRules);
|
|
9
|
+
|
|
10
|
+
assert.ok(keys.length > 0);
|
|
11
|
+
assert.ok(keys.every((key) => key.startsWith('@stylistic/')));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('references rules that the Stylistic plugin actually provides', () => {
|
|
15
|
+
for (const key of Object.keys(blankLineRules)) {
|
|
16
|
+
const ruleName = key.slice('@stylistic/'.length);
|
|
17
|
+
|
|
18
|
+
assert.ok(stylisticPlugin.rules[ruleName], `missing rule: ${ruleName}`);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('configures padding-line-between-statements as a warning', () => {
|
|
23
|
+
const [severity] = blankLineRules['@stylistic/padding-line-between-statements'];
|
|
24
|
+
|
|
25
|
+
assert.equal(severity, 'warn');
|
|
26
|
+
});
|
|
27
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noxickon/codex",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"author": "noxickon",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Shared ESLint & Prettier configuration for noxickon projects",
|
|
@@ -12,19 +12,14 @@
|
|
|
12
12
|
"test": "node --test \"eslint/**/*.test.js\""
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
|
-
"@eslint
|
|
16
|
-
"
|
|
17
|
-
"eslint-plugin-react-hooks": ">=6.0.0",
|
|
18
|
-
"vue-eslint-parser": ">=10.0.0"
|
|
15
|
+
"@stylistic/eslint-plugin": "^5.10.0",
|
|
16
|
+
"eslint-plugin-react-hooks": ">=6.0.0"
|
|
19
17
|
},
|
|
20
18
|
"exports": {
|
|
21
19
|
"./prettier": "./prettier.config.js",
|
|
22
20
|
"./eslint/base": "./eslint/base.config.js",
|
|
23
21
|
"./eslint/node": "./eslint/node.config.js",
|
|
24
|
-
"./eslint/react": "./eslint/react.config.js"
|
|
25
|
-
"./eslint/vue": "./eslint/vue.config.js",
|
|
26
|
-
"./eslint/html": "./eslint/html.config.js",
|
|
27
|
-
"./eslint/css": "./eslint/css.config.js"
|
|
22
|
+
"./eslint/react": "./eslint/react.config.js"
|
|
28
23
|
},
|
|
29
24
|
"files": [
|
|
30
25
|
"eslint",
|
|
@@ -37,9 +32,8 @@
|
|
|
37
32
|
],
|
|
38
33
|
"peerDependencies": {
|
|
39
34
|
"@eslint-react/eslint-plugin": ">=4.0.0",
|
|
40
|
-
"@eslint/css": ">=0.12.0",
|
|
41
35
|
"@eslint/js": ">=10.0.0",
|
|
42
|
-
"@
|
|
36
|
+
"@stylistic/eslint-plugin": ">=5.0.0",
|
|
43
37
|
"eslint": ">=10.4.0",
|
|
44
38
|
"eslint-config-prettier": ">=10.0.0",
|
|
45
39
|
"eslint-plugin-better-tailwindcss": ">=4.0.0",
|
|
@@ -55,22 +49,12 @@
|
|
|
55
49
|
"globals": ">=17.1.0",
|
|
56
50
|
"prettier": ">=3.8.1",
|
|
57
51
|
"prettier-plugin-tailwindcss": ">=0.7.0",
|
|
58
|
-
"typescript-eslint": ">=8.57.0"
|
|
59
|
-
"vue-eslint-parser": ">=10.0.0"
|
|
52
|
+
"typescript-eslint": ">=8.57.0"
|
|
60
53
|
},
|
|
61
54
|
"peerDependenciesMeta": {
|
|
62
55
|
"eslint-plugin-better-tailwindcss": {
|
|
63
56
|
"optional": true
|
|
64
57
|
},
|
|
65
|
-
"vue-eslint-parser": {
|
|
66
|
-
"optional": true
|
|
67
|
-
},
|
|
68
|
-
"@html-eslint/parser": {
|
|
69
|
-
"optional": true
|
|
70
|
-
},
|
|
71
|
-
"@eslint/css": {
|
|
72
|
-
"optional": true
|
|
73
|
-
},
|
|
74
58
|
"eslint-plugin-n": {
|
|
75
59
|
"optional": true
|
|
76
60
|
},
|
package/eslint/css.config.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CSS ESLint configuration (stripped-down blank-line spacing).
|
|
3
|
-
* Requires the optional peer dependency `@eslint/css`.
|
|
4
|
-
* @returns {Array} ESLint flat config array
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import css from '@eslint/css';
|
|
8
|
-
|
|
9
|
-
import { codexPlugin } from './plugin.js';
|
|
10
|
-
|
|
11
|
-
export function createCssConfig() {
|
|
12
|
-
return [
|
|
13
|
-
{
|
|
14
|
-
files: ['**/*.css'],
|
|
15
|
-
language: 'css/css',
|
|
16
|
-
languageOptions: {
|
|
17
|
-
tolerant: true,
|
|
18
|
-
},
|
|
19
|
-
plugins: {
|
|
20
|
-
css,
|
|
21
|
-
'@noxickon': codexPlugin,
|
|
22
|
-
},
|
|
23
|
-
rules: {
|
|
24
|
-
'@noxickon/blank-line-spacing-css': 'warn',
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
];
|
|
28
|
-
}
|
package/eslint/html.config.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HTML ESLint configuration (blank-line spacing for elements).
|
|
3
|
-
* Requires the optional peer dependency `@html-eslint/parser`.
|
|
4
|
-
* @returns {Array} ESLint flat config array
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import htmlParser from '@html-eslint/parser';
|
|
8
|
-
|
|
9
|
-
import { codexPlugin } from './plugin.js';
|
|
10
|
-
|
|
11
|
-
export function createHtmlConfig() {
|
|
12
|
-
return [
|
|
13
|
-
{
|
|
14
|
-
files: ['**/*.html'],
|
|
15
|
-
languageOptions: {
|
|
16
|
-
parser: htmlParser,
|
|
17
|
-
},
|
|
18
|
-
plugins: {
|
|
19
|
-
'@noxickon': codexPlugin,
|
|
20
|
-
},
|
|
21
|
-
rules: {
|
|
22
|
-
'@noxickon/blank-line-spacing-html': 'warn',
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
];
|
|
26
|
-
}
|
package/eslint/plugin.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import blankLineSpacing from './rules/blank-line-spacing.js';
|
|
2
|
-
import blankLineSpacingCss from './rules/blank-line-spacing-css.js';
|
|
3
|
-
import blankLineSpacingHtml from './rules/blank-line-spacing-html.js';
|
|
4
|
-
|
|
5
|
-
export const codexPlugin = {
|
|
6
|
-
meta: {
|
|
7
|
-
name: '@noxickon/codex',
|
|
8
|
-
version: '1.3.1',
|
|
9
|
-
},
|
|
10
|
-
rules: {
|
|
11
|
-
'blank-line-spacing': blankLineSpacing,
|
|
12
|
-
'blank-line-spacing-html': blankLineSpacingHtml,
|
|
13
|
-
'blank-line-spacing-css': blankLineSpacingCss,
|
|
14
|
-
},
|
|
15
|
-
};
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stripped-down blank-line rule for CSS (H1): after a multiline block
|
|
3
|
-
* (`{ ... }` spanning multiple lines), require one blank line before the
|
|
4
|
-
* next rule/at-rule. Declarations inside a block are not touched (H2).
|
|
5
|
-
* Requires the optional peer dependency `@eslint/css`.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export default {
|
|
9
|
-
meta: {
|
|
10
|
-
type: 'layout',
|
|
11
|
-
docs: {
|
|
12
|
-
description: 'Require a blank line after a multiline CSS block',
|
|
13
|
-
},
|
|
14
|
-
fixable: 'whitespace',
|
|
15
|
-
schema: [],
|
|
16
|
-
messages: {
|
|
17
|
-
expected: 'Expected a blank line after the previous block.',
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
create(context) {
|
|
21
|
-
function process(children) {
|
|
22
|
-
for (let i = 1; i < children.length; i += 1) {
|
|
23
|
-
const prev = children[i - 1];
|
|
24
|
-
const curr = children[i];
|
|
25
|
-
|
|
26
|
-
if (prev.loc.start.line === prev.loc.end.line) {
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (curr.loc.start.line - prev.loc.end.line - 1 < 1) {
|
|
31
|
-
const end = prev.loc.end.offset;
|
|
32
|
-
|
|
33
|
-
context.report({
|
|
34
|
-
loc: curr.loc.start,
|
|
35
|
-
messageId: 'expected',
|
|
36
|
-
fix: (fixer) => fixer.insertTextAfterRange([end, end], '\n'),
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
StyleSheet: (node) => process(node.children),
|
|
44
|
-
Atrule: (node) => {
|
|
45
|
-
if (node.block && node.block.children) {
|
|
46
|
-
process(node.block.children);
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
},
|
|
51
|
-
};
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Enforces direction-aware blank lines between HTML elements (Tag nodes).
|
|
3
|
-
* Mirrors the element matrix used for JSX/Vue templates:
|
|
4
|
-
* single el -> single el : no blank
|
|
5
|
-
* single el -> multiline el (lone) : no blank
|
|
6
|
-
* multiline el -> el : blank
|
|
7
|
-
* group(2+ singles) -> multiline el : blank
|
|
8
|
-
* Requires the optional peer dependency `@html-eslint/parser`.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { processElementChildren } from './shared.js';
|
|
12
|
-
|
|
13
|
-
const HTML_ELEMENT_TYPES = new Set(['Tag']);
|
|
14
|
-
|
|
15
|
-
export default {
|
|
16
|
-
meta: {
|
|
17
|
-
type: 'layout',
|
|
18
|
-
docs: {
|
|
19
|
-
description: 'Enforce direction-aware blank lines between HTML elements',
|
|
20
|
-
},
|
|
21
|
-
fixable: 'whitespace',
|
|
22
|
-
schema: [],
|
|
23
|
-
messages: {
|
|
24
|
-
expected: 'Expected a blank line before this element.',
|
|
25
|
-
unexpected: 'Unexpected blank line before this element.',
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
create(context) {
|
|
29
|
-
const sourceCode = context.sourceCode;
|
|
30
|
-
const process = (children) =>
|
|
31
|
-
processElementChildren(context, sourceCode, children, HTML_ELEMENT_TYPES, 'Text', (node) => node.range);
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
Document: (node) => process(node.children),
|
|
35
|
-
Tag: (node) => process(node.children),
|
|
36
|
-
};
|
|
37
|
-
},
|
|
38
|
-
};
|
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Enforces direction-aware blank lines between statements.
|
|
3
|
-
*
|
|
4
|
-
* Matrix (single = one source line, multiline = spans multiple lines):
|
|
5
|
-
* single -> single : no blank (M1)
|
|
6
|
-
* single -> multiline : no blank (M2, "lone setup line")
|
|
7
|
-
* multiline-> single : blank required (M3)
|
|
8
|
-
* multiline-> multiline : blank required (M4)
|
|
9
|
-
* group(2+ singles) -> multiline : blank required (M5)
|
|
10
|
-
*
|
|
11
|
-
* Extra rules:
|
|
12
|
-
* - Block G: in a block of >= 3 statements, a blank is required before a
|
|
13
|
-
* trailing single-line exit (return/throw/break/continue).
|
|
14
|
-
* - N1: function/class/top-level declarations always get a blank before them
|
|
15
|
-
* (M2 "no blank" applies only to control-flow/expression blocks).
|
|
16
|
-
* - N2a: blanks that violate "no blank" are removed (normalisation).
|
|
17
|
-
* - N2b: a single blank directly above a leading comment is preserved.
|
|
18
|
-
* - D4: import statements and re-export lists are left untouched.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { processElementChildren } from './shared.js';
|
|
22
|
-
|
|
23
|
-
const EXIT_TYPES = new Set(['ReturnStatement', 'ThrowStatement', 'BreakStatement', 'ContinueStatement']);
|
|
24
|
-
const FUNCTION_INIT_TYPES = new Set(['ArrowFunctionExpression', 'FunctionExpression', 'ClassExpression']);
|
|
25
|
-
const JSX_ELEMENT_TYPES = new Set(['JSXElement', 'JSXFragment']);
|
|
26
|
-
const VUE_ELEMENT_TYPES = new Set(['VElement']);
|
|
27
|
-
|
|
28
|
-
function isMultiline(node) {
|
|
29
|
-
return node.loc.start.line !== node.loc.end.line;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function isExit(node) {
|
|
33
|
-
return EXIT_TYPES.has(node.type);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function isSkipped(node) {
|
|
37
|
-
if (node.type === 'ImportDeclaration' || node.type === 'ExportAllDeclaration') {
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return node.type === 'ExportNamedDeclaration' && !node.declaration;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function isDefinitionLike(node) {
|
|
45
|
-
switch (node.type) {
|
|
46
|
-
case 'FunctionDeclaration':
|
|
47
|
-
case 'ClassDeclaration':
|
|
48
|
-
case 'TSInterfaceDeclaration':
|
|
49
|
-
case 'TSTypeAliasDeclaration':
|
|
50
|
-
case 'TSEnumDeclaration':
|
|
51
|
-
case 'TSModuleDeclaration':
|
|
52
|
-
case 'ExportDefaultDeclaration':
|
|
53
|
-
return true;
|
|
54
|
-
case 'ExportNamedDeclaration':
|
|
55
|
-
return Boolean(node.declaration);
|
|
56
|
-
case 'VariableDeclaration':
|
|
57
|
-
return node.declarations.some((d) => d.init && FUNCTION_INIT_TYPES.has(d.init.type));
|
|
58
|
-
default:
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function caseIsMultiline(switchCase) {
|
|
64
|
-
const body = switchCase.consequent;
|
|
65
|
-
|
|
66
|
-
if (body.length === 0) {
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (body.length === 1) {
|
|
71
|
-
return isMultiline(body[0]);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function getActualLastToken(sourceCode, node) {
|
|
78
|
-
const last = sourceCode.getLastToken(node);
|
|
79
|
-
|
|
80
|
-
if (!last || last.type !== 'Punctuator' || last.value !== ';') {
|
|
81
|
-
return last;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const prevToken = sourceCode.getTokenBefore(last);
|
|
85
|
-
const nextToken = sourceCode.getTokenAfter(last);
|
|
86
|
-
const semicolonLess = Boolean(
|
|
87
|
-
prevToken &&
|
|
88
|
-
nextToken &&
|
|
89
|
-
prevToken.range[0] >= node.range[0] &&
|
|
90
|
-
last.loc.start.line !== prevToken.loc.end.line &&
|
|
91
|
-
last.loc.end.line === nextToken.loc.start.line,
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
return semicolonLess ? prevToken : last;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export default {
|
|
98
|
-
meta: {
|
|
99
|
-
type: 'layout',
|
|
100
|
-
docs: {
|
|
101
|
-
description: 'Enforce direction-aware blank lines between statements',
|
|
102
|
-
},
|
|
103
|
-
fixable: 'whitespace',
|
|
104
|
-
schema: [],
|
|
105
|
-
messages: {
|
|
106
|
-
expected: 'Expected a blank line before this statement.',
|
|
107
|
-
unexpected: 'Unexpected blank line before this statement.',
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
create(context) {
|
|
111
|
-
const sourceCode = context.sourceCode;
|
|
112
|
-
|
|
113
|
-
function leadingComments(prevAnchor, node) {
|
|
114
|
-
return sourceCode
|
|
115
|
-
.getCommentsBefore(node)
|
|
116
|
-
.filter((comment) => comment.loc.start.line !== prevAnchor.loc.end.line);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function trailingAnchor(prevNode) {
|
|
120
|
-
const lastToken = getActualLastToken(sourceCode, prevNode);
|
|
121
|
-
const trailing = sourceCode
|
|
122
|
-
.getCommentsAfter(lastToken)
|
|
123
|
-
.filter((comment) => comment.loc.start.line === lastToken.loc.end.line);
|
|
124
|
-
|
|
125
|
-
return trailing.length > 0 ? trailing.at(-1) : lastToken;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function blankCountBetween(prevNode, node) {
|
|
129
|
-
const anchor = trailingAnchor(prevNode);
|
|
130
|
-
const comments = leadingComments(anchor, node);
|
|
131
|
-
const start = comments.length > 0 ? comments[0] : sourceCode.getFirstToken(node);
|
|
132
|
-
|
|
133
|
-
return start.loc.start.line - anchor.loc.end.line - 1;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function isGroupedSingle(prevPrev, prev, multilineOf) {
|
|
137
|
-
if (!prevPrev || isSkipped(prevPrev) || multilineOf(prevPrev)) {
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return blankCountBetween(prevPrev, prev) === 0;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function needsBlank({ prevPrev, prev, curr, isLast, listLength, kind }) {
|
|
145
|
-
const multilineOf = kind === 'switch' ? caseIsMultiline : isMultiline;
|
|
146
|
-
const prevMulti = multilineOf(prev);
|
|
147
|
-
const currMulti = multilineOf(curr);
|
|
148
|
-
|
|
149
|
-
if (currMulti) {
|
|
150
|
-
if (kind === 'statement' && isDefinitionLike(curr)) {
|
|
151
|
-
return true;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (prevMulti) {
|
|
155
|
-
return true;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return isGroupedSingle(prevPrev, prev, multilineOf);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (prevMulti) {
|
|
162
|
-
return true;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return kind === 'statement' && isLast && listLength >= 3 && isExit(curr);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function checkPair(prev, curr, needBlank) {
|
|
169
|
-
const anchor = trailingAnchor(prev);
|
|
170
|
-
const comments = leadingComments(anchor, curr);
|
|
171
|
-
const start = comments.length > 0 ? comments[0] : sourceCode.getFirstToken(curr);
|
|
172
|
-
const blankCount = start.loc.start.line - anchor.loc.end.line - 1;
|
|
173
|
-
|
|
174
|
-
if (needBlank && blankCount < 1) {
|
|
175
|
-
context.report({
|
|
176
|
-
node: curr,
|
|
177
|
-
loc: start.loc,
|
|
178
|
-
messageId: 'expected',
|
|
179
|
-
fix: (fixer) => fixer.insertTextAfter(anchor, '\n'),
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (!needBlank && blankCount > 0) {
|
|
186
|
-
if (comments.length > 0) {
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const between = sourceCode.text.slice(anchor.range[1], start.range[0]);
|
|
191
|
-
const indent = between.slice(between.lastIndexOf('\n') + 1);
|
|
192
|
-
|
|
193
|
-
context.report({
|
|
194
|
-
node: curr,
|
|
195
|
-
loc: start.loc,
|
|
196
|
-
messageId: 'unexpected',
|
|
197
|
-
fix: (fixer) => fixer.replaceTextRange([anchor.range[1], start.range[0]], `\n${indent}`),
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function processList(list, kind = 'statement') {
|
|
203
|
-
for (let i = 1; i < list.length; i += 1) {
|
|
204
|
-
const prev = list[i - 1];
|
|
205
|
-
const curr = list[i];
|
|
206
|
-
|
|
207
|
-
if (isSkipped(prev) || isSkipped(curr)) {
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const needBlank = needsBlank({
|
|
212
|
-
prevPrev: list[i - 2],
|
|
213
|
-
prev,
|
|
214
|
-
curr,
|
|
215
|
-
isLast: i === list.length - 1,
|
|
216
|
-
listLength: list.length,
|
|
217
|
-
kind,
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
checkPair(prev, curr, needBlank);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const rangeOf = (node) => node.range;
|
|
225
|
-
const processJsx = (children) =>
|
|
226
|
-
processElementChildren(context, sourceCode, children, JSX_ELEMENT_TYPES, 'JSXText', rangeOf);
|
|
227
|
-
|
|
228
|
-
const scriptVisitors = {
|
|
229
|
-
Program: (node) => processList(node.body),
|
|
230
|
-
BlockStatement: (node) => processList(node.body),
|
|
231
|
-
StaticBlock: (node) => processList(node.body),
|
|
232
|
-
SwitchCase: (node) => processList(node.consequent),
|
|
233
|
-
SwitchStatement: (node) => processList(node.cases, 'switch'),
|
|
234
|
-
ClassBody: (node) => processList(node.body),
|
|
235
|
-
TSModuleBlock: (node) => processList(node.body),
|
|
236
|
-
JSXElement: (node) => processJsx(node.children),
|
|
237
|
-
JSXFragment: (node) => processJsx(node.children),
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
const services = sourceCode.parserServices;
|
|
241
|
-
|
|
242
|
-
if (services && typeof services.defineTemplateBodyVisitor === 'function') {
|
|
243
|
-
return services.defineTemplateBodyVisitor(
|
|
244
|
-
{
|
|
245
|
-
VElement: (node) =>
|
|
246
|
-
processElementChildren(context, sourceCode, node.children, VUE_ELEMENT_TYPES, 'VText', rangeOf),
|
|
247
|
-
},
|
|
248
|
-
scriptVisitors,
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return scriptVisitors;
|
|
253
|
-
},
|
|
254
|
-
};
|
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
import { RuleTester } from 'eslint';
|
|
2
|
-
import { describe, it } from 'node:test';
|
|
3
|
-
import tseslint from 'typescript-eslint';
|
|
4
|
-
|
|
5
|
-
import rule from './blank-line-spacing.js';
|
|
6
|
-
|
|
7
|
-
RuleTester.describe = describe;
|
|
8
|
-
RuleTester.it = it;
|
|
9
|
-
|
|
10
|
-
const js = new RuleTester({
|
|
11
|
-
languageOptions: {
|
|
12
|
-
ecmaVersion: 'latest',
|
|
13
|
-
sourceType: 'module',
|
|
14
|
-
parserOptions: { ecmaFeatures: { jsx: true } },
|
|
15
|
-
},
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
const ts = new RuleTester({
|
|
19
|
-
languageOptions: {
|
|
20
|
-
parser: tseslint.parser,
|
|
21
|
-
ecmaVersion: 'latest',
|
|
22
|
-
sourceType: 'module',
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
js.run('blank-line-spacing', rule, {
|
|
27
|
-
valid: [
|
|
28
|
-
// M1: single -> single grouped
|
|
29
|
-
'const a = 1;\nconst b = 2;\nconst c = 3;',
|
|
30
|
-
|
|
31
|
-
// M2: lone setup -> multiline
|
|
32
|
-
'const userId = getId();\nif (userId === null) {\n fail();\n}',
|
|
33
|
-
|
|
34
|
-
// M3: multiline -> single
|
|
35
|
-
'if (x) {\n go();\n}\n\nconst y = 1;',
|
|
36
|
-
|
|
37
|
-
// M4: multiline -> multiline
|
|
38
|
-
'if (x) {\n go();\n}\n\nfor (const i of list) {\n use(i);\n}',
|
|
39
|
-
|
|
40
|
-
// M5: group of singles -> multiline
|
|
41
|
-
'const a = 1;\nconst b = 2;\n\nif (a + b > 0) {\n go();\n}',
|
|
42
|
-
|
|
43
|
-
// Block G: <= 2 statements stay tight
|
|
44
|
-
'function f() {\n doIt();\n return true;\n}',
|
|
45
|
-
|
|
46
|
-
// Block G: >= 3 statements -> blank before exit
|
|
47
|
-
'function f() {\n a();\n b();\n\n return true;\n}',
|
|
48
|
-
|
|
49
|
-
// Block G with throw
|
|
50
|
-
'function f() {\n const x = a();\n const y = b();\n\n throw new Error(x + y);\n}',
|
|
51
|
-
|
|
52
|
-
// N1: function declaration always gets a blank before it
|
|
53
|
-
'const max = 10;\n\nfunction make() {\n return max;\n}',
|
|
54
|
-
|
|
55
|
-
// N2b: blank above a comment is preserved (section divider)
|
|
56
|
-
'const config = load();\n\n// SECTION\nconst valid = config.ok;',
|
|
57
|
-
|
|
58
|
-
// comment does not break a group (no blank) -> group still needs blank before block
|
|
59
|
-
'const a = 1;\n// explains b\nconst b = 2;\n\nif (a + b) {\n go();\n}',
|
|
60
|
-
|
|
61
|
-
// imports untouched (perfectionist owns them)
|
|
62
|
-
"import a from 'a';\n\nimport b from 'b';\nconst x = 1;",
|
|
63
|
-
|
|
64
|
-
// class: fields grouped, blank before method
|
|
65
|
-
'class C {\n a = 1;\n b = 2;\n\n run() {\n go();\n }\n}',
|
|
66
|
-
|
|
67
|
-
// switch: single-line cases grouped, multiline case separated
|
|
68
|
-
'function pick(t) {\n switch (t) {\n case 1:\n return a();\n case 2:\n return b();\n\n case 3: {\n const x = c();\n return x;\n }\n\n default:\n return d();\n }\n}',
|
|
69
|
-
|
|
70
|
-
// multiline before exit already separated by M3, no extra blank
|
|
71
|
-
'function f() {\n if (a) {\n return 0;\n }\n\n return 1;\n}',
|
|
72
|
-
|
|
73
|
-
// JSX: single-line sibling elements grouped
|
|
74
|
-
'const x = (\n <div>\n <span>a</span>\n <span>b</span>\n </div>\n);',
|
|
75
|
-
|
|
76
|
-
// JSX: lone single element -> multiline element = no blank (T4/M2)
|
|
77
|
-
'const x = (\n <div>\n <span>a</span>\n <ul>\n <li>b</li>\n </ul>\n </div>\n);',
|
|
78
|
-
|
|
79
|
-
// JSX: multiline element -> following element = blank (T3)
|
|
80
|
-
'const x = (\n <div>\n <ul>\n <li>a</li>\n </ul>\n\n <span>b</span>\n </div>\n);',
|
|
81
|
-
|
|
82
|
-
// JSX: group of single elements -> multiline element = blank
|
|
83
|
-
'const x = (\n <div>\n <span>a</span>\n <span>b</span>\n\n <form>\n <input />\n </form>\n </div>\n);',
|
|
84
|
-
],
|
|
85
|
-
invalid: [
|
|
86
|
-
// M1 violation: stray blank between singles -> removed
|
|
87
|
-
{
|
|
88
|
-
code: 'const a = 1;\n\nconst b = 2;',
|
|
89
|
-
output: 'const a = 1;\nconst b = 2;',
|
|
90
|
-
errors: [{ messageId: 'unexpected' }],
|
|
91
|
-
},
|
|
92
|
-
|
|
93
|
-
// M2 violation: lone setup -> multiline should not have blank
|
|
94
|
-
{
|
|
95
|
-
code: 'const x = get();\n\nif (x) {\n go();\n}',
|
|
96
|
-
output: 'const x = get();\nif (x) {\n go();\n}',
|
|
97
|
-
errors: [{ messageId: 'unexpected' }],
|
|
98
|
-
},
|
|
99
|
-
|
|
100
|
-
// M3 violation: multiline -> single missing blank
|
|
101
|
-
{
|
|
102
|
-
code: 'if (x) {\n go();\n}\nconst y = 1;',
|
|
103
|
-
output: 'if (x) {\n go();\n}\n\nconst y = 1;',
|
|
104
|
-
errors: [{ messageId: 'expected' }],
|
|
105
|
-
},
|
|
106
|
-
|
|
107
|
-
// M5 violation: group of singles -> multiline missing blank
|
|
108
|
-
{
|
|
109
|
-
code: 'const a = 1;\nconst b = 2;\nif (a + b) {\n go();\n}',
|
|
110
|
-
output: 'const a = 1;\nconst b = 2;\n\nif (a + b) {\n go();\n}',
|
|
111
|
-
errors: [{ messageId: 'expected' }],
|
|
112
|
-
},
|
|
113
|
-
|
|
114
|
-
// Block G: 3 statements, missing blank before return
|
|
115
|
-
{
|
|
116
|
-
code: 'function f() {\n a();\n b();\n return true;\n}',
|
|
117
|
-
output: 'function f() {\n a();\n b();\n\n return true;\n}',
|
|
118
|
-
errors: [{ messageId: 'expected' }],
|
|
119
|
-
},
|
|
120
|
-
|
|
121
|
-
// N1: missing blank before function declaration
|
|
122
|
-
{
|
|
123
|
-
code: 'const max = 10;\nfunction make() {\n return max;\n}',
|
|
124
|
-
output: 'const max = 10;\n\nfunction make() {\n return max;\n}',
|
|
125
|
-
errors: [{ messageId: 'expected' }],
|
|
126
|
-
},
|
|
127
|
-
|
|
128
|
-
// insert blank before a leading comment (blank goes above the comment)
|
|
129
|
-
{
|
|
130
|
-
code: 'if (x) {\n go();\n}\n// note\nconst y = 1;',
|
|
131
|
-
output: 'if (x) {\n go();\n}\n\n// note\nconst y = 1;',
|
|
132
|
-
errors: [{ messageId: 'expected' }],
|
|
133
|
-
},
|
|
134
|
-
|
|
135
|
-
// trailing comment on prev does not get split when inserting
|
|
136
|
-
{
|
|
137
|
-
code: 'const a = 1; // keep\nif (a) {\n go();\n}\nconst b = 2;',
|
|
138
|
-
output: 'const a = 1; // keep\nif (a) {\n go();\n}\n\nconst b = 2;',
|
|
139
|
-
errors: [{ messageId: 'expected' }],
|
|
140
|
-
},
|
|
141
|
-
|
|
142
|
-
// JSX: stray blank between single elements removed
|
|
143
|
-
{
|
|
144
|
-
code: 'const x = (\n <div>\n <span>a</span>\n\n <span>b</span>\n </div>\n);',
|
|
145
|
-
output: 'const x = (\n <div>\n <span>a</span>\n <span>b</span>\n </div>\n);',
|
|
146
|
-
errors: [{ messageId: 'unexpected' }],
|
|
147
|
-
},
|
|
148
|
-
|
|
149
|
-
// JSX: multiline element -> single element missing blank
|
|
150
|
-
{
|
|
151
|
-
code: 'const x = (\n <div>\n <ul>\n <li>a</li>\n </ul>\n <span>b</span>\n </div>\n);',
|
|
152
|
-
output: 'const x = (\n <div>\n <ul>\n <li>a</li>\n </ul>\n\n <span>b</span>\n </div>\n);',
|
|
153
|
-
errors: [{ messageId: 'expected' }],
|
|
154
|
-
},
|
|
155
|
-
],
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
let vueParser;
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
vueParser = (await import('vue-eslint-parser')).default;
|
|
162
|
-
} catch {
|
|
163
|
-
vueParser = null;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (vueParser) {
|
|
167
|
-
const vue = new RuleTester({
|
|
168
|
-
languageOptions: {
|
|
169
|
-
parser: vueParser,
|
|
170
|
-
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
|
171
|
-
},
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
vue.run('blank-line-spacing (vue)', rule, {
|
|
175
|
-
valid: [
|
|
176
|
-
// script grouped + blank before function; template grouped + blank before multiline
|
|
177
|
-
'<script setup>\nconst a = 1;\nconst b = 2;\n\nfunction go() {\n a();\n}\n</script>\n\n<template>\n <span>a</span>\n <span>b</span>\n\n <ul>\n <li>x</li>\n </ul>\n</template>\n',
|
|
178
|
-
],
|
|
179
|
-
invalid: [
|
|
180
|
-
// template: stray blank between single elements removed
|
|
181
|
-
{
|
|
182
|
-
code: '<template>\n <span>a</span>\n\n <span>b</span>\n</template>\n',
|
|
183
|
-
output: '<template>\n <span>a</span>\n <span>b</span>\n</template>\n',
|
|
184
|
-
errors: [{ messageId: 'unexpected' }],
|
|
185
|
-
},
|
|
186
|
-
|
|
187
|
-
// template: group of single elements -> multiline element = blank
|
|
188
|
-
{
|
|
189
|
-
code: '<template>\n <span>a</span>\n <span>b</span>\n <ul>\n <li>x</li>\n </ul>\n</template>\n',
|
|
190
|
-
output: '<template>\n <span>a</span>\n <span>b</span>\n\n <ul>\n <li>x</li>\n </ul>\n</template>\n',
|
|
191
|
-
errors: [{ messageId: 'expected' }],
|
|
192
|
-
},
|
|
193
|
-
],
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
let htmlParser;
|
|
198
|
-
|
|
199
|
-
try {
|
|
200
|
-
htmlParser = (await import('@html-eslint/parser')).default;
|
|
201
|
-
} catch {
|
|
202
|
-
htmlParser = null;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (htmlParser) {
|
|
206
|
-
const htmlRule = (await import('./blank-line-spacing-html.js')).default;
|
|
207
|
-
const html = new RuleTester({ languageOptions: { parser: htmlParser } });
|
|
208
|
-
|
|
209
|
-
html.run('blank-line-spacing-html', htmlRule, {
|
|
210
|
-
valid: [
|
|
211
|
-
'<div>\n <span>a</span>\n <span>b</span>\n</div>\n',
|
|
212
|
-
'<div>\n <ul>\n <li>a</li>\n </ul>\n\n <span>b</span>\n</div>\n',
|
|
213
|
-
],
|
|
214
|
-
invalid: [
|
|
215
|
-
{
|
|
216
|
-
code: '<div>\n <span>a</span>\n\n <span>b</span>\n</div>\n',
|
|
217
|
-
output: '<div>\n <span>a</span>\n <span>b</span>\n</div>\n',
|
|
218
|
-
errors: [{ messageId: 'unexpected' }],
|
|
219
|
-
},
|
|
220
|
-
{
|
|
221
|
-
code: '<div>\n <ul>\n <li>a</li>\n </ul>\n <span>b</span>\n</div>\n',
|
|
222
|
-
output: '<div>\n <ul>\n <li>a</li>\n </ul>\n\n <span>b</span>\n</div>\n',
|
|
223
|
-
errors: [{ messageId: 'expected' }],
|
|
224
|
-
},
|
|
225
|
-
],
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
let cssPlugin;
|
|
230
|
-
|
|
231
|
-
try {
|
|
232
|
-
cssPlugin = (await import('@eslint/css')).default;
|
|
233
|
-
} catch {
|
|
234
|
-
cssPlugin = null;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (cssPlugin) {
|
|
238
|
-
const cssRule = (await import('./blank-line-spacing-css.js')).default;
|
|
239
|
-
const cssTester = new RuleTester({ plugins: { css: cssPlugin }, language: 'css/css' });
|
|
240
|
-
|
|
241
|
-
cssTester.run('blank-line-spacing-css', cssRule, {
|
|
242
|
-
valid: [
|
|
243
|
-
'.a {\n color: red;\n}\n\n.b {\n color: blue;\n}\n',
|
|
244
|
-
'.a { color: red; }\n.b { color: blue; }\n',
|
|
245
|
-
],
|
|
246
|
-
invalid: [
|
|
247
|
-
{
|
|
248
|
-
code: '.a {\n color: red;\n}\n.b {\n color: blue;\n}\n',
|
|
249
|
-
output: '.a {\n color: red;\n}\n\n.b {\n color: blue;\n}\n',
|
|
250
|
-
errors: [{ messageId: 'expected' }],
|
|
251
|
-
},
|
|
252
|
-
],
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
ts.run('blank-line-spacing (ts)', rule, {
|
|
257
|
-
valid: [
|
|
258
|
-
// export const arrow = definition-like -> blank before it (present)
|
|
259
|
-
'const max = 10;\n\nexport const make = (): number => {\n return max;\n};',
|
|
260
|
-
|
|
261
|
-
// interface members are not touched; blank after the interface (M3) is required
|
|
262
|
-
'interface User {\n id: string;\n name: string;\n}\n\nconst x = 1;',
|
|
263
|
-
],
|
|
264
|
-
invalid: [
|
|
265
|
-
// missing blank before exported arrow declaration
|
|
266
|
-
{
|
|
267
|
-
code: 'const max = 10;\nexport const make = (): number => {\n return max;\n};',
|
|
268
|
-
output: 'const max = 10;\n\nexport const make = (): number => {\n return max;\n};',
|
|
269
|
-
errors: [{ messageId: 'expected' }],
|
|
270
|
-
},
|
|
271
|
-
],
|
|
272
|
-
});
|
package/eslint/rules/shared.js
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared element-spacing helpers used by the JSX/Vue, HTML rules.
|
|
3
|
-
* Works on any node that exposes `loc` (line info) and a pair of absolute
|
|
4
|
-
* text offsets via the `offsetsOf` accessor.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
function isMultilineNode(node) {
|
|
8
|
-
return node.loc.start.line !== node.loc.end.line;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function elementNeedsBlank(prevPrev, prev, curr) {
|
|
12
|
-
const prevMulti = isMultilineNode(prev);
|
|
13
|
-
const currMulti = isMultilineNode(curr);
|
|
14
|
-
|
|
15
|
-
if (currMulti) {
|
|
16
|
-
if (prevMulti) {
|
|
17
|
-
return true;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return Boolean(
|
|
21
|
-
prevPrev && !isMultilineNode(prevPrev) && prev.loc.start.line - prevPrev.loc.end.line - 1 === 0,
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return prevMulti;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function reportElementPair(context, sourceCode, prev, curr, needBlank, offsetsOf) {
|
|
29
|
-
const prevEnd = offsetsOf(prev)[1];
|
|
30
|
-
const currStart = offsetsOf(curr)[0];
|
|
31
|
-
const blankCount = curr.loc.start.line - prev.loc.end.line - 1;
|
|
32
|
-
|
|
33
|
-
if (needBlank && blankCount < 1) {
|
|
34
|
-
context.report({
|
|
35
|
-
node: curr,
|
|
36
|
-
loc: curr.loc.start,
|
|
37
|
-
messageId: 'expected',
|
|
38
|
-
fix: (fixer) => fixer.insertTextAfterRange([prevEnd, prevEnd], '\n'),
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (!needBlank && blankCount > 0) {
|
|
45
|
-
const between = sourceCode.text.slice(prevEnd, currStart);
|
|
46
|
-
const indent = between.slice(between.lastIndexOf('\n') + 1);
|
|
47
|
-
|
|
48
|
-
context.report({
|
|
49
|
-
node: curr,
|
|
50
|
-
loc: curr.loc.start,
|
|
51
|
-
messageId: 'unexpected',
|
|
52
|
-
fix: (fixer) => fixer.replaceTextRange([prevEnd, currStart], `\n${indent}`),
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function processElementChildren(context, sourceCode, children, elementTypes, textType, offsetsOf) {
|
|
58
|
-
let run = [];
|
|
59
|
-
|
|
60
|
-
const flush = () => {
|
|
61
|
-
for (let i = 1; i < run.length; i += 1) {
|
|
62
|
-
reportElementPair(
|
|
63
|
-
context,
|
|
64
|
-
sourceCode,
|
|
65
|
-
run[i - 1],
|
|
66
|
-
run[i],
|
|
67
|
-
elementNeedsBlank(run[i - 2], run[i - 1], run[i]),
|
|
68
|
-
offsetsOf,
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
run = [];
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
for (const child of children) {
|
|
76
|
-
if (elementTypes.has(child.type)) {
|
|
77
|
-
run.push(child);
|
|
78
|
-
} else if (child.type === textType && (child.value || '').trim() === '') {
|
|
79
|
-
continue;
|
|
80
|
-
} else {
|
|
81
|
-
flush();
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
flush();
|
|
86
|
-
}
|
package/eslint/vue.config.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vue ESLint configuration extending base config.
|
|
3
|
-
* Requires the optional peer dependency `vue-eslint-parser`.
|
|
4
|
-
* @param {Object} options - Configuration options
|
|
5
|
-
* @returns {Array} ESLint flat config array
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import tseslint from 'typescript-eslint';
|
|
9
|
-
import vueParser from 'vue-eslint-parser';
|
|
10
|
-
|
|
11
|
-
import { createBaseConfig } from './base.config.js';
|
|
12
|
-
import { codexPlugin } from './plugin.js';
|
|
13
|
-
|
|
14
|
-
export function createVueConfig(options = {}) {
|
|
15
|
-
const baseConfig = createBaseConfig(options);
|
|
16
|
-
|
|
17
|
-
const vueConfig = {
|
|
18
|
-
files: ['**/*.vue'],
|
|
19
|
-
languageOptions: {
|
|
20
|
-
parser: vueParser,
|
|
21
|
-
parserOptions: {
|
|
22
|
-
parser: tseslint.parser,
|
|
23
|
-
ecmaVersion: 'latest',
|
|
24
|
-
sourceType: 'module',
|
|
25
|
-
extraFileExtensions: ['.vue'],
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
plugins: {
|
|
29
|
-
'@noxickon': codexPlugin,
|
|
30
|
-
},
|
|
31
|
-
rules: {
|
|
32
|
-
'@noxickon/blank-line-spacing': 'warn',
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
return [...baseConfig, vueConfig];
|
|
37
|
-
}
|