diff --git a/.editorconfig b/.editorconfig index d4479bea2..9364f19c0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,3 +5,9 @@ end_of_line = lf indent_size = 2 indent_style = tab trim_trailing_whitespace = true + +[docs/rules/no-trailing-spaces.md] +trim_trailing_whitespace = false + +[packages/eslint-plugin-svelte/tests/fixtures/**/*-input.svelte] +trim_trailing_whitespace = false diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index ce9bdc71a..e0e7fc31d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -68,13 +68,13 @@ body: attributes: label: Link to **GitHub Repo** with Minimal Reproducible Example description: | - Create a minimal reproduction of the problem. **A minimal reproduction is required** so that others can help debug your issue. If a report is vague (e.g. just a generic error message) and has no reproduction, it may be closed. - [Why Reproductions are Required](https://antfu.me/posts/why-reproductions-are-required) + Create a minimal reproduction of the problem. **A minimal reproduction is required** so that others can help debug your issue. If a report is vague (e.g. just a generic error message) and has no reproduction, it may be closed.\ + [Why Reproductions are Required](https://antfu.me/posts/why-reproductions-are-required)\ - Be sure to share the repo on GitHub. GitHub's repo is ready to debug using Codespace. - Please DON'T USE non-GitHub repos such as GitLab as repro. - It takes me a long time to prepare my local PC for debugging. - Please DON'T USE stackblitz as a repro. + Be sure to share the repo on GitHub. GitHub's repo is ready to debug using Codespace.\ + Please DON'T USE non-GitHub repos such as GitLab as repro.\ + It takes me a long time to prepare my local PC for debugging.\ + Please DON'T USE stackblitz as a repro.\ We can't debug eslint with stackblitz. placeholder: | https://github.com/[your]/[repo] diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 7a81ca70f..88dc427e9 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -7,7 +7,7 @@ body: attributes: label: Description description: | - A clear and concise description of the new feature. + A clear and concise description of the new feature.\ Also give a few code examples. validations: required: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1f1d95462..fcddde4a3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,7 +34,7 @@ Rule tests typically use fixtures. For example, for the `indent` rule, the test - The `invalid` directory contains test cases where the rule should report errors. - The `valid` directory contains test cases where no errors are reported. -Fixture input files should be named `*-input.svelte` and are automatically collected. +Fixture input files should be named `*-input.svelte` and are automatically collected.\ If configuration is needed, include a JSON file: - For a specific test file (e.g., `my-test-input.svelte`), add `my-test-config.json`. @@ -52,7 +52,7 @@ pnpm run test -- -g indent Refer to [this Stack Overflow post](https://stackoverflow.com/questions/10832031/how-to-run-a-single-test-with-mocha) for details. -To test a single file (e.g., `my-test-input.svelte`), add a `my-test-config.json` with `{"only": true}`. +To test a single file (e.g., `my-test-input.svelte`), add a `my-test-config.json` with `{"only": true}`. (Remember to remove `{"only": true}` before submitting a pull request.) ## Preview Docs diff --git a/README.md b/README.md index c379a3d82..9cccc33bd 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,8 @@ ## Introduction -`eslint-plugin-svelte` is the official [ESLint](https://eslint.org/) plugin for [Svelte](https://svelte.dev/). -It leverages the AST generated by [svelte-eslint-parser](https://github.com/sveltejs/svelte-eslint-parser) to provide custom linting for Svelte. +`eslint-plugin-svelte` is the official [ESLint](https://eslint.org/) plugin for [Svelte](https://svelte.dev/).\ +It leverages the AST generated by [svelte-eslint-parser](https://github.com/sveltejs/svelte-eslint-parser) to provide custom linting for Svelte.\ Note that `eslint-plugin-svelte` and `svelte-eslint-parser` cannot be used alongside [eslint-plugin-svelte3](https://github.com/sveltejs/eslint-plugin-svelte3). @@ -219,8 +219,8 @@ export default [ ## Editor Integrations -**Visual Studio Code** -Install [dbaeumer.vscode-eslint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint). +**Visual Studio Code**\ +Install [dbaeumer.vscode-eslint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint).\ Configure `.svelte` files in `.vscode/settings.json`: ```json @@ -247,8 +247,8 @@ This project follows [Semantic Versioning](https://semver.org/). Unlike [ESLint -: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. -: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). +: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.\ +: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).\ :star: Indicates that the rule is included in the `plugin:svelte/recommended` config. @@ -271,6 +271,7 @@ These rules relate to possible syntax or logic errors in Svelte code: | [svelte/no-reactive-reassign](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-reactive-reassign/) | disallow reassigning reactive values | :star: | | [svelte/no-shorthand-style-property-overrides](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-shorthand-style-property-overrides/) | disallow shorthand style properties that override related longhand properties | :star: | | [svelte/no-store-async](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-store-async/) | disallow using async/await inside svelte stores because it causes issues with the auto-unsubscribing features | :star: | +| [svelte/no-top-level-browser-globals](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-top-level-browser-globals/) | disallow using top-level browser global variables | | | [svelte/no-unknown-style-directive-property](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unknown-style-directive-property/) | disallow unknown `style:property` | :star: | | [svelte/require-store-callbacks-use-set-param](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-store-callbacks-use-set-param/) | store callbacks must use `set` param | :bulb: | | [svelte/require-store-reactive-access](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-store-reactive-access/) | disallow to use of the store itself as an operand. Need to use $ prefix or get function. | :star::wrench: | @@ -294,6 +295,7 @@ These rules relate to better ways of doing things to help you avoid problems: |:--------|:------------|:---| | [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 ` +``` + + + +```svelte + + +``` + +## :books: Further reading + +- [svelte - event delegation] +- [svelte/events `on` documentation] + +[svelte - event delegation]: https://svelte.dev/docs/svelte/basic-markup#Events-Event-delegation +[svelte/events `on` documentation]: https://svelte.dev/docs/svelte/svelte-events#on + +## :rocket: Version + +This rule was introduced in eslint-plugin-svelte v3.6.0 + +## :mag: Implementation + +- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/no-add-event-listener.ts) +- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/no-add-event-listener.ts) diff --git a/docs/rules/no-at-html-tags.md b/docs/rules/no-at-html-tags.md index bdec6d96a..aba37a4bb 100644 --- a/docs/rules/no-at-html-tags.md +++ b/docs/rules/no-at-html-tags.md @@ -40,7 +40,7 @@ If you are certain the content passed to `{@html}` is sanitized HTML you can dis ## :books: Further Reading -- [Svelte - Tutorial > 1. Introduction / HTML tags](https://svelte.dev/tutorial/html-tags) +- [Svelte - Tutorial > Basic Svelte / Introduction / HTML tags](https://svelte.dev/tutorial/svelte/html-tags) ## :rocket: Version diff --git a/docs/rules/no-dynamic-slot-name.md b/docs/rules/no-dynamic-slot-name.md index 805e5e923..8ad69b959 100644 --- a/docs/rules/no-dynamic-slot-name.md +++ b/docs/rules/no-dynamic-slot-name.md @@ -15,7 +15,7 @@ since: 'v0.14.0' ## :book: Rule Details -This rule reports the dynamically specified `` name. +This rule reports the dynamically specified `` name.\ Dynamic `` names are not allowed in Svelte, so you must use static names. The auto-fix of this rule can be replaced with a static `` name if the expression given to the `` name is static and resolvable. diff --git a/docs/rules/no-extra-reactive-curlies.md b/docs/rules/no-extra-reactive-curlies.md index 6d8a9f63e..bc0a6eae9 100644 --- a/docs/rules/no-extra-reactive-curlies.md +++ b/docs/rules/no-extra-reactive-curlies.md @@ -38,7 +38,7 @@ Nothing. ## :heart: Compatibility -This rule was taken from [@tivac/eslint-plugin-svelte]. +This rule was taken from [@tivac/eslint-plugin-svelte].\ This rule is compatible with `@tivac/svelte/reactive-curlies` rule. [@tivac/eslint-plugin-svelte]: https://github.com/tivac/eslint-plugin-svelte/ diff --git a/docs/rules/no-inner-declarations.md b/docs/rules/no-inner-declarations.md index 50cff3ecd..aa35c9b31 100644 --- a/docs/rules/no-inner-declarations.md +++ b/docs/rules/no-inner-declarations.md @@ -16,7 +16,7 @@ since: 'v0.0.8' This rule requires that function declarations and, optionally, variable declarations be in the root of a program or the body of a function. -This rule extends the base ESLint's [no-inner-declarations] rule. The AST generated by [svelte-eslint-parser] will false positives in [no-inner-declarations] rule because the root node of the script is not the `Program`. +This rule extends the base ESLint's [no-inner-declarations] rule. The AST generated by [svelte-eslint-parser] will false positives in [no-inner-declarations] rule because the root node of the script is not the `Program`.\ This rule supports [svelte-eslint-parser]'s AST. [svelte-eslint-parser]: https://github.com/sveltejs/svelte-eslint-parser diff --git a/docs/rules/no-not-function-handler.md b/docs/rules/no-not-function-handler.md index c1736a0a2..ae0055171 100644 --- a/docs/rules/no-not-function-handler.md +++ b/docs/rules/no-not-function-handler.md @@ -14,7 +14,7 @@ since: 'v0.5.0' ## :book: Rule Details -This rule reports where you used not function value in event handlers. +This rule reports where you used not function value in event handlers.\ If you use a non-function value for the event handler, it event handler will not be called. It's almost always a mistake. You may have written a lot of unnecessary curly braces. diff --git a/docs/rules/no-object-in-text-mustaches.md b/docs/rules/no-object-in-text-mustaches.md index c6f2a3587..ed7078833 100644 --- a/docs/rules/no-object-in-text-mustaches.md +++ b/docs/rules/no-object-in-text-mustaches.md @@ -14,7 +14,7 @@ since: 'v0.5.0' ## :book: Rule Details -This rule disallows the use of objects in text mustache interpolation. +This rule disallows the use of objects in text mustache interpolation.\ When you use an object for text interpolation, it is drawn as `[object Object]`. It's almost always a mistake. You may have written a lot of unnecessary curly braces. diff --git a/docs/rules/no-reactive-functions.md b/docs/rules/no-reactive-functions.md index d81f1861f..5b5c90407 100644 --- a/docs/rules/no-reactive-functions.md +++ b/docs/rules/no-reactive-functions.md @@ -47,7 +47,7 @@ Nothing ## :heart: Compatibility -This rule was taken from [@tivac/eslint-plugin-svelte]. +This rule was taken from [@tivac/eslint-plugin-svelte].\ This rule is compatible with `@tivac/svelte/reactive-functions` rule. [@tivac/eslint-plugin-svelte]: https://github.com/tivac/eslint-plugin-svelte/ diff --git a/docs/rules/no-reactive-literals.md b/docs/rules/no-reactive-literals.md index d0633ae10..6fb6e44f8 100644 --- a/docs/rules/no-reactive-literals.md +++ b/docs/rules/no-reactive-literals.md @@ -36,7 +36,7 @@ Nothing. ## :heart: Compatibility -This rule was taken from [@tivac/eslint-plugin-svelte]. +This rule was taken from [@tivac/eslint-plugin-svelte].\ This rule is compatible with `@tivac/svelte/reactive-literals` rule. [@tivac/eslint-plugin-svelte]: https://github.com/tivac/eslint-plugin-svelte/ diff --git a/docs/rules/no-top-level-browser-globals.md b/docs/rules/no-top-level-browser-globals.md new file mode 100644 index 000000000..85d599ba6 --- /dev/null +++ b/docs/rules/no-top-level-browser-globals.md @@ -0,0 +1,65 @@ +--- +pageClass: 'rule-details' +sidebarDepth: 0 +title: 'svelte/no-top-level-browser-globals' +description: 'disallow using top-level browser global variables' +since: 'v3.8.0' +--- + +# svelte/no-top-level-browser-globals + +> disallow using top-level browser global variables + +## :book: Rule Details + +This rule reports top-level browser global variables in Svelte components. +This rule helps prevent the use of browser global variables that can cause errors in SSR (Server Side Rendering). + + + +```svelte + +``` + +## :wrench: Options + +Nothing. + +## :books: Further Reading + +- [`$app/environment` documentation > browser](https://svelte.dev/docs/kit/$app-environment#browser) + +## :rocket: Version + +This rule was introduced in eslint-plugin-svelte v3.8.0 + +## :mag: Implementation + +- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/no-top-level-browser-globals.ts) +- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/no-top-level-browser-globals.ts) diff --git a/docs/rules/no-unused-props.md b/docs/rules/no-unused-props.md index fffe3864f..d87e29b5b 100644 --- a/docs/rules/no-unused-props.md +++ b/docs/rules/no-unused-props.md @@ -14,7 +14,7 @@ since: 'v3.2.0' ## :book: Rule Details -This rule reports properties that are defined in Props but never used in the component code. +This rule reports properties that are defined in Props but never used in the component code.\ It helps to detect dead code and improve component clarity by ensuring that every declared prop is utilized. This rule checks various usage patterns of props: @@ -163,6 +163,8 @@ Note: Properties of class types are not checked for usage, as they might be used "ignoreTypePatterns": [], // Patterns to ignore when checking for unused props "ignorePropertyPatterns": [], + // Whether to allow unused nested properties + "allowUnusedNestedProperties": false }] } ``` @@ -170,6 +172,7 @@ Note: Properties of class types are not checked for usage, as they might be used - `checkImportedTypes` ... Controls whether to check properties from types defined in external files. Default is `false`, meaning the rule only checks types defined within the component file itself. When set to `true`, the rule will also check properties from imported and extended types. - `ignoreTypePatterns` ... Regular expression patterns for type names to exclude from checks. Default is `[]` (no exclusions). Most useful when `checkImportedTypes` is `true`, allowing you to exclude specific imported types (like utility types or third-party types) from being checked. - `ignorePropertyPatterns` ... Regular expression patterns for property names to exclude from unused checks. Default is `[]` (no exclusions). Most useful when `checkImportedTypes` is `true`, allowing you to ignore specific properties from external types that shouldn't trigger warnings. +- `allowUnusedNestedProperties` ... Controls whether to allow unused nested properties. Default is `false`, meaning the rule will report unused properties from nested objects. Examples: @@ -219,6 +222,21 @@ Examples: ``` +```svelte + + +``` + ## :gear: Required Configuration This rule requires `@typescript-eslint/parser` to work. Please refer to the [User Guide](../user-guide.md) for more information. diff --git a/docs/rules/no-useless-mustaches.md b/docs/rules/no-useless-mustaches.md index 5ded18531..781793cbf 100644 --- a/docs/rules/no-useless-mustaches.md +++ b/docs/rules/no-useless-mustaches.md @@ -15,7 +15,7 @@ since: 'v0.0.4' ## :book: Rule Details -This rule reports mustache interpolation with a string literal value. +This rule reports mustache interpolation with a string literal value.\ The mustache interpolation with a string literal value can be changed to a static contents. diff --git a/docs/rules/prefer-class-directive.md b/docs/rules/prefer-class-directive.md index 5b5c6a165..3eb3ed69d 100644 --- a/docs/rules/prefer-class-directive.md +++ b/docs/rules/prefer-class-directive.md @@ -62,7 +62,7 @@ You cannot enforce this style by using [prettier-plugin-svelte]. That is, this r ## :books: Further Reading -- [Svelte - Tutorial > 13. Classes / The class directive](https://svelte.dev/tutorial/classes) +- [Svelte - Tutorial > Basic Svelte / Classes and styles / The class atribute](https://svelte.dev/tutorial/svelte/classes) ## :rocket: Version diff --git a/docs/rules/prefer-const.md b/docs/rules/prefer-const.md index 8029ea881..f158d47b6 100644 --- a/docs/rules/prefer-const.md +++ b/docs/rules/prefer-const.md @@ -46,7 +46,7 @@ This rule reports the same as the base ESLint `prefer-const` rule, except that i "error", { "destructuring": "any", - "ignoreReadonly": true, + "additionalProperties": false, "excludedRunes": ["$props", "$derived"] } ] @@ -54,14 +54,14 @@ This rule reports the same as the base ESLint `prefer-const` rule, except that i ``` - `destructuring`: The kind of the way to address variables in destructuring. There are 2 values: - - `any` (default): if any variables in destructuring should be const, this rule warns for those variables. - - `all`: if all variables in destructuring should be const, this rule warns the variables. Otherwise, ignores them. -- `ignoreReadonly`: If `true`, this rule will ignore variables that are read between the declaration and the _first_ assignment. + - `any` (default) - If any variables in destructuring should be const, this rule warns for those variables. + - `all`: If all variables in destructuring should be const, this rule warns the variables. Otherwise, ignores them. +- `ignoreReadBeforeAssign`: This is an option to avoid conflicting with `no-use-before-define` rule (without "nofunc" option). If `true` is specified, this rule will ignore variables that are read between the declaration and the first assignment. Default is `false`. - `excludedRunes`: An array of rune names that should be ignored. Even if a rune is declared with `let`, it will still be ignored. ## :books: Further Reading -- See [ESLint `prefer-const` rule](https://eslint.org/docs/latest/rules/prefer-const) for more information about the base rule. +- See [ESLint prefer-const rule](https://eslint.org/docs/latest/rules/prefer-const) for more information about the base rule. ## :rocket: Version diff --git a/docs/rules/prefer-destructured-store-props.md b/docs/rules/prefer-destructured-store-props.md index ce9890b3f..6d7e2db51 100644 --- a/docs/rules/prefer-destructured-store-props.md +++ b/docs/rules/prefer-destructured-store-props.md @@ -43,7 +43,7 @@ Nothing ## :heart: Compatibility -This rule was taken from [@tivac/eslint-plugin-svelte]. +This rule was taken from [@tivac/eslint-plugin-svelte].\ This rule is compatible with `@tivac/svelte/store-prop-destructuring` rule. [@tivac/eslint-plugin-svelte]: https://github.com/tivac/eslint-plugin-svelte/ diff --git a/docs/rules/prefer-writable-derived.md b/docs/rules/prefer-writable-derived.md new file mode 100644 index 000000000..95d24804b --- /dev/null +++ b/docs/rules/prefer-writable-derived.md @@ -0,0 +1,64 @@ +--- +pageClass: 'rule-details' +sidebarDepth: 0 +title: 'svelte/prefer-writable-derived' +description: 'Prefer using writable $derived instead of $state and $effect' +since: 'v3.6.0' +--- + +# svelte/prefer-writable-derived + +> Prefer using writable $derived instead of $state and $effect + +- :gear: This rule is included in `"plugin:svelte/recommended"`. +- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions). + +## :book: Rule Details + +This rule reports when you use a combination of `$state` and `$effect` to create a derived value that can be written to. It encourages using the more concise and clearer `$derived` syntax instead. + + + +```svelte + +``` + +The rule specifically looks for patterns where: + +1. You initialize a variable with `$state()` +2. You then use `$effect()` or `$effect.pre()` to assign a new value to that same variable +3. The effect function contains only a single assignment statement + +When this pattern is detected, the rule suggests refactoring to use `$derived()` instead, which provides the same functionality in a more concise way. + +## :wrench: Options + +Nothing. + +- This rule has no options. + +## :books: Further Reading + +- [Svelte Documentation on Reactivity Primitives](https://svelte.dev/docs/svelte-components#script-2-assignments-are-reactive) +- [Svelte RFC for Reactivity Primitives](https://github.com/sveltejs/rfcs/blob/rfc-better-primitives/text/0000-better-primitives.md) + +## :rocket: Version + +This rule was introduced in eslint-plugin-svelte v3.6.0 + +## :mag: Implementation + +- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts) +- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/prefer-writable-derived.ts) diff --git a/docs/rules/require-each-key.md b/docs/rules/require-each-key.md index cd0069cb1..0daa7ec84 100644 --- a/docs/rules/require-each-key.md +++ b/docs/rules/require-each-key.md @@ -44,7 +44,7 @@ Nothing. ## :books: Further Reading -- [Svelte - Tutorial > 4. Logic / Keyed each blocks](https://svelte.dev/tutorial/svelte/keyed-each-blocks) +- [Svelte - Tutorial > Basic Svelte / Logic / Keyed each blocks](https://svelte.dev/tutorial/svelte/keyed-each-blocks) ## :rocket: Version diff --git a/docs/rules/require-event-prefix.md b/docs/rules/require-event-prefix.md new file mode 100644 index 000000000..9d4d2861c --- /dev/null +++ b/docs/rules/require-event-prefix.md @@ -0,0 +1,74 @@ +--- +pageClass: 'rule-details' +sidebarDepth: 0 +title: 'svelte/require-event-prefix' +description: 'require component event names to start with "on"' +since: 'v3.6.0' +--- + +# svelte/require-event-prefix + +> require component event names to start with "on" + +## :book: Rule Details + +Starting with Svelte 5, component events are just component props that are functions and so can be called like any function. Events for HTML elements all have their name begin with "on" (e.g. `onclick`). This rule enforces that all component events (i.e. function props) also begin with "on". + + + +```svelte + +``` + +```svelte + +``` + +## :wrench: Options + +```json +{ + "svelte/require-event-prefix": [ + "error", + { + "checkAsyncFunctions": false + } + ] +} +``` + +- `checkAsyncFunctions` ... Whether to also report asychronous function properties. Default `false`. + +## :books: Further Reading + +- [Svelte docs on events in version 5](https://svelte.dev/docs/svelte/v5-migration-guide#Event-changes) + +## :rocket: Version + +This rule was introduced in eslint-plugin-svelte v3.6.0 + +## :mag: Implementation + +- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/require-event-prefix.ts) +- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/require-event-prefix.ts) diff --git a/docs/rules/require-optimized-style-attribute.md b/docs/rules/require-optimized-style-attribute.md index dc526df82..60380edaf 100644 --- a/docs/rules/require-optimized-style-attribute.md +++ b/docs/rules/require-optimized-style-attribute.md @@ -14,7 +14,7 @@ since: 'v0.32.0' This rule reports `style` attributes written in a format that cannot be optimized. -Svelte parses the content written in the style attribute and tries to optimize it. (See [https://github.com/sveltejs/svelte/pull/810](https://github.com/sveltejs/svelte/pull/810)) +Svelte parses the content written in the style attribute and tries to optimize it. (See [https://github.com/sveltejs/svelte/pull/810](https://github.com/sveltejs/svelte/pull/810))\ If Svelte can be successfully optimized, Svelte can minimize the number of re-renders. e.g. diff --git a/docs/rules/require-store-reactive-access.md b/docs/rules/require-store-reactive-access.md index b8d917cef..b8013f306 100644 --- a/docs/rules/require-store-reactive-access.md +++ b/docs/rules/require-store-reactive-access.md @@ -15,7 +15,7 @@ since: 'v2.12.0' ## :book: Rule Details -This rule disallow to use of the store itself as an operand. +This rule disallow to use of the store itself as an operand.\ You should access the store value using the `$` prefix or the `get` function. @@ -58,7 +58,7 @@ You should access the store value using the `$` prefix or the `get` function. ``` -This rule checks the usage of store variables only if the store can be determined within a single file. +This rule checks the usage of store variables only if the store can be determined within a single file. However, when using `@typescript-eslint/parser` and full type information, this rule uses the type information to determine if the expression is a store. diff --git a/docs/rules/require-stores-init.md b/docs/rules/require-stores-init.md index 13dcc9985..78222fb42 100644 --- a/docs/rules/require-stores-init.md +++ b/docs/rules/require-stores-init.md @@ -38,7 +38,7 @@ Nothing. ## :heart: Compatibility -This rule was taken from [@tivac/eslint-plugin-svelte]. +This rule was taken from [@tivac/eslint-plugin-svelte].\ This rule is compatible with `@tivac/svelte/stores-initial-value` rule. [@tivac/eslint-plugin-svelte]: https://github.com/tivac/eslint-plugin-svelte/ diff --git a/docs/rules/sort-attributes.md b/docs/rules/sort-attributes.md index 4f45af126..5d427d575 100644 --- a/docs/rules/sort-attributes.md +++ b/docs/rules/sort-attributes.md @@ -14,7 +14,7 @@ since: 'v2.4.0' ## :book: Rule Details -This rule aims to enforce ordering of attributes. +This rule aims to enforce ordering of attributes.\ The default order is: - `this` property. @@ -175,8 +175,8 @@ If there is a spread attribute between the attributes, it will not be reported a - `"alphabetical"` ... Sorts the attributes of the same group in alphabetical order. - `"ignore"` ... Attributes in the same group are not sorted. -Note that the behavior may change depending on how you specify the `order` setting. -For example, `bind:value` and `on:input={() => console.log(value)}` behave differently depending on the order. See for details. +Note that the behavior may change depending on how you specify the `order` setting.\ +For example, `bind:value` and `on:input={() => console.log(value)}` behave differently depending on the order. See for details.\ By default it is designed to be sorted safely. You can use the following formats for names or patterns: diff --git a/docs/rules/valid-each-key.md b/docs/rules/valid-each-key.md index 5e6aa5795..2515751ba 100644 --- a/docs/rules/valid-each-key.md +++ b/docs/rules/valid-each-key.md @@ -53,7 +53,7 @@ Nothing. ## :books: Further Reading -- [Svelte - Tutorial > 4. Logic / Keyed each blocks](https://svelte.dev/tutorial/keyed-each-blocks) +- [Svelte - Tutorial > Basic Svelte / Logic / Keyed each blocks](https://svelte.dev/tutorial/svelte/keyed-each-blocks) ## :rocket: Version diff --git a/docs/user-guide.md b/docs/user-guide.md index babec87fa..14c061354 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -184,8 +184,8 @@ export default [ ## Editor Integrations -**Visual Studio Code** -Install [dbaeumer.vscode-eslint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint). +**Visual Studio Code**\ +Install [dbaeumer.vscode-eslint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint).\ Configure `.svelte` files in `.vscode/settings.json`: ```json diff --git a/package.json b/package.json index 254352fc5..36bd2fad9 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@typescript-eslint/parser": "^8.16.0", "c8": "^10.1.2", "env-cmd": "^10.1.0", - "eslint": "~9.23.0", + "eslint": "~9.28.0", "eslint-config-prettier": "^10.0.0", "eslint-formatter-friendly": "^7.0.0", "eslint-plugin-eslint-plugin": "^6.3.2", @@ -34,15 +34,15 @@ "eslint-plugin-markdown": "^5.1.0", "eslint-plugin-mdx": "^3.1.5", "eslint-plugin-n": "^17.14.0", - "eslint-plugin-node-dependencies": "^0.12.0", + "eslint-plugin-node-dependencies": "^1.0.0", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-regexp": "^2.7.0", "eslint-plugin-yml": "^1.15.0", - "npm-run-all2": "^7.0.1", + "npm-run-all2": "^8.0.0", "prettier": "^3.4.1", "prettier-plugin-svelte": "^3.3.2", "rimraf": "^6.0.1", - "typescript": "~5.7.2", + "typescript": "~5.8.0", "typescript-eslint": "^8.16.0" }, "publishConfig": { diff --git a/packages/eslint-plugin-svelte/CHANGELOG.md b/packages/eslint-plugin-svelte/CHANGELOG.md index 9f46a3f5f..0b0f3dee0 100644 --- a/packages/eslint-plugin-svelte/CHANGELOG.md +++ b/packages/eslint-plugin-svelte/CHANGELOG.md @@ -1,5 +1,73 @@ # eslint-plugin-svelte +## 3.9.1 + +### Patch Changes + +- [#1239](https://github.com/sveltejs/eslint-plugin-svelte/pull/1239) [`a3d4224`](https://github.com/sveltejs/eslint-plugin-svelte/commit/a3d42245fbb6a6663a1b3c6a4e211dce2b6dfbbb) Thanks [@baseballyama](https://github.com/baseballyama)! - fix(prefer-const): Use `additionalProperties` instead of `ignoreReadonly` to match the ESLint core rule option name. + +## 3.9.0 + +### Minor Changes + +- [#1235](https://github.com/sveltejs/eslint-plugin-svelte/pull/1235) [`6e86e30`](https://github.com/sveltejs/eslint-plugin-svelte/commit/6e86e30cd766181dce5849ae739eedd2adfd8d8e) Thanks [@43081j](https://github.com/43081j)! - Improve performance of ignore comment extraction and add support for comma-separated ignore codes + +## 3.8.2 + +### Patch Changes + +- [#1231](https://github.com/sveltejs/eslint-plugin-svelte/pull/1231) [`0681f90`](https://github.com/sveltejs/eslint-plugin-svelte/commit/0681f901196cf81a87169155f8f632bf12666908) Thanks [@marekdedic](https://github.com/marekdedic)! - fix(consistent-selector-style): Fixed detections of repeated elements such as in {#each} + +## 3.8.1 + +### Patch Changes + +- [#1227](https://github.com/sveltejs/eslint-plugin-svelte/pull/1227) [`c938185`](https://github.com/sveltejs/eslint-plugin-svelte/commit/c938185b8a413f200049bc11376db76d768f2ae3) Thanks [@ota-meshi](https://github.com/ota-meshi)! - fix(no-top-level-browser-globals): false positives for type annotations + +## 3.8.0 + +### Minor Changes + +- [#1210](https://github.com/sveltejs/eslint-plugin-svelte/pull/1210) [`9cffd3b`](https://github.com/sveltejs/eslint-plugin-svelte/commit/9cffd3ba86926793f3240263e38914cdb2180f0a) Thanks [@ota-meshi](https://github.com/ota-meshi)! - feat: add `svelte/no-top-level-browser-globals` rule + +## 3.7.0 + +### Minor Changes + +- [#1221](https://github.com/sveltejs/eslint-plugin-svelte/pull/1221) [`534ad78`](https://github.com/sveltejs/eslint-plugin-svelte/commit/534ad78221f040ea5f9c32835d6973f4a3643580) Thanks [@baseballyama](https://github.com/baseballyama)! - feat(sort-attributes): support `{@attach}` + +## 3.6.0 + +### Minor Changes + +- [#1170](https://github.com/sveltejs/eslint-plugin-svelte/pull/1170) [`3ddbd83`](https://github.com/sveltejs/eslint-plugin-svelte/commit/3ddbd83068a8a82ed5bfe638483c7dd7839e681a) Thanks [@baseballyama](https://github.com/baseballyama)! - feat: add `prefer-writable-derived` rule + +- [#1069](https://github.com/sveltejs/eslint-plugin-svelte/pull/1069) [`73f23ae`](https://github.com/sveltejs/eslint-plugin-svelte/commit/73f23ae0321aa4cb87e700f8478b140059e18c64) Thanks [@marekdedic](https://github.com/marekdedic)! - feat: added the `require-event-prefix` rule + +- [#1197](https://github.com/sveltejs/eslint-plugin-svelte/pull/1197) [`e9aec7f`](https://github.com/sveltejs/eslint-plugin-svelte/commit/e9aec7f16efb56d5d841a2f5f28cfa05281bcd9d) Thanks [@43081j](https://github.com/43081j)! - Added `no-add-event-listener` rule to disallow usages of `addEventListener` + +- [#1148](https://github.com/sveltejs/eslint-plugin-svelte/pull/1148) [`87c74fe`](https://github.com/sveltejs/eslint-plugin-svelte/commit/87c74feef892cb7e04e5709e66dfeda4e27ec820) Thanks [@marekdedic](https://github.com/marekdedic)! - feat(consistent-selector-style): added support for dynamic classes and IDs + +### Patch Changes + +- [#1208](https://github.com/sveltejs/eslint-plugin-svelte/pull/1208) [`78d0f78`](https://github.com/sveltejs/eslint-plugin-svelte/commit/78d0f78e0740305f62d6e702f89b333d6d97dbec) Thanks [@ota-meshi](https://github.com/ota-meshi)! - fix(no-unused-svelte-ignore): ignore reactive-component warnings + +## 3.5.1 + +### Patch Changes + +- [#1188](https://github.com/sveltejs/eslint-plugin-svelte/pull/1188) [`9126f1f`](https://github.com/sveltejs/eslint-plugin-svelte/commit/9126f1f017ff990704498570d77fd2d3ff528efe) Thanks [@43081j](https://github.com/43081j)! - Removed eslint-compat-utils from dependencies + +## 3.5.0 + +### Minor Changes + +- [#1171](https://github.com/sveltejs/eslint-plugin-svelte/pull/1171) [`842757f`](https://github.com/sveltejs/eslint-plugin-svelte/commit/842757fe45dd2433e801acdce5f66127e2216d7e) Thanks [@baseballyama](https://github.com/baseballyama)! - feat(no-unused-props): add `allowUnusedNestedProperties` option + +### Patch Changes + +- [#1178](https://github.com/sveltejs/eslint-plugin-svelte/pull/1178) [`7892f4c`](https://github.com/sveltejs/eslint-plugin-svelte/commit/7892f4c56a569ebdf80fa791d39c02754bfebe25) Thanks [@baseballyama](https://github.com/baseballyama)! - fix(no-unused-props): handle alias props name properly + ## 3.4.1 ### Patch Changes diff --git a/packages/eslint-plugin-svelte/package.json b/packages/eslint-plugin-svelte/package.json index 6fd2e18b9..ffce141a8 100644 --- a/packages/eslint-plugin-svelte/package.json +++ b/packages/eslint-plugin-svelte/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-svelte", - "version": "3.4.1", + "version": "3.9.1", "description": "ESLint plugin for Svelte using AST", "repository": "git+https://github.com/sveltejs/eslint-plugin-svelte.git", "homepage": "https://sveltejs.github.io/eslint-plugin-svelte", @@ -56,16 +56,16 @@ } }, "dependencies": { - "@eslint-community/eslint-utils": "^4.4.1", + "@eslint-community/eslint-utils": "^4.6.1", "@jridgewell/sourcemap-codec": "^1.5.0", - "eslint-compat-utils": "^0.6.4", "esutils": "^2.0.3", - "known-css-properties": "^0.35.0", + "globals": "^16.0.0", + "known-css-properties": "^0.36.0", "postcss": "^8.4.49", "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^7.0.0", "semver": "^7.6.3", - "svelte-eslint-parser": "^1.1.1" + "svelte-eslint-parser": "^1.2.0" }, "devDependencies": { "@babel/core": "^7.26.0", @@ -96,7 +96,7 @@ "sass": "^1.81.0", "source-map-js": "^1.2.1", "stylus": "^0.64.0", - "svelte": "^5.2.9", + "svelte": "^5.30.1", "svelte-i18n": "^4.0.1", "tsx": "^4.19.2", "type-coverage": "^2.29.7", diff --git a/packages/eslint-plugin-svelte/src/configs/flat/recommended.ts b/packages/eslint-plugin-svelte/src/configs/flat/recommended.ts index b1b2d51ad..7a08befc8 100644 --- a/packages/eslint-plugin-svelte/src/configs/flat/recommended.ts +++ b/packages/eslint-plugin-svelte/src/configs/flat/recommended.ts @@ -37,6 +37,7 @@ const config: Linter.Config[] = [ 'svelte/no-unused-svelte-ignore': 'error', 'svelte/no-useless-children-snippet': 'error', 'svelte/no-useless-mustaches': 'error', + 'svelte/prefer-writable-derived': 'error', 'svelte/require-each-key': 'error', 'svelte/require-event-dispatcher-types': 'error', 'svelte/require-store-reactive-access': 'error', diff --git a/packages/eslint-plugin-svelte/src/meta.ts b/packages/eslint-plugin-svelte/src/meta.ts index 7a0a0d8f0..1ea9551ba 100644 --- a/packages/eslint-plugin-svelte/src/meta.ts +++ b/packages/eslint-plugin-svelte/src/meta.ts @@ -1,5 +1,5 @@ // IMPORTANT! // This file has been automatically generated, // in order to update its content execute "pnpm run update" -export const name = 'eslint-plugin-svelte'; -export const version = '3.4.1'; +export const name = 'eslint-plugin-svelte' as const; +export const version = '3.9.1' as const; diff --git a/packages/eslint-plugin-svelte/src/rule-types.ts b/packages/eslint-plugin-svelte/src/rule-types.ts index 19e30a345..81657270d 100644 --- a/packages/eslint-plugin-svelte/src/rule-types.ts +++ b/packages/eslint-plugin-svelte/src/rule-types.ts @@ -99,6 +99,11 @@ export interface RuleOptions { * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/mustache-spacing/ */ 'svelte/mustache-spacing'?: Linter.RuleEntry + /** + * Warns against the use of `addEventListener` + * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-add-event-listener/ + */ + 'svelte/no-add-event-listener'?: Linter.RuleEntry<[]> /** * disallow the use of `{@debug}` * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-at-debug-tags/ @@ -246,6 +251,11 @@ export interface RuleOptions { * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-target-blank/ */ 'svelte/no-target-blank'?: Linter.RuleEntry + /** + * disallow using top-level browser global variables + * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-top-level-browser-globals/ + */ + 'svelte/no-top-level-browser-globals'?: Linter.RuleEntry<[]> /** * disallow trailing whitespace at the end of lines * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-trailing-spaces/ @@ -306,6 +316,11 @@ export interface RuleOptions { * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-style-directive/ */ 'svelte/prefer-style-directive'?: Linter.RuleEntry<[]> + /** + * Prefer using writable $derived instead of $state and $effect + * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-writable-derived/ + */ + 'svelte/prefer-writable-derived'?: Linter.RuleEntry<[]> /** * require keyed `{#each}` block * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/require-each-key/ @@ -316,6 +331,11 @@ export interface RuleOptions { * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-dispatcher-types/ */ 'svelte/require-event-dispatcher-types'?: Linter.RuleEntry<[]> + /** + * require component event names to start with "on" + * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-prefix/ + */ + 'svelte/require-event-prefix'?: Linter.RuleEntry /** * require style attributes that can be optimized * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/require-optimized-style-attribute/ @@ -537,6 +557,7 @@ type SvelteNoUnusedProps = []|[{ checkImportedTypes?: boolean ignoreTypePatterns?: string[] ignorePropertyPatterns?: string[] + allowUnusedNestedProperties?: boolean }] // ----- svelte/no-useless-mustaches ----- type SvelteNoUselessMustaches = []|[{ @@ -552,6 +573,11 @@ type SveltePreferConst = []|[{ destructuring?: ("any" | "all") ignoreReadBeforeAssign?: boolean excludedRunes?: string[] + [k: string]: unknown | undefined +}] +// ----- svelte/require-event-prefix ----- +type SvelteRequireEventPrefix = []|[{ + checkAsyncFunctions?: boolean }] // ----- svelte/shorthand-attribute ----- type SvelteShorthandAttribute = []|[{ diff --git a/packages/eslint-plugin-svelte/src/rules/block-lang.ts b/packages/eslint-plugin-svelte/src/rules/block-lang.ts index 39a95ca9e..1a673b3ec 100644 --- a/packages/eslint-plugin-svelte/src/rules/block-lang.ts +++ b/packages/eslint-plugin-svelte/src/rules/block-lang.ts @@ -1,6 +1,6 @@ import { createRule } from '../utils/index.js'; import { findAttribute, getLangValue } from '../utils/ast-utils.js'; -import type { SvelteScriptElement, SvelteStyleElement } from 'svelte-eslint-parser/lib/ast'; +import type { AST } from 'svelte-eslint-parser'; import type { SuggestionReportDescriptor, SourceCode } from '../types.js'; export default createRule('block-lang', { @@ -68,13 +68,13 @@ export default createRule('block-lang', { const allowedScriptLangs: (string | null)[] = Array.isArray(scriptOption) ? scriptOption : [scriptOption]; - const scriptNodes: SvelteScriptElement[] = []; + const scriptNodes: AST.SvelteScriptElement[] = []; const styleOption: string | null | (string | null)[] = context.options[0]?.style ?? null; const allowedStyleLangs: (string | null)[] = Array.isArray(styleOption) ? styleOption : [styleOption]; - const styleNodes: SvelteStyleElement[] = []; + const styleNodes: AST.SvelteStyleElement[] = []; return { SvelteScriptElement(node) { @@ -153,7 +153,7 @@ function buildAddLangSuggestions( function buildReplaceLangSuggestions( langs: (string | null)[], - node: SvelteScriptElement | SvelteStyleElement + node: AST.SvelteScriptElement | AST.SvelteStyleElement ): SuggestionReportDescriptor[] { const tagName = node.name.name; const langAttribute = findAttribute(node, 'lang'); diff --git a/packages/eslint-plugin-svelte/src/rules/consistent-selector-style.ts b/packages/eslint-plugin-svelte/src/rules/consistent-selector-style.ts index 93f2a2cba..0864dc6a9 100644 --- a/packages/eslint-plugin-svelte/src/rules/consistent-selector-style.ts +++ b/packages/eslint-plugin-svelte/src/rules/consistent-selector-style.ts @@ -7,7 +7,19 @@ import type { Tag as SelectorTag } from 'postcss-selector-parser'; import { findClassesInAttribute } from '../utils/ast-utils.js'; +import { + extractExpressionPrefixLiteral, + extractExpressionSuffixLiteral +} from '../utils/expression-affixes.js'; import { createRule } from '../utils/index.js'; +import { ElementOccurenceCount, elementOccurrenceCount } from '../utils/element-occurences.js'; + +interface Selections { + exact: Map; + // [prefix, suffix] + affixes: Map<[string | null, string | null], AST.SvelteHTMLElement[]>; + universalSelector: boolean; +} export default createRule('consistent-selector-style', { meta: { @@ -62,9 +74,24 @@ export default createRule('consistent-selector-style', { const style = context.options[0]?.style ?? ['type', 'id', 'class']; const whitelistedClasses: string[] = []; - const classSelections: Map = new Map(); - const idSelections: Map = new Map(); - const typeSelections: Map = new Map(); + + const selections: { + class: Selections; + id: Selections; + type: Map; + } = { + class: { + exact: new Map(), + affixes: new Map(), + universalSelector: false + }, + id: { + exact: new Map(), + affixes: new Map(), + universalSelector: false + }, + type: new Map() + }; /** * Checks selectors in a given PostCSS node @@ -109,22 +136,22 @@ export default createRule('consistent-selector-style', { * Checks a class selector */ function checkClassSelector(node: SelectorClass): void { - if (whitelistedClasses.includes(node.value)) { + if (selections.class.universalSelector || whitelistedClasses.includes(node.value)) { return; } - const selection = classSelections.get(node.value) ?? []; + const selection = matchSelection(selections.class, node.value); for (const styleValue of style) { if (styleValue === 'class') { return; } - if (styleValue === 'id' && canUseIdSelector(selection)) { + if (styleValue === 'id' && canUseIdSelector(selection.map(([elem]) => elem))) { context.report({ messageId: 'classShouldBeId', loc: styleSelectorNodeLoc(node) as AST.SourceLocation }); return; } - if (styleValue === 'type' && canUseTypeSelector(selection, typeSelections)) { + if (styleValue === 'type' && canUseTypeSelector(selection, selections.type)) { context.report({ messageId: 'classShouldBeType', loc: styleSelectorNodeLoc(node) as AST.SourceLocation @@ -138,7 +165,10 @@ export default createRule('consistent-selector-style', { * Checks an ID selector */ function checkIdSelector(node: SelectorIdentifier): void { - const selection = idSelections.get(node.value) ?? []; + if (selections.id.universalSelector) { + return; + } + const selection = matchSelection(selections.id, node.value); for (const styleValue of style) { if (styleValue === 'class') { context.report({ @@ -150,7 +180,7 @@ export default createRule('consistent-selector-style', { if (styleValue === 'id') { return; } - if (styleValue === 'type' && canUseTypeSelector(selection, typeSelections)) { + if (styleValue === 'type' && canUseTypeSelector(selection, selections.type)) { context.report({ messageId: 'idShouldBeType', loc: styleSelectorNodeLoc(node) as AST.SourceLocation @@ -164,7 +194,7 @@ export default createRule('consistent-selector-style', { * Checks a type selector */ function checkTypeSelector(node: SelectorTag): void { - const selection = typeSelections.get(node.value) ?? []; + const selection = selections.type.get(node.value) ?? []; for (const styleValue of style) { if (styleValue === 'class') { context.report({ @@ -191,21 +221,39 @@ export default createRule('consistent-selector-style', { if (node.kind !== 'html') { return; } - addToArrayMap(typeSelections, node.name.name, node); - const classes = node.startTag.attributes.flatMap(findClassesInAttribute); - for (const className of classes) { - addToArrayMap(classSelections, className, node); - } + addToArrayMap(selections.type, node.name.name, node); for (const attribute of node.startTag.attributes) { if (attribute.type === 'SvelteDirective' && attribute.kind === 'Class') { whitelistedClasses.push(attribute.key.name.name); } - if (attribute.type !== 'SvelteAttribute' || attribute.key.name !== 'id') { + for (const className of findClassesInAttribute(attribute)) { + addToArrayMap(selections.class.exact, className, node); + } + if (attribute.type !== 'SvelteAttribute') { continue; } for (const value of attribute.value) { - if (value.type === 'SvelteLiteral') { - addToArrayMap(idSelections, value.value, node); + if (attribute.key.name === 'class' && value.type === 'SvelteMustacheTag') { + const prefix = extractExpressionPrefixLiteral(context, value.expression); + const suffix = extractExpressionSuffixLiteral(context, value.expression); + if (prefix === null && suffix === null) { + selections.class.universalSelector = true; + } else { + addToArrayMap(selections.class.affixes, [prefix, suffix], node); + } + } + if (attribute.key.name === 'id') { + if (value.type === 'SvelteLiteral') { + addToArrayMap(selections.id.exact, value.value, node); + } else if (value.type === 'SvelteMustacheTag') { + const prefix = extractExpressionPrefixLiteral(context, value.expression); + const suffix = extractExpressionSuffixLiteral(context, value.expression); + if (prefix === null && suffix === null) { + selections.id.universalSelector = true; + } else { + addToArrayMap(selections.id.affixes, [prefix, suffix], node); + } + } } } } @@ -227,38 +275,70 @@ export default createRule('consistent-selector-style', { /** * Helper function to add a value to a Map of arrays */ -function addToArrayMap( - map: Map, - key: string, +function addToArrayMap( + map: Map, + key: T, value: AST.SvelteHTMLElement ): void { map.set(key, (map.get(key) ?? []).concat(value)); } +/** + * Finds all nodes in selections that could be matched by key + */ +function matchSelection(selections: Selections, key: string): [AST.SvelteHTMLElement, boolean][] { + const selection = (selections.exact.get(key) ?? []).map<[AST.SvelteHTMLElement, boolean]>( + (elem) => [elem, true] + ); + selections.affixes.forEach((nodes, [prefix, suffix]) => { + if ((prefix === null || key.startsWith(prefix)) && (suffix === null || key.endsWith(suffix))) { + selection.push(...nodes.map<[AST.SvelteHTMLElement, boolean]>((elem) => [elem, false])); + } + }); + return selection; +} + /** * Checks whether a given selection could be obtained using an ID selector */ function canUseIdSelector(selection: AST.SvelteHTMLElement[]): boolean { - return selection.length <= 1; + return ( + selection.length === 0 || + (selection.length === 1 && + elementOccurrenceCount(selection[0]) !== ElementOccurenceCount.ZeroToInf) + ); } /** * Checks whether a given selection could be obtained using a type selector */ function canUseTypeSelector( - selection: AST.SvelteHTMLElement[], + selection: [AST.SvelteHTMLElement, boolean][], typeSelections: Map ): boolean { - const types = new Set(selection.map((node) => node.name.name)); + const types = new Set(selection.map(([node]) => node.name.name)); if (types.size > 1) { return false; } if (types.size < 1) { return true; } + if ( + selection.some( + ([elem, exact]) => !exact && elementOccurrenceCount(elem) === ElementOccurenceCount.ZeroToInf + ) + ) { + return false; + } const type = [...types][0]; const typeSelection = typeSelections.get(type); - return typeSelection !== undefined && arrayEquals(typeSelection, selection); + return ( + typeSelection !== undefined && + arrayEquals( + typeSelection, + selection.map(([elem]) => elem) + ) + ); } /** diff --git a/packages/eslint-plugin-svelte/src/rules/no-add-event-listener.ts b/packages/eslint-plugin-svelte/src/rules/no-add-event-listener.ts new file mode 100644 index 000000000..9ba558393 --- /dev/null +++ b/packages/eslint-plugin-svelte/src/rules/no-add-event-listener.ts @@ -0,0 +1,69 @@ +import type { TSESTree } from '@typescript-eslint/types'; + +import { createRule } from '../utils/index.js'; +import type { SuggestionReportDescriptor } from '../types.js'; + +export default createRule('no-add-event-listener', { + meta: { + docs: { + description: 'Warns against the use of `addEventListener`', + category: 'Best Practices', + recommended: false + }, + hasSuggestions: true, + schema: [], + messages: { + unexpected: + 'Do not use `addEventListener`. Use the `on` function from `svelte/events` instead.' + }, + type: 'suggestion', + conditions: [ + { + svelteVersions: ['5'] + } + ] + }, + create(context) { + return { + CallExpression(node: TSESTree.CallExpression) { + const { callee } = node; + let target: string | null = null; + + if ( + callee.type === 'MemberExpression' && + callee.property.type === 'Identifier' && + callee.property.name === 'addEventListener' + ) { + target = context.sourceCode.getText(callee.object); + } else if (callee.type === 'Identifier' && callee.name === 'addEventListener') { + target = 'window'; + } + + if (target === null) { + return; + } + + const openParen = context.sourceCode.getTokenAfter(callee); + const suggest: SuggestionReportDescriptor[] = []; + + if (openParen !== null) { + suggest.push({ + desc: 'Use `on` from `svelte/events` instead', + fix(fixer) { + return [ + fixer.replaceText(callee, 'on'), + fixer.insertTextAfter(openParen, `${target}, `) + ]; + } + }); + } + + context.report({ + node, + messageId: 'unexpected', + suggest + }); + } + }; + } +}); diff --git a/packages/eslint-plugin-svelte/src/rules/no-goto-without-base.ts b/packages/eslint-plugin-svelte/src/rules/no-goto-without-base.ts index 021bdb5b4..78182e0d0 100644 --- a/packages/eslint-plugin-svelte/src/rules/no-goto-without-base.ts +++ b/packages/eslint-plugin-svelte/src/rules/no-goto-without-base.ts @@ -106,7 +106,7 @@ function extractGotoReferences(referenceTracker: ReferenceTracker): TSESTree.Cal } } }), - ({ node }) => node + ({ node }) => node as TSESTree.CallExpression ); } diff --git a/packages/eslint-plugin-svelte/src/rules/no-navigation-without-base.ts b/packages/eslint-plugin-svelte/src/rules/no-navigation-without-base.ts index 00f73d02c..1e19b9b99 100644 --- a/packages/eslint-plugin-svelte/src/rules/no-navigation-without-base.ts +++ b/packages/eslint-plugin-svelte/src/rules/no-navigation-without-base.ts @@ -2,8 +2,9 @@ import type { TSESTree } from '@typescript-eslint/types'; import { createRule } from '../utils/index.js'; import { ReferenceTracker } from '@eslint-community/eslint-utils'; import { findVariable } from '../utils/ast-utils.js'; +import { extractExpressionPrefixVariable } from '../utils/expression-affixes.js'; import type { RuleContext } from '../types.js'; -import type { SvelteLiteral } from 'svelte-eslint-parser/lib/ast'; +import type { AST } from 'svelte-eslint-parser'; export default createRule('no-navigation-without-base', { meta: { @@ -170,13 +171,13 @@ function extractFunctionCallReferences(referenceTracker: ReferenceTracker): { return { goto: rawReferences .filter(({ path }) => path[path.length - 1] === 'goto') - .map(({ node }) => node), + .map(({ node }) => node as TSESTree.CallExpression), pushState: rawReferences .filter(({ path }) => path[path.length - 1] === 'pushState') - .map(({ node }) => node), + .map(({ node }) => node as TSESTree.CallExpression), replaceState: rawReferences .filter(({ path }) => path[path.length - 1] === 'replaceState') - .map(({ node }) => node) + .map(({ node }) => node as TSESTree.CallExpression) }; } @@ -221,87 +222,8 @@ function expressionStartsWithBase( url: TSESTree.Expression, basePathNames: Set ): boolean { - switch (url.type) { - case 'BinaryExpression': - return binaryExpressionStartsWithBase(context, url, basePathNames); - case 'Identifier': - return variableStartsWithBase(context, url, basePathNames); - case 'MemberExpression': - return memberExpressionStartsWithBase(url, basePathNames); - case 'TemplateLiteral': - return templateLiteralStartsWithBase(context, url, basePathNames); - default: - return false; - } -} - -function binaryExpressionStartsWithBase( - context: RuleContext, - url: TSESTree.BinaryExpression, - basePathNames: Set -): boolean { - return ( - url.left.type !== 'PrivateIdentifier' && - expressionStartsWithBase(context, url.left, basePathNames) - ); -} - -function memberExpressionStartsWithBase( - url: TSESTree.MemberExpression, - basePathNames: Set -): boolean { - return url.property.type === 'Identifier' && basePathNames.has(url.property); -} - -function variableStartsWithBase( - context: RuleContext, - url: TSESTree.Identifier, - basePathNames: Set -): boolean { - if (basePathNames.has(url)) { - return true; - } - const variable = findVariable(context, url); - if ( - variable === null || - variable.identifiers.length !== 1 || - variable.identifiers[0].parent.type !== 'VariableDeclarator' || - variable.identifiers[0].parent.init === null - ) { - return false; - } - return expressionStartsWithBase(context, variable.identifiers[0].parent.init, basePathNames); -} - -function templateLiteralStartsWithBase( - context: RuleContext, - url: TSESTree.TemplateLiteral, - basePathNames: Set -): boolean { - const startingIdentifier = extractLiteralStartingExpression(url); - return ( - startingIdentifier !== undefined && - expressionStartsWithBase(context, startingIdentifier, basePathNames) - ); -} - -function extractLiteralStartingExpression( - templateLiteral: TSESTree.TemplateLiteral -): TSESTree.Expression | undefined { - const literalParts = [...templateLiteral.expressions, ...templateLiteral.quasis].sort((a, b) => - a.range[0] < b.range[0] ? -1 : 1 - ); - for (const part of literalParts) { - if (part.type === 'TemplateElement' && part.value.raw === '') { - // Skip empty quasi in the begining - continue; - } - if (part.type !== 'TemplateElement') { - return part; - } - return undefined; - } - return undefined; + const prefixVariable = extractExpressionPrefixVariable(context, url); + return prefixVariable !== null && basePathNames.has(prefixVariable); } function expressionIsEmpty(url: TSESTree.Expression): boolean { @@ -314,7 +236,7 @@ function expressionIsEmpty(url: TSESTree.Expression): boolean { ); } -function expressionIsAbsolute(url: SvelteLiteral | TSESTree.Expression): boolean { +function expressionIsAbsolute(url: AST.SvelteLiteral | TSESTree.Expression): boolean { switch (url.type) { case 'BinaryExpression': return binaryExpressionIsAbsolute(url); @@ -347,7 +269,7 @@ function urlValueIsAbsolute(url: string): boolean { return url.includes('://'); } -function expressionIsFragment(url: SvelteLiteral | TSESTree.Expression): boolean { +function expressionIsFragment(url: AST.SvelteLiteral | TSESTree.Expression): boolean { switch (url.type) { case 'BinaryExpression': return binaryExpressionIsFragment(url); diff --git a/packages/eslint-plugin-svelte/src/rules/no-top-level-browser-globals.ts b/packages/eslint-plugin-svelte/src/rules/no-top-level-browser-globals.ts new file mode 100644 index 000000000..1a997eca3 --- /dev/null +++ b/packages/eslint-plugin-svelte/src/rules/no-top-level-browser-globals.ts @@ -0,0 +1,416 @@ +import type { TrackedReferences } from '@eslint-community/eslint-utils'; +import { ReferenceTracker, getStaticValue } from '@eslint-community/eslint-utils'; +import { createRule } from '../utils/index.js'; +import globals from 'globals'; +import type { TSESTree } from '@typescript-eslint/types'; +import { findVariable, getScope } from '../utils/ast-utils.js'; + +export default createRule('no-top-level-browser-globals', { + meta: { + docs: { + description: 'disallow using top-level browser global variables', + category: 'Possible Errors', + recommended: false + }, + schema: [], + messages: { + unexpectedGlobal: 'Unexpected top-level browser global variable "{{name}}".' + }, + type: 'problem', + conditions: [{ svelteFileTypes: ['.svelte', '.svelte.[js|ts]'] }] + }, + create(context) { + const sourceCode = context.sourceCode; + const blowerGlobals = getBrowserGlobals(); + + const referenceTracker = new ReferenceTracker(sourceCode.scopeManager.globalScope!, { + // Specifies the global variables that are allowed to prevent `window.window` from being iterated over. + globalObjectNames: ['globalThis'] + }); + + type MaybeGuard = { + reference?: { node: TSESTree.Node; name: string }; + isAvailableLocation: (node: TSESTree.Node) => boolean; + // The guard that checks whether the browser environment is set to true. + browserEnvironment: boolean; + }; + const maybeGuards: MaybeGuard[] = []; + + const functions: TSESTree.FunctionLike[] = []; + const typeAnnotations: (TSESTree.TypeNode | TSESTree.TSTypeAnnotation)[] = []; + + function enterFunction(node: TSESTree.FunctionLike) { + if (isTopLevelLocation(node)) { + functions.push(node); + } + } + + function enterTypeAnnotation(node: TSESTree.TypeNode | TSESTree.TSTypeAnnotation) { + if (!isInTypeAnnotation(node)) { + typeAnnotations.push(node); + } + } + + function enterMetaProperty(node: TSESTree.MetaProperty) { + if (node.meta.name !== 'import' || node.property.name !== 'meta') return; + for (const ref of referenceTracker.iteratePropertyReferences(node, { + env: { + // See https://vite.dev/guide/ssr#conditional-logic + SSR: { + [ReferenceTracker.READ]: true + } + } + })) { + if (ref.node.type === 'Identifier' || ref.node.type === 'MemberExpression') { + const guardChecker = getGuardChecker({ node: ref.node, not: true }); + if (guardChecker) { + maybeGuards.push({ + isAvailableLocation: guardChecker, + browserEnvironment: true + }); + } + } + } + } + + function verifyGlobalReferences() { + // Collects guarded location checkers by checking module references + // that can check the browser environment. + for (const referenceNode of iterateBrowserCheckerModuleReferences()) { + if (!isTopLevelLocation(referenceNode)) continue; + const guardChecker = getGuardChecker({ node: referenceNode }); + if (guardChecker) { + maybeGuards.push({ + isAvailableLocation: guardChecker, + browserEnvironment: true + }); + } + } + + const reportCandidates: TrackedReferences[] = []; + + // Collects references to global variables. + for (const ref of iterateBrowserGlobalReferences()) { + if (!isTopLevelLocation(ref.node) || isInTypeAnnotation(ref.node)) continue; + const guardChecker = getGuardCheckerFromReference(ref.node); + if (guardChecker) { + const name = ref.path.join('.'); + maybeGuards.push({ + reference: { node: ref.node, name }, + isAvailableLocation: guardChecker, + browserEnvironment: name === 'window' || name === 'document' + }); + } else { + reportCandidates.push(ref); + } + } + + for (const ref of reportCandidates) { + const name = ref.path.join('.'); + if (isAvailableLocation({ node: ref.node, name })) { + continue; + } + context.report({ + node: ref.node, + messageId: 'unexpectedGlobal', + data: { name } + }); + } + } + + return { + ':function': enterFunction, + '*.typeAnnotation': enterTypeAnnotation, + MetaProperty: enterMetaProperty, + 'Program:exit': verifyGlobalReferences + }; + + /** + * Checks whether the node is in a location where the expression is available or not. + * @returns `true` if the expression is available. + */ + function isAvailableLocation(ref: { node: TSESTree.Node; name: string }) { + for (const guard of maybeGuards.reverse()) { + if (guard.isAvailableLocation(ref.node)) { + if (guard.browserEnvironment || guard.reference?.name === ref.name) { + return true; + } + } + } + return false; + } + + /** + * Checks whether the node is in a top-level location. + * @returns `true` if the node is in a top-level location. + */ + function isTopLevelLocation(node: TSESTree.Node) { + for (const func of functions) { + if (func.range[0] <= node.range[0] && node.range[1] <= func.range[1]) { + return false; + } + } + return true; + } + + /** + * Checks whether the node is in type annotation. + * @returns `true` if the node is in type annotation. + */ + function isInTypeAnnotation(node: TSESTree.Node) { + for (const typeAnnotation of typeAnnotations) { + if (typeAnnotation.range[0] <= node.range[0] && node.range[1] <= typeAnnotation.range[1]) { + return true; + } + } + return false; + } + + /** + * Iterate over the references of modules that can check the browser environment. + */ + function* iterateBrowserCheckerModuleReferences(): Iterable { + for (const ref of referenceTracker.iterateEsmReferences({ + 'esm-env': { + [ReferenceTracker.ESM]: true, + // See https://www.npmjs.com/package/esm-env + BROWSER: { + [ReferenceTracker.READ]: true + } + }, + '$app/environment': { + [ReferenceTracker.ESM]: true, + // See https://svelte.dev/docs/kit/$app-environment#browser + browser: { + [ReferenceTracker.READ]: true + } + } + })) { + if (ref.node.type === 'Identifier' || ref.node.type === 'MemberExpression') { + yield ref.node; + } else if (ref.node.type === 'ImportSpecifier') { + const variable = findVariable(context, ref.node.local); + if (variable) { + for (const reference of variable.references) { + if (reference.isRead() && reference.identifier.type === 'Identifier') { + yield reference.identifier; + } + } + } + } + } + } + + /** + * Iterate over the used references of global variables. + */ + function* iterateBrowserGlobalReferences(): Iterable> { + yield* referenceTracker.iterateGlobalReferences( + Object.fromEntries( + blowerGlobals.map((name) => [ + name, + { + [ReferenceTracker.READ]: true + } + ]) + ) + ); + } + + /** + * If the node is a reference used in a guard clause that checks if the node is in a browser environment, + * it returns information about the expression that checks if the browser variable is available. + * @returns The guard info. + */ + function getGuardCheckerFromReference( + node: TSESTree.Node + ): ((node: TSESTree.Node) => boolean) | null { + const parent = node.parent; + if (!parent) return null; + if (parent.type === 'BinaryExpression') { + if ( + parent.operator === 'instanceof' && + parent.left === node && + node.type === 'MemberExpression' + ) { + // e.g. if (globalThis.window instanceof X) + return getGuardChecker({ node: parent }); + } + const operand = + parent.left === node ? parent.right : parent.right === node ? parent.left : null; + if (!operand) return null; + + const staticValue = getStaticValue(operand, getScope(context, operand)); + if (!staticValue) return null; + + if (staticValue.value === undefined && node.type === 'MemberExpression') { + if (parent.operator === '!==' || parent.operator === '!=') { + // e.g. if (globalThis.window !== undefined), if (globalThis.window != undefined) + return getGuardChecker({ node: parent }); + } else if (parent.operator === '===' || parent.operator === '==') { + // e.g. if (globalThis.window === undefined), if (globalThis.window == undefined) + return getGuardChecker({ node: parent, not: true }); + } + } else if (staticValue.value === null && node.type === 'MemberExpression') { + if (parent.operator === '!=') { + // e.g. if (globalThis.window != null) + return getGuardChecker({ node: parent }); + } else if (parent.operator === '==') { + // e.g. if (globalThis.window == null) + return getGuardChecker({ node: parent, not: true }); + } + } + return null; + } + if ( + parent.type === 'UnaryExpression' && + parent.operator === 'typeof' && + parent.argument === node + ) { + const pp = parent.parent; + if (!pp || pp.type !== 'BinaryExpression') { + return null; + } + const staticValue = getStaticValue( + pp.left === parent ? pp.right : pp.left, + getScope(context, node) + ); + if (!staticValue) return null; + + if (staticValue.value !== 'undefined' && staticValue.value !== 'object') { + return null; + } + if (pp.operator === '!==' || pp.operator === '!=') { + if (staticValue.value === 'undefined') { + // e.g. if (typeof window !== "undefined"), if (typeof window != "undefined") + return getGuardChecker({ node: pp }); + } + // e.g. if (typeof window !== "object"), if (typeof window != "object") + return getGuardChecker({ node: pp, not: true }); + } else if (pp.operator === '===' || pp.operator === '==') { + if (staticValue.value === 'undefined') { + // e.g. if (typeof window === "undefined"), if (typeof window == "undefined") + return getGuardChecker({ node: pp, not: true }); + } + // e.g. if (typeof window === "object"), if (typeof window == "object") + return getGuardChecker({ node: pp }); + } + return null; + } + + if (node.type === 'MemberExpression') { + if ( + ((parent.type === 'CallExpression' && parent.callee === node) || + (parent.type === 'MemberExpression' && parent.object === node)) && + parent.optional + ) { + // e.g. globalThis.location?.href + return (n) => n === node; + } + // e.g. if (globalThis.window) + return getGuardChecker({ node }); + } + return null; + } + + /** + * If the node is a guard clause checking, + * returns a function to check if the node is available. + */ + function getGuardChecker(guardInfo: { + node: TSESTree.Expression; + not?: boolean; + }): ((node: TSESTree.Node) => boolean) | null { + const parent = guardInfo.node.parent; + if (!parent) return null; + + if (parent.type === 'ConditionalExpression') { + const block = guardInfo.not ? parent.alternate : parent.consequent; + return (n) => block.range[0] <= n.range[0] && n.range[1] <= block.range[1]; + } + if (parent.type === 'UnaryExpression' && parent.operator === '!') { + return getGuardChecker({ not: !guardInfo.not, node: parent }); + } + if (parent.type === 'IfStatement' && parent.test === guardInfo.node) { + if (!guardInfo.not) { + const block = parent.consequent; + return (n) => block.range[0] <= n.range[0] && n.range[1] <= block.range[1]; + } + if (parent.alternate) { + const block = parent.alternate; + return (n) => block.range[0] <= n.range[0] && n.range[1] <= block.range[1]; + } + if (!hasJumpStatementInAllPath(parent.consequent)) { + return null; + } + const pp = parent.parent; + if (!pp || (pp.type !== 'BlockStatement' && pp.type !== 'Program')) { + return null; + } + const start = parent.range[1]; + const end = pp.range[1]; + + return (n) => start <= n.range[0] && n.range[1] <= end; + } + if ( + !guardInfo.not && + parent.type === 'LogicalExpression' && + parent.operator === '&&' && + parent.left === guardInfo.node + ) { + const block = parent.right; + return (n) => block.range[0] <= n.range[0] && n.range[1] <= block.range[1]; + } + return null; + } + } +}); + +/** + * Get the list of browser-specific globals. + */ +function getBrowserGlobals() { + const nodeGlobals = new Set(Object.keys(globals.node)); + return [ + 'window', + 'document', + ...Object.keys(globals.browser).filter((name) => !nodeGlobals.has(name)) + ]; +} + +/** + * Checks whether all paths of a given statement have jump statements. + * @param {Statement} statement + * @returns {boolean} + */ +function hasJumpStatementInAllPath(statement: TSESTree.Statement): boolean { + if (isJumpStatement(statement)) { + return true; + } + if (statement.type === 'BlockStatement') { + return statement.body.some(hasJumpStatementInAllPath); + } + if (statement.type === 'IfStatement') { + if (!statement.alternate) { + return false; + } + return ( + hasJumpStatementInAllPath(statement.alternate) && + hasJumpStatementInAllPath(statement.consequent) + ); + } + return false; +} + +/** + * Checks whether the given statement is a jump statement. + * @param {Statement} statement + * @returns {statement is JumpStatement} + */ +function isJumpStatement(statement: TSESTree.Statement) { + return ( + statement.type === 'ReturnStatement' || + statement.type === 'ContinueStatement' || + statement.type === 'BreakStatement' + ); +} diff --git a/packages/eslint-plugin-svelte/src/rules/no-unnecessary-state-wrap.ts b/packages/eslint-plugin-svelte/src/rules/no-unnecessary-state-wrap.ts index db54b7440..f89f52765 100644 --- a/packages/eslint-plugin-svelte/src/rules/no-unnecessary-state-wrap.ts +++ b/packages/eslint-plugin-svelte/src/rules/no-unnecessary-state-wrap.ts @@ -53,8 +53,12 @@ export default createRule('no-unnecessary-state-wrap', { const options = context.options[0] ?? {}; const additionalReactiveClasses = options.additionalReactiveClasses ?? []; const allowReassign = options.allowReassign ?? false; + const { globalScope } = context.sourceCode.scopeManager; + if (globalScope == null) { + return {}; + } - const referenceTracker = new ReferenceTracker(context.sourceCode.scopeManager.globalScope!); + const referenceTracker = new ReferenceTracker(globalScope); const traceMap: Record> = {}; for (const reactiveClass of REACTIVE_CLASSES) { traceMap[reactiveClass] = { @@ -79,8 +83,10 @@ export default createRule('no-unnecessary-state-wrap', { }); function isReassigned(identifier: TSESTree.Identifier): boolean { - const variable = context.sourceCode.scopeManager.getDeclaredVariables(identifier.parent)[0]; - return variable.references.some((ref) => { + const references = context.sourceCode.scopeManager + .getDeclaredVariables(identifier.parent) + .flatMap((v) => v.references); + return references.some((ref) => { return ref.isWrite() && ref.identifier !== identifier; }); } diff --git a/packages/eslint-plugin-svelte/src/rules/no-unused-props.ts b/packages/eslint-plugin-svelte/src/rules/no-unused-props.ts index 61b60e99e..0ccefcebf 100644 --- a/packages/eslint-plugin-svelte/src/rules/no-unused-props.ts +++ b/packages/eslint-plugin-svelte/src/rules/no-unused-props.ts @@ -6,6 +6,7 @@ import { findVariable } from '../utils/ast-utils.js'; import { toRegExp } from '../utils/regexp.js'; type PropertyPathArray = string[]; +type DeclaredPropertyNames = Set<{ originalName: string; aliasName: string }>; let isRemovedWarningShown = false; @@ -37,6 +38,10 @@ export default createRule('no-unused-props', { type: 'string' }, default: [] + }, + allowUnusedNestedProperties: { + type: 'boolean', + default: false } }, additionalProperties: false @@ -182,16 +187,27 @@ export default createRule('no-unused-props', { return sourceFile.fileName.includes('node_modules/typescript/lib/'); } - function getUsedPropertyNamesFromPattern(pattern: TSESTree.ObjectPattern): Set { - const usedProps = new Set(); + function getUsedPropertyNamesFromPattern( + pattern: TSESTree.ObjectPattern + ): DeclaredPropertyNames { + const usedProps: DeclaredPropertyNames = new Set(); for (const prop of pattern.properties) { - if (prop.type === 'Property' && prop.key.type === 'Identifier') { - usedProps.add(prop.key.name); + if (prop.type === 'Property') { + if (prop.key.type === 'Identifier') { + usedProps.add({ originalName: prop.key.name, aliasName: prop.key.name }); + } else if ( + prop.key.type === 'Literal' && + typeof prop.key.value === 'string' && + prop.value.type === 'Identifier' + ) { + usedProps.add({ originalName: prop.key.value, aliasName: prop.value.name }); + } } else if (prop.type === 'RestElement') { // If there's a rest element, all properties are potentially used return new Set(); } } + return usedProps; } @@ -229,7 +245,7 @@ export default createRule('no-unused-props', { }: { propsType: ts.Type; usedPropertyPaths: string[]; - declaredPropertyNames: Set; + declaredPropertyNames: DeclaredPropertyNames; reportNode: TSESTree.Node; parentPath: string[]; checkedPropsTypes: Set; @@ -287,7 +303,9 @@ export default createRule('no-unused-props', { continue; } - const isUsedInProps = declaredPropertyNames.has(propName); + const isUsedInProps = Array.from(declaredPropertyNames).some((p) => { + return p.originalName === propName; + }); if (!isUsedInPath && !isUsedInProps) { reportedPropertyPaths.add(currentPathStr); @@ -338,11 +356,14 @@ export default createRule('no-unused-props', { * Returns true if the destructuring pattern includes a rest element, * which means all remaining properties are potentially used. */ - function hasRestElement(usedProps: Set): boolean { - return usedProps.size === 0; + function hasRestElement(declaredPropertyNames: DeclaredPropertyNames): boolean { + return declaredPropertyNames.size === 0; } - function normalizeUsedPaths(paths: PropertyPathArray[]): PropertyPathArray[] { + function normalizeUsedPaths( + paths: PropertyPathArray[], + allowUnusedNestedProperties: boolean + ): PropertyPathArray[] { const normalized: PropertyPathArray[] = []; for (const path of paths.sort((a, b) => a.length - b.length)) { if (path.length === 0) continue; @@ -351,7 +372,11 @@ export default createRule('no-unused-props', { } normalized.push(path); } - return normalized; + return normalized.map((path) => { + // If we allow unused nested properties, only return first level properties + if (allowUnusedNestedProperties) return [path[0]]; + return path; + }); } return { @@ -370,7 +395,7 @@ export default createRule('no-unused-props', { const propsType = typeChecker.getTypeFromTypeNode(tsNode.type); let usedPropertyPathsArray: PropertyPathArray[] = []; - let declaredPropertyNames = new Set(); + let declaredPropertyNames: DeclaredPropertyNames = new Set(); if (node.id.type === 'ObjectPattern') { declaredPropertyNames = getUsedPropertyNamesFromPattern(node.id); @@ -396,7 +421,10 @@ export default createRule('no-unused-props', { checkUnusedProperties({ propsType, - usedPropertyPaths: normalizeUsedPaths(usedPropertyPathsArray).map((pathArray) => { + usedPropertyPaths: normalizeUsedPaths( + usedPropertyPathsArray, + options.allowUnusedNestedProperties + ).map((pathArray) => { return pathArray.join('.'); }), declaredPropertyNames, diff --git a/packages/eslint-plugin-svelte/src/rules/no-unused-svelte-ignore.ts b/packages/eslint-plugin-svelte/src/rules/no-unused-svelte-ignore.ts index 01b137d93..51fc63e13 100644 --- a/packages/eslint-plugin-svelte/src/rules/no-unused-svelte-ignore.ts +++ b/packages/eslint-plugin-svelte/src/rules/no-unused-svelte-ignore.ts @@ -2,6 +2,8 @@ import { getSvelteCompileWarnings } from '../shared/svelte-compile-warns/index.j import { createRule } from '../utils/index.js'; import type { IgnoreItem } from '../shared/svelte-compile-warns/ignore-comment.js'; import { getSvelteIgnoreItems } from '../shared/svelte-compile-warns/ignore-comment.js'; +import { VERSION as SVELTE_VERSION } from 'svelte/compiler'; +import semver from 'semver'; export default createRule('no-unused-svelte-ignore', { meta: { @@ -46,6 +48,14 @@ export default createRule('no-unused-svelte-ignore', { } for (const unused of warnings.unusedIgnores) { + if (unused.code === 'reactive-component' && semver.satisfies(SVELTE_VERSION, '<5')) { + // Svelte v4 `reactive-component` warnings are not emitted + // when we use the `generate: false` compiler option. + // This is probably not the intended behavior of Svelte v4, but it's not going to be fixed, + // so as a workaround we'll ignore the `reactive-component` warnings. + // See https://github.com/sveltejs/eslint-plugin-svelte/issues/1192 + continue; + } context.report({ loc: { start: sourceCode.getLocFromIndex(unused.range[0]), diff --git a/packages/eslint-plugin-svelte/src/rules/prefer-const.ts b/packages/eslint-plugin-svelte/src/rules/prefer-const.ts index 842bbc924..e28765baa 100644 --- a/packages/eslint-plugin-svelte/src/rules/prefer-const.ts +++ b/packages/eslint-plugin-svelte/src/rules/prefer-const.ts @@ -68,7 +68,8 @@ export default createRule('prefer-const', { } } }, - additionalProperties: false + // Allow ESLint core rule properties in case new options are added in the future. + additionalProperties: true } ] }, diff --git a/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts b/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts new file mode 100644 index 000000000..c7f859dbd --- /dev/null +++ b/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts @@ -0,0 +1,147 @@ +import type { TSESTree } from '@typescript-eslint/types'; +import { createRule } from '../utils/index.js'; +import { getScope } from '../utils/ast-utils.js'; +import { VERSION as SVELTE_VERSION } from 'svelte/compiler'; +import semver from 'semver'; + +// Writable derived were introduced in Svelte 5.25.0 +const shouldRun = semver.satisfies(SVELTE_VERSION, '>=5.25.0'); + +type ValidFunctionType = TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression; +type ValidFunction = ValidFunctionType & { + body: TSESTree.BlockStatement; +}; + +type ValidAssignmentExpression = TSESTree.AssignmentExpression & { + operator: '='; + left: TSESTree.Identifier; +}; + +type ValidExpressionStatement = TSESTree.ExpressionStatement & { + expression: ValidAssignmentExpression; +}; + +function isEffectOrEffectPre(node: TSESTree.CallExpression) { + if (node.callee.type === 'Identifier') { + return node.callee.name === '$effect'; + } + if (node.callee.type === 'MemberExpression') { + return ( + node.callee.object.type === 'Identifier' && + node.callee.object.name === '$effect' && + node.callee.property.type === 'Identifier' && + node.callee.property.name === 'pre' + ); + } + + return false; +} + +function isValidFunctionArgument(argument: TSESTree.Node): argument is ValidFunction { + if ( + (argument.type !== 'FunctionExpression' && argument.type !== 'ArrowFunctionExpression') || + argument.params.length !== 0 + ) { + return false; + } + + if (argument.body.type !== 'BlockStatement') { + return false; + } + + return argument.body.body.length === 1; +} + +function isValidAssignment(statement: TSESTree.Statement): statement is ValidExpressionStatement { + if (statement.type !== 'ExpressionStatement') return false; + + const { expression } = statement; + return ( + expression.type === 'AssignmentExpression' && + expression.operator === '=' && + expression.left.type === 'Identifier' + ); +} + +function isStateVariable(init: TSESTree.Expression | null): init is TSESTree.CallExpression { + return ( + init?.type === 'CallExpression' && + init.callee.type === 'Identifier' && + init.callee.name === '$state' + ); +} + +export default createRule('prefer-writable-derived', { + meta: { + docs: { + description: 'Prefer using writable $derived instead of $state and $effect', + category: 'Best Practices', + recommended: true + }, + schema: [], + messages: { + unexpected: 'Prefer using writable $derived instead of $state and $effect', + suggestRewrite: 'Rewrite $state and $effect to $derived' + }, + type: 'suggestion', + conditions: [ + { + svelteVersions: ['5'], + runes: [true, 'undetermined'] + } + ], + hasSuggestions: true + }, + create(context) { + if (!shouldRun) { + return {}; + } + return { + CallExpression: (node: TSESTree.CallExpression) => { + if (!isEffectOrEffectPre(node) || node.arguments.length !== 1) { + return; + } + + const argument = node.arguments[0]; + if (!isValidFunctionArgument(argument)) { + return; + } + + const statement = argument.body.body[0]; + if (!isValidAssignment(statement)) { + return; + } + + const { left, right } = statement.expression; + const scope = getScope(context, statement); + const reference = scope.references.find( + (ref) => ref.identifier.type === 'Identifier' && ref.identifier.name === left.name + ); + + const def = reference?.resolved?.defs?.[0]; + if (!def || def.type !== 'Variable' || def.node.type !== 'VariableDeclarator') { + return; + } + + const { init } = def.node; + if (!isStateVariable(init)) { + return; + } + + context.report({ + node: def.node, + messageId: 'unexpected', + suggest: [ + { + messageId: 'suggestRewrite', + fix: (fixer) => { + const rightCode = context.sourceCode.getText(right); + return [fixer.replaceText(init, `$derived(${rightCode})`), fixer.remove(node)]; + } + } + ] + }); + } + }; + } +}); diff --git a/packages/eslint-plugin-svelte/src/rules/require-event-prefix.ts b/packages/eslint-plugin-svelte/src/rules/require-event-prefix.ts new file mode 100644 index 000000000..ed3c3ad08 --- /dev/null +++ b/packages/eslint-plugin-svelte/src/rules/require-event-prefix.ts @@ -0,0 +1,123 @@ +import { createRule } from '../utils/index.js'; +import { + type TSTools, + getTypeScriptTools, + isMethodSymbol, + isPropertySignatureKind, + isFunctionTypeKind, + isMethodSignatureKind, + isTypeReferenceKind, + isIdentifierKind +} from '../utils/ts-utils/index.js'; +import type { Symbol, Type } from 'typescript'; +import type { CallExpression } from 'estree'; + +export default createRule('require-event-prefix', { + meta: { + docs: { + description: 'require component event names to start with "on"', + category: 'Stylistic Issues', + conflictWithPrettier: false, + recommended: false + }, + schema: [ + { + type: 'object', + properties: { + checkAsyncFunctions: { + type: 'boolean' + } + }, + additionalProperties: false + } + ], + messages: { + nonPrefixedFunction: 'Component event name must start with "on".' + }, + type: 'suggestion', + conditions: [ + { + svelteVersions: ['5'], + svelteFileTypes: ['.svelte'] + } + ] + }, + create(context) { + const tsTools = getTypeScriptTools(context); + if (!tsTools) { + return {}; + } + + const checkAsyncFunctions = context.options[0]?.checkAsyncFunctions ?? false; + + return { + CallExpression(node) { + const propsType = getPropsType(node, tsTools); + if (propsType === undefined) { + return; + } + for (const property of propsType.getProperties()) { + if ( + isFunctionLike(property, tsTools) && + !property.getName().startsWith('on') && + (checkAsyncFunctions || !isFunctionAsync(property, tsTools)) + ) { + const declarationTsNode = property.getDeclarations()?.[0]; + const declarationEstreeNode = + declarationTsNode !== undefined + ? tsTools.service.tsNodeToESTreeNodeMap.get(declarationTsNode) + : undefined; + context.report({ + node: declarationEstreeNode ?? node, + messageId: 'nonPrefixedFunction' + }); + } + } + } + }; + } +}); + +function getPropsType(node: CallExpression, tsTools: TSTools): Type | undefined { + if ( + node.callee.type !== 'Identifier' || + node.callee.name !== '$props' || + node.parent.type !== 'VariableDeclarator' + ) { + return undefined; + } + + const tsNode = tsTools.service.esTreeNodeToTSNodeMap.get(node.parent.id); + if (tsNode === undefined) { + return undefined; + } + + return tsTools.service.program.getTypeChecker().getTypeAtLocation(tsNode); +} + +function isFunctionLike(functionSymbol: Symbol, tsTools: TSTools): boolean { + return ( + isMethodSymbol(functionSymbol, tsTools.ts) || + (functionSymbol.valueDeclaration !== undefined && + isPropertySignatureKind(functionSymbol.valueDeclaration, tsTools.ts) && + functionSymbol.valueDeclaration.type !== undefined && + isFunctionTypeKind(functionSymbol.valueDeclaration.type, tsTools.ts)) + ); +} + +function isFunctionAsync(functionSymbol: Symbol, tsTools: TSTools): boolean { + return ( + functionSymbol.getDeclarations()?.some((declaration) => { + if (!isMethodSignatureKind(declaration, tsTools.ts)) { + return false; + } + if (declaration.type === undefined || !isTypeReferenceKind(declaration.type, tsTools.ts)) { + return false; + } + return ( + isIdentifierKind(declaration.type.typeName, tsTools.ts) && + declaration.type.typeName.escapedText === 'Promise' + ); + }) ?? false + ); +} diff --git a/packages/eslint-plugin-svelte/src/shared/svelte-compile-warns/ignore-comment.ts b/packages/eslint-plugin-svelte/src/shared/svelte-compile-warns/ignore-comment.ts index 2655c789e..77cf5a0c4 100644 --- a/packages/eslint-plugin-svelte/src/shared/svelte-compile-warns/ignore-comment.ts +++ b/packages/eslint-plugin-svelte/src/shared/svelte-compile-warns/ignore-comment.ts @@ -1,7 +1,7 @@ import type { AST } from 'svelte-eslint-parser'; import type { RuleContext } from '../../types.js'; -const SVELTE_IGNORE_PATTERN = /^\s*svelte-ignore/m; +const SVELTE_IGNORE_PATTERN = /^\s*svelte-ignore\s+/; /** * Map of legacy code -> new code @@ -37,29 +37,45 @@ export function getSvelteIgnoreItems(context: RuleContext): (IgnoreItem | Ignore const ignoreComments: (IgnoreItem | IgnoreItemWithoutCode)[] = []; for (const comment of sourceCode.getAllComments()) { - const ignores = extractSvelteIgnore(comment.value, comment.range[0] + 2, comment); - if (ignores) { - ignoreComments.push(...ignores); - } else if (hasMissingCodeIgnore(comment.value)) { + const match = SVELTE_IGNORE_PATTERN.exec(comment.value); + if (!match) { + continue; + } + const codeListStart = match.index + match[0].length; + const codeList = comment.value.slice(codeListStart); + if (hasMissingCodeIgnore(codeList)) { ignoreComments.push({ range: comment.range, code: null, token: comment }); + } else { + const ignores = extractSvelteIgnore(comment.range[0] + 2, comment, codeList, codeListStart); + if (ignores) { + ignoreComments.push(...ignores); + } } } for (const token of sourceCode.ast.tokens) { if (token.type === 'HTMLComment') { const text = token.value.slice(4, -3); - const ignores = extractSvelteIgnore(text, token.range[0] + 4, token); - if (ignores) { - ignoreComments.push(...ignores); - } else if (hasMissingCodeIgnore(text)) { + const match = SVELTE_IGNORE_PATTERN.exec(text); + if (!match) { + continue; + } + const codeListStart = match.index + match[0].length; + const codeList = text.slice(codeListStart); + if (hasMissingCodeIgnore(codeList)) { ignoreComments.push({ range: token.range, code: null, token }); + } else { + const ignores = extractSvelteIgnore(token.range[0] + 4, token, codeList, codeListStart); + if (ignores) { + ignoreComments.push(...ignores); + } } } } @@ -69,46 +85,42 @@ export function getSvelteIgnoreItems(context: RuleContext): (IgnoreItem | Ignore /** Extract svelte-ignore rule names */ function extractSvelteIgnore( - text: string, startIndex: number, - token: AST.Token | AST.Comment + token: AST.Token | AST.Comment, + codeList: string, + ignoreStart: number ): IgnoreItem[] | null { - const m1 = SVELTE_IGNORE_PATTERN.exec(text); - if (!m1) { - return null; - } - const ignoreStart = m1.index + m1[0].length; - const beforeText = text.slice(ignoreStart); - if (!/^\s/.test(beforeText) || !beforeText.trim()) { - return null; - } - let start = startIndex + ignoreStart; - + const start = startIndex + ignoreStart; const results: IgnoreItem[] = []; - for (const code of beforeText.split(/\s/)) { - const end = start + code.length; - const trimmed = code.trim(); - if (trimmed) { + const separatorPattern = /\s*[\s,]\s*/g; + const separators = codeList.matchAll(separatorPattern); + let lastSeparatorEnd = 0; + for (const separator of separators) { + const code = codeList.slice(lastSeparatorEnd, separator.index); + if (code) { results.push({ - code: trimmed, - codeForV5: V5_REPLACEMENTS[trimmed] || trimmed.replace(/-/gu, '_'), - range: [start, end], + code, + codeForV5: V5_REPLACEMENTS[code] || code.replace(/-/gu, '_'), + range: [start + lastSeparatorEnd, start + separator.index], token }); } - start = end + 1; /* space */ + lastSeparatorEnd = separator.index + separator[0].length; + } + if (results.length === 0) { + const code = codeList; + results.push({ + code, + codeForV5: V5_REPLACEMENTS[code] || code.replace(/-/gu, '_'), + range: [start, start + code.length], + token + }); } return results; } /** Checks whether given comment has missing code svelte-ignore */ -function hasMissingCodeIgnore(text: string) { - const m1 = SVELTE_IGNORE_PATTERN.exec(text); - if (!m1) { - return false; - } - const ignoreStart = m1.index + m1[0].length; - const beforeText = text.slice(ignoreStart); - return !beforeText.trim(); +function hasMissingCodeIgnore(codeList: string) { + return !codeList.trim(); } diff --git a/packages/eslint-plugin-svelte/src/type-defs/@eslint-community/eslint-utils.d.ts b/packages/eslint-plugin-svelte/src/type-defs/@eslint-community/eslint-utils.d.ts index 474538df0..e4e6c3ab4 100644 --- a/packages/eslint-plugin-svelte/src/type-defs/@eslint-community/eslint-utils.d.ts +++ b/packages/eslint-plugin-svelte/src/type-defs/@eslint-community/eslint-utils.d.ts @@ -1,15 +1,18 @@ +import type { + CALL, + CONSTRUCT, + ESM, + READ, + StaticValue, + TraceMap, + TrackedReferences, + ReferenceTrackerOptions +} from '../../../node_modules/@eslint-community/eslint-utils/index.mjs'; +import type { AST } from 'svelte-eslint-parser'; +import type { TSESTree } from '@typescript-eslint/types'; +import type { Scope } from '@typescript-eslint/scope-manager'; + declare module '@eslint-community/eslint-utils' { - import type { AST } from 'svelte-eslint-parser'; - import type { TSESTree } from '@typescript-eslint/types'; - import type { Scope } from '@typescript-eslint/scope-manager'; - import type { - CALL, - CONSTRUCT, - ESM, - READ, - TraceMap - } from '@eslint-community/eslint-utils/referenceTracker'; - export { ReferenceTracker, TrackedReferences } from '../../../node_modules/@types/eslint-utils'; type Token = { type: string; value: string }; export function isArrowToken(token: Token): boolean; export function isCommaToken(token: Token): boolean; @@ -87,5 +90,17 @@ declare module '@eslint-community/eslint-utils' { public iterateGlobalReferences( traceMap: TraceMap ): IterableIterator>; + + /** + * Iterate the property references for a given expression AST node. + */ + public iteratePropertyReferences( + node: TSESTree.Expression, + traceMap: TraceMap + ): IterableIterator>; } + export function getStaticValue( + node: TSESTree.Node, + initialScope?: Scope | null | undefined + ): StaticValue | null; } diff --git a/packages/eslint-plugin-svelte/src/types-for-node.ts b/packages/eslint-plugin-svelte/src/types-for-node.ts index e67a7c5b3..d49592bd2 100644 --- a/packages/eslint-plugin-svelte/src/types-for-node.ts +++ b/packages/eslint-plugin-svelte/src/types-for-node.ts @@ -475,6 +475,8 @@ export type ASTNodeListener = { ) => void; SvelteSpreadAttribute?: (node: AST.SvelteSpreadAttribute & ASTNodeWithParent) => void; 'SvelteSpreadAttribute:exit'?: (node: AST.SvelteSpreadAttribute & ASTNodeWithParent) => void; + SvelteAttachTag?: (node: AST.SvelteAttachTag & ASTNodeWithParent) => void; + 'SvelteAttachTag:exit'?: (node: AST.SvelteAttachTag & ASTNodeWithParent) => void; SvelteDirective?: (node: AST.SvelteDirective & ASTNodeWithParent) => void; 'SvelteDirective:exit'?: (node: AST.SvelteDirective & ASTNodeWithParent) => void; SvelteStyleDirective?: (node: AST.SvelteStyleDirective & ASTNodeWithParent) => void; @@ -938,6 +940,8 @@ export type SvelteNodeListener = { ) => void; SvelteSpreadAttribute?: (node: AST.SvelteSpreadAttribute & ASTNodeWithParent) => void; 'SvelteSpreadAttribute:exit'?: (node: AST.SvelteSpreadAttribute & ASTNodeWithParent) => void; + SvelteAttachTag?: (node: AST.SvelteAttachTag & ASTNodeWithParent) => void; + 'SvelteAttachTag:exit'?: (node: AST.SvelteAttachTag & ASTNodeWithParent) => void; SvelteDirective?: (node: AST.SvelteDirective & ASTNodeWithParent) => void; 'SvelteDirective:exit'?: (node: AST.SvelteDirective & ASTNodeWithParent) => void; SvelteStyleDirective?: (node: AST.SvelteStyleDirective & ASTNodeWithParent) => void; diff --git a/packages/eslint-plugin-svelte/src/utils/ast-utils.ts b/packages/eslint-plugin-svelte/src/utils/ast-utils.ts index dd426e137..bff6c0951 100644 --- a/packages/eslint-plugin-svelte/src/utils/ast-utils.ts +++ b/packages/eslint-plugin-svelte/src/utils/ast-utils.ts @@ -462,7 +462,8 @@ export function getAttributeKeyText( | SvAST.SvelteStyleDirective | SvAST.SvelteDirective | SvAST.SvelteSpecialDirective - | SvAST.SvelteGenericsDirective, + | SvAST.SvelteGenericsDirective + | SvAST.SvelteAttachTag, context: RuleContext ): string { switch (node.type) { @@ -480,6 +481,8 @@ export function getAttributeKeyText( node.key.modifiers.length ? `|${node.key.modifiers.join('|')}` : '' }`; } + case 'SvelteAttachTag': + return '@attach'; default: throw new Error( `Unknown node type: ${ @@ -556,6 +559,7 @@ export function findClassesInAttribute( | SvAST.SvelteStyleDirective | SvAST.SvelteSpecialDirective | SvAST.SvelteGenericsDirective + | SvAST.SvelteAttachTag ): string[] { if (attribute.type === 'SvelteAttribute' && attribute.key.name === 'class') { return attribute.value.flatMap((value) => diff --git a/packages/eslint-plugin-svelte/src/utils/element-occurences.ts b/packages/eslint-plugin-svelte/src/utils/element-occurences.ts new file mode 100644 index 000000000..6bc30fa06 --- /dev/null +++ b/packages/eslint-plugin-svelte/src/utils/element-occurences.ts @@ -0,0 +1,55 @@ +import type { AST } from 'svelte-eslint-parser'; + +export enum ElementOccurenceCount { + ZeroOrOne, + One, + ZeroToInf +} + +function multiplyCounts( + left: ElementOccurenceCount, + right: ElementOccurenceCount +): ElementOccurenceCount { + if (left === ElementOccurenceCount.One) { + return right; + } + if (right === ElementOccurenceCount.One) { + return left; + } + if (left === right) { + return left; + } + return ElementOccurenceCount.ZeroToInf; +} + +function childElementOccurenceCount(parent: AST.SvelteHTMLNode | null): ElementOccurenceCount { + if (parent === null) { + return ElementOccurenceCount.One; + } + if ( + [ + 'SvelteIfBlock', + 'SvelteElseBlock', + 'SvelteAwaitBlock', + 'SvelteAwaitPendingBlock', + 'SvelteAwaitThenBlock', + 'SvelteAwaitCatchBlock' + ].includes(parent.type) + ) { + return ElementOccurenceCount.ZeroOrOne; + } + if ( + ['SvelteEachBlock', 'SvelteSnippetBlock'].includes(parent.type) || + (parent.type === 'SvelteElement' && parent.kind === 'component') + ) { + return ElementOccurenceCount.ZeroToInf; + } + return ElementOccurenceCount.One; +} + +export function elementOccurrenceCount(element: AST.SvelteHTMLNode): ElementOccurenceCount { + const parentCount = + element.parent !== null ? elementOccurrenceCount(element.parent) : ElementOccurenceCount.One; + const parentChildCount = childElementOccurenceCount(element.parent); + return multiplyCounts(parentCount, parentChildCount); +} diff --git a/packages/eslint-plugin-svelte/src/utils/expression-affixes.ts b/packages/eslint-plugin-svelte/src/utils/expression-affixes.ts new file mode 100644 index 000000000..53b6562bc --- /dev/null +++ b/packages/eslint-plugin-svelte/src/utils/expression-affixes.ts @@ -0,0 +1,209 @@ +import type { TSESTree } from '@typescript-eslint/types'; +import { findVariable } from './ast-utils.js'; +import type { RuleContext } from '../types.js'; +import type { AST } from 'svelte-eslint-parser'; + +// Variable prefix extraction + +export function extractExpressionPrefixVariable( + context: RuleContext, + expression: TSESTree.Expression +): TSESTree.Identifier | null { + switch (expression.type) { + case 'BinaryExpression': + return extractBinaryExpressionPrefixVariable(context, expression); + case 'Identifier': + return extractVariablePrefixVariable(context, expression); + case 'MemberExpression': + return extractMemberExpressionPrefixVariable(expression); + case 'TemplateLiteral': + return extractTemplateLiteralPrefixVariable(context, expression); + default: + return null; + } +} + +function extractBinaryExpressionPrefixVariable( + context: RuleContext, + expression: TSESTree.BinaryExpression +): TSESTree.Identifier | null { + return expression.left.type !== 'PrivateIdentifier' + ? extractExpressionPrefixVariable(context, expression.left) + : null; +} + +function extractVariablePrefixVariable( + context: RuleContext, + expression: TSESTree.Identifier +): TSESTree.Identifier | null { + const variable = findVariable(context, expression); + if ( + variable === null || + variable.identifiers.length !== 1 || + variable.identifiers[0].parent.type !== 'VariableDeclarator' || + variable.identifiers[0].parent.init === null + ) { + return expression; + } + return ( + extractExpressionPrefixVariable(context, variable.identifiers[0].parent.init) ?? expression + ); +} + +function extractMemberExpressionPrefixVariable( + expression: TSESTree.MemberExpression +): TSESTree.Identifier | null { + return expression.property.type === 'Identifier' ? expression.property : null; +} + +function extractTemplateLiteralPrefixVariable( + context: RuleContext, + expression: TSESTree.TemplateLiteral +): TSESTree.Identifier | null { + const literalParts = [...expression.expressions, ...expression.quasis].sort((a, b) => + a.range[0] < b.range[0] ? -1 : 1 + ); + for (const part of literalParts) { + if (part.type === 'TemplateElement' && part.value.raw === '') { + // Skip empty quasi in the begining + continue; + } + if (part.type !== 'TemplateElement') { + return extractExpressionPrefixVariable(context, part); + } + return null; + } + return null; +} + +// Literal prefix extraction + +export function extractExpressionPrefixLiteral( + context: RuleContext, + expression: AST.SvelteLiteral | TSESTree.Node +): string | null { + switch (expression.type) { + case 'BinaryExpression': + return extractBinaryExpressionPrefixLiteral(context, expression); + case 'Identifier': + return extractVariablePrefixLiteral(context, expression); + case 'Literal': + return typeof expression.value === 'string' ? expression.value : null; + case 'SvelteLiteral': + return expression.value; + case 'TemplateLiteral': + return extractTemplateLiteralPrefixLiteral(context, expression); + default: + return null; + } +} + +function extractBinaryExpressionPrefixLiteral( + context: RuleContext, + expression: TSESTree.BinaryExpression +): string | null { + return expression.left.type !== 'PrivateIdentifier' + ? extractExpressionPrefixLiteral(context, expression.left) + : null; +} + +function extractVariablePrefixLiteral( + context: RuleContext, + expression: TSESTree.Identifier +): string | null { + const variable = findVariable(context, expression); + if ( + variable === null || + variable.identifiers.length !== 1 || + variable.identifiers[0].parent.type !== 'VariableDeclarator' || + variable.identifiers[0].parent.init === null + ) { + return null; + } + return extractExpressionPrefixLiteral(context, variable.identifiers[0].parent.init); +} + +function extractTemplateLiteralPrefixLiteral( + context: RuleContext, + expression: TSESTree.TemplateLiteral +): string | null { + const literalParts = [...expression.expressions, ...expression.quasis].sort((a, b) => + a.range[0] < b.range[0] ? -1 : 1 + ); + for (const part of literalParts) { + if (part.type === 'TemplateElement') { + if (part.value.raw === '') { + // Skip empty quasi + continue; + } + return part.value.raw; + } + return extractExpressionPrefixLiteral(context, part); + } + return null; +} + +// Literal suffix extraction + +export function extractExpressionSuffixLiteral( + context: RuleContext, + expression: AST.SvelteLiteral | TSESTree.Node +): string | null { + switch (expression.type) { + case 'BinaryExpression': + return extractBinaryExpressionSuffixLiteral(context, expression); + case 'Identifier': + return extractVariableSuffixLiteral(context, expression); + case 'Literal': + return typeof expression.value === 'string' ? expression.value : null; + case 'SvelteLiteral': + return expression.value; + case 'TemplateLiteral': + return extractTemplateLiteralSuffixLiteral(context, expression); + default: + return null; + } +} + +function extractBinaryExpressionSuffixLiteral( + context: RuleContext, + expression: TSESTree.BinaryExpression +): string | null { + return extractExpressionSuffixLiteral(context, expression.right); +} + +function extractVariableSuffixLiteral( + context: RuleContext, + expression: TSESTree.Identifier +): string | null { + const variable = findVariable(context, expression); + if ( + variable === null || + variable.identifiers.length !== 1 || + variable.identifiers[0].parent.type !== 'VariableDeclarator' || + variable.identifiers[0].parent.init === null + ) { + return null; + } + return extractExpressionSuffixLiteral(context, variable.identifiers[0].parent.init); +} + +function extractTemplateLiteralSuffixLiteral( + context: RuleContext, + expression: TSESTree.TemplateLiteral +): string | null { + const literalParts = [...expression.expressions, ...expression.quasis].sort((a, b) => + a.range[0] < b.range[0] ? -1 : 1 + ); + for (const part of literalParts.reverse()) { + if (part.type === 'TemplateElement') { + if (part.value.raw === '') { + // Skip empty quasi + continue; + } + return part.value.raw; + } + return extractExpressionSuffixLiteral(context, part); + } + return null; +} diff --git a/packages/eslint-plugin-svelte/src/utils/rules.ts b/packages/eslint-plugin-svelte/src/utils/rules.ts index 3151d17f1..bc62cc8c6 100644 --- a/packages/eslint-plugin-svelte/src/utils/rules.ts +++ b/packages/eslint-plugin-svelte/src/utils/rules.ts @@ -19,6 +19,7 @@ import indent from '../rules/indent.js'; import infiniteReactiveLoop from '../rules/infinite-reactive-loop.js'; import maxAttributesPerLine from '../rules/max-attributes-per-line.js'; import mustacheSpacing from '../rules/mustache-spacing.js'; +import noAddEventListener from '../rules/no-add-event-listener.js'; import noAtDebugTags from '../rules/no-at-debug-tags.js'; import noAtHtmlTags from '../rules/no-at-html-tags.js'; import noDomManipulating from '../rules/no-dom-manipulating.js'; @@ -48,6 +49,7 @@ import noSpacesAroundEqualSignsInAttribute from '../rules/no-spaces-around-equal import noStoreAsync from '../rules/no-store-async.js'; import noSvelteInternal from '../rules/no-svelte-internal.js'; import noTargetBlank from '../rules/no-target-blank.js'; +import noTopLevelBrowserGlobals from '../rules/no-top-level-browser-globals.js'; import noTrailingSpaces from '../rules/no-trailing-spaces.js'; import noUnknownStyleDirectiveProperty from '../rules/no-unknown-style-directive-property.js'; import noUnnecessaryStateWrap from '../rules/no-unnecessary-state-wrap.js'; @@ -60,8 +62,10 @@ import preferClassDirective from '../rules/prefer-class-directive.js'; import preferConst from '../rules/prefer-const.js'; import preferDestructuredStoreProps from '../rules/prefer-destructured-store-props.js'; import preferStyleDirective from '../rules/prefer-style-directive.js'; +import preferWritableDerived from '../rules/prefer-writable-derived.js'; import requireEachKey from '../rules/require-each-key.js'; import requireEventDispatcherTypes from '../rules/require-event-dispatcher-types.js'; +import requireEventPrefix from '../rules/require-event-prefix.js'; import requireOptimizedStyleAttribute from '../rules/require-optimized-style-attribute.js'; import requireStoreCallbacksUseSetParam from '../rules/require-store-callbacks-use-set-param.js'; import requireStoreReactiveAccess from '../rules/require-store-reactive-access.js'; @@ -94,6 +98,7 @@ export const rules = [ infiniteReactiveLoop, maxAttributesPerLine, mustacheSpacing, + noAddEventListener, noAtDebugTags, noAtHtmlTags, noDomManipulating, @@ -123,6 +128,7 @@ export const rules = [ noStoreAsync, noSvelteInternal, noTargetBlank, + noTopLevelBrowserGlobals, noTrailingSpaces, noUnknownStyleDirectiveProperty, noUnnecessaryStateWrap, @@ -135,8 +141,10 @@ export const rules = [ preferConst, preferDestructuredStoreProps, preferStyleDirective, + preferWritableDerived, requireEachKey, requireEventDispatcherTypes, + requireEventPrefix, requireOptimizedStyleAttribute, requireStoreCallbacksUseSetParam, requireStoreReactiveAccess, diff --git a/packages/eslint-plugin-svelte/src/utils/ts-utils/index.ts b/packages/eslint-plugin-svelte/src/utils/ts-utils/index.ts index f386698b5..00762becc 100644 --- a/packages/eslint-plugin-svelte/src/utils/ts-utils/index.ts +++ b/packages/eslint-plugin-svelte/src/utils/ts-utils/index.ts @@ -306,3 +306,48 @@ export function getTypeOfPropertyOfType( // eslint-disable-next-line @typescript-eslint/no-explicit-any -- getTypeOfPropertyOfType is an internal API of TS. return (checker as any).getTypeOfPropertyOfType(type, name); } + +/** + * Check whether the given symbol is a method type or not. + */ +export function isMethodSymbol(type: TS.Symbol, ts: TypeScript): boolean { + return (type.getFlags() & ts.SymbolFlags.Method) !== 0; +} + +/** + * Check whether the given node is a property signature kind or not. + */ +export function isPropertySignatureKind( + node: TS.Node, + ts: TypeScript +): node is TS.PropertySignature { + return node.kind === ts.SyntaxKind.PropertySignature; +} + +/** + * Check whether the given node is a function type kind or not. + */ +export function isFunctionTypeKind(node: TS.Node, ts: TypeScript): node is TS.FunctionTypeNode { + return node.kind === ts.SyntaxKind.FunctionType; +} + +/** + * Check whether the given node is a method signature kind or not. + */ +export function isMethodSignatureKind(node: TS.Node, ts: TypeScript): node is TS.MethodSignature { + return node.kind === ts.SyntaxKind.MethodSignature; +} + +/** + * Check whether the given node is a type reference kind or not. + */ +export function isTypeReferenceKind(node: TS.Node, ts: TypeScript): node is TS.TypeReferenceNode { + return node.kind === ts.SyntaxKind.TypeReference; +} + +/** + * Check whether the given node is an identifier kind or not. + */ +export function isIdentifierKind(node: TS.Node, ts: TypeScript): node is TS.Identifier { + return node.kind === ts.SyntaxKind.Identifier; +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/class01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/class01-input.svelte index b849abc73..8109e77f1 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/class01-input.svelte +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/class01-input.svelte @@ -10,6 +10,14 @@ Text 3 +{#each ["one", "two"] as iter} + {iter} +{/each} + + + Text 5 + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/svelte-5/_requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/svelte-5/_requirements.json new file mode 100644 index 000000000..498661308 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/svelte-5/_requirements.json @@ -0,0 +1,3 @@ +{ + "svelte": ">=5.0.0" +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/svelte-5/class01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/svelte-5/class01-input.svelte new file mode 100644 index 000000000..a44da9098 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/svelte-5/class01-input.svelte @@ -0,0 +1,11 @@ +Outside + +{#snippet iterated()} + Text 4 +{/snippet} + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-prefix01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-prefix01-input.svelte new file mode 100644 index 000000000..d3437cb3b --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-prefix01-input.svelte @@ -0,0 +1,33 @@ + + +Click me! + +Click me two! + +Click me two! + +Click me three! + +Click me three! + +Click me four! + +Click me four! + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-suffix01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-suffix01-input.svelte new file mode 100644 index 000000000..2dd97dc31 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-suffix01-input.svelte @@ -0,0 +1,46 @@ + + +Click me! + +Click me two! + +Click me two! + +Click me three! + +Click me three! + + +Click me four! + +Click me four! + +{#each ["one", "two"] as count} + Bold in each +{/each} + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-universal01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-universal01-input.svelte new file mode 100644 index 000000000..72ea834f2 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-universal01-input.svelte @@ -0,0 +1,23 @@ + + +Click me! + +Click me two! + +Click me two! + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/id-dynamic-prefix01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/id-dynamic-prefix01-input.svelte new file mode 100644 index 000000000..3e6743a71 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/id-dynamic-prefix01-input.svelte @@ -0,0 +1,27 @@ + + +Click me! + +Click me two! + +Click me three! + +Click me four! + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/id-dynamic-suffix01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/id-dynamic-suffix01-input.svelte new file mode 100644 index 000000000..dc701edaf --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/id-dynamic-suffix01-input.svelte @@ -0,0 +1,27 @@ + + +Click me! + +Click me two! + +Click me three! + +Click me four! + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/id-dynamic-universal01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/id-dynamic-universal01-input.svelte new file mode 100644 index 000000000..457936530 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/id-dynamic-universal01-input.svelte @@ -0,0 +1,23 @@ + + +Click me! + +Click me two! + +Click me two! + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/_requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/_requirements.json new file mode 100644 index 000000000..0192b1098 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/_requirements.json @@ -0,0 +1,3 @@ +{ + "svelte": ">=5.0.0-0" +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/test01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/test01-errors.yaml new file mode 100644 index 000000000..db04267ef --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/test01-errors.yaml @@ -0,0 +1,396 @@ +- message: Do not use `addEventListener`. Use the `on` function from + `svelte/events` instead. + line: 17 + column: 3 + suggestions: + - desc: Use `on` from `svelte/events` instead + output: | + + + +- message: Do not use `addEventListener`. Use the `on` function from + `svelte/events` instead. + line: 18 + column: 3 + suggestions: + - desc: Use `on` from `svelte/events` instead + output: | + + + +- message: Do not use `addEventListener`. Use the `on` function from + `svelte/events` instead. + line: 21 + column: 2 + suggestions: + - desc: Use `on` from `svelte/events` instead + output: | + + + +- message: Do not use `addEventListener`. Use the `on` function from + `svelte/events` instead. + line: 22 + column: 2 + suggestions: + - desc: Use `on` from `svelte/events` instead + output: | + + + +- message: Do not use `addEventListener`. Use the `on` function from + `svelte/events` instead. + line: 23 + column: 2 + suggestions: + - desc: Use `on` from `svelte/events` instead + output: | + + + +- message: Do not use `addEventListener`. Use the `on` function from + `svelte/events` instead. + line: 26 + column: 2 + suggestions: + - desc: Use `on` from `svelte/events` instead + output: | + + + +- message: Do not use `addEventListener`. Use the `on` function from + `svelte/events` instead. + line: 28 + column: 2 + suggestions: + - desc: Use `on` from `svelte/events` instead + output: | + + + +- message: Do not use `addEventListener`. Use the `on` function from + `svelte/events` instead. + line: 31 + column: 2 + suggestions: + - desc: Use `on` from `svelte/events` instead + output: | + + + +- message: Do not use `addEventListener`. Use the `on` function from + `svelte/events` instead. + line: 34 + column: 2 + suggestions: + - desc: Use `on` from `svelte/events` instead + output: | + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/test01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/test01-input.svelte new file mode 100644 index 000000000..5f8a27c6c --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/test01-input.svelte @@ -0,0 +1,37 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/typescript01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/typescript01-errors.yaml new file mode 100644 index 000000000..b1c8fc473 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/typescript01-errors.yaml @@ -0,0 +1,17 @@ +- message: Do not use `addEventListener`. Use the `on` function from + `svelte/events` instead. + line: 6 + column: 2 + suggestions: + - desc: Use `on` from `svelte/events` instead + output: | + + +
Hello
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/typescript01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/typescript01-input.svelte new file mode 100644 index 000000000..180366a12 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/typescript01-input.svelte @@ -0,0 +1,10 @@ + + +
Hello
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/valid/_requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/valid/_requirements.json new file mode 100644 index 000000000..0192b1098 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/valid/_requirements.json @@ -0,0 +1,3 @@ +{ + "svelte": ">=5.0.0-0" +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/valid/test01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/valid/test01-input.svelte new file mode 100644 index 000000000..0b48a7d94 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/valid/test01-input.svelte @@ -0,0 +1,19 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env01-errors.yaml new file mode 100644 index 000000000..1c1b0d997 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env01-errors.yaml @@ -0,0 +1,8 @@ +- message: Unexpected top-level browser global variable "location". + line: 8 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 13 + column: 15 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env01-input.svelte new file mode 100644 index 000000000..98cf30b68 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env01-input.svelte @@ -0,0 +1,15 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env02-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env02-errors.yaml new file mode 100644 index 000000000..1c1b0d997 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env02-errors.yaml @@ -0,0 +1,8 @@ +- message: Unexpected top-level browser global variable "location". + line: 8 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 13 + column: 15 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env02-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env02-input.svelte new file mode 100644 index 000000000..124d00c83 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env02-input.svelte @@ -0,0 +1,15 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env03-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env03-errors.yaml new file mode 100644 index 000000000..c40513248 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env03-errors.yaml @@ -0,0 +1,4 @@ +- message: Unexpected top-level browser global variable "location". + line: 3 + column: 15 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env03-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env03-input.svelte new file mode 100644 index 000000000..373f21648 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env03-input.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards01-errors.yaml new file mode 100644 index 000000000..d02090e05 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards01-errors.yaml @@ -0,0 +1,44 @@ +- message: Unexpected top-level browser global variable "location". + line: 5 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 10 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 15 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 18 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 23 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 30 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 35 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 38 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 43 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 50 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 55 + column: 15 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards01-input.svelte new file mode 100644 index 000000000..7a520431c --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards01-input.svelte @@ -0,0 +1,57 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards02-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards02-errors.yaml new file mode 100644 index 000000000..2d829d83c --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards02-errors.yaml @@ -0,0 +1,16 @@ +- message: Unexpected top-level browser global variable "location". + line: 5 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 10 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 15 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 18 + column: 15 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards02-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards02-input.svelte new file mode 100644 index 000000000..f02de6661 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards02-input.svelte @@ -0,0 +1,22 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards03-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards03-errors.yaml new file mode 100644 index 000000000..b6c66c354 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards03-errors.yaml @@ -0,0 +1,60 @@ +- message: Unexpected top-level browser global variable "location". + line: 5 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 10 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 15 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 18 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 25 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 28 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 35 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 38 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 42 + column: 6 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 44 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 46 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 48 + column: 6 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 50 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 52 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 57 + column: 15 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards03-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards03-input.svelte new file mode 100644 index 000000000..3d39e87c9 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards03-input.svelte @@ -0,0 +1,59 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards04-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards04-errors.yaml new file mode 100644 index 000000000..aee75b03c --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards04-errors.yaml @@ -0,0 +1,12 @@ +- message: Unexpected top-level browser global variable "location". + line: 5 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 10 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 15 + column: 15 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards04-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards04-input.svelte new file mode 100644 index 000000000..0512877d0 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards04-input.svelte @@ -0,0 +1,17 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards05-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards05-errors.yaml new file mode 100644 index 000000000..b519bfe9b --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards05-errors.yaml @@ -0,0 +1,8 @@ +- message: Unexpected top-level browser global variable "location". + line: 3 + column: 31 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 4 + column: 25 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards05-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards05-input.svelte new file mode 100644 index 000000000..f2ad9a9c8 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards05-input.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards06-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards06-errors.yaml new file mode 100644 index 000000000..6f17d46f4 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards06-errors.yaml @@ -0,0 +1,8 @@ +- message: Unexpected top-level browser global variable "location". + line: 9 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 13 + column: 16 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards06-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards06-input.svelte new file mode 100644 index 000000000..8177f7e20 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards06-input.svelte @@ -0,0 +1,18 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards07-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards07-errors.yaml new file mode 100644 index 000000000..0dfb9d5c5 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards07-errors.yaml @@ -0,0 +1,16 @@ +- message: Unexpected top-level browser global variable "location". + line: 13 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 17 + column: 16 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 28 + column: 16 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 33 + column: 15 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards07-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards07-input.svelte new file mode 100644 index 000000000..0b36ee3a2 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards07-input.svelte @@ -0,0 +1,35 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards08-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards08-errors.yaml new file mode 100644 index 000000000..8e2a6880c --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards08-errors.yaml @@ -0,0 +1,20 @@ +- message: Unexpected top-level browser global variable "location". + line: 3 + column: 21 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 3 + column: 49 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 5 + column: 14 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 5 + column: 37 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 7 + column: 14 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards08-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards08-input.svelte new file mode 100644 index 000000000..094bc97d8 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards08-input.svelte @@ -0,0 +1,8 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test01-errors.yaml new file mode 100644 index 000000000..63e4b8cff --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test01-errors.yaml @@ -0,0 +1,4 @@ +- message: Unexpected top-level browser global variable "window". + line: 2 + column: 12 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test01-input.svelte new file mode 100644 index 000000000..d6b7eee7e --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test01-input.svelte @@ -0,0 +1,4 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test02-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test02-errors.yaml new file mode 100644 index 000000000..247c0c4a8 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test02-errors.yaml @@ -0,0 +1,4 @@ +- message: Unexpected top-level browser global variable "location". + line: 2 + column: 14 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test02-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test02-input.svelte new file mode 100644 index 000000000..90b80b1bb --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test02-input.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test03-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test03-errors.yaml new file mode 100644 index 000000000..63d8ba42a --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test03-errors.yaml @@ -0,0 +1,4 @@ +- message: Unexpected top-level browser global variable "localStorage". + line: 2 + column: 12 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test03-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test03-input.svelte new file mode 100644 index 000000000..895ae2616 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test03-input.svelte @@ -0,0 +1,4 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/effect01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/effect01-input.svelte new file mode 100644 index 000000000..cc4ecd464 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/effect01-input.svelte @@ -0,0 +1,6 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env-guards01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env-guards01-input.svelte new file mode 100644 index 000000000..312b9b4fb --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env-guards01-input.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env01-input.svelte new file mode 100644 index 000000000..9ec1e8c38 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env01-input.svelte @@ -0,0 +1,8 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env02-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env02-input.svelte new file mode 100644 index 000000000..8bf76db6d --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env02-input.svelte @@ -0,0 +1,8 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env03-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env03-input.svelte new file mode 100644 index 000000000..513c6b096 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env03-input.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards01-input.svelte new file mode 100644 index 000000000..aaf4ad2c1 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards01-input.svelte @@ -0,0 +1,52 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards02-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards02-input.svelte new file mode 100644 index 000000000..ce58a5882 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards02-input.svelte @@ -0,0 +1,22 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards03-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards03-input.svelte new file mode 100644 index 000000000..86d47e6eb --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards03-input.svelte @@ -0,0 +1,52 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards04-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards04-input.svelte new file mode 100644 index 000000000..e42820f31 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards04-input.svelte @@ -0,0 +1,17 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards05-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards05-input.svelte new file mode 100644 index 000000000..6bbaabf75 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards05-input.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards06-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards06-input.svelte new file mode 100644 index 000000000..c414702c7 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards06-input.svelte @@ -0,0 +1,18 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards07-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards07-input.svelte new file mode 100644 index 000000000..add9f9744 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards07-input.svelte @@ -0,0 +1,26 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards08-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards08-input.svelte new file mode 100644 index 000000000..5afe336b0 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards08-input.svelte @@ -0,0 +1,8 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/on-mount01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/on-mount01-input.svelte new file mode 100644 index 000000000..7065d04e9 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/on-mount01-input.svelte @@ -0,0 +1,8 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/ts01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/ts01-input.svelte new file mode 100644 index 000000000..93b8f55b9 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/ts01-input.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-bind1-config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-bind1-config.json new file mode 100644 index 000000000..e68fc5b00 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-bind1-config.json @@ -0,0 +1,7 @@ +{ + "options": [ + { + "allowReassign": true + } + ] +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-bind1-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-bind1-input.svelte new file mode 100644 index 000000000..a59bfcf6f --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-bind1-input.svelte @@ -0,0 +1,8 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-bind2-config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-bind2-config.json new file mode 100644 index 000000000..e68fc5b00 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-bind2-config.json @@ -0,0 +1,7 @@ +{ + "options": [ + { + "allowReassign": true + } + ] +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-bind2-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-bind2-input.svelte new file mode 100644 index 000000000..ec454a7bf --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-bind2-input.svelte @@ -0,0 +1,8 @@ + + + svelteSet, (v) => (svelteSet = v)} /> diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/invalid/alias-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/invalid/alias-errors.yaml new file mode 100644 index 000000000..89c1a84af --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/invalid/alias-errors.yaml @@ -0,0 +1,4 @@ +- message: "'test' is an unused Props property." + line: 7 + column: 8 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/invalid/alias-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/invalid/alias-input.svelte new file mode 100644 index 000000000..59f23192b --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/invalid/alias-input.svelte @@ -0,0 +1,10 @@ + + +{foo} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/alias-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/alias-input.svelte new file mode 100644 index 000000000..857e6534d --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/alias-input.svelte @@ -0,0 +1,11 @@ + + +

{test}

+
svelte/no-unused-props does not always respect aliases
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/nested-unused-config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/nested-unused-config.json new file mode 100644 index 000000000..57afa3f3f --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/nested-unused-config.json @@ -0,0 +1,7 @@ +{ + "options": [ + { + "allowUnusedNestedProperties": true + } + ] +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/nested-unused-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/nested-unused-input.svelte new file mode 100644 index 000000000..8293761a6 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/nested-unused-input.svelte @@ -0,0 +1,10 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/nested-unused2-config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/nested-unused2-config.json new file mode 100644 index 000000000..57afa3f3f --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/nested-unused2-config.json @@ -0,0 +1,7 @@ +{ + "options": [ + { + "allowUnusedNestedProperties": true + } + ] +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/nested-unused2-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/nested-unused2-input.svelte new file mode 100644 index 000000000..0d38a3136 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/nested-unused2-input.svelte @@ -0,0 +1,10 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-svelte-ignore/valid/reactive-component-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-svelte-ignore/valid/reactive-component-input.svelte new file mode 100644 index 000000000..d612deb43 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-svelte-ignore/valid/reactive-component-input.svelte @@ -0,0 +1,15 @@ + + +{#if MyComponent} + + +{/if} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-svelte-ignore/valid/reactive-component-requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-svelte-ignore/valid/reactive-component-requirements.json new file mode 100644 index 000000000..b224b893c --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-svelte-ignore/valid/reactive-component-requirements.json @@ -0,0 +1,3 @@ +{ + "svelte": "^4" +} \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-svelte-ignore/valid/svelte-ignore-comma-separated-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-svelte-ignore/valid/svelte-ignore-comma-separated-input.svelte new file mode 100644 index 000000000..7e2559f55 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-svelte-ignore/valid/svelte-ignore-comma-separated-input.svelte @@ -0,0 +1,5 @@ + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-svelte-ignore/valid/svelte-ignore-comma-separated-requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-svelte-ignore/valid/svelte-ignore-comma-separated-requirements.json new file mode 100644 index 000000000..0192b1098 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-svelte-ignore/valid/svelte-ignore-comma-separated-requirements.json @@ -0,0 +1,3 @@ +{ + "svelte": ">=5.0.0-0" +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/1238/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/1238/_config.json new file mode 100644 index 000000000..88200a87e --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/1238/_config.json @@ -0,0 +1,8 @@ +{ + "options": [ + { + "destructuring": "all", + "additionalProperties": true + } + ] +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/1238/input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/1238/input.svelte new file mode 100644 index 000000000..190a18037 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/1238/input.svelte @@ -0,0 +1 @@ +123 diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/_requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/_requirements.json new file mode 100644 index 000000000..0192b1098 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/_requirements.json @@ -0,0 +1,3 @@ +{ + "svelte": ">=5.0.0-0" +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-errors.yaml new file mode 100644 index 000000000..600473adf --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-errors.yaml @@ -0,0 +1,15 @@ +- message: Prefer using writable $derived instead of $state and $effect + line: 4 + column: 6 + suggestions: + - desc: Rewrite $state and $effect to $derived + messageId: suggestRewrite + output: | + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-input.svelte new file mode 100644 index 000000000..51523f5c3 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-input.svelte @@ -0,0 +1,10 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-errors.yaml new file mode 100644 index 000000000..b89390b86 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-errors.yaml @@ -0,0 +1,15 @@ +- message: Prefer using writable $derived instead of $state and $effect + line: 4 + column: 6 + suggestions: + - desc: Rewrite $state and $effect to $derived + messageId: suggestRewrite + output: | + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-input.svelte new file mode 100644 index 000000000..e8b6769c6 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-input.svelte @@ -0,0 +1,10 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-errors.yaml new file mode 100644 index 000000000..600473adf --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-errors.yaml @@ -0,0 +1,15 @@ +- message: Prefer using writable $derived instead of $state and $effect + line: 4 + column: 6 + suggestions: + - desc: Rewrite $state and $effect to $derived + messageId: suggestRewrite + output: | + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-input.svelte new file mode 100644 index 000000000..fdafad808 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-input.svelte @@ -0,0 +1,10 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-errors.yaml new file mode 100644 index 000000000..b89390b86 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-errors.yaml @@ -0,0 +1,15 @@ +- message: Prefer using writable $derived instead of $state and $effect + line: 4 + column: 6 + suggestions: + - desc: Rewrite $state and $effect to $derived + messageId: suggestRewrite + output: | + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-input.svelte new file mode 100644 index 000000000..ee0e91b2b --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-input.svelte @@ -0,0 +1,10 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-errors.yaml new file mode 100644 index 000000000..f443ce89a --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-errors.yaml @@ -0,0 +1,19 @@ +- message: Prefer using writable $derived instead of $state and $effect + line: 4 + column: 6 + suggestions: + - desc: Rewrite $state and $effect to $derived + messageId: suggestRewrite + output: | + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-input.svelte new file mode 100644 index 000000000..52fce48ad --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-input.svelte @@ -0,0 +1,14 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-errors.yaml new file mode 100644 index 000000000..d77136489 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-errors.yaml @@ -0,0 +1,19 @@ +- message: Prefer using writable $derived instead of $state and $effect + line: 4 + column: 6 + suggestions: + - desc: Rewrite $state and $effect to $derived + messageId: suggestRewrite + output: | + + + { + newAlbumName = value; + }} + /> diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-input.svelte new file mode 100644 index 000000000..9f42e657a --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-input.svelte @@ -0,0 +1,14 @@ + + + { + newAlbumName = value; + }} +/> diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign3-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign3-errors.yaml new file mode 100644 index 000000000..0118d5e29 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign3-errors.yaml @@ -0,0 +1,38 @@ +- message: Prefer using writable $derived instead of $state and $effect + line: 4 + column: 6 + suggestions: + - desc: Rewrite $state and $effect to $derived + messageId: suggestRewrite + output: | + + + +- message: Prefer using writable $derived instead of $state and $effect + line: 4 + column: 6 + suggestions: + - desc: Rewrite $state and $effect to $derived + messageId: suggestRewrite + output: | + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign3-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign3-input.svelte new file mode 100644 index 000000000..bb6169e60 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign3-input.svelte @@ -0,0 +1,14 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/_requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/_requirements.json new file mode 100644 index 000000000..0192b1098 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/_requirements.json @@ -0,0 +1,3 @@ +{ + "svelte": ">=5.0.0-0" +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition1-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition1-input.svelte new file mode 100644 index 000000000..57bd65848 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition1-input.svelte @@ -0,0 +1,13 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition2-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition2-input.svelte new file mode 100644 index 000000000..9083881f1 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition2-input.svelte @@ -0,0 +1,17 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/_requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/_requirements.json new file mode 100644 index 000000000..498661308 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/_requirements.json @@ -0,0 +1,3 @@ +{ + "svelte": ">=5.0.0" +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/_config.json new file mode 100644 index 000000000..a0f52ed6e --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/_config.json @@ -0,0 +1,3 @@ +{ + "options": [{ "checkAsyncFunctions": true }] +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/_requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/_requirements.json new file mode 100644 index 000000000..498661308 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/_requirements.json @@ -0,0 +1,3 @@ +{ + "svelte": ">=5.0.0" +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async-arrow01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async-arrow01-errors.yaml new file mode 100644 index 000000000..4c26d4e62 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async-arrow01-errors.yaml @@ -0,0 +1,4 @@ +- message: Component event name must start with "on". + line: 3 + column: 5 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async-arrow01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async-arrow01-input.svelte new file mode 100644 index 000000000..0b0502984 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async-arrow01-input.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async01-errors.yaml new file mode 100644 index 000000000..4c26d4e62 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async01-errors.yaml @@ -0,0 +1,4 @@ +- message: Component event name must start with "on". + line: 3 + column: 5 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async01-input.svelte new file mode 100644 index 000000000..4f6f3ee49 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async01-input.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-arrow01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-arrow01-errors.yaml new file mode 100644 index 000000000..4c26d4e62 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-arrow01-errors.yaml @@ -0,0 +1,4 @@ +- message: Component event name must start with "on". + line: 3 + column: 5 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-arrow01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-arrow01-input.svelte new file mode 100644 index 000000000..7874e51da --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-arrow01-input.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-inline-type01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-inline-type01-errors.yaml new file mode 100644 index 000000000..affa6e169 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-inline-type01-errors.yaml @@ -0,0 +1,4 @@ +- message: Component event name must start with "on". + line: 2 + column: 21 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-inline-type01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-inline-type01-input.svelte new file mode 100644 index 000000000..3fc3c616b --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-inline-type01-input.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix01-errors.yaml new file mode 100644 index 000000000..4c26d4e62 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix01-errors.yaml @@ -0,0 +1,4 @@ +- message: Component event name must start with "on". + line: 3 + column: 5 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix01-input.svelte new file mode 100644 index 000000000..036a1d65b --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix01-input.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/_requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/_requirements.json new file mode 100644 index 000000000..498661308 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/_requirements.json @@ -0,0 +1,3 @@ +{ + "svelte": ">=5.0.0" +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/any01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/any01-input.svelte new file mode 100644 index 000000000..deeb82509 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/any01-input.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/async01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/async01-input.svelte new file mode 100644 index 000000000..4f6f3ee49 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/async01-input.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/non-function01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/non-function01-input.svelte new file mode 100644 index 000000000..d23e387a1 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/non-function01-input.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/with-prefix01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/with-prefix01-input.svelte new file mode 100644 index 000000000..e3396af33 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/with-prefix01-input.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-attributes/invalid/attach-tag-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-attributes/invalid/attach-tag-errors.yaml new file mode 100644 index 000000000..5972a097c --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-attributes/invalid/attach-tag-errors.yaml @@ -0,0 +1,4 @@ +- message: Attribute '@attach' should go before 'foo'. + line: 14 + column: 10 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-attributes/invalid/attach-tag-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-attributes/invalid/attach-tag-input.svelte new file mode 100644 index 000000000..5adcffbca --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-attributes/invalid/attach-tag-input.svelte @@ -0,0 +1,14 @@ + + +
...
\ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-attributes/invalid/attach-tag-output.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-attributes/invalid/attach-tag-output.svelte new file mode 100644 index 000000000..02c44ab90 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-attributes/invalid/attach-tag-output.svelte @@ -0,0 +1,14 @@ + + +
...
\ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-attributes/invalid/attach-tag-requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-attributes/invalid/attach-tag-requirements.json new file mode 100644 index 000000000..1a22befbb --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-attributes/invalid/attach-tag-requirements.json @@ -0,0 +1,3 @@ +{ + "svelte": "^5.0.0" +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-attributes/valid/attach-tag-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-attributes/valid/attach-tag-input.svelte new file mode 100644 index 000000000..e1bc8a5ba --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-attributes/valid/attach-tag-input.svelte @@ -0,0 +1,14 @@ + + +
...
\ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-attributes/valid/attach-tag-requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-attributes/valid/attach-tag-requirements.json new file mode 100644 index 000000000..1a22befbb --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-attributes/valid/attach-tag-requirements.json @@ -0,0 +1,3 @@ +{ + "svelte": "^5.0.0" +} diff --git a/packages/eslint-plugin-svelte/tests/src/rules/no-add-event-listener.ts b/packages/eslint-plugin-svelte/tests/src/rules/no-add-event-listener.ts new file mode 100644 index 000000000..a2b5928d1 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/src/rules/no-add-event-listener.ts @@ -0,0 +1,12 @@ +import { RuleTester } from '../../utils/eslint-compat.js'; +import rule from '../../../src/rules/no-add-event-listener.js'; +import { loadTestCases } from '../../utils/utils.js'; + +const tester = new RuleTester({ + languageOptions: { + ecmaVersion: 2020, + sourceType: 'module' + } +}); + +tester.run('no-add-event-listener', rule as any, loadTestCases('no-add-event-listener')); diff --git a/packages/eslint-plugin-svelte/tests/src/rules/no-top-level-browser-globals.ts b/packages/eslint-plugin-svelte/tests/src/rules/no-top-level-browser-globals.ts new file mode 100644 index 000000000..c13228031 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/src/rules/no-top-level-browser-globals.ts @@ -0,0 +1,16 @@ +import { RuleTester } from '../../utils/eslint-compat.js'; +import rule from '../../../src/rules/no-top-level-browser-globals.js'; +import { loadTestCases } from '../../utils/utils.js'; + +const tester = new RuleTester({ + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module' + } +}); + +tester.run( + 'no-top-level-browser-globals', + rule as any, + loadTestCases('no-top-level-browser-globals') +); diff --git a/packages/eslint-plugin-svelte/tests/src/rules/prefer-writable-derived.ts b/packages/eslint-plugin-svelte/tests/src/rules/prefer-writable-derived.ts new file mode 100644 index 000000000..80e9065a7 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/src/rules/prefer-writable-derived.ts @@ -0,0 +1,12 @@ +import { RuleTester } from '../../utils/eslint-compat.js'; +import rule from '../../../src/rules/prefer-writable-derived.js'; +import { loadTestCases } from '../../utils/utils.js'; + +const tester = new RuleTester({ + languageOptions: { + ecmaVersion: 2020, + sourceType: 'module' + } +}); + +tester.run('prefer-writable-derived', rule as any, loadTestCases('prefer-writable-derived')); diff --git a/packages/eslint-plugin-svelte/tests/src/rules/require-event-prefix.ts b/packages/eslint-plugin-svelte/tests/src/rules/require-event-prefix.ts new file mode 100644 index 000000000..7818735b5 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/src/rules/require-event-prefix.ts @@ -0,0 +1,12 @@ +import { RuleTester } from '../../utils/eslint-compat.js'; +import rule from '../../../src/rules/require-event-prefix.js'; +import { loadTestCases } from '../../utils/utils.js'; + +const tester = new RuleTester({ + languageOptions: { + ecmaVersion: 2020, + sourceType: 'module' + } +}); + +tester.run('require-event-prefix', rule as any, loadTestCases('require-event-prefix')); diff --git a/packages/eslint-plugin-svelte/tests/utils/eslint-compat.ts b/packages/eslint-plugin-svelte/tests/utils/eslint-compat.ts index d9d2b73d2..7c9e93fa4 100644 --- a/packages/eslint-plugin-svelte/tests/utils/eslint-compat.ts +++ b/packages/eslint-plugin-svelte/tests/utils/eslint-compat.ts @@ -1,3 +1,9 @@ -import { getRuleTester } from 'eslint-compat-utils/rule-tester'; +import * as eslint from 'eslint'; +import * as experimental from 'eslint/use-at-your-own-risk'; -export const RuleTester = getRuleTester(); +type MaybeHasRuleTester = { + FlatRuleTester?: typeof eslint.RuleTester; +}; + +export const RuleTester = + (experimental as never as MaybeHasRuleTester).FlatRuleTester ?? eslint.RuleTester; diff --git a/packages/eslint-plugin-svelte/tests/utils/utils.ts b/packages/eslint-plugin-svelte/tests/utils/utils.ts index 845ea656f..cfd4d81a9 100644 --- a/packages/eslint-plugin-svelte/tests/utils/utils.ts +++ b/packages/eslint-plugin-svelte/tests/utils/utils.ts @@ -11,18 +11,11 @@ import { Linter } from 'eslint'; import * as svelteParser from 'svelte-eslint-parser'; import * as typescriptParser from '@typescript-eslint/parser'; import Module from 'module'; +import globals from 'globals'; const __dirname = path.dirname(new URL(import.meta.url).pathname); const require = Module.createRequire(import.meta.url); -const globals = { - console: 'readonly', - setTimeout: 'readonly', - setInterval: 'readonly', - queueMicrotask: 'readonly', - window: 'readonly', - globalThis: 'readonly' -}; /** * Prevents leading spaces in a multiline template literal from appearing in the resulting string */ @@ -248,7 +241,7 @@ function writeFixtures( [`svelte/${ruleName}`]: ['error', ...(options || [])] }, languageOptions: { - globals, + globals: globals.browser, ecmaVersion: 2020, sourceType: 'module', ...verifyConfig?.languageOptions, @@ -332,7 +325,7 @@ function getConfig(ruleName: string, inputFile: string) { { ...config, languageOptions: { - globals, + globals: globals.browser, ecmaVersion: 2020, sourceType: 'module', ...config?.languageOptions, diff --git a/packages/eslint-plugin-svelte/tools/update-docs-rules-index.ts b/packages/eslint-plugin-svelte/tools/update-docs-rules-index.ts index e0c9a420a..ae38b30c0 100644 --- a/packages/eslint-plugin-svelte/tools/update-docs-rules-index.ts +++ b/packages/eslint-plugin-svelte/tools/update-docs-rules-index.ts @@ -14,8 +14,8 @@ sidebarDepth: 0 # Available Rules -: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. -: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). +: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.\\ +: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).\\ :star: Indicates that the rule is included in the \`plugin:svelte/recommended\` config.