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 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..d1d2f0ee3 --- /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; + // eg: {bar} + 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; + }; + // eg: foo + 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/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 a341ec478..f4915e0ab 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", + "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", @@ -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/meta.ts b/packages/eslint-plugin-svelte/src/meta.ts index 4619d5534..01ea183f2 100644 --- a/packages/eslint-plugin-svelte/src/meta.ts +++ b/packages/eslint-plugin-svelte/src/meta.ts @@ -2,4 +2,4 @@ // 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 version = '2.50.0'; 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..e230a78ca --- /dev/null +++ b/packages/eslint-plugin-svelte/src/rules/restrict-mustache-expressions.ts @@ -0,0 +1,398 @@ +/* eslint-disable @eslint-community/eslint-comments/require-description */ +import type { AST } from 'svelte-eslint-parser'; +import { createRule } from '../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: { + 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; + +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: { + 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 `{{disallowed}}` to be one of the following: {{types}}. You must cast or convert the expression to one of the allowed types.' + }, + type: 'problem' + }, + create(context) { + // 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] || {}); + + 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 + // (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 + opts = config?.stringTemplateExpressions + ? Object.assign(getDefaultOptions(), config.stringTemplateExpressions) + : config; + } 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; + } + + const { allowBoolean, allowNull, allowUndefined, allowNumber } = opts; + 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'); + + 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 === null ? 'unknown' : checker.typeToString(type), + types: [...allowed_types].map((t) => `\`${t}\``).join(', ') + } + }); + } + + return { + SvelteMustacheTag: checkMustacheExpression + }; + } +}); + +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 compute_identifier_type( + expression: TSESTree.Identifier, + context: RuleContext, + services: ParserServicesWithTypeInformation +): TS.Type | null { + return get_variable_type(expression, context, services); +} + +function get_variable_type( + identifier: TSESTree.Identifier, + context: RuleContext, + 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; + + // if (variable?.name === 'side') { + // console.log('Variable', variable); + + // // 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]; + + // if (!identifiers) return get_node_type(identifier, services; + + // const type = get_node_type(variable.identifiers[0], services; + + // if (type === null) return null; + + // return narrow_variable_type(identifier, type, services; +} + +function narrow_variable_type( + identifier: TSESTree.Identifier, + type: TS.Type, + services: ParserServicesWithTypeInformation +): TS.Type { + 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); + } + } + currentNode = currentNode.parent as TSESTree.Node | AST.SvelteNode; + } + + return type; +} + +function is_allowed_type( + type: TS.Type, + allowed_types: Set, + context: RuleContext, + services: ParserServicesWithTypeInformation +): 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 (!is_allowed_type(sub_type, allowed_types, context, services)) return false; + } + return true; + } + + return false; +} + +function compute_literal_type( + expression: TSESTree.Literal, + 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, + 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); + } + + return compute_property(object_type, expression.property.name, services); +} + +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 compute_expression_type( + expression: TSESTree.Expression, + context: RuleContext, + services: ParserServicesWithTypeInformation +): TS.Type | null { + switch (expression.type) { + case 'Identifier': + return compute_identifier_type(expression, context, services); + case 'Literal': + return compute_literal_type(expression, services); + case 'MemberExpression': + return compute_member_expression_type(expression, context, services); + case 'ArrayExpression': + return compute_array_expression_type(expression, context, services); + case 'LogicalExpression': + return compute_logical_expression_type(expression, services); + case 'ConditionalExpression': + return compute_conditional_expression_type(expression, context, services); + default: + return null; + } +} + +function create_union_type( + types: TS.Type[], + services: ParserServicesWithTypeInformation +): TS.Type | null { + const checker = services.program.getTypeChecker(); + + const new_types: TS.TypeNode[] = []; + + for (const type of types) { + const node = checker.typeToTypeNode(type, undefined, ts.NodeBuilderFlags.NoTruncation); + if (!node) return null; + new_types.push(node); + } + + const union_type = ts.factory.createUnionTypeNode(new_types); + + return checker.getTypeFromTypeNode(union_type); +} + +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; +} + +function compute_array_expression_type( + expression: TSESTree.ArrayExpression, + context: RuleContext, + services: ParserServicesWithTypeInformation +): TS.Type | null { + return get_node_type(expression, services); +} + +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/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/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/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/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..d5575f817 --- /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..c4bec53b2 --- /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..e8ef0168e --- /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..127c94e2a --- /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..21fde401b --- /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 `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: 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..4333a26fb --- /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..a83d973a9 --- /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..8f2fe4a22 --- /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`, `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 + 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..cbe7dd441 --- /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..b390fda51 --- /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`, `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 + 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..2c284b1d0 --- /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..5975884ca --- /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..9a205ee8a --- /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/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/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 new file mode 100644 index 000000000..eae0fad1a --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/ignore-style-directive/ignore-style-directive.svelte @@ -0,0 +1,4 @@ + + \ No newline at end of file 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/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..a48d67f15 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/restrict-mustache-expressions/valid/object-access/object-access-input.svelte @@ -0,0 +1,12 @@ + +{foo.bar} +{foo.a.b.c} \ 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/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 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..1b29b11ef --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/src/rules/restrict-mustache-expressions.ts @@ -0,0 +1,18 @@ +import { RuleTester } from '../../utils/eslint-compat'; +import rule from '../../../src/rules/restrict-mustache-expressions'; +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, + } + }, +}); + +tester.run('restrict-mustache-expressions', rule as any, loadTestCases('restrict-mustache-expressions'));