eslint-plugin-svelte 3.5.0 → 3.6.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 CHANGED
@@ -30,8 +30,8 @@
30
30
 
31
31
  ## Introduction
32
32
 
33
- `eslint-plugin-svelte` is the official [ESLint](https://eslint.org/) plugin for [Svelte](https://svelte.dev/).
34
- It leverages the AST generated by [svelte-eslint-parser](https://github.com/sveltejs/svelte-eslint-parser) to provide custom linting for Svelte.
33
+ `eslint-plugin-svelte` is the official [ESLint](https://eslint.org/) plugin for [Svelte](https://svelte.dev/).\
34
+ It leverages the AST generated by [svelte-eslint-parser](https://github.com/sveltejs/svelte-eslint-parser) to provide custom linting for Svelte.\
35
35
  Note that `eslint-plugin-svelte` and `svelte-eslint-parser` cannot be used alongside [eslint-plugin-svelte3](https://github.com/sveltejs/eslint-plugin-svelte3).
36
36
 
37
37
  <!--USAGE_SECTION_START-->
@@ -219,8 +219,8 @@ export default [
219
219
 
220
220
  ## Editor Integrations
221
221
 
222
- **Visual Studio Code**
223
- Install [dbaeumer.vscode-eslint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint).
222
+ **Visual Studio Code**\
223
+ Install [dbaeumer.vscode-eslint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint).\
224
224
  Configure `.svelte` files in `.vscode/settings.json`:
225
225
 
226
226
  ```json
@@ -247,8 +247,8 @@ This project follows [Semantic Versioning](https://semver.org/). Unlike [ESLint
247
247
  <!-- prettier-ignore-start -->
248
248
  <!--RULES_SECTION_START-->
249
249
 
250
- :wrench: Indicates that the rule is fixable, and using `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the reported problems.
251
- :bulb: Indicates that some problems reported by the rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
250
+ :wrench: Indicates that the rule is fixable, and using `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the reported problems.\
251
+ :bulb: Indicates that some problems reported by the rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).\
252
252
  :star: Indicates that the rule is included in the `plugin:svelte/recommended` config.
253
253
 
254
254
  <!--RULES_TABLE_START-->
@@ -294,6 +294,7 @@ These rules relate to better ways of doing things to help you avoid problems:
294
294
  |:--------|:------------|:---|
295
295
  | [svelte/block-lang](https://sveltejs.github.io/eslint-plugin-svelte/rules/block-lang/) | disallows the use of languages other than those specified in the configuration for the lang attribute of `<script>` and `<style>` blocks. | :bulb: |
296
296
  | [svelte/button-has-type](https://sveltejs.github.io/eslint-plugin-svelte/rules/button-has-type/) | disallow usage of button without an explicit type attribute | |
297
+ | [svelte/no-add-event-listener](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-add-event-listener/) | Warns against the use of `addEventListener` | :bulb: |
297
298
  | [svelte/no-at-debug-tags](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-at-debug-tags/) | disallow the use of `{@debug}` | :star::bulb: |
298
299
  | [svelte/no-ignored-unsubscribe](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-ignored-unsubscribe/) | disallow ignoring the unsubscribe method returned by the `subscribe()` on Svelte stores. | |
299
300
  | [svelte/no-immutable-reactive-statements](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-immutable-reactive-statements/) | disallow reactive statements that don't reference reactive values. | :star: |
@@ -310,6 +311,7 @@ These rules relate to better ways of doing things to help you avoid problems:
310
311
  | [svelte/no-useless-mustaches](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-useless-mustaches/) | disallow unnecessary mustache interpolations | :star::wrench: |
311
312
  | [svelte/prefer-const](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-const/) | Require `const` declarations for variables that are never reassigned after declared | :wrench: |
312
313
  | [svelte/prefer-destructured-store-props](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-destructured-store-props/) | destructure values from object stores for better change tracking & fewer redraws | :bulb: |
314
+ | [svelte/prefer-writable-derived](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-writable-derived/) | Prefer using writable $derived instead of $state and $effect | :star::bulb: |
313
315
  | [svelte/require-each-key](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-each-key/) | require keyed `{#each}` block | :star: |
314
316
  | [svelte/require-event-dispatcher-types](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-dispatcher-types/) | require type parameters for `createEventDispatcher` | :star: |
315
317
  | [svelte/require-optimized-style-attribute](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-optimized-style-attribute/) | require style attributes that can be optimized | |
@@ -337,6 +339,7 @@ These rules relate to style guidelines, and are therefore quite subjective:
337
339
  | [svelte/no-spaces-around-equal-signs-in-attribute](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-spaces-around-equal-signs-in-attribute/) | disallow spaces around equal signs in attribute | :wrench: |
338
340
  | [svelte/prefer-class-directive](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-class-directive/) | require class directives instead of ternary expressions | :wrench: |
339
341
  | [svelte/prefer-style-directive](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-style-directive/) | require style directives instead of style attribute | :wrench: |
342
+ | [svelte/require-event-prefix](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-prefix/) | require component event names to start with "on" | |
340
343
  | [svelte/shorthand-attribute](https://sveltejs.github.io/eslint-plugin-svelte/rules/shorthand-attribute/) | enforce use of shorthand syntax in attribute | :wrench: |
341
344
  | [svelte/shorthand-directive](https://sveltejs.github.io/eslint-plugin-svelte/rules/shorthand-directive/) | enforce use of shorthand syntax in directives | :wrench: |
342
345
  | [svelte/sort-attributes](https://sveltejs.github.io/eslint-plugin-svelte/rules/sort-attributes/) | enforce order of attributes | :wrench: |
@@ -398,7 +401,7 @@ These rules relate to this plugin works:
398
401
 
399
402
  ## Contributing
400
403
 
401
- Contributions are welcome! Please open an issue or submit a PR. For more details, see [CONTRIBUTING.md](./CONTRIBUTING.md).
404
+ Contributions are welcome! Please open an issue or submit a PR. For more details, see [CONTRIBUTING.md](./CONTRIBUTING.md).\
402
405
  Refer to [svelte-eslint-parser](https://github.com/sveltejs/svelte-eslint-parser) for AST details.
403
406
 
404
407
  <!--DOCS_IGNORE_END-->
@@ -33,6 +33,7 @@ const config = [
33
33
  'svelte/no-unused-svelte-ignore': 'error',
34
34
  'svelte/no-useless-children-snippet': 'error',
35
35
  'svelte/no-useless-mustaches': 'error',
36
+ 'svelte/prefer-writable-derived': 'error',
36
37
  'svelte/require-each-key': 'error',
37
38
  'svelte/require-event-dispatcher-types': 'error',
38
39
  'svelte/require-store-reactive-access': 'error',
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.5.0";
17
+ version: "3.6.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
- export declare const name = "eslint-plugin-svelte";
2
- export declare const version = "3.5.0";
1
+ export declare const name: "eslint-plugin-svelte";
2
+ export declare const version: "3.6.0";
package/lib/meta.js CHANGED
@@ -2,4 +2,4 @@
2
2
  // This file has been automatically generated,
3
3
  // in order to update its content execute "pnpm run update"
4
4
  export const name = 'eslint-plugin-svelte';
5
- export const version = '3.5.0';
5
+ export const version = '3.6.0';
@@ -92,6 +92,11 @@ export interface RuleOptions {
92
92
  * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/mustache-spacing/
93
93
  */
94
94
  'svelte/mustache-spacing'?: Linter.RuleEntry<SvelteMustacheSpacing>;
95
+ /**
96
+ * Warns against the use of `addEventListener`
97
+ * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-add-event-listener/
98
+ */
99
+ 'svelte/no-add-event-listener'?: Linter.RuleEntry<[]>;
95
100
  /**
96
101
  * disallow the use of `{@debug}`
97
102
  * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-at-debug-tags/
@@ -299,6 +304,11 @@ export interface RuleOptions {
299
304
  * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-style-directive/
300
305
  */
301
306
  'svelte/prefer-style-directive'?: Linter.RuleEntry<[]>;
307
+ /**
308
+ * Prefer using writable $derived instead of $state and $effect
309
+ * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-writable-derived/
310
+ */
311
+ 'svelte/prefer-writable-derived'?: Linter.RuleEntry<[]>;
302
312
  /**
303
313
  * require keyed `{#each}` block
304
314
  * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/require-each-key/
@@ -309,6 +319,11 @@ export interface RuleOptions {
309
319
  * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-dispatcher-types/
310
320
  */
311
321
  'svelte/require-event-dispatcher-types'?: Linter.RuleEntry<[]>;
322
+ /**
323
+ * require component event names to start with "on"
324
+ * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-prefix/
325
+ */
326
+ 'svelte/require-event-prefix'?: Linter.RuleEntry<SvelteRequireEventPrefix>;
312
327
  /**
313
328
  * require style attributes that can be optimized
314
329
  * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/require-optimized-style-attribute/
@@ -568,6 +583,11 @@ type SveltePreferConst = [] | [
568
583
  excludedRunes?: string[];
569
584
  }
570
585
  ];
586
+ type SvelteRequireEventPrefix = [] | [
587
+ {
588
+ checkAsyncFunctions?: boolean;
589
+ }
590
+ ];
571
591
  type SvelteShorthandAttribute = [] | [
572
592
  {
573
593
  prefer?: ("always" | "never");
@@ -1,4 +1,5 @@
1
1
  import { findClassesInAttribute } from '../utils/ast-utils.js';
2
+ import { extractExpressionPrefixLiteral, extractExpressionSuffixLiteral } from '../utils/expression-affixes.js';
2
3
  import { createRule } from '../utils/index.js';
3
4
  export default createRule('consistent-selector-style', {
4
5
  meta: {
@@ -49,9 +50,19 @@ export default createRule('consistent-selector-style', {
49
50
  const checkGlobal = context.options[0]?.checkGlobal ?? false;
50
51
  const style = context.options[0]?.style ?? ['type', 'id', 'class'];
51
52
  const whitelistedClasses = [];
52
- const classSelections = new Map();
53
- const idSelections = new Map();
54
- const typeSelections = new Map();
53
+ const selections = {
54
+ class: {
55
+ exact: new Map(),
56
+ affixes: new Map(),
57
+ universalSelector: false
58
+ },
59
+ id: {
60
+ exact: new Map(),
61
+ affixes: new Map(),
62
+ universalSelector: false
63
+ },
64
+ type: new Map()
65
+ };
55
66
  /**
56
67
  * Checks selectors in a given PostCSS node
57
68
  */
@@ -89,10 +100,10 @@ export default createRule('consistent-selector-style', {
89
100
  * Checks a class selector
90
101
  */
91
102
  function checkClassSelector(node) {
92
- if (whitelistedClasses.includes(node.value)) {
103
+ if (selections.class.universalSelector || whitelistedClasses.includes(node.value)) {
93
104
  return;
94
105
  }
95
- const selection = classSelections.get(node.value) ?? [];
106
+ const selection = matchSelection(selections.class, node.value);
96
107
  for (const styleValue of style) {
97
108
  if (styleValue === 'class') {
98
109
  return;
@@ -104,7 +115,7 @@ export default createRule('consistent-selector-style', {
104
115
  });
105
116
  return;
106
117
  }
107
- if (styleValue === 'type' && canUseTypeSelector(selection, typeSelections)) {
118
+ if (styleValue === 'type' && canUseTypeSelector(selection, selections.type)) {
108
119
  context.report({
109
120
  messageId: 'classShouldBeType',
110
121
  loc: styleSelectorNodeLoc(node)
@@ -117,7 +128,10 @@ export default createRule('consistent-selector-style', {
117
128
  * Checks an ID selector
118
129
  */
119
130
  function checkIdSelector(node) {
120
- const selection = idSelections.get(node.value) ?? [];
131
+ if (selections.id.universalSelector) {
132
+ return;
133
+ }
134
+ const selection = matchSelection(selections.id, node.value);
121
135
  for (const styleValue of style) {
122
136
  if (styleValue === 'class') {
123
137
  context.report({
@@ -129,7 +143,7 @@ export default createRule('consistent-selector-style', {
129
143
  if (styleValue === 'id') {
130
144
  return;
131
145
  }
132
- if (styleValue === 'type' && canUseTypeSelector(selection, typeSelections)) {
146
+ if (styleValue === 'type' && canUseTypeSelector(selection, selections.type)) {
133
147
  context.report({
134
148
  messageId: 'idShouldBeType',
135
149
  loc: styleSelectorNodeLoc(node)
@@ -142,7 +156,7 @@ export default createRule('consistent-selector-style', {
142
156
  * Checks a type selector
143
157
  */
144
158
  function checkTypeSelector(node) {
145
- const selection = typeSelections.get(node.value) ?? [];
159
+ const selection = selections.type.get(node.value) ?? [];
146
160
  for (const styleValue of style) {
147
161
  if (styleValue === 'class') {
148
162
  context.report({
@@ -168,21 +182,42 @@ export default createRule('consistent-selector-style', {
168
182
  if (node.kind !== 'html') {
169
183
  return;
170
184
  }
171
- addToArrayMap(typeSelections, node.name.name, node);
172
- const classes = node.startTag.attributes.flatMap(findClassesInAttribute);
173
- for (const className of classes) {
174
- addToArrayMap(classSelections, className, node);
175
- }
185
+ addToArrayMap(selections.type, node.name.name, node);
176
186
  for (const attribute of node.startTag.attributes) {
177
187
  if (attribute.type === 'SvelteDirective' && attribute.kind === 'Class') {
178
188
  whitelistedClasses.push(attribute.key.name.name);
179
189
  }
180
- if (attribute.type !== 'SvelteAttribute' || attribute.key.name !== 'id') {
190
+ for (const className of findClassesInAttribute(attribute)) {
191
+ addToArrayMap(selections.class.exact, className, node);
192
+ }
193
+ if (attribute.type !== 'SvelteAttribute') {
181
194
  continue;
182
195
  }
183
196
  for (const value of attribute.value) {
184
- if (value.type === 'SvelteLiteral') {
185
- addToArrayMap(idSelections, value.value, node);
197
+ if (attribute.key.name === 'class' && value.type === 'SvelteMustacheTag') {
198
+ const prefix = extractExpressionPrefixLiteral(context, value.expression);
199
+ const suffix = extractExpressionSuffixLiteral(context, value.expression);
200
+ if (prefix === null && suffix === null) {
201
+ selections.class.universalSelector = true;
202
+ }
203
+ else {
204
+ addToArrayMap(selections.class.affixes, [prefix, suffix], node);
205
+ }
206
+ }
207
+ if (attribute.key.name === 'id') {
208
+ if (value.type === 'SvelteLiteral') {
209
+ addToArrayMap(selections.id.exact, value.value, node);
210
+ }
211
+ else if (value.type === 'SvelteMustacheTag') {
212
+ const prefix = extractExpressionPrefixLiteral(context, value.expression);
213
+ const suffix = extractExpressionSuffixLiteral(context, value.expression);
214
+ if (prefix === null && suffix === null) {
215
+ selections.id.universalSelector = true;
216
+ }
217
+ else {
218
+ addToArrayMap(selections.id.affixes, [prefix, suffix], node);
219
+ }
220
+ }
186
221
  }
187
222
  }
188
223
  }
@@ -204,6 +239,18 @@ export default createRule('consistent-selector-style', {
204
239
  function addToArrayMap(map, key, value) {
205
240
  map.set(key, (map.get(key) ?? []).concat(value));
206
241
  }
242
+ /**
243
+ * Finds all nodes in selections that could be matched by key
244
+ */
245
+ function matchSelection(selections, key) {
246
+ const selection = selections.exact.get(key) ?? [];
247
+ selections.affixes.forEach((nodes, [prefix, suffix]) => {
248
+ if ((prefix === null || key.startsWith(prefix)) && (suffix === null || key.endsWith(suffix))) {
249
+ selection.push(...nodes);
250
+ }
251
+ });
252
+ return selection;
253
+ }
207
254
  /**
208
255
  * Checks whether a given selection could be obtained using an ID selector
209
256
  */
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../types.js").RuleModule;
2
+ export default _default;
@@ -0,0 +1,58 @@
1
+ import { createRule } from '../utils/index.js';
2
+ export default createRule('no-add-event-listener', {
3
+ meta: {
4
+ docs: {
5
+ description: 'Warns against the use of `addEventListener`',
6
+ category: 'Best Practices',
7
+ recommended: false
8
+ },
9
+ hasSuggestions: true,
10
+ schema: [],
11
+ messages: {
12
+ unexpected: 'Do not use `addEventListener`. Use the `on` function from `svelte/events` instead.'
13
+ },
14
+ type: 'suggestion',
15
+ conditions: [
16
+ {
17
+ svelteVersions: ['5']
18
+ }
19
+ ]
20
+ },
21
+ create(context) {
22
+ return {
23
+ CallExpression(node) {
24
+ const { callee } = node;
25
+ let target = null;
26
+ if (callee.type === 'MemberExpression' &&
27
+ callee.property.type === 'Identifier' &&
28
+ callee.property.name === 'addEventListener') {
29
+ target = context.sourceCode.getText(callee.object);
30
+ }
31
+ else if (callee.type === 'Identifier' && callee.name === 'addEventListener') {
32
+ target = 'window';
33
+ }
34
+ if (target === null) {
35
+ return;
36
+ }
37
+ const openParen = context.sourceCode.getTokenAfter(callee);
38
+ const suggest = [];
39
+ if (openParen !== null) {
40
+ suggest.push({
41
+ desc: 'Use `on` from `svelte/events` instead',
42
+ fix(fixer) {
43
+ return [
44
+ fixer.replaceText(callee, 'on'),
45
+ fixer.insertTextAfter(openParen, `${target}, `)
46
+ ];
47
+ }
48
+ });
49
+ }
50
+ context.report({
51
+ node,
52
+ messageId: 'unexpected',
53
+ suggest
54
+ });
55
+ }
56
+ };
57
+ }
58
+ });
@@ -1,6 +1,7 @@
1
1
  import { createRule } from '../utils/index.js';
2
2
  import { ReferenceTracker } from '@eslint-community/eslint-utils';
3
3
  import { findVariable } from '../utils/ast-utils.js';
4
+ import { extractExpressionPrefixVariable } from '../utils/expression-affixes.js';
4
5
  export default createRule('no-navigation-without-base', {
5
6
  meta: {
6
7
  docs: {
@@ -163,57 +164,8 @@ function checkShallowNavigationCall(context, call, basePathNames, messageId) {
163
164
  }
164
165
  // Helper functions
165
166
  function expressionStartsWithBase(context, url, basePathNames) {
166
- switch (url.type) {
167
- case 'BinaryExpression':
168
- return binaryExpressionStartsWithBase(context, url, basePathNames);
169
- case 'Identifier':
170
- return variableStartsWithBase(context, url, basePathNames);
171
- case 'MemberExpression':
172
- return memberExpressionStartsWithBase(url, basePathNames);
173
- case 'TemplateLiteral':
174
- return templateLiteralStartsWithBase(context, url, basePathNames);
175
- default:
176
- return false;
177
- }
178
- }
179
- function binaryExpressionStartsWithBase(context, url, basePathNames) {
180
- return (url.left.type !== 'PrivateIdentifier' &&
181
- expressionStartsWithBase(context, url.left, basePathNames));
182
- }
183
- function memberExpressionStartsWithBase(url, basePathNames) {
184
- return url.property.type === 'Identifier' && basePathNames.has(url.property);
185
- }
186
- function variableStartsWithBase(context, url, basePathNames) {
187
- if (basePathNames.has(url)) {
188
- return true;
189
- }
190
- const variable = findVariable(context, url);
191
- if (variable === null ||
192
- variable.identifiers.length !== 1 ||
193
- variable.identifiers[0].parent.type !== 'VariableDeclarator' ||
194
- variable.identifiers[0].parent.init === null) {
195
- return false;
196
- }
197
- return expressionStartsWithBase(context, variable.identifiers[0].parent.init, basePathNames);
198
- }
199
- function templateLiteralStartsWithBase(context, url, basePathNames) {
200
- const startingIdentifier = extractLiteralStartingExpression(url);
201
- return (startingIdentifier !== undefined &&
202
- expressionStartsWithBase(context, startingIdentifier, basePathNames));
203
- }
204
- function extractLiteralStartingExpression(templateLiteral) {
205
- const literalParts = [...templateLiteral.expressions, ...templateLiteral.quasis].sort((a, b) => a.range[0] < b.range[0] ? -1 : 1);
206
- for (const part of literalParts) {
207
- if (part.type === 'TemplateElement' && part.value.raw === '') {
208
- // Skip empty quasi in the begining
209
- continue;
210
- }
211
- if (part.type !== 'TemplateElement') {
212
- return part;
213
- }
214
- return undefined;
215
- }
216
- return undefined;
167
+ const prefixVariable = extractExpressionPrefixVariable(context, url);
168
+ return prefixVariable !== null && basePathNames.has(prefixVariable);
217
169
  }
218
170
  function expressionIsEmpty(url) {
219
171
  return ((url.type === 'Literal' && url.value === '') ||
@@ -1,6 +1,8 @@
1
1
  import { getSvelteCompileWarnings } from '../shared/svelte-compile-warns/index.js';
2
2
  import { createRule } from '../utils/index.js';
3
3
  import { getSvelteIgnoreItems } from '../shared/svelte-compile-warns/ignore-comment.js';
4
+ import { VERSION as SVELTE_VERSION } from 'svelte/compiler';
5
+ import semver from 'semver';
4
6
  export default createRule('no-unused-svelte-ignore', {
5
7
  meta: {
6
8
  docs: {
@@ -40,6 +42,14 @@ export default createRule('no-unused-svelte-ignore', {
40
42
  return {};
41
43
  }
42
44
  for (const unused of warnings.unusedIgnores) {
45
+ if (unused.code === 'reactive-component' && semver.satisfies(SVELTE_VERSION, '<5')) {
46
+ // Svelte v4 `reactive-component` warnings are not emitted
47
+ // when we use the `generate: false` compiler option.
48
+ // This is probably not the intended behavior of Svelte v4, but it's not going to be fixed,
49
+ // so as a workaround we'll ignore the `reactive-component` warnings.
50
+ // See https://github.com/sveltejs/eslint-plugin-svelte/issues/1192
51
+ continue;
52
+ }
43
53
  context.report({
44
54
  loc: {
45
55
  start: sourceCode.getLocFromIndex(unused.range[0]),
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../types.js").RuleModule;
2
+ export default _default;
@@ -0,0 +1,107 @@
1
+ import { createRule } from '../utils/index.js';
2
+ import { getScope } from '../utils/ast-utils.js';
3
+ import { VERSION as SVELTE_VERSION } from 'svelte/compiler';
4
+ import semver from 'semver';
5
+ // Writable derived were introduced in Svelte 5.25.0
6
+ const shouldRun = semver.satisfies(SVELTE_VERSION, '>=5.25.0');
7
+ function isEffectOrEffectPre(node) {
8
+ if (node.callee.type === 'Identifier') {
9
+ return node.callee.name === '$effect';
10
+ }
11
+ if (node.callee.type === 'MemberExpression') {
12
+ return (node.callee.object.type === 'Identifier' &&
13
+ node.callee.object.name === '$effect' &&
14
+ node.callee.property.type === 'Identifier' &&
15
+ node.callee.property.name === 'pre');
16
+ }
17
+ return false;
18
+ }
19
+ function isValidFunctionArgument(argument) {
20
+ if ((argument.type !== 'FunctionExpression' && argument.type !== 'ArrowFunctionExpression') ||
21
+ argument.params.length !== 0) {
22
+ return false;
23
+ }
24
+ if (argument.body.type !== 'BlockStatement') {
25
+ return false;
26
+ }
27
+ return argument.body.body.length === 1;
28
+ }
29
+ function isValidAssignment(statement) {
30
+ if (statement.type !== 'ExpressionStatement')
31
+ return false;
32
+ const { expression } = statement;
33
+ return (expression.type === 'AssignmentExpression' &&
34
+ expression.operator === '=' &&
35
+ expression.left.type === 'Identifier');
36
+ }
37
+ function isStateVariable(init) {
38
+ return (init?.type === 'CallExpression' &&
39
+ init.callee.type === 'Identifier' &&
40
+ init.callee.name === '$state');
41
+ }
42
+ export default createRule('prefer-writable-derived', {
43
+ meta: {
44
+ docs: {
45
+ description: 'Prefer using writable $derived instead of $state and $effect',
46
+ category: 'Best Practices',
47
+ recommended: true
48
+ },
49
+ schema: [],
50
+ messages: {
51
+ unexpected: 'Prefer using writable $derived instead of $state and $effect',
52
+ suggestRewrite: 'Rewrite $state and $effect to $derived'
53
+ },
54
+ type: 'suggestion',
55
+ conditions: [
56
+ {
57
+ svelteVersions: ['5'],
58
+ runes: [true, 'undetermined']
59
+ }
60
+ ],
61
+ hasSuggestions: true
62
+ },
63
+ create(context) {
64
+ if (!shouldRun) {
65
+ return {};
66
+ }
67
+ return {
68
+ CallExpression: (node) => {
69
+ if (!isEffectOrEffectPre(node) || node.arguments.length !== 1) {
70
+ return;
71
+ }
72
+ const argument = node.arguments[0];
73
+ if (!isValidFunctionArgument(argument)) {
74
+ return;
75
+ }
76
+ const statement = argument.body.body[0];
77
+ if (!isValidAssignment(statement)) {
78
+ return;
79
+ }
80
+ const { left, right } = statement.expression;
81
+ const scope = getScope(context, statement);
82
+ const reference = scope.references.find((ref) => ref.identifier.type === 'Identifier' && ref.identifier.name === left.name);
83
+ const def = reference?.resolved?.defs?.[0];
84
+ if (!def || def.type !== 'Variable' || def.node.type !== 'VariableDeclarator') {
85
+ return;
86
+ }
87
+ const { init } = def.node;
88
+ if (!isStateVariable(init)) {
89
+ return;
90
+ }
91
+ context.report({
92
+ node: def.node,
93
+ messageId: 'unexpected',
94
+ suggest: [
95
+ {
96
+ messageId: 'suggestRewrite',
97
+ fix: (fixer) => {
98
+ const rightCode = context.sourceCode.getText(right);
99
+ return [fixer.replaceText(init, `$derived(${rightCode})`), fixer.remove(node)];
100
+ }
101
+ }
102
+ ]
103
+ });
104
+ }
105
+ };
106
+ }
107
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../types.js").RuleModule;
2
+ export default _default;
@@ -0,0 +1,93 @@
1
+ import { createRule } from '../utils/index.js';
2
+ import { getTypeScriptTools, isMethodSymbol, isPropertySignatureKind, isFunctionTypeKind, isMethodSignatureKind, isTypeReferenceKind, isIdentifierKind } from '../utils/ts-utils/index.js';
3
+ export default createRule('require-event-prefix', {
4
+ meta: {
5
+ docs: {
6
+ description: 'require component event names to start with "on"',
7
+ category: 'Stylistic Issues',
8
+ conflictWithPrettier: false,
9
+ recommended: false
10
+ },
11
+ schema: [
12
+ {
13
+ type: 'object',
14
+ properties: {
15
+ checkAsyncFunctions: {
16
+ type: 'boolean'
17
+ }
18
+ },
19
+ additionalProperties: false
20
+ }
21
+ ],
22
+ messages: {
23
+ nonPrefixedFunction: 'Component event name must start with "on".'
24
+ },
25
+ type: 'suggestion',
26
+ conditions: [
27
+ {
28
+ svelteVersions: ['5'],
29
+ svelteFileTypes: ['.svelte']
30
+ }
31
+ ]
32
+ },
33
+ create(context) {
34
+ const tsTools = getTypeScriptTools(context);
35
+ if (!tsTools) {
36
+ return {};
37
+ }
38
+ const checkAsyncFunctions = context.options[0]?.checkAsyncFunctions ?? false;
39
+ return {
40
+ CallExpression(node) {
41
+ const propsType = getPropsType(node, tsTools);
42
+ if (propsType === undefined) {
43
+ return;
44
+ }
45
+ for (const property of propsType.getProperties()) {
46
+ if (isFunctionLike(property, tsTools) &&
47
+ !property.getName().startsWith('on') &&
48
+ (checkAsyncFunctions || !isFunctionAsync(property, tsTools))) {
49
+ const declarationTsNode = property.getDeclarations()?.[0];
50
+ const declarationEstreeNode = declarationTsNode !== undefined
51
+ ? tsTools.service.tsNodeToESTreeNodeMap.get(declarationTsNode)
52
+ : undefined;
53
+ context.report({
54
+ node: declarationEstreeNode ?? node,
55
+ messageId: 'nonPrefixedFunction'
56
+ });
57
+ }
58
+ }
59
+ }
60
+ };
61
+ }
62
+ });
63
+ function getPropsType(node, tsTools) {
64
+ if (node.callee.type !== 'Identifier' ||
65
+ node.callee.name !== '$props' ||
66
+ node.parent.type !== 'VariableDeclarator') {
67
+ return undefined;
68
+ }
69
+ const tsNode = tsTools.service.esTreeNodeToTSNodeMap.get(node.parent.id);
70
+ if (tsNode === undefined) {
71
+ return undefined;
72
+ }
73
+ return tsTools.service.program.getTypeChecker().getTypeAtLocation(tsNode);
74
+ }
75
+ function isFunctionLike(functionSymbol, tsTools) {
76
+ return (isMethodSymbol(functionSymbol, tsTools.ts) ||
77
+ (functionSymbol.valueDeclaration !== undefined &&
78
+ isPropertySignatureKind(functionSymbol.valueDeclaration, tsTools.ts) &&
79
+ functionSymbol.valueDeclaration.type !== undefined &&
80
+ isFunctionTypeKind(functionSymbol.valueDeclaration.type, tsTools.ts)));
81
+ }
82
+ function isFunctionAsync(functionSymbol, tsTools) {
83
+ return (functionSymbol.getDeclarations()?.some((declaration) => {
84
+ if (!isMethodSignatureKind(declaration, tsTools.ts)) {
85
+ return false;
86
+ }
87
+ if (declaration.type === undefined || !isTypeReferenceKind(declaration.type, tsTools.ts)) {
88
+ return false;
89
+ }
90
+ return (isIdentifierKind(declaration.type.typeName, tsTools.ts) &&
91
+ declaration.type.typeName.escapedText === 'Promise');
92
+ }) ?? false);
93
+ }
@@ -0,0 +1,6 @@
1
+ import type { TSESTree } from '@typescript-eslint/types';
2
+ import type { RuleContext } from '../types.js';
3
+ import type { AST } from 'svelte-eslint-parser';
4
+ export declare function extractExpressionPrefixVariable(context: RuleContext, expression: TSESTree.Expression): TSESTree.Identifier | null;
5
+ export declare function extractExpressionPrefixLiteral(context: RuleContext, expression: AST.SvelteLiteral | TSESTree.Node): string | null;
6
+ export declare function extractExpressionSuffixLiteral(context: RuleContext, expression: AST.SvelteLiteral | TSESTree.Node): string | null;
@@ -0,0 +1,138 @@
1
+ import { findVariable } from './ast-utils.js';
2
+ // Variable prefix extraction
3
+ export function extractExpressionPrefixVariable(context, expression) {
4
+ switch (expression.type) {
5
+ case 'BinaryExpression':
6
+ return extractBinaryExpressionPrefixVariable(context, expression);
7
+ case 'Identifier':
8
+ return extractVariablePrefixVariable(context, expression);
9
+ case 'MemberExpression':
10
+ return extractMemberExpressionPrefixVariable(expression);
11
+ case 'TemplateLiteral':
12
+ return extractTemplateLiteralPrefixVariable(context, expression);
13
+ default:
14
+ return null;
15
+ }
16
+ }
17
+ function extractBinaryExpressionPrefixVariable(context, expression) {
18
+ return expression.left.type !== 'PrivateIdentifier'
19
+ ? extractExpressionPrefixVariable(context, expression.left)
20
+ : null;
21
+ }
22
+ function extractVariablePrefixVariable(context, expression) {
23
+ const variable = findVariable(context, expression);
24
+ if (variable === null ||
25
+ variable.identifiers.length !== 1 ||
26
+ variable.identifiers[0].parent.type !== 'VariableDeclarator' ||
27
+ variable.identifiers[0].parent.init === null) {
28
+ return expression;
29
+ }
30
+ return (extractExpressionPrefixVariable(context, variable.identifiers[0].parent.init) ?? expression);
31
+ }
32
+ function extractMemberExpressionPrefixVariable(expression) {
33
+ return expression.property.type === 'Identifier' ? expression.property : null;
34
+ }
35
+ function extractTemplateLiteralPrefixVariable(context, expression) {
36
+ const literalParts = [...expression.expressions, ...expression.quasis].sort((a, b) => a.range[0] < b.range[0] ? -1 : 1);
37
+ for (const part of literalParts) {
38
+ if (part.type === 'TemplateElement' && part.value.raw === '') {
39
+ // Skip empty quasi in the begining
40
+ continue;
41
+ }
42
+ if (part.type !== 'TemplateElement') {
43
+ return extractExpressionPrefixVariable(context, part);
44
+ }
45
+ return null;
46
+ }
47
+ return null;
48
+ }
49
+ // Literal prefix extraction
50
+ export function extractExpressionPrefixLiteral(context, expression) {
51
+ switch (expression.type) {
52
+ case 'BinaryExpression':
53
+ return extractBinaryExpressionPrefixLiteral(context, expression);
54
+ case 'Identifier':
55
+ return extractVariablePrefixLiteral(context, expression);
56
+ case 'Literal':
57
+ return typeof expression.value === 'string' ? expression.value : null;
58
+ case 'SvelteLiteral':
59
+ return expression.value;
60
+ case 'TemplateLiteral':
61
+ return extractTemplateLiteralPrefixLiteral(context, expression);
62
+ default:
63
+ return null;
64
+ }
65
+ }
66
+ function extractBinaryExpressionPrefixLiteral(context, expression) {
67
+ return expression.left.type !== 'PrivateIdentifier'
68
+ ? extractExpressionPrefixLiteral(context, expression.left)
69
+ : null;
70
+ }
71
+ function extractVariablePrefixLiteral(context, expression) {
72
+ const variable = findVariable(context, expression);
73
+ if (variable === null ||
74
+ variable.identifiers.length !== 1 ||
75
+ variable.identifiers[0].parent.type !== 'VariableDeclarator' ||
76
+ variable.identifiers[0].parent.init === null) {
77
+ return null;
78
+ }
79
+ return extractExpressionPrefixLiteral(context, variable.identifiers[0].parent.init);
80
+ }
81
+ function extractTemplateLiteralPrefixLiteral(context, expression) {
82
+ const literalParts = [...expression.expressions, ...expression.quasis].sort((a, b) => a.range[0] < b.range[0] ? -1 : 1);
83
+ for (const part of literalParts) {
84
+ if (part.type === 'TemplateElement') {
85
+ if (part.value.raw === '') {
86
+ // Skip empty quasi
87
+ continue;
88
+ }
89
+ return part.value.raw;
90
+ }
91
+ return extractExpressionPrefixLiteral(context, part);
92
+ }
93
+ return null;
94
+ }
95
+ // Literal suffix extraction
96
+ export function extractExpressionSuffixLiteral(context, expression) {
97
+ switch (expression.type) {
98
+ case 'BinaryExpression':
99
+ return extractBinaryExpressionSuffixLiteral(context, expression);
100
+ case 'Identifier':
101
+ return extractVariableSuffixLiteral(context, expression);
102
+ case 'Literal':
103
+ return typeof expression.value === 'string' ? expression.value : null;
104
+ case 'SvelteLiteral':
105
+ return expression.value;
106
+ case 'TemplateLiteral':
107
+ return extractTemplateLiteralSuffixLiteral(context, expression);
108
+ default:
109
+ return null;
110
+ }
111
+ }
112
+ function extractBinaryExpressionSuffixLiteral(context, expression) {
113
+ return extractExpressionSuffixLiteral(context, expression.right);
114
+ }
115
+ function extractVariableSuffixLiteral(context, expression) {
116
+ const variable = findVariable(context, expression);
117
+ if (variable === null ||
118
+ variable.identifiers.length !== 1 ||
119
+ variable.identifiers[0].parent.type !== 'VariableDeclarator' ||
120
+ variable.identifiers[0].parent.init === null) {
121
+ return null;
122
+ }
123
+ return extractExpressionSuffixLiteral(context, variable.identifiers[0].parent.init);
124
+ }
125
+ function extractTemplateLiteralSuffixLiteral(context, expression) {
126
+ const literalParts = [...expression.expressions, ...expression.quasis].sort((a, b) => a.range[0] < b.range[0] ? -1 : 1);
127
+ for (const part of literalParts.reverse()) {
128
+ if (part.type === 'TemplateElement') {
129
+ if (part.value.raw === '') {
130
+ // Skip empty quasi
131
+ continue;
132
+ }
133
+ return part.value.raw;
134
+ }
135
+ return extractExpressionSuffixLiteral(context, part);
136
+ }
137
+ return null;
138
+ }
@@ -15,6 +15,7 @@ import indent from '../rules/indent.js';
15
15
  import infiniteReactiveLoop from '../rules/infinite-reactive-loop.js';
16
16
  import maxAttributesPerLine from '../rules/max-attributes-per-line.js';
17
17
  import mustacheSpacing from '../rules/mustache-spacing.js';
18
+ import noAddEventListener from '../rules/no-add-event-listener.js';
18
19
  import noAtDebugTags from '../rules/no-at-debug-tags.js';
19
20
  import noAtHtmlTags from '../rules/no-at-html-tags.js';
20
21
  import noDomManipulating from '../rules/no-dom-manipulating.js';
@@ -56,8 +57,10 @@ import preferClassDirective from '../rules/prefer-class-directive.js';
56
57
  import preferConst from '../rules/prefer-const.js';
57
58
  import preferDestructuredStoreProps from '../rules/prefer-destructured-store-props.js';
58
59
  import preferStyleDirective from '../rules/prefer-style-directive.js';
60
+ import preferWritableDerived from '../rules/prefer-writable-derived.js';
59
61
  import requireEachKey from '../rules/require-each-key.js';
60
62
  import requireEventDispatcherTypes from '../rules/require-event-dispatcher-types.js';
63
+ import requireEventPrefix from '../rules/require-event-prefix.js';
61
64
  import requireOptimizedStyleAttribute from '../rules/require-optimized-style-attribute.js';
62
65
  import requireStoreCallbacksUseSetParam from '../rules/require-store-callbacks-use-set-param.js';
63
66
  import requireStoreReactiveAccess from '../rules/require-store-reactive-access.js';
@@ -89,6 +92,7 @@ export const rules = [
89
92
  infiniteReactiveLoop,
90
93
  maxAttributesPerLine,
91
94
  mustacheSpacing,
95
+ noAddEventListener,
92
96
  noAtDebugTags,
93
97
  noAtHtmlTags,
94
98
  noDomManipulating,
@@ -130,8 +134,10 @@ export const rules = [
130
134
  preferConst,
131
135
  preferDestructuredStoreProps,
132
136
  preferStyleDirective,
137
+ preferWritableDerived,
133
138
  requireEachKey,
134
139
  requireEventDispatcherTypes,
140
+ requireEventPrefix,
135
141
  requireOptimizedStyleAttribute,
136
142
  requireStoreCallbacksUseSetParam,
137
143
  requireStoreReactiveAccess,
@@ -112,3 +112,27 @@ export declare function getTypeName(type: TS.Type, tsTools: TSTools): string;
112
112
  * Return the type of the given property in the given type, or undefined if no such property exists
113
113
  */
114
114
  export declare function getTypeOfPropertyOfType(type: TS.Type, name: string, checker: TS.TypeChecker): TS.Type | undefined;
115
+ /**
116
+ * Check whether the given symbol is a method type or not.
117
+ */
118
+ export declare function isMethodSymbol(type: TS.Symbol, ts: TypeScript): boolean;
119
+ /**
120
+ * Check whether the given node is a property signature kind or not.
121
+ */
122
+ export declare function isPropertySignatureKind(node: TS.Node, ts: TypeScript): node is TS.PropertySignature;
123
+ /**
124
+ * Check whether the given node is a function type kind or not.
125
+ */
126
+ export declare function isFunctionTypeKind(node: TS.Node, ts: TypeScript): node is TS.FunctionTypeNode;
127
+ /**
128
+ * Check whether the given node is a method signature kind or not.
129
+ */
130
+ export declare function isMethodSignatureKind(node: TS.Node, ts: TypeScript): node is TS.MethodSignature;
131
+ /**
132
+ * Check whether the given node is a type reference kind or not.
133
+ */
134
+ export declare function isTypeReferenceKind(node: TS.Node, ts: TypeScript): node is TS.TypeReferenceNode;
135
+ /**
136
+ * Check whether the given node is an identifier kind or not.
137
+ */
138
+ export declare function isIdentifierKind(node: TS.Node, ts: TypeScript): node is TS.Identifier;
@@ -260,3 +260,39 @@ export function getTypeOfPropertyOfType(type, name, checker) {
260
260
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- getTypeOfPropertyOfType is an internal API of TS.
261
261
  return checker.getTypeOfPropertyOfType(type, name);
262
262
  }
263
+ /**
264
+ * Check whether the given symbol is a method type or not.
265
+ */
266
+ export function isMethodSymbol(type, ts) {
267
+ return (type.getFlags() & ts.SymbolFlags.Method) !== 0;
268
+ }
269
+ /**
270
+ * Check whether the given node is a property signature kind or not.
271
+ */
272
+ export function isPropertySignatureKind(node, ts) {
273
+ return node.kind === ts.SyntaxKind.PropertySignature;
274
+ }
275
+ /**
276
+ * Check whether the given node is a function type kind or not.
277
+ */
278
+ export function isFunctionTypeKind(node, ts) {
279
+ return node.kind === ts.SyntaxKind.FunctionType;
280
+ }
281
+ /**
282
+ * Check whether the given node is a method signature kind or not.
283
+ */
284
+ export function isMethodSignatureKind(node, ts) {
285
+ return node.kind === ts.SyntaxKind.MethodSignature;
286
+ }
287
+ /**
288
+ * Check whether the given node is a type reference kind or not.
289
+ */
290
+ export function isTypeReferenceKind(node, ts) {
291
+ return node.kind === ts.SyntaxKind.TypeReference;
292
+ }
293
+ /**
294
+ * Check whether the given node is an identifier kind or not.
295
+ */
296
+ export function isIdentifierKind(node, ts) {
297
+ return node.kind === ts.SyntaxKind.Identifier;
298
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-svelte",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "description": "ESLint plugin for Svelte using AST",
5
5
  "repository": "git+https://github.com/sveltejs/eslint-plugin-svelte.git",
6
6
  "homepage": "https://sveltejs.github.io/eslint-plugin-svelte",
@@ -34,9 +34,8 @@
34
34
  "dependencies": {
35
35
  "@eslint-community/eslint-utils": "^4.4.1",
36
36
  "@jridgewell/sourcemap-codec": "^1.5.0",
37
- "eslint-compat-utils": "^0.6.4",
38
37
  "esutils": "^2.0.3",
39
- "known-css-properties": "^0.35.0",
38
+ "known-css-properties": "^0.36.0",
40
39
  "postcss": "^8.4.49",
41
40
  "postcss-load-config": "^3.1.4",
42
41
  "postcss-safe-parser": "^7.0.0",