From 482eea4daf8db4b31f7037df8874ecef23e03483 Mon Sep 17 00:00:00 2001 From: Albert Date: Sat, 10 Aug 2024 02:35:22 +0930 Subject: [PATCH 01/12] Add restrict-mustache-expressions --- README.md | 13 +- docs/README.md | 6 +- docs/rules.md | 1 + docs/rules/restrict-mustache-expressions.md | 204 ++++++++++++++++++ docs/user-guide.md | 2 +- packages/eslint-plugin-svelte/package.json | 36 ++-- .../src/configs/flat/recommended.ts | 1 + .../src/configs/recommended.ts | 1 + .../eslint-plugin-svelte/src/rule-types.ts | 18 ++ .../rules/restrict-mustache-expressions.ts | 153 +++++++++++++ .../eslint-plugin-svelte/src/utils/rules.ts | 2 + .../array/array-errors.yaml | 12 ++ .../array/array-input.svelte | 5 + .../boolean/_config.json | 9 + .../boolean/boolean-errors.yaml | 10 + .../boolean/boolean-input.svelte | 5 + .../number/_config.json | 9 + .../number/number-errors.yaml | 10 + .../number/number-input.svelte | 5 + .../object/object-errors.yaml | 12 ++ .../object/object-input.svelte | 7 + .../undefined/undefined-errors.yaml | 12 ++ .../undefined/undefined-input.svelte | 5 + .../textExpression/array/array-errors.yaml | 12 ++ .../textExpression/array/array-input.svelte | 5 + .../textExpression/boolean/_config.json | 9 + .../boolean/boolean-errors.yaml | 10 + .../boolean/boolean-input.svelte | 5 + .../textExpression/null/null-errors.yaml | 12 ++ .../textExpression/null/null-input.svelte | 5 + .../textExpression/number/_config.json | 9 + .../textExpression/number/number-errors.yaml | 10 + .../textExpression/number/number-input.svelte | 5 + .../textExpression/object/object-errors.yaml | 12 ++ .../textExpression/object/object-input.svelte | 5 + .../undefined/undefined-errors.yaml | 12 ++ .../undefined/undefined-input.svelte | 5 + .../boolean/_config.json | 9 + .../boolean/boolean-input.svelte | 5 + .../direct/direct-input.svelte | 8 + .../null/_config.json | 9 + .../null/null-input.svelte | 5 + .../number/_config.json | 9 + .../number/number-input.svelte | 5 + .../template-string/attribute-input.svelte | 2 + .../undefined/_config.json | 9 + .../undefined/undefined-input.svelte | 5 + .../boolean/boolean-input.svelte | 5 + .../valid/textExpression/null/_config.json | 9 + .../textExpression/null/null-input.svelte | 5 + .../textExpression/number/number-input.svelte | 5 + .../textExpression/undefined/_config.json | 9 + .../undefined/undefined-input.svelte | 5 + .../rules/restrict-mustache-expressions.ts | 12 ++ 54 files changed, 742 insertions(+), 28 deletions(-) create mode 100644 docs/rules/restrict-mustache-expressions.md create mode 100644 packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/array/array-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/array/array-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/boolean/_config.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/boolean/boolean-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/boolean/boolean-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/number/_config.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/number/number-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/number/number-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/object/object-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/object/object-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/undefined/undefined-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/undefined/undefined-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/array/array-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/array/array-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/boolean/_config.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/boolean/boolean-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/boolean/boolean-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/null/null-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/null/null-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/number/_config.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/number/number-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/number/number-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/object/object-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/object/object-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/undefined/undefined-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/undefined/undefined-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/boolean/_config.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/boolean/boolean-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/direct/direct-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/null/_config.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/null/null-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/number/_config.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/number/number-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/template-string/attribute-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/undefined/_config.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/undefined/undefined-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/textExpression/boolean/boolean-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/textExpression/null/_config.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/textExpression/null/null-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/textExpression/number/number-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/textExpression/undefined/_config.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/textExpression/undefined/undefined-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/src/rules/restrict-mustache-expressions.ts diff --git a/README.md b/README.md index 610a270d6..23e942ea2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Introduction -`eslint-plugin-svelte` is the official [ESLint] plugin for [Svelte]. -It provides many unique check rules by using the template AST. +`eslint-plugin-svelte` is the official [ESLint] plugin for [Svelte]. +It provides many unique check rules by using the template AST. You can check on the [Online DEMO](https://eslint-online-playground.netlify.app/#eslint-plugin-svelte%20with%20typescript). **_We are working on experimental support for Svelte v5, but may break with new versions of Svelte v5._** @@ -22,7 +22,7 @@ You can check on the [Online DEMO](https://eslint-online-playground.netlify.app/ ## :name_badge: What is this plugin? -[ESLint] plugin for [Svelte]. +[ESLint] plugin for [Svelte]. It provides many unique check rules using the AST generated by [svelte-eslint-parser]. ### ❗ Attention @@ -292,7 +292,7 @@ module.exports = { #### settings.svelte.ignoreWarnings -Specifies an array of rules that ignore reports in the template. +Specifies an array of rules that ignore reports in the template. For example, set rules on the template that cannot avoid false positives. #### settings.svelte.compileOptions @@ -367,8 +367,8 @@ Example **.vscode/settings.json**: -: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. @@ -395,6 +395,7 @@ These rules relate to possible syntax or logic errors in Svelte code: | [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 | | | [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. | :wrench: | +| [svelte/restrict-mustache-expressions](https://sveltejs.github.io/eslint-plugin-svelte/rules/restrict-mustache-expressions/) | disallow non-string values in string contexts | :star: | | [svelte/valid-compile](https://sveltejs.github.io/eslint-plugin-svelte/rules/valid-compile/) | disallow warnings when compiling. | :star: | | [svelte/valid-prop-names-in-kit-pages](https://sveltejs.github.io/eslint-plugin-svelte/rules/valid-prop-names-in-kit-pages/) | disallow props other than data or errors in SvelteKit page components. | | diff --git a/docs/README.md b/docs/README.md index 6bfbba28b..03cc74e99 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,8 +4,8 @@ title: 'eslint-plugin-svelte' # Introduction -`eslint-plugin-svelte` is the official [ESLint] plugin for [Svelte]. -It provides many unique check rules by using the template AST. +`eslint-plugin-svelte` is the official [ESLint] plugin for [Svelte]. +It provides many unique check rules by using the template AST. You can check on the [Online DEMO](https://eslint-online-playground.netlify.app/#eslint-plugin-svelte%20with%20typescript). **_We are working on experimental support for Svelte v5, but may break with new versions of Svelte v5._** @@ -26,7 +26,7 @@ You can check on the [Online DEMO](https://eslint-online-playground.netlify.app/ ## :name_badge: What is this plugin? -[ESLint] plugin for [Svelte]. +[ESLint] plugin for [Svelte]. It provides many unique check rules using the AST generated by [svelte-eslint-parser]. ### ❗ Attention diff --git a/docs/rules.md b/docs/rules.md index 7f115da5c..397d03336 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -32,6 +32,7 @@ These rules relate to possible syntax or logic errors in Svelte code: | [svelte/no-unknown-style-directive-property](./rules/no-unknown-style-directive-property.md) | disallow unknown `style:property` | :star: | | [svelte/require-store-callbacks-use-set-param](./rules/require-store-callbacks-use-set-param.md) | store callbacks must use `set` param | | | [svelte/require-store-reactive-access](./rules/require-store-reactive-access.md) | disallow to use of the store itself as an operand. Need to use $ prefix or get function. | :wrench: | +| [svelte/restrict-mustache-expressions](./rules/restrict-mustache-expressions.md) | disallow non-string values in string contexts | :star: | | [svelte/valid-compile](./rules/valid-compile.md) | disallow warnings when compiling. | :star: | | [svelte/valid-prop-names-in-kit-pages](./rules/valid-prop-names-in-kit-pages.md) | disallow props other than data or errors in SvelteKit page components. | | diff --git a/docs/rules/restrict-mustache-expressions.md b/docs/rules/restrict-mustache-expressions.md new file mode 100644 index 000000000..d4a15a45d --- /dev/null +++ b/docs/rules/restrict-mustache-expressions.md @@ -0,0 +1,204 @@ +--- +pageClass: 'rule-details' +sidebarDepth: 0 +title: 'svelte/restrict-mustache-expressions' +description: 'disallow non-string values in string contexts' +--- + +# svelte/restrict-mustache-expressions + +> disallow non-string values in string contexts + +- :exclamation: **_This rule has not been released yet._** +- :gear: This rule is included in `"plugin:svelte/recommended"`. + +## :book: Rule Details + +JavaScript automatically converts an [object to a string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#string_coercion) +in a string context, such as when concatenating strings or using them in a template string. The default toString() method of objects returns +`[object Object]`. This is typically incorrect behavior. + +This rule prevents non-stringifiable values from being used in contexts where a string is expected. + +This rule is based off the [restrict-template-expressions](https://typescript-eslint.io/rules/restrict-template-expressions) rule, and it is recommended to be used +with that rule, as this only performs checks on svelte template strings (eg: `foo`), and not on ``foo``. + + + + + +```svelte + + + +foo +foo +foo + +{123} +{str} +{true} + + +foo +foo +foo +foo + +{null} +{undefined} +{[1, 2, 3]} +{not_stringifiable} +``` + + + +## :wrench: Options + +```json +{ + "svelte/restrict-mustache-expressions": ["error", {}] +} +``` + +```ts + +type Options = { + // allows numbers in both svelte template literals and text expressions + allowNumbers?: boolean; + // allows booleans in both svelte template literals and text expressions + allowBooleans?: boolean; + // allows null in both svelte template literals and text expressions + allowNull?: boolean; + // allows undefined in both svelte template literals and text expressions + allowUndefined?: boolean; + textExpressions?: { + // allows numbers in text expressions + allowNumbers?: boolean; + // allows booleans in text expressions + allowBooleans?: boolean; + // allows null in text expressions + allowNull?: boolean; + // allows undefined in text expressions + allowUndefined?: boolean; + }, + stringTemplateExpressions?: { + // allows numbers in string template expressions + allowNumbers?: boolean; + // allows booleans in string template expressions + allowBooleans?: boolean; + // allows null in string template expressions + allowNull?: boolean; + // allows undefined in string template expressions + allowUndefined?: boolean; + } +}; + +type DefaultOptions = { + allowNumbers: true; + allowBooleans: true; + allowNull: false; + allowUndefined: false; +} +``` + +## More examples + + + +### Disallowing numbers + + +```svelte + + + +foo +{str} + + +foo +{123} +``` + + + +### Disallowing booleans + + + + + +```svelte + + + +{str} + + +foo +``` + + + +### Disallowing numbers specifically for text expressions + + + + + +```svelte + + + +foo + + +{123} +``` + + + +### Disallowing booleans specifically for string template expressions + + + + + +```svelte + + + +{true} + + +foo +``` + + + + +## :books: Further Reading + +- [no-base-to-string](https://typescript-eslint.io/rules/no-base-to-string) +- [restrict-plus-operands](https://typescript-eslint.io/rules/restrict-plus-operands) +- [restrict-template-expressions](https://typescript-eslint.io/rules/restrict-template-expressions) + +## :mag: Implementation + +- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts) +- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/restrict-mustache-expressions.ts) diff --git a/docs/user-guide.md b/docs/user-guide.md index 0818976de..7a3871dfc 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -243,7 +243,7 @@ module.exports = { #### settings.svelte.ignoreWarnings -Specifies an array of rules that ignore reports in the template. +Specifies an array of rules that ignore reports in the template. For example, set rules on the template that cannot avoid false positives. #### settings.svelte.compileOptions diff --git a/packages/eslint-plugin-svelte/package.json b/packages/eslint-plugin-svelte/package.json index a341ec478..f4dcbb7ce 100644 --- a/packages/eslint-plugin-svelte/package.json +++ b/packages/eslint-plugin-svelte/package.json @@ -57,20 +57,20 @@ }, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@jridgewell/sourcemap-codec": "^1.4.15", + "@jridgewell/sourcemap-codec": "^1.5.0", "eslint-compat-utils": "^0.5.1", "esutils": "^2.0.3", "known-css-properties": "^0.34.0", - "postcss": "^8.4.38", + "postcss": "^8.4.41", "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^6.0.0", - "postcss-selector-parser": "^6.1.0", - "semver": "^7.6.2", + "postcss-selector-parser": "^6.1.1", + "semver": "^7.6.3", "svelte-eslint-parser": "^0.41.0" }, "devDependencies": { - "@babel/core": "^7.24.7", - "@babel/eslint-parser": "^7.24.7", + "@babel/core": "^7.25.2", + "@babel/eslint-parser": "^7.25.1", "@babel/plugin-proposal-function-bind": "^7.24.7", "@eslint-community/eslint-plugin-eslint-comments": "^4.3.0", "@types/babel__core": "^7.20.5", @@ -78,30 +78,30 @@ "@types/esutils": "^2.0.2", "@types/json-schema": "^7.0.15", "@types/less": "^3.0.6", - "@types/mocha": "^10.0.6", - "@types/node": "^20.14.2", + "@types/mocha": "^10.0.7", + "@types/node": "^20.14.14", "@types/postcss-safe-parser": "^5.0.4", "@types/semver": "^7.5.8", "@types/stylus": "^0.48.42", - "acorn": "^8.12.0", + "acorn": "^8.12.1", "assert": "^2.1.0", "esbuild": "^0.23.0", - "esbuild-register": "^3.5.0", - "eslint-scope": "^8.0.1", + "esbuild-register": "^3.6.0", + "eslint-scope": "^8.0.2", "eslint-typegen": "^0.3.0", "eslint-visitor-keys": "^4.0.0", - "espree": "^10.0.1", + "espree": "^10.1.0", "less": "^4.2.0", - "mocha": "^10.4.0", + "mocha": "^10.7.3", "nyc": "^17.0.0", - "postcss-nested": "^6.0.1", - "sass": "^1.77.5", + "postcss-nested": "^6.2.0", + "sass": "^1.77.8", "source-map-js": "^1.2.0", "stylus": "^0.63.0", - "svelte": "^5.0.0-next.191", + "svelte": "5.0.0-next.210", "svelte-i18n": "^4.0.0", - "type-coverage": "^2.29.0", - "yaml": "^2.4.5" + "type-coverage": "^2.29.1", + "yaml": "^2.5.0" }, "publishConfig": { "access": "public" diff --git a/packages/eslint-plugin-svelte/src/configs/flat/recommended.ts b/packages/eslint-plugin-svelte/src/configs/flat/recommended.ts index eff2aa8d4..04323ddd6 100644 --- a/packages/eslint-plugin-svelte/src/configs/flat/recommended.ts +++ b/packages/eslint-plugin-svelte/src/configs/flat/recommended.ts @@ -21,6 +21,7 @@ const config: Linter.FlatConfig[] = [ 'svelte/no-shorthand-style-property-overrides': 'error', 'svelte/no-unknown-style-directive-property': 'error', 'svelte/no-unused-svelte-ignore': 'error', + 'svelte/restrict-mustache-expressions': 'error', 'svelte/system': 'error', 'svelte/valid-compile': 'error' } diff --git a/packages/eslint-plugin-svelte/src/configs/recommended.ts b/packages/eslint-plugin-svelte/src/configs/recommended.ts index 5e53547be..c97b9b35d 100644 --- a/packages/eslint-plugin-svelte/src/configs/recommended.ts +++ b/packages/eslint-plugin-svelte/src/configs/recommended.ts @@ -21,6 +21,7 @@ const config: Linter.Config = { 'svelte/no-shorthand-style-property-overrides': 'error', 'svelte/no-unknown-style-directive-property': 'error', 'svelte/no-unused-svelte-ignore': 'error', + 'svelte/restrict-mustache-expressions': 'error', 'svelte/system': 'error', 'svelte/valid-compile': 'error' } diff --git a/packages/eslint-plugin-svelte/src/rule-types.ts b/packages/eslint-plugin-svelte/src/rule-types.ts index 77d3c4ca4..0911d0daa 100644 --- a/packages/eslint-plugin-svelte/src/rule-types.ts +++ b/packages/eslint-plugin-svelte/src/rule-types.ts @@ -289,6 +289,11 @@ export interface RuleOptions { * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/require-stores-init/ */ 'svelte/require-stores-init'?: Linter.RuleEntry<[]> + /** + * disallow non-string values in string contexts + * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/restrict-mustache-expressions/ + */ + 'svelte/restrict-mustache-expressions'?: Linter.RuleEntry /** * enforce use of shorthand syntax in attribute * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/shorthand-attribute/ @@ -460,6 +465,19 @@ type SvelteNoUselessMustaches = []|[{ type SveltePreferClassDirective = []|[{ prefer?: ("always" | "empty") }] +// ----- svelte/restrict-mustache-expressions ----- +type SvelteRestrictMustacheExpressions = []|[{ + allowBoolean?: boolean + allowNull?: boolean + allowUndefined?: boolean + allowNumber?: boolean + textExpressions?: { + [k: string]: unknown | undefined + } + stringTemplateExpressions?: { + [k: string]: unknown | undefined + } +}] // ----- svelte/shorthand-attribute ----- type SvelteShorthandAttribute = []|[{ prefer?: ("always" | "never") diff --git a/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts b/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts new file mode 100644 index 000000000..ab800e9b8 --- /dev/null +++ b/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts @@ -0,0 +1,153 @@ +import type { AST } from 'svelte-eslint-parser'; +import { createRule } from '../utils'; +import { TypeFlags } from 'typescript'; +import { type TSESTree } from '@typescript-eslint/types'; +import type { TS } from '../utils/ts-utils'; +import { getConstrainedTypeAtLocation, getTypeName, getTypeScriptTools } from '../utils/ts-utils'; +import { getSourceCode } from '../utils/compat'; + +const props = { + allowBoolean: { + type: 'boolean' + }, + allowNull: { + type: 'boolean' + }, + allowUndefined: { + type: 'boolean' + }, + allowNumber: { + type: 'boolean' + } +}; + +function getDefaultOptions() { + return { + allowBoolean: true, + allowNumber: true, + allowNull: false, + allowUndefined: false + }; +} + +type Props = { + allowBoolean: boolean; + allowNumber: boolean; + allowNull: boolean; + allowUndefined: boolean; +}; + +type Config = { + stringTemplateExpressions?: Props; + textExpressions?: Props; +} & Props; + +export default createRule('restrict-mustache-expressions', { + meta: { + docs: { + description: 'disallow non-string values in string contexts', + category: 'Possible Errors', + recommended: true + }, + schema: [ + { + type: 'object', + properties: { + ...props, + textExpressions: { + ...props + }, + stringTemplateExpressions: { + ...props + } + }, + additionalProperties: false + } + ], + messages: { + expectedStringifyableType: + 'Expected {{type}} to be one of the following: {{types}}. You must cast or convert the expression to one of the allowed types.' + }, + type: 'problem' + }, + create(context) { + const tools = getTypeScriptTools(context); + if (!tools) { + return {}; + } + + const { service } = tools; + const checker = service.program.getTypeChecker(); + + function getNodeType( + node: TSESTree.Expression | TSESTree.PrivateIdentifier | TSESTree.SpreadElement + ): TS.Type | undefined { + const tsNode = service.esTreeNodeToTSNodeMap.get(node); + return tsNode && getConstrainedTypeAtLocation(checker, tsNode); + } + + const config: Config = context.options[0] || getDefaultOptions(); + + function checkExpression(node: AST.SvelteMustacheTag) { + const allowed_types: string[] = ['string']; + let opts: Props; + if (node.parent.type === 'SvelteAttribute') { + if (!node.parent.value.find((n) => n.type === 'SvelteLiteral')) { + // we are rendering a non-literal attribute (eg: class:disabled={disabled}, so we allow any type + return; + } + // we are rendering an template string attribute (eg: href="/page/{page.id}"), so we only allow stringifiable types + opts = config?.stringTemplateExpressions + ? Object.assign(getDefaultOptions(), config.stringTemplateExpressions) + : config; + } else { + // we are rendering a text expression, so we only allow stringifiable types + opts = config?.textExpressions + ? Object.assign(getDefaultOptions(), config.textExpressions) + : config; + } + + const { allowBoolean, allowNull, allowUndefined, allowNumber } = opts; + if (allowBoolean === true) allowed_types.push('boolean'); + if (allowNumber === true) allowed_types.push('number'); + if (allowNull) allowed_types.push('null'); + if (allowUndefined) allowed_types.push('undefined'); + + // const sourceCode = getSourceCode(context); + // console.log(node.parent); + // console.log(sourceCode.getText(node)); + + const type = getNodeType(node.expression); + + if (type) { + if (type.flags & TypeFlags.StringLike) { + return; + } + if (type.flags & TypeFlags.BooleanLike && allowBoolean) { + return; + } + if (type.flags & TypeFlags.NumberLike && allowNumber) { + return; + } + if (type.flags & TypeFlags.Null && allowNull) { + return; + } + if (type.flags & TypeFlags.Undefined && allowUndefined) { + return; + } + context.report({ + node, + messageId: 'expectedStringifyableType', + data: { + type: getTypeName(type, tools!), + types: allowed_types.map((t) => `\`${t}\``).join(', ') + } + }); + } + } + + return { + SvelteMustacheTag: checkExpression + }; + } +}); diff --git a/packages/eslint-plugin-svelte/src/utils/rules.ts b/packages/eslint-plugin-svelte/src/utils/rules.ts index af0dd20e6..0049e3c35 100644 --- a/packages/eslint-plugin-svelte/src/utils/rules.ts +++ b/packages/eslint-plugin-svelte/src/utils/rules.ts @@ -57,6 +57,7 @@ import requireOptimizedStyleAttribute from '../rules/require-optimized-style-att import requireStoreCallbacksUseSetParam from '../rules/require-store-callbacks-use-set-param'; import requireStoreReactiveAccess from '../rules/require-store-reactive-access'; import requireStoresInit from '../rules/require-stores-init'; +import restrictMustacheExpressions from '../rules/restrict-mustache-expressions'; import shorthandAttribute from '../rules/shorthand-attribute'; import shorthandDirective from '../rules/shorthand-directive'; import sortAttributes from '../rules/sort-attributes'; @@ -122,6 +123,7 @@ export const rules = [ requireStoreCallbacksUseSetParam, requireStoreReactiveAccess, requireStoresInit, + restrictMustacheExpressions, shorthandAttribute, shorthandDirective, sortAttributes, diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/array/array-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/array/array-errors.yaml new file mode 100644 index 000000000..04368921c --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/array/array-errors.yaml @@ -0,0 +1,12 @@ +- message: "Expected string[] to be one of the following: `string`, `boolean`, + `number`. You must cast or convert the expression to one of the allowed + types." + line: 4 + column: 14 + suggestions: null +- message: "Expected string[] to be one of the following: `string`, `boolean`, + `number`. You must cast or convert the expression to one of the allowed + types." + line: 5 + column: 14 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/array/array-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/array/array-input.svelte new file mode 100644 index 000000000..01437f86e --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/array/array-input.svelte @@ -0,0 +1,5 @@ + +foo +foo \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/boolean/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/boolean/_config.json new file mode 100644 index 000000000..758734e43 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/boolean/_config.json @@ -0,0 +1,9 @@ +{ + "options": [ + { + "stringTemplateExpressions": { + "allowBoolean": false + } + } + ] +} \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/boolean/boolean-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/boolean/boolean-errors.yaml new file mode 100644 index 000000000..ffc880b95 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/boolean/boolean-errors.yaml @@ -0,0 +1,10 @@ +- message: "Expected true to be one of the following: `string`, `number`. You must + cast or convert the expression to one of the allowed types." + line: 4 + column: 14 + suggestions: null +- message: "Expected boolean to be one of the following: `string`, `number`. You + must cast or convert the expression to one of the allowed types." + line: 5 + column: 14 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/boolean/boolean-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/boolean/boolean-input.svelte new file mode 100644 index 000000000..1e5535d0f --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/boolean/boolean-input.svelte @@ -0,0 +1,5 @@ + +foo +foo \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/number/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/number/_config.json new file mode 100644 index 000000000..9770285cc --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/number/_config.json @@ -0,0 +1,9 @@ +{ + "options": [ + { + "stringTemplateExpressions": { + "allowNumber": false + } + } + ] +} \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/number/number-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/number/number-errors.yaml new file mode 100644 index 000000000..831440c04 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/number/number-errors.yaml @@ -0,0 +1,10 @@ +- message: "Expected 123 to be one of the following: `string`, `boolean`. You must + cast or convert the expression to one of the allowed types." + line: 4 + column: 14 + suggestions: null +- message: "Expected number to be one of the following: `string`, `boolean`. You + must cast or convert the expression to one of the allowed types." + line: 5 + column: 14 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/number/number-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/number/number-input.svelte new file mode 100644 index 000000000..bb86448df --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/number/number-input.svelte @@ -0,0 +1,5 @@ + +foo +foo \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/object/object-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/object/object-errors.yaml new file mode 100644 index 000000000..658166f29 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/object/object-errors.yaml @@ -0,0 +1,12 @@ +- message: "Expected { invalid: string; } to be one of the following: `string`, + `boolean`, `number`. You must cast or convert the expression to one of the + allowed types." + line: 6 + column: 14 + suggestions: null +- message: "Expected { bar: string; } to be one of the following: `string`, + `boolean`, `number`. You must cast or convert the expression to one of the + allowed types." + line: 7 + column: 14 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/object/object-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/object/object-input.svelte new file mode 100644 index 000000000..de771cf5c --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/object/object-input.svelte @@ -0,0 +1,7 @@ + +foo +foo \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/undefined/undefined-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/undefined/undefined-errors.yaml new file mode 100644 index 000000000..9286cf69a --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/undefined/undefined-errors.yaml @@ -0,0 +1,12 @@ +- message: 'Expected undefined to be one of the following: `string`, `boolean`, + `number`. You must cast or convert the expression to one of the allowed + types.' + line: 4 + column: 14 + suggestions: null +- message: 'Expected any to be one of the following: `string`, `boolean`, + `number`. You must cast or convert the expression to one of the allowed + types.' + line: 5 + column: 14 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/undefined/undefined-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/undefined/undefined-input.svelte new file mode 100644 index 000000000..f02dd2e3b --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/undefined/undefined-input.svelte @@ -0,0 +1,5 @@ + +foo +foo \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/array/array-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/array/array-errors.yaml new file mode 100644 index 000000000..2aace51d7 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/array/array-errors.yaml @@ -0,0 +1,12 @@ +- message: 'Expected number[] to be one of the following: `string`, `boolean`, + `number`. You must cast or convert the expression to one of the allowed + types.' + line: 4 + column: 1 + suggestions: null +- message: 'Expected number[] to be one of the following: `string`, `boolean`, + `number`. You must cast or convert the expression to one of the allowed + types.' + line: 5 + column: 1 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/array/array-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/array/array-input.svelte new file mode 100644 index 000000000..cfb3c1456 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/array/array-input.svelte @@ -0,0 +1,5 @@ + +{ [1, 2, 3] } +{ invalid_array } \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/boolean/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/boolean/_config.json new file mode 100644 index 000000000..e79470454 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/boolean/_config.json @@ -0,0 +1,9 @@ +{ + "options": [ + { + "textExpressions": { + "allowBoolean": false + } + } + ] +} \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/boolean/boolean-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/boolean/boolean-errors.yaml new file mode 100644 index 000000000..b99fd665f --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/boolean/boolean-errors.yaml @@ -0,0 +1,10 @@ +- message: 'Expected true to be one of the following: `string`. You must cast or + convert the expression to one of the allowed types.' + line: 4 + column: 1 + suggestions: null +- message: 'Expected boolean to be one of the following: `string`. You must cast + or convert the expression to one of the allowed types.' + line: 5 + column: 1 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/boolean/boolean-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/boolean/boolean-input.svelte new file mode 100644 index 000000000..5acb3f9bb --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/boolean/boolean-input.svelte @@ -0,0 +1,5 @@ + +{ true } +{ invalid_boolean } \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/null/null-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/null/null-errors.yaml new file mode 100644 index 000000000..f17566ea9 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/null/null-errors.yaml @@ -0,0 +1,12 @@ +- message: 'Expected null to be one of the following: `string`, `boolean`, + `number`. You must cast or convert the expression to one of the allowed + types.' + line: 4 + column: 1 + suggestions: null +- message: 'Expected null to be one of the following: `string`, `boolean`, + `number`. You must cast or convert the expression to one of the allowed + types.' + line: 5 + column: 1 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/null/null-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/null/null-input.svelte new file mode 100644 index 000000000..bc32e2f7f --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/null/null-input.svelte @@ -0,0 +1,5 @@ + +{ null } +{ invalid_null } \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/number/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/number/_config.json new file mode 100644 index 000000000..ee72b6545 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/number/_config.json @@ -0,0 +1,9 @@ +{ + "options": [ + { + "textExpressions": { + "allowNumber": false + } + } + ] +} \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/number/number-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/number/number-errors.yaml new file mode 100644 index 000000000..02938548b --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/number/number-errors.yaml @@ -0,0 +1,10 @@ +- message: 'Expected 123 to be one of the following: `string`. You must cast or + convert the expression to one of the allowed types.' + line: 4 + column: 1 + suggestions: null +- message: 'Expected number to be one of the following: `string`. You must cast or + convert the expression to one of the allowed types.' + line: 5 + column: 1 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/number/number-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/number/number-input.svelte new file mode 100644 index 000000000..7c88e7929 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/number/number-input.svelte @@ -0,0 +1,5 @@ + +{ 123 } +{ invalid_number } \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/object/object-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/object/object-errors.yaml new file mode 100644 index 000000000..77a4d8173 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/object/object-errors.yaml @@ -0,0 +1,12 @@ +- message: 'Expected { foo: string; } to be one of the following: `string`, + `boolean`, `number`. You must cast or convert the expression to one of the + allowed types.' + line: 4 + column: 1 + suggestions: null +- message: 'Expected { foo: string; } to be one of the following: `string`, + `boolean`, `number`. You must cast or convert the expression to one of the + allowed types.' + line: 5 + column: 1 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/object/object-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/object/object-input.svelte new file mode 100644 index 000000000..e5da44fb1 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/object/object-input.svelte @@ -0,0 +1,5 @@ + +{ { foo: 'bar' } } +{ invalid_object } \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/undefined/undefined-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/undefined/undefined-errors.yaml new file mode 100644 index 000000000..c78e8e074 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/undefined/undefined-errors.yaml @@ -0,0 +1,12 @@ +- message: 'Expected undefined to be one of the following: `string`, `boolean`, + `number`. You must cast or convert the expression to one of the allowed + types.' + line: 4 + column: 1 + suggestions: null +- message: 'Expected undefined to be one of the following: `string`, `boolean`, + `number`. You must cast or convert the expression to one of the allowed + types.' + line: 5 + column: 1 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/undefined/undefined-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/undefined/undefined-input.svelte new file mode 100644 index 000000000..004540f2d --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/undefined/undefined-input.svelte @@ -0,0 +1,5 @@ + +{ undefined } +{ invalid_undefined } \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/boolean/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/boolean/_config.json new file mode 100644 index 000000000..9002d0e7f --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/boolean/_config.json @@ -0,0 +1,9 @@ +{ + "options": [ + { + "stringTemplateExpressions": { + + } + } + ] +} \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/boolean/boolean-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/boolean/boolean-input.svelte new file mode 100644 index 000000000..e5d59b75d --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/boolean/boolean-input.svelte @@ -0,0 +1,5 @@ + +foo +foo \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/direct/direct-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/direct/direct-input.svelte new file mode 100644 index 000000000..076cf606a --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/direct/direct-input.svelte @@ -0,0 +1,8 @@ + +foo +foo +foo +foo +foo +foo +foo \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/null/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/null/_config.json new file mode 100644 index 000000000..861309fc9 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/null/_config.json @@ -0,0 +1,9 @@ +{ + "options": [ + { + "stringTemplateExpressions": { + "allowNull": true + } + } + ] +} \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/null/null-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/null/null-input.svelte new file mode 100644 index 000000000..f12723e69 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/null/null-input.svelte @@ -0,0 +1,5 @@ + +foo +foo \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/number/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/number/_config.json new file mode 100644 index 000000000..9002d0e7f --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/number/_config.json @@ -0,0 +1,9 @@ +{ + "options": [ + { + "stringTemplateExpressions": { + + } + } + ] +} \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/number/number-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/number/number-input.svelte new file mode 100644 index 000000000..19a64aef5 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/number/number-input.svelte @@ -0,0 +1,5 @@ + +foo +foo \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/template-string/attribute-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/template-string/attribute-input.svelte new file mode 100644 index 000000000..39740448e --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/template-string/attribute-input.svelte @@ -0,0 +1,2 @@ + +foo \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/undefined/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/undefined/_config.json new file mode 100644 index 000000000..9eddd9b40 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/undefined/_config.json @@ -0,0 +1,9 @@ +{ + "options": [ + { + "stringTemplateExpressions": { + "allowUndefined": true + } + } + ] +} \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/undefined/undefined-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/undefined/undefined-input.svelte new file mode 100644 index 000000000..c0fc90bf7 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/stringTemplateExpression/undefined/undefined-input.svelte @@ -0,0 +1,5 @@ + +foo +foo \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/textExpression/boolean/boolean-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/textExpression/boolean/boolean-input.svelte new file mode 100644 index 000000000..2ae6476d0 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/textExpression/boolean/boolean-input.svelte @@ -0,0 +1,5 @@ + +{ true } +{ valid_boolean } \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/textExpression/null/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/textExpression/null/_config.json new file mode 100644 index 000000000..5b1b41c61 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/textExpression/null/_config.json @@ -0,0 +1,9 @@ +{ + "options": [ + { + "textExpressions": { + "allowNull": true + } + } + ] +} \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/textExpression/null/null-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/textExpression/null/null-input.svelte new file mode 100644 index 000000000..b2f4256ff --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/textExpression/null/null-input.svelte @@ -0,0 +1,5 @@ + +{ null } +{ valid_null } \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/textExpression/number/number-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/textExpression/number/number-input.svelte new file mode 100644 index 000000000..06c55b28c --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/textExpression/number/number-input.svelte @@ -0,0 +1,5 @@ + +{ 123 } +{ valid_number } \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/textExpression/undefined/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/textExpression/undefined/_config.json new file mode 100644 index 000000000..a6a81bcfc --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/textExpression/undefined/_config.json @@ -0,0 +1,9 @@ +{ + "options": [ + { + "textExpressions": { + "allowUndefined": true + } + } + ] +} \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/textExpression/undefined/undefined-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/textExpression/undefined/undefined-input.svelte new file mode 100644 index 000000000..982d1b1b2 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/textExpression/undefined/undefined-input.svelte @@ -0,0 +1,5 @@ + +{ undefined } +{ valid_undefined } \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/src/rules/restrict-mustache-expressions.ts b/packages/eslint-plugin-svelte/tests/src/rules/restrict-mustache-expressions.ts new file mode 100644 index 000000000..045eb661a --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/src/rules/restrict-mustache-expressions.ts @@ -0,0 +1,12 @@ +import { RuleTester } from '../../utils/eslint-compat'; +import rule from '../../../src/rules/restrict-mustache-expressions'; +import { loadTestCases } from '../../utils/utils'; + +const tester = new RuleTester({ + languageOptions: { + ecmaVersion: 2020, + sourceType: 'module' + } +}); + +tester.run('restrict-mustache-expressions', rule as any, loadTestCases('restrict-mustache-expressions')); From 37256a775973567e4093331567990c661d5382b0 Mon Sep 17 00:00:00 2001 From: Albert Date: Sat, 10 Aug 2024 02:36:36 +0930 Subject: [PATCH 02/12] Fix broken test --- .../invalid/textExpression/boolean/boolean-errors.yaml | 8 ++++---- .../invalid/textExpression/number/number-errors.yaml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/boolean/boolean-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/boolean/boolean-errors.yaml index b99fd665f..28da2c309 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/boolean/boolean-errors.yaml +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/boolean/boolean-errors.yaml @@ -1,10 +1,10 @@ -- message: 'Expected true to be one of the following: `string`. You must cast or - convert the expression to one of the allowed types.' +- message: 'Expected true to be one of the following: `string`, `number`. You must + cast or convert the expression to one of the allowed types.' line: 4 column: 1 suggestions: null -- message: 'Expected boolean to be one of the following: `string`. You must cast - or convert the expression to one of the allowed types.' +- message: 'Expected boolean to be one of the following: `string`, `number`. You + must cast or convert the expression to one of the allowed types.' line: 5 column: 1 suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/number/number-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/number/number-errors.yaml index 02938548b..85667509b 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/number/number-errors.yaml +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/number/number-errors.yaml @@ -1,10 +1,10 @@ -- message: 'Expected 123 to be one of the following: `string`. You must cast or - convert the expression to one of the allowed types.' +- message: 'Expected 123 to be one of the following: `string`, `boolean`. You must + cast or convert the expression to one of the allowed types.' line: 4 column: 1 suggestions: null -- message: 'Expected number to be one of the following: `string`. You must cast or - convert the expression to one of the allowed types.' +- message: 'Expected number to be one of the following: `string`, `boolean`. You + must cast or convert the expression to one of the allowed types.' line: 5 column: 1 suggestions: null From 13c05e55801296f65885b57057a2c6c6941338c3 Mon Sep 17 00:00:00 2001 From: Albert Date: Sat, 10 Aug 2024 02:40:51 +0930 Subject: [PATCH 03/12] Clean some unused code --- docs/rules/restrict-mustache-expressions.md | 10 ++++------ .../src/rules/restrict-mustache-expressions.ts | 5 ----- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/docs/rules/restrict-mustache-expressions.md b/docs/rules/restrict-mustache-expressions.md index d4a15a45d..549ccd8ce 100644 --- a/docs/rules/restrict-mustache-expressions.md +++ b/docs/rules/restrict-mustache-expressions.md @@ -67,7 +67,6 @@ with that rule, as this only performs checks on svelte template strings (eg: ` ### Disallowing numbers + ```svelte @@ -191,7 +190,6 @@ type DefaultOptions = { - ## :books: Further Reading - [no-base-to-string](https://typescript-eslint.io/rules/no-base-to-string) diff --git a/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts b/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts index ab800e9b8..0c50b792d 100644 --- a/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts +++ b/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts @@ -4,7 +4,6 @@ import { TypeFlags } from 'typescript'; import { type TSESTree } from '@typescript-eslint/types'; import type { TS } from '../utils/ts-utils'; import { getConstrainedTypeAtLocation, getTypeName, getTypeScriptTools } from '../utils/ts-utils'; -import { getSourceCode } from '../utils/compat'; const props = { allowBoolean: { @@ -113,10 +112,6 @@ export default createRule('restrict-mustache-expressions', { if (allowNull) allowed_types.push('null'); if (allowUndefined) allowed_types.push('undefined'); - // const sourceCode = getSourceCode(context); - // console.log(node.parent); - // console.log(sourceCode.getText(node)); - const type = getNodeType(node.expression); if (type) { From 92145166f2037d0134b207251a4d3200c25d0c07 Mon Sep 17 00:00:00 2001 From: Albert Date: Sat, 10 Aug 2024 02:42:18 +0930 Subject: [PATCH 04/12] improve docs slightly --- docs/rules/restrict-mustache-expressions.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/rules/restrict-mustache-expressions.md b/docs/rules/restrict-mustache-expressions.md index 549ccd8ce..d1d2f0ee3 100644 --- a/docs/rules/restrict-mustache-expressions.md +++ b/docs/rules/restrict-mustache-expressions.md @@ -76,6 +76,7 @@ type Options = { allowNull?: boolean; // allows undefined in both svelte template literals and text expressions allowUndefined?: boolean; + // eg: {bar} textExpressions?: { // allows numbers in text expressions allowNumbers?: boolean; @@ -86,6 +87,7 @@ type Options = { // allows undefined in text expressions allowUndefined?: boolean; }; + // eg: foo stringTemplateExpressions?: { // allows numbers in string template expressions allowNumbers?: boolean; From 2d614df89472f08ec30e528210bb1d8a19041ba0 Mon Sep 17 00:00:00 2001 From: Albert Date: Sat, 10 Aug 2024 02:54:05 +0930 Subject: [PATCH 05/12] Add changeset --- .changeset/unlucky-ducks-explain.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/unlucky-ducks-explain.md diff --git a/.changeset/unlucky-ducks-explain.md b/.changeset/unlucky-ducks-explain.md new file mode 100644 index 000000000..1bf2da5ee --- /dev/null +++ b/.changeset/unlucky-ducks-explain.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-svelte': minor +--- + +Added restrict-mustache-expressions rule to prevent surface area for bugs where non-stringifiable objects or arrays get turned into `[object Object]` which is almost never wanted behavior From 759c9007b2da4e1696fb59cae7adbb4e6c802999 Mon Sep 17 00:00:00 2001 From: Albert Date: Sat, 10 Aug 2024 03:41:09 +0930 Subject: [PATCH 06/12] Add support for union types --- .../rules/restrict-mustache-expressions.ts | 67 ++++++++++++------- .../valid/non-literal/_config.json | 3 + .../non-literal/non-literal-input.svelte | 6 ++ 3 files changed, 53 insertions(+), 23 deletions(-) create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/non-literal/_config.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/non-literal/non-literal-input.svelte diff --git a/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts b/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts index 0c50b792d..0c9e475c7 100644 --- a/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts +++ b/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts @@ -2,8 +2,9 @@ import type { AST } from 'svelte-eslint-parser'; import { createRule } from '../utils'; import { TypeFlags } from 'typescript'; import { type TSESTree } from '@typescript-eslint/types'; -import type { TS } from '../utils/ts-utils'; +import type { TS, TSTools } from '../utils/ts-utils'; import { getConstrainedTypeAtLocation, getTypeName, getTypeScriptTools } from '../utils/ts-utils'; +import { getSourceCode } from '../utils/compat'; const props = { allowBoolean: { @@ -85,10 +86,10 @@ export default createRule('restrict-mustache-expressions', { return tsNode && getConstrainedTypeAtLocation(checker, tsNode); } - const config: Config = context.options[0] || getDefaultOptions(); + const config: Config = Object.assign(getDefaultOptions(), context.options[0] || {}); function checkExpression(node: AST.SvelteMustacheTag) { - const allowed_types: string[] = ['string']; + const allowed_types: Set = new Set(['string']); let opts: Props; if (node.parent.type === 'SvelteAttribute') { if (!node.parent.value.find((n) => n.type === 'SvelteLiteral')) { @@ -99,35 +100,26 @@ export default createRule('restrict-mustache-expressions', { opts = config?.stringTemplateExpressions ? Object.assign(getDefaultOptions(), config.stringTemplateExpressions) : config; - } else { + } else if (node.parent.type !== 'SvelteStyleDirective') { // we are rendering a text expression, so we only allow stringifiable types opts = config?.textExpressions ? Object.assign(getDefaultOptions(), config.textExpressions) : config; + } else { + return; } + console.log(getSourceCode(context).getText(node)); + console.log(node); const { allowBoolean, allowNull, allowUndefined, allowNumber } = opts; - if (allowBoolean === true) allowed_types.push('boolean'); - if (allowNumber === true) allowed_types.push('number'); - if (allowNull) allowed_types.push('null'); - if (allowUndefined) allowed_types.push('undefined'); + if (allowBoolean === true) allowed_types.add('boolean'); + if (allowNumber === true) allowed_types.add('number'); + if (allowNull) allowed_types.add('null'); + if (allowUndefined) allowed_types.add('undefined'); const type = getNodeType(node.expression); - if (type) { - if (type.flags & TypeFlags.StringLike) { - return; - } - if (type.flags & TypeFlags.BooleanLike && allowBoolean) { - return; - } - if (type.flags & TypeFlags.NumberLike && allowNumber) { - return; - } - if (type.flags & TypeFlags.Null && allowNull) { - return; - } - if (type.flags & TypeFlags.Undefined && allowUndefined) { + if (type_allowed(type, allowed_types, tools!)) { return; } context.report({ @@ -135,7 +127,7 @@ export default createRule('restrict-mustache-expressions', { messageId: 'expectedStringifyableType', data: { type: getTypeName(type, tools!), - types: allowed_types.map((t) => `\`${t}\``).join(', ') + types: [...allowed_types].map((t) => `\`${t}\``).join(', ') } }); } @@ -146,3 +138,32 @@ export default createRule('restrict-mustache-expressions', { }; } }); + +function type_allowed(type: TS.Type, allowed_types: Set, tools: TSTools): boolean { + if (type.flags & TypeFlags.StringLike) { + return true; + } + if (type.flags & TypeFlags.BooleanLike) { + return allowed_types.has('boolean'); + } + if (type.flags & TypeFlags.NumberLike) { + return allowed_types.has('number'); + } + if (type.flags & TypeFlags.Null) { + return allowed_types.has('null'); + } + if (type.flags & TypeFlags.Undefined) { + return allowed_types.has('undefined'); + } + if (type.isUnion()) { + for (const sub_type of type.types) { + if (!type_allowed(sub_type, allowed_types, tools)) { + return false; + } + } + + return true; + } + + return false; +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/non-literal/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/non-literal/_config.json new file mode 100644 index 000000000..0e0dcd235 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/non-literal/_config.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/non-literal/non-literal-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/non-literal/non-literal-input.svelte new file mode 100644 index 000000000..f0bcae452 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/non-literal/non-literal-input.svelte @@ -0,0 +1,6 @@ + + +{ num || ""} \ No newline at end of file From 8b1cfb404eb86561f128b00b727f7e8012c1b860 Mon Sep 17 00:00:00 2001 From: Albert Date: Sat, 10 Aug 2024 03:47:06 +0930 Subject: [PATCH 07/12] Rename test & remove console logs --- .../src/rules/restrict-mustache-expressions.ts | 3 --- .../{non-literal => ignore-style-directive}/_config.json | 0 .../ignore-style-directive.svelte} | 4 +--- .../restrict-mustache-expressions/valid/union/_config.json | 3 +++ .../valid/union/union-input.svelte | 5 +++++ 5 files changed, 9 insertions(+), 6 deletions(-) rename packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/{non-literal => ignore-style-directive}/_config.json (100%) rename packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/{non-literal/non-literal-input.svelte => ignore-style-directive/ignore-style-directive.svelte} (55%) create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/union/_config.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/union/union-input.svelte diff --git a/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts b/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts index 0c9e475c7..53bd92800 100644 --- a/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts +++ b/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts @@ -4,7 +4,6 @@ import { TypeFlags } from 'typescript'; import { type TSESTree } from '@typescript-eslint/types'; import type { TS, TSTools } from '../utils/ts-utils'; import { getConstrainedTypeAtLocation, getTypeName, getTypeScriptTools } from '../utils/ts-utils'; -import { getSourceCode } from '../utils/compat'; const props = { allowBoolean: { @@ -108,8 +107,6 @@ export default createRule('restrict-mustache-expressions', { } else { return; } - console.log(getSourceCode(context).getText(node)); - console.log(node); const { allowBoolean, allowNull, allowUndefined, allowNumber } = opts; if (allowBoolean === true) allowed_types.add('boolean'); diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/non-literal/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/ignore-style-directive/_config.json similarity index 100% rename from packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/non-literal/_config.json rename to packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/ignore-style-directive/_config.json diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/non-literal/non-literal-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/ignore-style-directive/ignore-style-directive.svelte similarity index 55% rename from packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/non-literal/non-literal-input.svelte rename to packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/ignore-style-directive/ignore-style-directive.svelte index f0bcae452..cdaaed05f 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/non-literal/non-literal-input.svelte +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/ignore-style-directive/ignore-style-directive.svelte @@ -1,6 +1,4 @@ - -{ num || ""} \ No newline at end of file + \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/union/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/union/_config.json new file mode 100644 index 000000000..0e0dcd235 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/union/_config.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/union/union-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/union/union-input.svelte new file mode 100644 index 000000000..88b7b1f41 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/union/union-input.svelte @@ -0,0 +1,5 @@ + +{ num || "" || bool} \ No newline at end of file From 9c0385e4b70ba8d0f8df52558a50a0ecdd914614 Mon Sep 17 00:00:00 2001 From: Albert Date: Sat, 10 Aug 2024 05:39:59 +0930 Subject: [PATCH 08/12] retrieve variable types --- packages/eslint-plugin-svelte/package.json | 4 +- .../rules/restrict-mustache-expressions.ts | 247 +++++++++++++++--- .../array/array-errors.yaml | 8 +- .../boolean/boolean-errors.yaml | 8 +- .../number/number-errors.yaml | 8 +- .../object/object-errors.yaml | 8 +- .../undefined/undefined-errors.yaml | 4 +- .../undefined/undefined-input.svelte | 2 +- .../textExpression/array/array-errors.yaml | 4 +- .../boolean/boolean-errors.yaml | 6 +- .../textExpression/null/null-errors.yaml | 4 +- .../textExpression/number/number-errors.yaml | 6 +- .../textExpression/object/object-errors.yaml | 4 +- .../undefined/undefined-errors.yaml | 4 +- .../valid/ignore-style-directive/_config.json | 3 - .../ignore-style-directive.svelte | 2 +- .../valid/object-access/_config.json | 3 + .../object-access/object-access-input.svelte | 6 + .../valid/union/_config.json | 3 - 19 files changed, 255 insertions(+), 79 deletions(-) delete mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/ignore-style-directive/_config.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/object-access/_config.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/object-access/object-access-input.svelte delete mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/union/_config.json diff --git a/packages/eslint-plugin-svelte/package.json b/packages/eslint-plugin-svelte/package.json index f4dcbb7ce..5800671bc 100644 --- a/packages/eslint-plugin-svelte/package.json +++ b/packages/eslint-plugin-svelte/package.json @@ -1,6 +1,6 @@ { - "name": "eslint-plugin-svelte", - "version": "2.43.0", + "name": "eslint-plugin-svelte-albert", + "version": "2.50.0", "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", diff --git a/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts b/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts index 53bd92800..e83a913eb 100644 --- a/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts +++ b/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts @@ -1,9 +1,11 @@ import type { AST } from 'svelte-eslint-parser'; import { createRule } from '../utils'; import { TypeFlags } from 'typescript'; -import { type TSESTree } from '@typescript-eslint/types'; +import type { TSESTree } from '@typescript-eslint/types'; import type { TS, TSTools } from '../utils/ts-utils'; import { getConstrainedTypeAtLocation, getTypeName, getTypeScriptTools } from '../utils/ts-utils'; +import { findVariable } from '../utils/ast-utils'; +import type { RuleContext } from '../types'; const props = { allowBoolean: { @@ -65,7 +67,7 @@ export default createRule('restrict-mustache-expressions', { ], messages: { expectedStringifyableType: - 'Expected {{type}} to be one of the following: {{types}}. You must cast or convert the expression to one of the allowed types.' + 'Expected `{{disallowed}}` to be one of the following: {{types}}. You must cast or convert the expression to one of the allowed types.' }, type: 'problem' }, @@ -75,21 +77,12 @@ export default createRule('restrict-mustache-expressions', { return {}; } - const { service } = tools; - const checker = service.program.getTypeChecker(); - - function getNodeType( - node: TSESTree.Expression | TSESTree.PrivateIdentifier | TSESTree.SpreadElement - ): TS.Type | undefined { - const tsNode = service.esTreeNodeToTSNodeMap.get(node); - return tsNode && getConstrainedTypeAtLocation(checker, tsNode); - } - const config: Config = Object.assign(getDefaultOptions(), context.options[0] || {}); - function checkExpression(node: AST.SvelteMustacheTag) { + function checkMustacheExpression(node: AST.SvelteMustacheTag) { const allowed_types: Set = new Set(['string']); let opts: Props; + if (node.kind === 'raw') return; if (node.parent.type === 'SvelteAttribute') { if (!node.parent.value.find((n) => n.type === 'SvelteLiteral')) { // we are rendering a non-literal attribute (eg: class:disabled={disabled}, so we allow any type @@ -114,53 +107,233 @@ export default createRule('restrict-mustache-expressions', { if (allowNull) allowed_types.add('null'); if (allowUndefined) allowed_types.add('undefined'); - const type = getNodeType(node.expression); - if (type) { - if (type_allowed(type, allowed_types, tools!)) { - return; + const disallowed = disallowed_expression(node.expression, allowed_types, context, tools!); + if (!disallowed) return; + + context.report({ + node, + messageId: 'expectedStringifyableType', + data: { + disallowed: getTypeName(disallowed, tools!), + types: [...allowed_types].map((t) => `\`${t}\``).join(', ') } - context.report({ - node, - messageId: 'expectedStringifyableType', - data: { - type: getTypeName(type, tools!), - types: [...allowed_types].map((t) => `\`${t}\``).join(', ') - } - }); - } + }); } return { - SvelteMustacheTag: checkExpression + SvelteMustacheTag: checkMustacheExpression }; } }); -function type_allowed(type: TS.Type, allowed_types: Set, tools: TSTools): boolean { +function getNodeType( + node: TSESTree.Expression | TSESTree.PrivateIdentifier | TSESTree.SpreadElement, + tools: TSTools +): TS.Type | null { + const tsNode = tools.service.esTreeNodeToTSNodeMap.get(node); + return ( + (tsNode && getConstrainedTypeAtLocation(tools.service.program.getTypeChecker(), tsNode)) || null + ); +} + +function disallowed_identifier( + expression: TSESTree.Identifier, + allowed_types: Set, + context: RuleContext, + tools: TSTools +): TS.Type | null { + const type = getNodeType(expression, tools); + + if (!type) return null; + + return disallowed_type(type, allowed_types, context, tools); +} + +function disallowed_type( + type: TS.Type, + allowed_types: Set, + context: RuleContext, + tools: TSTools +): TS.Type | null { if (type.flags & TypeFlags.StringLike) { - return true; + return null; } if (type.flags & TypeFlags.BooleanLike) { - return allowed_types.has('boolean'); + return allowed_types.has('boolean') ? null : type; } if (type.flags & TypeFlags.NumberLike) { - return allowed_types.has('number'); + return allowed_types.has('number') ? null : type; } if (type.flags & TypeFlags.Null) { - return allowed_types.has('null'); + return allowed_types.has('null') ? null : type; } if (type.flags & TypeFlags.Undefined) { - return allowed_types.has('undefined'); + return allowed_types.has('undefined') ? null : type; } if (type.isUnion()) { for (const sub_type of type.types) { - if (!type_allowed(sub_type, allowed_types, tools)) { - return false; + const disallowed = disallowed_type(sub_type, allowed_types, context, tools); + if (disallowed) { + return disallowed; } } + return null; + } + + return type; +} + +function disallowed_literal( + expression: TSESTree.Literal, + allowed_types: Set, + context: RuleContext, + tools: TSTools +): TS.Type | null { + const type = getNodeType(expression, tools); + + if (!type) return null; + + return disallowed_type(type, allowed_types, context, tools); +} - return true; +function disallowed_expression( + expression: TSESTree.Expression, + allowed_types: Set, + context: RuleContext, + tools: TSTools +): TS.Type | null { + switch (expression.type) { + case 'Literal': + return disallowed_literal(expression, allowed_types, context, tools); + case 'Identifier': + return disallowed_identifier(expression, allowed_types, context, tools); + case 'ArrayExpression': + return getNodeType(expression, tools); + case 'MemberExpression': + return disallowed_member_expression(expression, allowed_types, context, tools); + case 'LogicalExpression': + return disallowed_logical_expression(expression, allowed_types, context, tools); + default: + return getNodeType(expression, tools); } +} + +function disallowed_logical_expression( + expression: TSESTree.LogicalExpression, + allowed_types: Set, + context: RuleContext, + tools: TSTools +): TS.Type | null { + const type = getNodeType(expression, tools); + + if (!type) return null; + + return disallowed_type(type, allowed_types, context, tools); +} - return false; +// function disallowed_member_expression( +// expression: TSESTree.MemberExpression, +// allowed_types: Set, +// context: RuleContext, +// tools: TSTools +// ): TS.Type | null { +// const checker = tools.service.program.getTypeChecker(); +// const type = getNodeType(expression, tools); +// if (!type) return null; + +// const object = expression.object; +// if (object.type === 'Identifier') { +// const variable = findVariable(context, object); +// if (!variable) return null; +// const node_def = variable.defs[0].node; +// if (node_def.type !== 'VariableDeclarator') return null; +// if (!node_def.init) return null; +// // let type = getNodeType(node_def.init, tools); +// if (node_def.init.type !== 'ObjectExpression') return null; +// if (expression.property.type !== 'Identifier') return null; + +// const type = getNodeType(node_def.init, tools); +// if (!type) return null; +// const symbol = checker.getPropertyOfType(type, expression.property.name); +// if (!symbol) return null; + +// const prop_type = checker.getTypeOfSymbol(symbol); + +// return disallowed_type(prop_type, allowed_types, context, tools); +// } + +// return disallowed_type(type, allowed_types, context, tools); +// } + +function disallowed_member_expression( + expression: TSESTree.MemberExpression, + allowed_types: Set, + context: RuleContext, + tools: TSTools +): TS.Type | null { + const checker = tools.service.program.getTypeChecker(); + let objectType = getNodeType(expression.object, tools); + + if (!objectType) return null; + + // Handle nested member expressions + if (expression.object.type === 'MemberExpression') { + const nestedType = disallowed_member_expression( + expression.object, + allowed_types, + context, + tools + ); + if (nestedType) objectType = nestedType; + } + + // Handle identifiers (variables) + if (expression.object.type === 'Identifier') { + const variable = findVariable(context, expression.object); + if (variable && variable.defs[0]?.node.type === 'VariableDeclarator') { + const initNode = variable.defs[0].node.init; + if (initNode) { + const initType = getNodeType(initNode, tools); + if (initType) objectType = initType; + } + } + } + + // Get property type + const propertyName = getPropertyName(expression.property); + if (!propertyName) return objectType; + + let propertyType: TS.Type | undefined; + + // Try to get property type using getPropertyOfType + const symbol = checker.getPropertyOfType(objectType, propertyName); + if (symbol) { + propertyType = checker.getTypeOfSymbol(symbol); + } + + // If property type is still not found, try using getTypeOfPropertyOfType + if (!propertyType) { + const property_symbol = checker.getPropertyOfType(objectType, propertyName); + if (property_symbol) { + propertyType = checker.getTypeOfSymbol(property_symbol); + } + } + + // If we found a property type, use it; otherwise, fall back to the object type + return propertyType + ? disallowed_type(propertyType, allowed_types, context, tools) + : disallowed_type(objectType, allowed_types, context, tools); +} + +function getPropertyName( + property: TSESTree.Expression | TSESTree.PrivateIdentifier +): string | undefined { + if (property.type === 'Identifier') { + return property.name; + } else if (property.type === 'Literal' && typeof property.value === 'string') { + return property.value; + } else if (property.type === 'TemplateLiteral' && property.quasis.length === 1) { + return property.quasis[0].value.cooked; + } + return undefined; } diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/array/array-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/array/array-errors.yaml index 04368921c..d5575f817 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/array/array-errors.yaml +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/array/array-errors.yaml @@ -1,12 +1,12 @@ -- message: "Expected string[] to be one of the following: `string`, `boolean`, +- message: 'Expected `string[]` to be one of the following: `string`, `boolean`, `number`. You must cast or convert the expression to one of the allowed - types." + types.' line: 4 column: 14 suggestions: null -- message: "Expected string[] to be one of the following: `string`, `boolean`, +- message: 'Expected `string[]` to be one of the following: `string`, `boolean`, `number`. You must cast or convert the expression to one of the allowed - types." + types.' line: 5 column: 14 suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/boolean/boolean-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/boolean/boolean-errors.yaml index ffc880b95..c4bec53b2 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/boolean/boolean-errors.yaml +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/boolean/boolean-errors.yaml @@ -1,10 +1,10 @@ -- message: "Expected true to be one of the following: `string`, `number`. You must - cast or convert the expression to one of the allowed types." +- message: 'Expected `true` to be one of the following: `string`, `number`. You + must cast or convert the expression to one of the allowed types.' line: 4 column: 14 suggestions: null -- message: "Expected boolean to be one of the following: `string`, `number`. You - must cast or convert the expression to one of the allowed types." +- message: 'Expected `boolean` to be one of the following: `string`, `number`. You + must cast or convert the expression to one of the allowed types.' line: 5 column: 14 suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/number/number-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/number/number-errors.yaml index 831440c04..e8ef0168e 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/number/number-errors.yaml +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/number/number-errors.yaml @@ -1,10 +1,10 @@ -- message: "Expected 123 to be one of the following: `string`, `boolean`. You must - cast or convert the expression to one of the allowed types." +- message: 'Expected `123` to be one of the following: `string`, `boolean`. You + must cast or convert the expression to one of the allowed types.' line: 4 column: 14 suggestions: null -- message: "Expected number to be one of the following: `string`, `boolean`. You - must cast or convert the expression to one of the allowed types." +- message: 'Expected `number` to be one of the following: `string`, `boolean`. You + must cast or convert the expression to one of the allowed types.' line: 5 column: 14 suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/object/object-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/object/object-errors.yaml index 658166f29..127c94e2a 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/object/object-errors.yaml +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/object/object-errors.yaml @@ -1,12 +1,12 @@ -- message: "Expected { invalid: string; } to be one of the following: `string`, +- message: 'Expected `{ invalid: string; }` to be one of the following: `string`, `boolean`, `number`. You must cast or convert the expression to one of the - allowed types." + allowed types.' line: 6 column: 14 suggestions: null -- message: "Expected { bar: string; } to be one of the following: `string`, +- message: 'Expected `{ bar: string; }` to be one of the following: `string`, `boolean`, `number`. You must cast or convert the expression to one of the - allowed types." + allowed types.' line: 7 column: 14 suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/undefined/undefined-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/undefined/undefined-errors.yaml index 9286cf69a..21fde401b 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/undefined/undefined-errors.yaml +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/undefined/undefined-errors.yaml @@ -1,10 +1,10 @@ -- message: 'Expected undefined to be one of the following: `string`, `boolean`, +- message: 'Expected `undefined` to be one of the following: `string`, `boolean`, `number`. You must cast or convert the expression to one of the allowed types.' line: 4 column: 14 suggestions: null -- message: 'Expected any to be one of the following: `string`, `boolean`, +- message: 'Expected `undefined` to be one of the following: `string`, `boolean`, `number`. You must cast or convert the expression to one of the allowed types.' line: 5 diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/undefined/undefined-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/undefined/undefined-input.svelte index f02dd2e3b..4333a26fb 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/undefined/undefined-input.svelte +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/stringTemplateExpression/undefined/undefined-input.svelte @@ -1,5 +1,5 @@ foo foo \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/array/array-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/array/array-errors.yaml index 2aace51d7..a83d973a9 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/array/array-errors.yaml +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/array/array-errors.yaml @@ -1,10 +1,10 @@ -- message: 'Expected number[] to be one of the following: `string`, `boolean`, +- message: 'Expected `number[]` to be one of the following: `string`, `boolean`, `number`. You must cast or convert the expression to one of the allowed types.' line: 4 column: 1 suggestions: null -- message: 'Expected number[] to be one of the following: `string`, `boolean`, +- message: 'Expected `number[]` to be one of the following: `string`, `boolean`, `number`. You must cast or convert the expression to one of the allowed types.' line: 5 diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/boolean/boolean-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/boolean/boolean-errors.yaml index 28da2c309..8f2fe4a22 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/boolean/boolean-errors.yaml +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/boolean/boolean-errors.yaml @@ -1,9 +1,9 @@ -- message: 'Expected true to be one of the following: `string`, `number`. You must - cast or convert the expression to one of the allowed types.' +- message: 'Expected `true` to be one of the following: `string`, `number`. You + must cast or convert the expression to one of the allowed types.' line: 4 column: 1 suggestions: null -- message: 'Expected boolean to be one of the following: `string`, `number`. You +- message: 'Expected `boolean` to be one of the following: `string`, `number`. You must cast or convert the expression to one of the allowed types.' line: 5 column: 1 diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/null/null-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/null/null-errors.yaml index f17566ea9..cbe7dd441 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/null/null-errors.yaml +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/null/null-errors.yaml @@ -1,10 +1,10 @@ -- message: 'Expected null to be one of the following: `string`, `boolean`, +- message: 'Expected `null` to be one of the following: `string`, `boolean`, `number`. You must cast or convert the expression to one of the allowed types.' line: 4 column: 1 suggestions: null -- message: 'Expected null to be one of the following: `string`, `boolean`, +- message: 'Expected `null` to be one of the following: `string`, `boolean`, `number`. You must cast or convert the expression to one of the allowed types.' line: 5 diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/number/number-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/number/number-errors.yaml index 85667509b..b390fda51 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/number/number-errors.yaml +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/number/number-errors.yaml @@ -1,9 +1,9 @@ -- message: 'Expected 123 to be one of the following: `string`, `boolean`. You must - cast or convert the expression to one of the allowed types.' +- message: 'Expected `123` to be one of the following: `string`, `boolean`. You + must cast or convert the expression to one of the allowed types.' line: 4 column: 1 suggestions: null -- message: 'Expected number to be one of the following: `string`, `boolean`. You +- message: 'Expected `number` to be one of the following: `string`, `boolean`. You must cast or convert the expression to one of the allowed types.' line: 5 column: 1 diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/object/object-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/object/object-errors.yaml index 77a4d8173..2c284b1d0 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/object/object-errors.yaml +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/object/object-errors.yaml @@ -1,10 +1,10 @@ -- message: 'Expected { foo: string; } to be one of the following: `string`, +- message: 'Expected `{ foo: string; }` to be one of the following: `string`, `boolean`, `number`. You must cast or convert the expression to one of the allowed types.' line: 4 column: 1 suggestions: null -- message: 'Expected { foo: string; } to be one of the following: `string`, +- message: 'Expected `{ foo: string; }` to be one of the following: `string`, `boolean`, `number`. You must cast or convert the expression to one of the allowed types.' line: 5 diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/undefined/undefined-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/undefined/undefined-errors.yaml index c78e8e074..9a205ee8a 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/undefined/undefined-errors.yaml +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/undefined/undefined-errors.yaml @@ -1,10 +1,10 @@ -- message: 'Expected undefined to be one of the following: `string`, `boolean`, +- message: 'Expected `undefined` to be one of the following: `string`, `boolean`, `number`. You must cast or convert the expression to one of the allowed types.' line: 4 column: 1 suggestions: null -- message: 'Expected undefined to be one of the following: `string`, `boolean`, +- message: 'Expected `undefined` to be one of the following: `string`, `boolean`, `number`. You must cast or convert the expression to one of the allowed types.' line: 5 diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/ignore-style-directive/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/ignore-style-directive/_config.json deleted file mode 100644 index 0e0dcd235..000000000 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/ignore-style-directive/_config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - -} \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/ignore-style-directive/ignore-style-directive.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/ignore-style-directive/ignore-style-directive.svelte index cdaaed05f..eae0fad1a 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/ignore-style-directive/ignore-style-directive.svelte +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/ignore-style-directive/ignore-style-directive.svelte @@ -1,4 +1,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/object-access/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/object-access/_config.json new file mode 100644 index 000000000..f6d673460 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/object-access/_config.json @@ -0,0 +1,3 @@ +{ + "only": false +} \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/object-access/object-access-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/object-access/object-access-input.svelte new file mode 100644 index 000000000..2dfeff4d1 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/object-access/object-access-input.svelte @@ -0,0 +1,6 @@ + +{foo.bar} \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/union/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/union/_config.json deleted file mode 100644 index 0e0dcd235..000000000 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/union/_config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - -} \ No newline at end of file From 4284cbb2de6c1ccbe8101b9492aa519e472a6de2 Mon Sep 17 00:00:00 2001 From: Albert Date: Sat, 10 Aug 2024 05:42:24 +0930 Subject: [PATCH 09/12] Enable object property access functionality --- .../invalid/object-access/_config.json | 3 +++ .../invalid/object-access/object-access-errors.yaml | 6 ++++++ .../invalid/object-access/object-access-input.svelte | 10 ++++++++++ .../valid/object-access/_config.json | 2 +- .../valid/object-access/object-access-input.svelte | 10 ++++++++-- 5 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/object-access/_config.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/object-access/object-access-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/object-access/object-access-input.svelte diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/object-access/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/object-access/_config.json new file mode 100644 index 000000000..0e0dcd235 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/object-access/_config.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/object-access/object-access-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/object-access/object-access-errors.yaml new file mode 100644 index 000000000..f5205e1bc --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/object-access/object-access-errors.yaml @@ -0,0 +1,6 @@ +- message: 'Expected `null` to be one of the following: `string`, `boolean`, + `number`. You must cast or convert the expression to one of the allowed + types.' + line: 10 + column: 1 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/object-access/object-access-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/object-access/object-access-input.svelte new file mode 100644 index 000000000..33575e161 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/object-access/object-access-input.svelte @@ -0,0 +1,10 @@ + +{foo.a.b.c} \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/object-access/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/object-access/_config.json index f6d673460..0e0dcd235 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/object-access/_config.json +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/object-access/_config.json @@ -1,3 +1,3 @@ { - "only": false + } \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/object-access/object-access-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/object-access/object-access-input.svelte index 2dfeff4d1..a48d67f15 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/object-access/object-access-input.svelte +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/object-access/object-access-input.svelte @@ -1,6 +1,12 @@ -{foo.bar} \ No newline at end of file +{foo.bar} +{foo.a.b.c} \ No newline at end of file From 113990c0db16603e9d48bf58e67d55f4da4cbdbe Mon Sep 17 00:00:00 2001 From: Albert Date: Sat, 10 Aug 2024 13:55:51 +0930 Subject: [PATCH 10/12] Add if block type narrowing --- packages/eslint-plugin-svelte/src/meta.ts | 4 +- .../rules/restrict-mustache-expressions.ts | 169 +++++++++--------- .../conditional/conditional-errors.yaml | 6 + .../conditional/conditional-input.svelte | 7 + .../textExpression/object/object-input.svelte | 2 +- .../conditional/conditional-input.svelte | 6 + .../valid/object-access/_config.json | 3 - .../rules/restrict-mustache-expressions.ts | 4 +- 8 files changed, 110 insertions(+), 91 deletions(-) create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/conditional/conditional-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/conditional/conditional-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/conditional/conditional-input.svelte delete mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/object-access/_config.json diff --git a/packages/eslint-plugin-svelte/src/meta.ts b/packages/eslint-plugin-svelte/src/meta.ts index 4619d5534..86914c0b7 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 = '2.43.0'; +export const name = 'eslint-plugin-svelte-albert'; +export const version = '2.50.0'; diff --git a/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts b/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts index e83a913eb..0c12f2eb2 100644 --- a/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts +++ b/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts @@ -3,9 +3,10 @@ import { createRule } from '../utils'; import { TypeFlags } from 'typescript'; import type { TSESTree } from '@typescript-eslint/types'; import type { TS, TSTools } from '../utils/ts-utils'; -import { getConstrainedTypeAtLocation, getTypeName, getTypeScriptTools } from '../utils/ts-utils'; +import { getTypeName, getTypeScriptTools } from '../utils/ts-utils'; import { findVariable } from '../utils/ast-utils'; import type { RuleContext } from '../types'; +import type { Scope, Variable } from '@typescript-eslint/scope-manager'; const props = { allowBoolean: { @@ -38,6 +39,11 @@ type Props = { allowUndefined: boolean; }; +enum NodeType { + Unknown, + Allowed +} + type Config = { stringTemplateExpressions?: Props; textExpressions?: Props; @@ -86,6 +92,7 @@ export default createRule('restrict-mustache-expressions', { if (node.parent.type === 'SvelteAttribute') { if (!node.parent.value.find((n) => n.type === 'SvelteLiteral')) { // we are rendering a non-literal attribute (eg: class:disabled={disabled}, so we allow any type + // (todo): maybe we could maybe check the expected type of the attribute here, but I think the language server already does that? return; } // we are rendering an template string attribute (eg: href="/page/{page.id}"), so we only allow stringifiable types @@ -107,14 +114,13 @@ export default createRule('restrict-mustache-expressions', { if (allowNull) allowed_types.add('null'); if (allowUndefined) allowed_types.add('undefined'); - const disallowed = disallowed_expression(node.expression, allowed_types, context, tools!); - if (!disallowed) return; - + const type = disallowed_expression(node.expression, allowed_types, context, tools!); + if (NodeType.Allowed === type) return; context.report({ node, messageId: 'expectedStringifyableType', data: { - disallowed: getTypeName(disallowed, tools!), + disallowed: type === NodeType.Unknown ? 'unknown' : getTypeName(type, tools!), types: [...allowed_types].map((t) => `\`${t}\``).join(', ') } }); @@ -126,14 +132,13 @@ export default createRule('restrict-mustache-expressions', { } }); -function getNodeType( - node: TSESTree.Expression | TSESTree.PrivateIdentifier | TSESTree.SpreadElement, - tools: TSTools -): TS.Type | null { - const tsNode = tools.service.esTreeNodeToTSNodeMap.get(node); - return ( - (tsNode && getConstrainedTypeAtLocation(tools.service.program.getTypeChecker(), tsNode)) || null - ); +function getNodeType(node: TSESTree.Node, tools: TSTools): TS.Type | NodeType.Unknown { + const checker = tools.service.program.getTypeChecker(); + const ts_node = tools.service.esTreeNodeToTSNodeMap.get(node); + if (!ts_node) return NodeType.Unknown; + const nodeType = checker.getTypeAtLocation(ts_node); + const constrained = checker.getBaseConstraintOfType(nodeType); + return constrained ?? nodeType; } function disallowed_identifier( @@ -141,34 +146,73 @@ function disallowed_identifier( allowed_types: Set, context: RuleContext, tools: TSTools -): TS.Type | null { - const type = getNodeType(expression, tools); +): TS.Type | NodeType { + const type = get_variable_type(expression, context, tools); - if (!type) return null; + if (type === NodeType.Unknown) return NodeType.Unknown; return disallowed_type(type, allowed_types, context, tools); } +function get_variable_type( + identifier: TSESTree.Identifier, + context: RuleContext, + tools: TSTools +): TS.Type | NodeType.Unknown { + const variable = findVariable(context, identifier); + + const identifiers = variable?.identifiers[0]; + + if (!identifiers) return getNodeType(identifier, tools); + + const type = getNodeType(variable.identifiers[0], tools); + + if (NodeType.Unknown === type) return NodeType.Unknown; + + return narrow_variable_type(identifier, type, tools); +} + +function narrow_variable_type( + identifier: TSESTree.Identifier, + type: TS.Type, + tools: TSTools +): TS.Type { + const checker = tools.service.program.getTypeChecker(); + let currentNode: TSESTree.Node | AST.SvelteNode | undefined = identifier as TSESTree.Node; + + while (currentNode) { + if (currentNode.type === 'SvelteIfBlock') { + const condition = currentNode.expression; + if (condition.type === 'Identifier' && condition.name === identifier.name) { + return checker.getNonNullableType(type); + } + } + currentNode = currentNode.parent as TSESTree.Node | AST.SvelteNode; + } + + return type; +} + function disallowed_type( type: TS.Type, allowed_types: Set, context: RuleContext, tools: TSTools -): TS.Type | null { +): TS.Type | NodeType { if (type.flags & TypeFlags.StringLike) { - return null; + return NodeType.Allowed; } if (type.flags & TypeFlags.BooleanLike) { - return allowed_types.has('boolean') ? null : type; + return allowed_types.has('boolean') ? NodeType.Allowed : type; } if (type.flags & TypeFlags.NumberLike) { - return allowed_types.has('number') ? null : type; + return allowed_types.has('number') ? NodeType.Allowed : type; } if (type.flags & TypeFlags.Null) { - return allowed_types.has('null') ? null : type; + return allowed_types.has('null') ? NodeType.Allowed : type; } if (type.flags & TypeFlags.Undefined) { - return allowed_types.has('undefined') ? null : type; + return allowed_types.has('undefined') ? NodeType.Allowed : type; } if (type.isUnion()) { for (const sub_type of type.types) { @@ -177,7 +221,7 @@ function disallowed_type( return disallowed; } } - return null; + return NodeType.Allowed; } return type; @@ -188,10 +232,10 @@ function disallowed_literal( allowed_types: Set, context: RuleContext, tools: TSTools -): TS.Type | null { +): TS.Type | NodeType { const type = getNodeType(expression, tools); - if (!type) return null; + if (NodeType.Unknown === type) return NodeType.Unknown; return disallowed_type(type, allowed_types, context, tools); } @@ -201,7 +245,7 @@ function disallowed_expression( allowed_types: Set, context: RuleContext, tools: TSTools -): TS.Type | null { +): TS.Type | NodeType { switch (expression.type) { case 'Literal': return disallowed_literal(expression, allowed_types, context, tools); @@ -213,8 +257,11 @@ function disallowed_expression( return disallowed_member_expression(expression, allowed_types, context, tools); case 'LogicalExpression': return disallowed_logical_expression(expression, allowed_types, context, tools); - default: - return getNodeType(expression, tools); + default: { + const type = getNodeType(expression, tools); + if (NodeType.Unknown === type) return NodeType.Unknown; + return disallowed_type(type, allowed_types, context, tools); + } } } @@ -223,58 +270,24 @@ function disallowed_logical_expression( allowed_types: Set, context: RuleContext, tools: TSTools -): TS.Type | null { +): TS.Type | NodeType { const type = getNodeType(expression, tools); - if (!type) return null; + if (NodeType.Unknown === type) return NodeType.Unknown; return disallowed_type(type, allowed_types, context, tools); } -// function disallowed_member_expression( -// expression: TSESTree.MemberExpression, -// allowed_types: Set, -// context: RuleContext, -// tools: TSTools -// ): TS.Type | null { -// const checker = tools.service.program.getTypeChecker(); -// const type = getNodeType(expression, tools); -// if (!type) return null; - -// const object = expression.object; -// if (object.type === 'Identifier') { -// const variable = findVariable(context, object); -// if (!variable) return null; -// const node_def = variable.defs[0].node; -// if (node_def.type !== 'VariableDeclarator') return null; -// if (!node_def.init) return null; -// // let type = getNodeType(node_def.init, tools); -// if (node_def.init.type !== 'ObjectExpression') return null; -// if (expression.property.type !== 'Identifier') return null; - -// const type = getNodeType(node_def.init, tools); -// if (!type) return null; -// const symbol = checker.getPropertyOfType(type, expression.property.name); -// if (!symbol) return null; - -// const prop_type = checker.getTypeOfSymbol(symbol); - -// return disallowed_type(prop_type, allowed_types, context, tools); -// } - -// return disallowed_type(type, allowed_types, context, tools); -// } - function disallowed_member_expression( expression: TSESTree.MemberExpression, allowed_types: Set, context: RuleContext, tools: TSTools -): TS.Type | null { +): TS.Type | NodeType { const checker = tools.service.program.getTypeChecker(); - let objectType = getNodeType(expression.object, tools); + let objectType: TS.Type | NodeType = getNodeType(expression.object, tools); - if (!objectType) return null; + if (NodeType.Unknown === objectType) return NodeType.Unknown; // Handle nested member expressions if (expression.object.type === 'MemberExpression') { @@ -287,6 +300,8 @@ function disallowed_member_expression( if (nestedType) objectType = nestedType; } + if (NodeType.Allowed === objectType) return NodeType.Allowed; + // Handle identifiers (variables) if (expression.object.type === 'Identifier') { const variable = findVariable(context, expression.object); @@ -303,26 +318,14 @@ function disallowed_member_expression( const propertyName = getPropertyName(expression.property); if (!propertyName) return objectType; - let propertyType: TS.Type | undefined; - // Try to get property type using getPropertyOfType const symbol = checker.getPropertyOfType(objectType, propertyName); if (symbol) { - propertyType = checker.getTypeOfSymbol(symbol); - } - - // If property type is still not found, try using getTypeOfPropertyOfType - if (!propertyType) { - const property_symbol = checker.getPropertyOfType(objectType, propertyName); - if (property_symbol) { - propertyType = checker.getTypeOfSymbol(property_symbol); - } + const property_type = checker.getTypeOfSymbol(symbol); + return disallowed_type(property_type, allowed_types, context, tools); } - // If we found a property type, use it; otherwise, fall back to the object type - return propertyType - ? disallowed_type(propertyType, allowed_types, context, tools) - : disallowed_type(objectType, allowed_types, context, tools); + return NodeType.Unknown; } function getPropertyName( @@ -333,7 +336,7 @@ function getPropertyName( } else if (property.type === 'Literal' && typeof property.value === 'string') { return property.value; } else if (property.type === 'TemplateLiteral' && property.quasis.length === 1) { - return property.quasis[0].value.cooked; + // return property.quasis[0].value.cooked; } return undefined; } diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/conditional/conditional-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/conditional/conditional-errors.yaml new file mode 100644 index 000000000..eaf640ef5 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/conditional/conditional-errors.yaml @@ -0,0 +1,6 @@ +- message: 'Expected `null` to be one of the following: `string`, `boolean`, + `number`. You must cast or convert the expression to one of the allowed + types.' + line: 6 + column: 5 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/conditional/conditional-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/conditional/conditional-input.svelte new file mode 100644 index 000000000..93c35c8d2 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/conditional/conditional-input.svelte @@ -0,0 +1,7 @@ + +{#if foo === null} + {foo } +{/if} \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/object/object-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/object/object-input.svelte index e5da44fb1..5975884ca 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/object/object-input.svelte +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/invalid/textExpression/object/object-input.svelte @@ -1,5 +1,5 @@ { { foo: 'bar' } } { invalid_object } \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/conditional/conditional-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/conditional/conditional-input.svelte new file mode 100644 index 000000000..b7b0f971c --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/conditional/conditional-input.svelte @@ -0,0 +1,6 @@ + +{#if foo} + { foo } +{/if} \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/object-access/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/object-access/_config.json deleted file mode 100644 index 0e0dcd235..000000000 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/object-access/_config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - -} \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/src/rules/restrict-mustache-expressions.ts b/packages/eslint-plugin-svelte/tests/src/rules/restrict-mustache-expressions.ts index 045eb661a..a3482b9b0 100644 --- a/packages/eslint-plugin-svelte/tests/src/rules/restrict-mustache-expressions.ts +++ b/packages/eslint-plugin-svelte/tests/src/rules/restrict-mustache-expressions.ts @@ -5,8 +5,8 @@ import { loadTestCases } from '../../utils/utils'; const tester = new RuleTester({ languageOptions: { ecmaVersion: 2020, - sourceType: 'module' - } + sourceType: 'module', + }, }); tester.run('restrict-mustache-expressions', rule as any, loadTestCases('restrict-mustache-expressions')); From ac6ae22ccf331c54fa5086c90b2f50410d84f63a Mon Sep 17 00:00:00 2001 From: Albert Date: Sat, 10 Aug 2024 13:59:56 +0930 Subject: [PATCH 11/12] Remove unused import --- .../src/rules/restrict-mustache-expressions.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts b/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts index 0c12f2eb2..e58445f7c 100644 --- a/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts +++ b/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts @@ -6,7 +6,6 @@ import type { TS, TSTools } from '../utils/ts-utils'; import { getTypeName, getTypeScriptTools } from '../utils/ts-utils'; import { findVariable } from '../utils/ast-utils'; import type { RuleContext } from '../types'; -import type { Scope, Variable } from '@typescript-eslint/scope-manager'; const props = { allowBoolean: { From e1c168dff0c0068d91ca138a6ddd92147b419468 Mon Sep 17 00:00:00 2001 From: Albert Date: Wed, 14 Aug 2024 19:17:45 +0930 Subject: [PATCH 12/12] Temp --- package.json | 36 +- packages/eslint-plugin-svelte/package.json | 2 +- packages/eslint-plugin-svelte/src/meta.ts | 2 +- .../rules/restrict-mustache-expressions.ts | 347 ++++++++++-------- .../src/types-for-node.ts | 2 + .../valid/index-access/index-input.svelte | 13 + .../rules/restrict-mustache-expressions.ts | 8 +- 7 files changed, 244 insertions(+), 166 deletions(-) create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/index-access/index-input.svelte diff --git a/package.json b/package.json index 342ab661f..e6eaee16d 100644 --- a/package.json +++ b/package.json @@ -15,34 +15,34 @@ }, "devDependencies": { "@changesets/changelog-github": "^0.5.0", - "@changesets/cli": "^2.27.5", - "@changesets/get-release-plan": "^4.0.2", + "@changesets/cli": "^2.27.7", + "@changesets/get-release-plan": "^4.0.3", "@eslint-community/eslint-plugin-eslint-comments": "^4.3.0", - "@ota-meshi/eslint-plugin": "^0.17.1", - "@types/eslint": "^8.56.10", - "@typescript-eslint/eslint-plugin": "^7.13.0", - "@typescript-eslint/parser": "^7.13.0", + "@ota-meshi/eslint-plugin": "^0.17.5", + "@types/eslint": "^8.56.11", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", "env-cmd": "^10.1.0", - "eslint": "^9.4.0", + "eslint": "^9.9.0", "eslint-config-prettier": "^9.1.0", "eslint-formatter-friendly": "^7.0.0", - "eslint-plugin-eslint-plugin": "^6.1.0", + "eslint-plugin-eslint-plugin": "^6.2.0", "eslint-plugin-jsdoc": "^50.0.0", - "eslint-plugin-json-schema-validator": "^5.1.0", + "eslint-plugin-json-schema-validator": "^5.1.2", "eslint-plugin-jsonc": "^2.16.0", - "eslint-plugin-markdown": "^5.0.0", + "eslint-plugin-markdown": "^5.1.0", "eslint-plugin-mdx": "^3.1.5", - "eslint-plugin-n": "^17.9.0", + "eslint-plugin-n": "^17.10.2", "eslint-plugin-node-dependencies": "^0.12.0", - "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-regexp": "^2.6.0", "eslint-plugin-yml": "^1.14.0", - "npm-run-all2": "^6.2.0", - "prettier": "^3.3.2", - "prettier-plugin-svelte": "^3.2.4", - "rimraf": "^6.0.0", - "typescript": "~5.5.0", - "typescript-eslint": "^7.13.0" + "npm-run-all2": "^6.2.2", + "prettier": "^3.3.3", + "prettier-plugin-svelte": "^3.2.6", + "rimraf": "^6.0.1", + "typescript": "~5.5.4", + "typescript-eslint": "^7.18.0" }, "publishConfig": { "access": "public" diff --git a/packages/eslint-plugin-svelte/package.json b/packages/eslint-plugin-svelte/package.json index 5800671bc..f4915e0ab 100644 --- a/packages/eslint-plugin-svelte/package.json +++ b/packages/eslint-plugin-svelte/package.json @@ -1,5 +1,5 @@ { - "name": "eslint-plugin-svelte-albert", + "name": "eslint-plugin-svelte", "version": "2.50.0", "description": "ESLint plugin for Svelte using AST", "repository": "git+https://github.com/sveltejs/eslint-plugin-svelte.git", diff --git a/packages/eslint-plugin-svelte/src/meta.ts b/packages/eslint-plugin-svelte/src/meta.ts index 86914c0b7..01ea183f2 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-albert'; +export const name = 'eslint-plugin-svelte'; export const version = '2.50.0'; diff --git a/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts b/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts index e58445f7c..e230a78ca 100644 --- a/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts +++ b/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts @@ -1,11 +1,15 @@ +/* eslint-disable @eslint-community/eslint-comments/require-description */ import type { AST } from 'svelte-eslint-parser'; import { createRule } from '../utils'; -import { TypeFlags } from 'typescript'; -import type { TSESTree } from '@typescript-eslint/types'; -import type { TS, TSTools } from '../utils/ts-utils'; -import { getTypeName, getTypeScriptTools } from '../utils/ts-utils'; -import { findVariable } from '../utils/ast-utils'; +import ts, { TypeFlags } from 'typescript'; +import { getScope } from '../utils/ast-utils'; import type { RuleContext } from '../types'; +import type { TSESTree } from '@typescript-eslint/utils'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports -- ignore +import { ESLintUtils } from '@typescript-eslint/utils'; +import type { ParserServicesWithTypeInformation } from '@typescript-eslint/parser'; +import type { TS } from '../utils/ts-utils'; +import { getInnermostScope } from '@eslint-community/eslint-utils'; const props = { allowBoolean: { @@ -38,16 +42,30 @@ type Props = { allowUndefined: boolean; }; -enum NodeType { - Unknown, - Allowed -} - type Config = { stringTemplateExpressions?: Props; textExpressions?: Props; } & Props; +function findVariable(context: RuleContext, node: TSESTree.Identifier): Variable | null { + const initialScope = getInnermostScope(context, node) + const variable = eslintUtils.findVariable(initialScope, node); + if (variable) { + return variable; + } + if (!node.name.startsWith('$')) { + return variable; + } + // Remove the $ and search for the variable again, as it may be a store access variable. + return eslintUtils.findVariable(initialScope, node.name.slice(1)); +} + +function print_node(node: TSESTree.Node, services: ParserServicesWithTypeInformation) { + const checker = services.program.getTypeChecker(); + const type = services.getTypeAtLocation(node); + console.log(checker.typeToString(type)); +} + export default createRule('restrict-mustache-expressions', { meta: { docs: { @@ -77,10 +95,9 @@ export default createRule('restrict-mustache-expressions', { type: 'problem' }, create(context) { - const tools = getTypeScriptTools(context); - if (!tools) { - return {}; - } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const services = ESLintUtils.getParserServices(context as any); + const checker = services.program.getTypeChecker(); const config: Config = Object.assign(getDefaultOptions(), context.options[0] || {}); @@ -113,13 +130,18 @@ export default createRule('restrict-mustache-expressions', { if (allowNull) allowed_types.add('null'); if (allowUndefined) allowed_types.add('undefined'); - const type = disallowed_expression(node.expression, allowed_types, context, tools!); - if (NodeType.Allowed === type) return; + print_node(node.expression as any, services); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const type = compute_expression_type(node.expression as any, context, services); + + if (type !== null && is_allowed_type(type, allowed_types, context, services)) return; + context.report({ node, messageId: 'expectedStringifyableType', data: { - disallowed: type === NodeType.Unknown ? 'unknown' : getTypeName(type, tools!), + disallowed: type === null ? 'unknown' : checker.typeToString(type), types: [...allowed_types].map((t) => `\`${t}\``).join(', ') } }); @@ -131,57 +153,83 @@ export default createRule('restrict-mustache-expressions', { } }); -function getNodeType(node: TSESTree.Node, tools: TSTools): TS.Type | NodeType.Unknown { - const checker = tools.service.program.getTypeChecker(); - const ts_node = tools.service.esTreeNodeToTSNodeMap.get(node); - if (!ts_node) return NodeType.Unknown; - const nodeType = checker.getTypeAtLocation(ts_node); - const constrained = checker.getBaseConstraintOfType(nodeType); - return constrained ?? nodeType; +function get_node_type( + node: TSESTree.Node, + services: ParserServicesWithTypeInformation +): TS.Type | null { + const checker = services.program.getTypeChecker(); + const type = services.getTypeAtLocation(node); + return checker.getBaseConstraintOfType(type) ?? type; } -function disallowed_identifier( +function compute_identifier_type( expression: TSESTree.Identifier, - allowed_types: Set, context: RuleContext, - tools: TSTools -): TS.Type | NodeType { - const type = get_variable_type(expression, context, tools); - - if (type === NodeType.Unknown) return NodeType.Unknown; - - return disallowed_type(type, allowed_types, context, tools); + services: ParserServicesWithTypeInformation +): TS.Type | null { + return get_variable_type(expression, context, services); } function get_variable_type( identifier: TSESTree.Identifier, context: RuleContext, - tools: TSTools -): TS.Type | NodeType.Unknown { + services: ParserServicesWithTypeInformation +): TS.Type | null { const variable = findVariable(context, identifier); + const checker = services.program.getTypeChecker(); + + if (variable) { + const identifierNode = variable.identifiers[0]; + const type_node = services.getTypeAtLocation(identifier); + const constrained = checker.getBaseConstraintOfType(type_node); + // const type = checker.getTypeFromTypeNode(; + // console.log(variable.name); + // console.log('Type from annotation', checker.typeToString(type_node)); + // console.log('Constrained type', constrained ? checker.typeToString(constrained) : null); + // console.log('-------'); + return type_node; + } + + const type = get_node_type(identifier, services); + // console.log('Type from node:', type ? checker.typeToString(type) : null); + return type; - const identifiers = variable?.identifiers[0]; + // if (variable?.name === 'side') { + // console.log('Variable', variable); - if (!identifiers) return getNodeType(identifier, tools); + // // checker. + // // const def = variable.defs[0]; + // // console.log(variable.identifiers[0].typeAnnotation?.typeAnnotation); + // const type = get_node_type(variable.identifiers[0], services; + // if (!type) return null; + // console.log(checker.getApparentType(type)); + // // console.log('NODE TYPE', type); + // // if (!type) return null; + // // console.log('TYPE STRING:', checker.typeToString(type)); + // } + // const identifiers = variable?.identifiers[0]; - const type = getNodeType(variable.identifiers[0], tools); + // if (!identifiers) return get_node_type(identifier, services; - if (NodeType.Unknown === type) return NodeType.Unknown; + // const type = get_node_type(variable.identifiers[0], services; - return narrow_variable_type(identifier, type, tools); + // if (type === null) return null; + + // return narrow_variable_type(identifier, type, services; } function narrow_variable_type( identifier: TSESTree.Identifier, type: TS.Type, - tools: TSTools + services: ParserServicesWithTypeInformation ): TS.Type { - const checker = tools.service.program.getTypeChecker(); + const checker = services.program.getTypeChecker(); let currentNode: TSESTree.Node | AST.SvelteNode | undefined = identifier as TSESTree.Node; while (currentNode) { if (currentNode.type === 'SvelteIfBlock') { const condition = currentNode.expression; + // TODO: other cases of conditionals if (condition.type === 'Identifier' && condition.name === identifier.name) { return checker.getNonNullableType(type); } @@ -192,150 +240,159 @@ function narrow_variable_type( return type; } -function disallowed_type( +function is_allowed_type( type: TS.Type, allowed_types: Set, context: RuleContext, - tools: TSTools -): TS.Type | NodeType { - if (type.flags & TypeFlags.StringLike) { - return NodeType.Allowed; - } + services: ParserServicesWithTypeInformation +): boolean { + if (type.flags & TypeFlags.StringLike) return true; if (type.flags & TypeFlags.BooleanLike) { - return allowed_types.has('boolean') ? NodeType.Allowed : type; + return allowed_types.has('boolean'); } if (type.flags & TypeFlags.NumberLike) { - return allowed_types.has('number') ? NodeType.Allowed : type; + return allowed_types.has('number'); } if (type.flags & TypeFlags.Null) { - return allowed_types.has('null') ? NodeType.Allowed : type; + return allowed_types.has('null'); } if (type.flags & TypeFlags.Undefined) { - return allowed_types.has('undefined') ? NodeType.Allowed : type; + return allowed_types.has('undefined'); } if (type.isUnion()) { for (const sub_type of type.types) { - const disallowed = disallowed_type(sub_type, allowed_types, context, tools); - if (disallowed) { - return disallowed; - } + if (!is_allowed_type(sub_type, allowed_types, context, services)) return false; } - return NodeType.Allowed; + return true; } - return type; + return false; } -function disallowed_literal( +function compute_literal_type( expression: TSESTree.Literal, - allowed_types: Set, + services: ParserServicesWithTypeInformation +): TS.Type | null { + return get_node_type(expression, services); +} + +function compute_logical_expression_type( + expression: TSESTree.LogicalExpression, + services: ParserServicesWithTypeInformation +): TS.Type | null { + // TODO: Do we need to check the type of the left and right expressions more? + return get_node_type(expression, services); +} + +function compute_member_expression_type( + expression: TSESTree.MemberExpression, context: RuleContext, - tools: TSTools -): TS.Type | NodeType { - const type = getNodeType(expression, tools); + services: ParserServicesWithTypeInformation +): TS.Type | null { + const object_type = compute_expression_type(expression.object, context, services); + if (!object_type) return null; + + if (expression.computed) { + const property_type = compute_expression_type(expression.property, context, services); + if (property_type === null) return null; + return compute_property_return_type(object_type, property_type, services); + } - if (NodeType.Unknown === type) return NodeType.Unknown; + return compute_property(object_type, expression.property.name, services); +} - return disallowed_type(type, allowed_types, context, tools); +function compute_property( + object_type: TS.Type, + property_name: string, + services: ParserServicesWithTypeInformation +): TS.Type | null { + const checker = services.program.getTypeChecker(); + const symbol = checker.getPropertyOfType(object_type, property_name); + return symbol ? checker.getTypeOfSymbol(symbol) : null; } -function disallowed_expression( +function compute_expression_type( expression: TSESTree.Expression, - allowed_types: Set, context: RuleContext, - tools: TSTools -): TS.Type | NodeType { + services: ParserServicesWithTypeInformation +): TS.Type | null { switch (expression.type) { - case 'Literal': - return disallowed_literal(expression, allowed_types, context, tools); case 'Identifier': - return disallowed_identifier(expression, allowed_types, context, tools); - case 'ArrayExpression': - return getNodeType(expression, tools); + return compute_identifier_type(expression, context, services); + case 'Literal': + return compute_literal_type(expression, services); case 'MemberExpression': - return disallowed_member_expression(expression, allowed_types, context, tools); + return compute_member_expression_type(expression, context, services); + case 'ArrayExpression': + return compute_array_expression_type(expression, context, services); case 'LogicalExpression': - return disallowed_logical_expression(expression, allowed_types, context, tools); - default: { - const type = getNodeType(expression, tools); - if (NodeType.Unknown === type) return NodeType.Unknown; - return disallowed_type(type, allowed_types, context, tools); - } + return compute_logical_expression_type(expression, services); + case 'ConditionalExpression': + return compute_conditional_expression_type(expression, context, services); + default: + return null; } } -function disallowed_logical_expression( - expression: TSESTree.LogicalExpression, - allowed_types: Set, - context: RuleContext, - tools: TSTools -): TS.Type | NodeType { - const type = getNodeType(expression, tools); - - if (NodeType.Unknown === type) return NodeType.Unknown; +function create_union_type( + types: TS.Type[], + services: ParserServicesWithTypeInformation +): TS.Type | null { + const checker = services.program.getTypeChecker(); - return disallowed_type(type, allowed_types, context, tools); -} + const new_types: TS.TypeNode[] = []; -function disallowed_member_expression( - expression: TSESTree.MemberExpression, - allowed_types: Set, - context: RuleContext, - tools: TSTools -): TS.Type | NodeType { - const checker = tools.service.program.getTypeChecker(); - let objectType: TS.Type | NodeType = getNodeType(expression.object, tools); - - if (NodeType.Unknown === objectType) return NodeType.Unknown; - - // Handle nested member expressions - if (expression.object.type === 'MemberExpression') { - const nestedType = disallowed_member_expression( - expression.object, - allowed_types, - context, - tools - ); - if (nestedType) objectType = nestedType; + for (const type of types) { + const node = checker.typeToTypeNode(type, undefined, ts.NodeBuilderFlags.NoTruncation); + if (!node) return null; + new_types.push(node); } - if (NodeType.Allowed === objectType) return NodeType.Allowed; - - // Handle identifiers (variables) - if (expression.object.type === 'Identifier') { - const variable = findVariable(context, expression.object); - if (variable && variable.defs[0]?.node.type === 'VariableDeclarator') { - const initNode = variable.defs[0].node.init; - if (initNode) { - const initType = getNodeType(initNode, tools); - if (initType) objectType = initType; - } - } - } + const union_type = ts.factory.createUnionTypeNode(new_types); - // Get property type - const propertyName = getPropertyName(expression.property); - if (!propertyName) return objectType; + return checker.getTypeFromTypeNode(union_type); +} - // Try to get property type using getPropertyOfType - const symbol = checker.getPropertyOfType(objectType, propertyName); - if (symbol) { - const property_type = checker.getTypeOfSymbol(symbol); - return disallowed_type(property_type, allowed_types, context, tools); +function compute_property_return_type( + object_type: TS.Type, + property_type: TS.Type, + services: ParserServicesWithTypeInformation +): TS.Type | null { + // const checker = tools.service.program.getTypeChecker(); + // // print the raw source code of the property type + // console.log('Property type', checker.typeToString(property_type)); + + if (property_type.isStringLiteral()) { + return compute_property(object_type, property_type.value, services); + } else if (property_type.isNumberLiteral()) { + const number_index_type = property_type.getNumberIndexType(); + if (number_index_type === undefined) return null; + console.log('Number index type', number_index_type); + return compute_property_return_type(object_type, number_index_type, services); + } else if (property_type.isUnion()) { + const types: TS.Type[] = []; + for (const type of property_type.types) { + const subtype = compute_property_return_type(object_type, type, services); + if (subtype === null) return null; + types.push(subtype); + } + return create_union_type([...types], services); } + return null; +} - return NodeType.Unknown; +function compute_array_expression_type( + expression: TSESTree.ArrayExpression, + context: RuleContext, + services: ParserServicesWithTypeInformation +): TS.Type | null { + return get_node_type(expression, services); } -function getPropertyName( - property: TSESTree.Expression | TSESTree.PrivateIdentifier -): string | undefined { - if (property.type === 'Identifier') { - return property.name; - } else if (property.type === 'Literal' && typeof property.value === 'string') { - return property.value; - } else if (property.type === 'TemplateLiteral' && property.quasis.length === 1) { - // return property.quasis[0].value.cooked; - } - return undefined; +function compute_conditional_expression_type( + expression: TSESTree.ConditionalExpression, + context: RuleContext, + services: ParserServicesWithTypeInformation +): TS.Type | null { + return get_node_type(expression, services); } diff --git a/packages/eslint-plugin-svelte/src/types-for-node.ts b/packages/eslint-plugin-svelte/src/types-for-node.ts index 866ea6dab..953fb7457 100644 --- a/packages/eslint-plugin-svelte/src/types-for-node.ts +++ b/packages/eslint-plugin-svelte/src/types-for-node.ts @@ -136,6 +136,7 @@ export type ASTNodeListener = { node: TSESTree.TSEmptyBodyFunctionExpression & ASTNodeWithParent ) => void; TSEnumDeclaration?: (node: TSESTree.TSEnumDeclaration & ASTNodeWithParent) => void; + TSEnumBody?: (node: TSESTree.TSEnumBody & ASTNodeWithParent) => void; TSEnumMember?: (node: TSESTree.TSEnumMember & ASTNodeWithParent) => void; TSExportAssignment?: (node: TSESTree.TSExportAssignment & ASTNodeWithParent) => void; TSExportKeyword?: (node: TSESTree.TSExportKeyword & ASTNodeWithParent) => void; @@ -355,6 +356,7 @@ export type TSNodeListener = { node: TSESTree.TSEmptyBodyFunctionExpression & ASTNodeWithParent ) => void; TSEnumDeclaration?: (node: TSESTree.TSEnumDeclaration & ASTNodeWithParent) => void; + TSEnumBody?: (node: TSESTree.TSEnumBody & ASTNodeWithParent) => void; TSEnumMember?: (node: TSESTree.TSEnumMember & ASTNodeWithParent) => void; TSExportAssignment?: (node: TSESTree.TSExportAssignment & ASTNodeWithParent) => void; TSExportKeyword?: (node: TSESTree.TSExportKeyword & ASTNodeWithParent) => void; diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/index-access/index-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/index-access/index-input.svelte new file mode 100644 index 000000000..885799ef4 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/index-access/index-input.svelte @@ -0,0 +1,13 @@ + +{ side } +{foo[side]} +{foo["left"]} +{foo.left} \ No newline at end of file diff --git a/packages/eslint-plugin-svelte/tests/src/rules/restrict-mustache-expressions.ts b/packages/eslint-plugin-svelte/tests/src/rules/restrict-mustache-expressions.ts index a3482b9b0..1b29b11ef 100644 --- a/packages/eslint-plugin-svelte/tests/src/rules/restrict-mustache-expressions.ts +++ b/packages/eslint-plugin-svelte/tests/src/rules/restrict-mustache-expressions.ts @@ -1,11 +1,17 @@ import { RuleTester } from '../../utils/eslint-compat'; import rule from '../../../src/rules/restrict-mustache-expressions'; -import { loadTestCases } from '../../utils/utils'; +import { loadTestCases, RULES_PROJECT } from '../../utils/utils'; const tester = new RuleTester({ languageOptions: { + parser: "@typescript-eslint/parser", ecmaVersion: 2020, sourceType: 'module', + parserOptions: { + // parser: '@typescript-eslint/parser', + projectService: true, + project: RULES_PROJECT, + } }, });