eslint-plugin-svelte 3.20.0 → 3.22.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 +2 -0
- package/lib/main.d.ts +1 -1
- package/lib/meta.d.ts +1 -1
- package/lib/meta.js +1 -1
- package/lib/rule-types.d.ts +10 -0
- package/lib/rules/no-bind-value-on-checkable-inputs.d.ts +2 -0
- package/lib/rules/no-bind-value-on-checkable-inputs.js +73 -0
- package/lib/rules/no-conflicting-module-names.d.ts +2 -0
- package/lib/rules/no-conflicting-module-names.js +104 -0
- package/lib/utils/rules.js +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -296,6 +296,8 @@ These rules relate to possible syntax or logic errors in Svelte code:
|
|
|
296
296
|
| Rule ID | Description | |
|
|
297
297
|
|:--------|:------------|:---|
|
|
298
298
|
| [svelte/infinite-reactive-loop](https://sveltejs.github.io/eslint-plugin-svelte/rules/infinite-reactive-loop/) | Svelte runtime prevents calling the same reactive statement twice in a microtask. But between different microtask, it doesn't prevent. | :star: |
|
|
299
|
+
| [svelte/no-bind-value-on-checkable-inputs](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-bind-value-on-checkable-inputs/) | disallow useless `bind:value` on `<input type="checkbox">` and `<input type="radio">` | :bulb: |
|
|
300
|
+
| [svelte/no-conflicting-module-names](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-conflicting-module-names/) | disallow a `.svelte` component and a same-named runes module (e.g. `Foo.svelte` and `Foo.svelte.ts`) from coexisting | |
|
|
299
301
|
| [svelte/no-dom-manipulating](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-dom-manipulating/) | disallow DOM manipulating | :star: |
|
|
300
302
|
| [svelte/no-dupe-else-if-blocks](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-dupe-else-if-blocks/) | disallow duplicate conditions in `{#if}` / `{:else if}` chains | :star: |
|
|
301
303
|
| [svelte/no-dupe-on-directives](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-dupe-on-directives/) | disallow duplicate `on:` directives | :star: |
|
package/lib/main.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export declare const configs: {
|
|
|
14
14
|
export declare const rules: Record<string, Rule.RuleModule>;
|
|
15
15
|
export declare const meta: {
|
|
16
16
|
name: "eslint-plugin-svelte";
|
|
17
|
-
version: "3.
|
|
17
|
+
version: "3.22.0";
|
|
18
18
|
};
|
|
19
19
|
export declare const processors: {
|
|
20
20
|
'.svelte': typeof processor;
|
package/lib/meta.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export declare const name: "eslint-plugin-svelte";
|
|
2
|
-
export declare const version: "3.
|
|
2
|
+
export declare const version: "3.22.0";
|
package/lib/meta.js
CHANGED
package/lib/rule-types.d.ts
CHANGED
|
@@ -115,6 +115,16 @@ export interface RuleOptions {
|
|
|
115
115
|
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-at-html-tags/
|
|
116
116
|
*/
|
|
117
117
|
'svelte/no-at-html-tags'?: Linter.RuleEntry<[]>;
|
|
118
|
+
/**
|
|
119
|
+
* disallow useless `bind:value` on `<input type="checkbox">` and `<input type="radio">`
|
|
120
|
+
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-bind-value-on-checkable-inputs/
|
|
121
|
+
*/
|
|
122
|
+
'svelte/no-bind-value-on-checkable-inputs'?: Linter.RuleEntry<[]>;
|
|
123
|
+
/**
|
|
124
|
+
* disallow a `.svelte` component and a same-named runes module (e.g. `Foo.svelte` and `Foo.svelte.ts`) from coexisting
|
|
125
|
+
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-conflicting-module-names/
|
|
126
|
+
*/
|
|
127
|
+
'svelte/no-conflicting-module-names'?: Linter.RuleEntry<[]>;
|
|
118
128
|
/**
|
|
119
129
|
* disallow DOM manipulating
|
|
120
130
|
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-dom-manipulating/
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { getStaticValue } from '@eslint-community/eslint-utils';
|
|
2
|
+
import { getScope } from '../utils/ast-utils.js';
|
|
3
|
+
import { createRule } from '../utils/index.js';
|
|
4
|
+
export default createRule('no-bind-value-on-checkable-inputs', {
|
|
5
|
+
meta: {
|
|
6
|
+
hasSuggestions: true,
|
|
7
|
+
docs: {
|
|
8
|
+
description: 'disallow useless `bind:value` on `<input type="checkbox">` and `<input type="radio">`',
|
|
9
|
+
category: 'Possible Errors',
|
|
10
|
+
recommended: false
|
|
11
|
+
},
|
|
12
|
+
schema: [],
|
|
13
|
+
type: 'problem',
|
|
14
|
+
messages: {
|
|
15
|
+
checkboxValueBinding: '`bind:value` does not work on checkbox inputs. Did you mean `bind:checked` or `bind:group`?',
|
|
16
|
+
radioValueBinding: '`bind:value` does not work on radio inputs. Did you mean `bind:group`?', // svelte compiler disallows `bind:checked` on radios
|
|
17
|
+
checkedSuggestion: 'Change `bind:value` to `bind:checked`.',
|
|
18
|
+
groupSuggestion: 'Change `bind:value` to `bind:group`.'
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
create(context) {
|
|
22
|
+
return {
|
|
23
|
+
"SvelteElement[name.name='input']"(node) {
|
|
24
|
+
function getType() {
|
|
25
|
+
const typeAttr = node.startTag?.attributes.find((attr) => attr.type === 'SvelteAttribute' && attr.key.name.toLowerCase() === 'type');
|
|
26
|
+
if (!typeAttr)
|
|
27
|
+
return null;
|
|
28
|
+
if (typeAttr.value.length !== 1)
|
|
29
|
+
return null;
|
|
30
|
+
const typeValue = typeAttr.value[0];
|
|
31
|
+
if (typeValue.type === 'SvelteLiteral') {
|
|
32
|
+
// <input type="checkbox" />
|
|
33
|
+
return typeValue.value.toLowerCase();
|
|
34
|
+
}
|
|
35
|
+
if (typeValue.type === 'SvelteMustacheTag') {
|
|
36
|
+
const staticValue = getStaticValue(typeValue.expression, getScope(context, typeValue.expression));
|
|
37
|
+
if (typeof staticValue?.value !== 'string')
|
|
38
|
+
return null;
|
|
39
|
+
// <input type={"checkbox"} />
|
|
40
|
+
return staticValue.value.toLowerCase();
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const type = getType();
|
|
45
|
+
if (!type || (type !== 'checkbox' && type !== 'radio'))
|
|
46
|
+
return;
|
|
47
|
+
const bindValue = node.startTag?.attributes.find((attr) => attr.type === 'SvelteDirective' &&
|
|
48
|
+
attr.kind === 'Binding' &&
|
|
49
|
+
attr.key.name.name === 'value');
|
|
50
|
+
if (!bindValue)
|
|
51
|
+
return;
|
|
52
|
+
function suggestion(suggestionType, bindValue // make typescript happy
|
|
53
|
+
) {
|
|
54
|
+
return {
|
|
55
|
+
messageId: `${suggestionType}Suggestion`,
|
|
56
|
+
fix(fixer) {
|
|
57
|
+
if (bindValue.shorthand)
|
|
58
|
+
return fixer.replaceText(bindValue, `bind:${suggestionType}={value}`);
|
|
59
|
+
return fixer.replaceText(bindValue.key, `bind:${suggestionType}`);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
context.report({
|
|
64
|
+
node: bindValue,
|
|
65
|
+
messageId: `${type}ValueBinding`,
|
|
66
|
+
suggest: type === 'checkbox'
|
|
67
|
+
? [suggestion('checked', bindValue), suggestion('group', bindValue)]
|
|
68
|
+
: [suggestion('group', bindValue)]
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createRule } from '../utils/index.js';
|
|
4
|
+
// Extensions that plain TypeScript resolution appends to an unknown `.svelte`
|
|
5
|
+
// specifier. `.d.ts` is intentionally excluded: `Foo.svelte.d.ts` is a
|
|
6
|
+
// hand-written component declaration and does not shadow the component.
|
|
7
|
+
const MODULE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts', '.mjs', '.cjs'];
|
|
8
|
+
function isFile(filePath) {
|
|
9
|
+
try {
|
|
10
|
+
return fs.statSync(filePath).isFile();
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
// Resolve the real on-disk casing of a path's basename. On case-insensitive
|
|
17
|
+
// file systems (macOS, Windows) `fs.statSync` matches a differently-cased
|
|
18
|
+
// sibling, so the constructed name may not exist with that exact casing. Read
|
|
19
|
+
// the directory and use the actual entry name when one matches.
|
|
20
|
+
function realBasename(filePath) {
|
|
21
|
+
const wanted = path.basename(filePath);
|
|
22
|
+
try {
|
|
23
|
+
const entries = fs.readdirSync(path.dirname(filePath));
|
|
24
|
+
return (entries.find((entry) => entry === wanted) ??
|
|
25
|
+
entries.find((entry) => entry.toLowerCase() === wanted.toLowerCase()) ??
|
|
26
|
+
wanted);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return wanted;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export default createRule('no-conflicting-module-names', {
|
|
33
|
+
meta: {
|
|
34
|
+
docs: {
|
|
35
|
+
description: 'disallow a `.svelte` component and a same-named runes module (e.g. `Foo.svelte` and `Foo.svelte.ts`) from coexisting',
|
|
36
|
+
category: 'Possible Errors',
|
|
37
|
+
recommended: false
|
|
38
|
+
},
|
|
39
|
+
schema: [],
|
|
40
|
+
messages: {
|
|
41
|
+
conflictOnComponent: 'The module `{{moduleName}}` has the same name as this component. TypeScript resolves the import `{{specifier}}` to that module, not to this component. Rename `{{moduleName}}`.',
|
|
42
|
+
conflictOnModule: 'This module has the same name as the component `{{svelteName}}`. TypeScript resolves the import `{{specifier}}` to this module, not to the component. Rename this file.'
|
|
43
|
+
},
|
|
44
|
+
type: 'problem'
|
|
45
|
+
},
|
|
46
|
+
create(context) {
|
|
47
|
+
const filename = context.physicalFilename;
|
|
48
|
+
// Skip virtual/untitled files that do not exist on disk (editors, tests
|
|
49
|
+
// with fake paths). Only real files on disk are checked.
|
|
50
|
+
if (!isFile(filename)) {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
// Both sides of the collision are linted by this plugin, and a lint run
|
|
54
|
+
// may contain only one of them, so each side reports on itself.
|
|
55
|
+
if (filename.endsWith('.svelte')) {
|
|
56
|
+
return {
|
|
57
|
+
Program(node) {
|
|
58
|
+
const svelteName = path.basename(filename);
|
|
59
|
+
for (const ext of MODULE_EXTENSIONS) {
|
|
60
|
+
const modulePath = `${filename}${ext}`;
|
|
61
|
+
if (isFile(modulePath)) {
|
|
62
|
+
context.report({
|
|
63
|
+
node,
|
|
64
|
+
loc: { line: 1, column: 0 },
|
|
65
|
+
messageId: 'conflictOnComponent',
|
|
66
|
+
data: {
|
|
67
|
+
moduleName: realBasename(modulePath),
|
|
68
|
+
specifier: `./${svelteName}`
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// A module named `<name>.svelte.<ext>`. Dropping the extension gives the
|
|
78
|
+
// component path it collides with. Declaration files such as
|
|
79
|
+
// `Foo.svelte.d.ts` do not match, because dropping `.ts` leaves
|
|
80
|
+
// `Foo.svelte.d`, which is not a component name.
|
|
81
|
+
const moduleExt = MODULE_EXTENSIONS.find((ext) => filename.endsWith(ext));
|
|
82
|
+
if (moduleExt == null) {
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
const sveltePath = filename.slice(0, -moduleExt.length);
|
|
86
|
+
if (!sveltePath.endsWith('.svelte') || !isFile(sveltePath)) {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
Program(node) {
|
|
91
|
+
const svelteName = realBasename(sveltePath);
|
|
92
|
+
context.report({
|
|
93
|
+
node,
|
|
94
|
+
loc: { line: 1, column: 0 },
|
|
95
|
+
messageId: 'conflictOnModule',
|
|
96
|
+
data: {
|
|
97
|
+
svelteName,
|
|
98
|
+
specifier: `./${svelteName}`
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
});
|
package/lib/utils/rules.js
CHANGED
|
@@ -20,6 +20,8 @@ import noAddEventListener from '../rules/no-add-event-listener.js';
|
|
|
20
20
|
import noAtConstTags from '../rules/no-at-const-tags.js';
|
|
21
21
|
import noAtDebugTags from '../rules/no-at-debug-tags.js';
|
|
22
22
|
import noAtHtmlTags from '../rules/no-at-html-tags.js';
|
|
23
|
+
import noBindValueOnCheckableInputs from '../rules/no-bind-value-on-checkable-inputs.js';
|
|
24
|
+
import noConflictingModuleNames from '../rules/no-conflicting-module-names.js';
|
|
23
25
|
import noDomManipulating from '../rules/no-dom-manipulating.js';
|
|
24
26
|
import noDupeElseIfBlocks from '../rules/no-dupe-else-if-blocks.js';
|
|
25
27
|
import noDupeOnDirectives from '../rules/no-dupe-on-directives.js';
|
|
@@ -104,6 +106,8 @@ export const rules = [
|
|
|
104
106
|
noAtConstTags,
|
|
105
107
|
noAtDebugTags,
|
|
106
108
|
noAtHtmlTags,
|
|
109
|
+
noBindValueOnCheckableInputs,
|
|
110
|
+
noConflictingModuleNames,
|
|
107
111
|
noDomManipulating,
|
|
108
112
|
noDupeElseIfBlocks,
|
|
109
113
|
noDupeOnDirectives,
|