From f8b9a472dc5e3c316dc890df9e872468aecf8784 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Fri, 23 Sep 2022 09:55:51 +0900 Subject: [PATCH 1/6] feat: wpi no-unnecessary-condition --- .eslintignore | 1 + README.md | 3 +- docs/rules.md | 3 +- .../no-unnecessary-condition.md | 57 + .../no-unnecessary-condition.ts | 788 +++++++++ src/types.ts | 14 +- src/utils/rules.ts | 2 + src/utils/ts-utils/index.ts | 348 ++++ .../invalid/binary-expression01-errors.yaml | 4 + .../invalid/binary-expression01-input.svelte | 6 + .../invalid/binary-expression01-output.svelte | 6 + .../invalid/nullish-coalescing01-errors.yaml | 5 + .../invalid/nullish-coalescing01-input.svelte | 7 + .../nullish-coalescing01-output.svelte | 7 + .../invalid/optional-chaining01-errors.yaml | 4 + .../invalid/optional-chaining01-input.svelte | 6 + .../invalid/optional-chaining01-output.svelte | 6 + .../invalid/test01-errors.yaml | 4 + .../invalid/test01-input.svelte | 6 + .../invalid/test01-output.svelte | 6 + .../valid/reactive-statement01-input.svelte | 6 + .../valid/template01-input.svelte | 15 + tests/fixtures/rules/tsconfig.json | 11 + .../no-unnecessary-condition.ts | 21 + .../original-tests/RuleTester.ts | 15 + .../original-tests/fixtures/file.ts | 0 .../original-tests/fixtures/react.tsx | 0 .../original-tests/fixtures/tsconfig.json | 12 + .../original-tests/fixtures/unstrict/file.ts | 0 .../fixtures/unstrict/react.tsx | 0 .../fixtures/unstrict/tsconfig.json | 12 + .../no-unnecessary-condition.ts | 1544 +++++++++++++++++ tests/utils/utils.ts | 41 +- tools/lib/load-rules.ts | 29 +- tools/new-rule.ts | 35 +- tools/render-rules.ts | 2 +- tools/update-docs.ts | 6 +- tools/update-rules.ts | 15 +- 38 files changed, 3019 insertions(+), 28 deletions(-) create mode 100644 docs/rules/@typescript-eslint/no-unnecessary-condition.md create mode 100644 src/rules/@typescript-eslint/no-unnecessary-condition.ts create mode 100644 src/utils/ts-utils/index.ts create mode 100644 tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/binary-expression01-errors.yaml create mode 100644 tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/binary-expression01-input.svelte create mode 100644 tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/binary-expression01-output.svelte create mode 100644 tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/nullish-coalescing01-errors.yaml create mode 100644 tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/nullish-coalescing01-input.svelte create mode 100644 tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/nullish-coalescing01-output.svelte create mode 100644 tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/optional-chaining01-errors.yaml create mode 100644 tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/optional-chaining01-input.svelte create mode 100644 tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/optional-chaining01-output.svelte create mode 100644 tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/test01-errors.yaml create mode 100644 tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/test01-input.svelte create mode 100644 tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/test01-output.svelte create mode 100644 tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/valid/reactive-statement01-input.svelte create mode 100644 tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/valid/template01-input.svelte create mode 100644 tests/fixtures/rules/tsconfig.json create mode 100644 tests/src/rules/@typescript-eslint/no-unnecessary-condition.ts create mode 100644 tests/src/rules/@typescript-eslint/original-tests/RuleTester.ts create mode 100644 tests/src/rules/@typescript-eslint/original-tests/fixtures/file.ts create mode 100644 tests/src/rules/@typescript-eslint/original-tests/fixtures/react.tsx create mode 100644 tests/src/rules/@typescript-eslint/original-tests/fixtures/tsconfig.json create mode 100644 tests/src/rules/@typescript-eslint/original-tests/fixtures/unstrict/file.ts create mode 100644 tests/src/rules/@typescript-eslint/original-tests/fixtures/unstrict/react.tsx create mode 100644 tests/src/rules/@typescript-eslint/original-tests/fixtures/unstrict/tsconfig.json create mode 100644 tests/src/rules/@typescript-eslint/original-tests/no-unnecessary-condition.ts diff --git a/.eslintignore b/.eslintignore index 2791e998b..c825251d6 100644 --- a/.eslintignore +++ b/.eslintignore @@ -11,6 +11,7 @@ /tests/fixtures/rules/valid-compile/valid/babel /tests/fixtures/rules/valid-compile/valid/ts /tests/fixtures/rules/prefer-style-directive +/tests/fixtures/rules/@typescript-eslint /.svelte-kit /svelte.config-dist.js /build diff --git a/README.md b/README.md index 8137b77d9..20aff841a 100644 --- a/README.md +++ b/README.md @@ -322,10 +322,11 @@ These rules relate to style guidelines, and are therefore quite subjective: ## Extension Rules -These rules extend the rules provided by ESLint itself to work well in Svelte: +These rules extend the rules provided by ESLint itself, or other plugins to work well in Svelte: | Rule ID | Description | | |:--------|:------------|:---| +| [svelte/@typescript-eslint/no-unnecessary-condition](https://ota-meshi.github.io/eslint-plugin-svelte/rules/@typescript-eslint/no-unnecessary-condition/) | disallow conditionals where the type is always truthy or always falsy | :wrench: | | [svelte/no-inner-declarations](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-inner-declarations/) | disallow variable or `function` declarations in nested blocks | :star: | | [svelte/no-trailing-spaces](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-trailing-spaces/) | disallow trailing whitespace at the end of lines | :wrench: | diff --git a/docs/rules.md b/docs/rules.md index 153cd880c..2a5fb4ebc 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -75,10 +75,11 @@ These rules relate to style guidelines, and are therefore quite subjective: ## Extension Rules -These rules extend the rules provided by ESLint itself to work well in Svelte: +These rules extend the rules provided by ESLint itself, or other plugins to work well in Svelte: | Rule ID | Description | | |:--------|:------------|:---| +| [svelte/@typescript-eslint/no-unnecessary-condition](./rules/@typescript-eslint/no-unnecessary-condition.md) | disallow conditionals where the type is always truthy or always falsy | :wrench: | | [svelte/no-inner-declarations](./rules/no-inner-declarations.md) | disallow variable or `function` declarations in nested blocks | :star: | | [svelte/no-trailing-spaces](./rules/no-trailing-spaces.md) | disallow trailing whitespace at the end of lines | :wrench: | diff --git a/docs/rules/@typescript-eslint/no-unnecessary-condition.md b/docs/rules/@typescript-eslint/no-unnecessary-condition.md new file mode 100644 index 000000000..479693242 --- /dev/null +++ b/docs/rules/@typescript-eslint/no-unnecessary-condition.md @@ -0,0 +1,57 @@ +--- +pageClass: "rule-details" +sidebarDepth: 0 +title: "svelte/@typescript-eslint/no-unnecessary-condition" +description: "disallow conditionals where the type is always truthy or always falsy" +--- + +# svelte/@typescript-eslint/no-unnecessary-condition + +> disallow conditionals where the type is always truthy or always falsy + +- :exclamation: **_This rule has not been released yet._** +- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule. + +## :book: Rule Details + +This rule reports ???. + + + + + +```svelte + + + + + + +``` + + + +## :wrench: Options + +```json +{ + "svelte/@typescript-eslint/no-unnecessary-condition": ["error", { + + }] +} +``` + +- + +## :books: Further Reading + +- + +## :mag: Implementation + +- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/@typescript-eslint/no-unnecessary-condition.ts) +- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/@typescript-eslint/no-unnecessary-condition.ts) + +Taken with ❤️ [from @typescript-eslint/eslint-plugin](https://typescript-eslint.io/rules/no-unnecessary-condition/) diff --git a/src/rules/@typescript-eslint/no-unnecessary-condition.ts b/src/rules/@typescript-eslint/no-unnecessary-condition.ts new file mode 100644 index 000000000..28fcb6aa3 --- /dev/null +++ b/src/rules/@typescript-eslint/no-unnecessary-condition.ts @@ -0,0 +1,788 @@ +// This rule is based on typescript-eslint's no-unnecessary-condition rule +// and modified to work well with Svelte components. +// https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +import type { TSESTree } from "@typescript-eslint/types" +import type { AST } from "svelte-eslint-parser" +import type * as ESTree from "estree" +import { createRule } from "../../utils" +import { + isFalsyType, + getConstrainedTypeAtLocation, + isTruthyLiteral, + isPossiblyFalsyType, + isNullishType, + isBooleanLiteralType, + getTypeScriptTools, + isAnyType, + isUnknownType, + isNeverType, + getCallSignaturesOfType, + isNullableType, + getTypeOfPropertyOfType, + getTypeName, + isTupleType, +} from "../../utils/ts-utils" +import type { TS, TSTools } from "../../utils/ts-utils" + +/** + * Returns all types of a union type or an array containing `type` itself if it's no union type. + * This method is heavily inspired by tsutils. https://github.com/ajafff/tsutils + * The MIT License (MIT) Copyright (c) 2017 Klaus Meinhardt + * https://github.com/ajafff/tsutils/blob/master/LICENSE + */ +function unionTypeParts(type: TS.Type): TS.Type[] { + return [...iterate(type)] + + /** + * iterate + */ + function* iterate(t: TS.Type): Iterable { + if (t.isUnion()) { + for (const type of t.types) { + yield* iterate(type) + } + } else { + yield t + } + } +} + +/** + * Check whether the given type can be a falsy type or not. + */ +function isPossiblyFalsy(type: TS.Type, tsTools: TSTools): boolean { + return ( + unionTypeParts(type) + // PossiblyFalsy flag includes literal values, so exclude ones that + // are definitely truthy + .filter((t) => !isTruthyLiteral(t, tsTools)) + .some((type) => isPossiblyFalsyType(type, tsTools.ts)) + ) +} + +/** + * Check whether the given type can be a truthy type or not. + */ +function isPossiblyTruthy(type: TS.Type, tsTools: TSTools): boolean { + return unionTypeParts(type).some((type) => !isFalsyType(type, tsTools)) +} + +/** + * Check whether the given type can be a nullish type or not. + */ +function isPossiblyNullish(type: TS.Type, tsTools: TSTools): boolean { + return isNullableType(type, tsTools.ts) +} + +/** + * Check whether the given type is a nullish type or not. + */ +function isAlwaysNullish(type: TS.Type, tsTools: TSTools): boolean { + return isNullishType(type, tsTools.ts) +} + +/** + * Check whether the given type is a literal type or not. + */ +function isLiteral(type: TS.Type, tsTools: TSTools): boolean { + return ( + isBooleanLiteralType(type, tsTools.ts) || + isNullishType(type, tsTools.ts) || + type.isLiteral() + ) +} + +export default createRule("@typescript-eslint/no-unnecessary-condition", { + meta: { + docs: { + description: + "disallow conditionals where the type is always truthy or always falsy", + category: "Extension Rules", + recommended: false, + extensionRule: { + plugin: "@typescript-eslint/eslint-plugin", + url: "https://typescript-eslint.io/rules/no-unnecessary-condition/", + }, + }, + schema: [ + { + type: "object", + properties: { + allowConstantLoopConditions: { + description: + "Whether to ignore constant loop conditions, such as `while (true)`.", + type: "boolean", + }, + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: { + description: + "Whether to not error when running with a tsconfig that has strictNullChecks turned.", + type: "boolean", + }, + }, + additionalProperties: false, + }, + ], + fixable: "code", + messages: { + alwaysTruthy: "Unnecessary conditional, value is always truthy.", + alwaysFalsy: "Unnecessary conditional, value is always falsy.", + alwaysTruthyFunc: + "This callback should return a conditional, but return is always truthy.", + alwaysFalsyFunc: + "This callback should return a conditional, but return is always falsy.", + neverNullish: + "Unnecessary conditional, expected left-hand side of `??` operator to be possibly null or undefined.", + alwaysNullish: + "Unnecessary conditional, left-hand side of `??` operator is always `null` or `undefined`.", + literalBooleanExpression: + "Unnecessary conditional, both sides of the expression are literal values.", + noOverlapBooleanExpression: + "Unnecessary conditional, the types have no overlap.", + never: "Unnecessary conditional, value is `never`.", + neverOptionalChain: "Unnecessary optional chain on a non-nullish value.", + noStrictNullCheck: + "This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.", + }, + type: "suggestion", // "problem", or "layout", + }, + create(context) { + const { + allowConstantLoopConditions = false, + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing = false, + } = (context.options[0] || {}) as { + allowConstantLoopConditions?: boolean + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean + } + const tools = getTypeScriptTools(context) + if (!tools) { + return {} + } + + const { service, ts } = tools + const checker = service.program.getTypeChecker() + const sourceCode = context.getSourceCode() + const compilerOptions = service.program.getCompilerOptions() + const isStrictNullChecks = compilerOptions.strict + ? compilerOptions.strictNullChecks !== false + : compilerOptions.strictNullChecks + + if ( + !isStrictNullChecks && + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing !== true + ) { + context.report({ + loc: { + start: { line: 0, column: 0 }, + end: { line: 0, column: 0 }, + }, + messageId: "noStrictNullCheck", + }) + } + + const mutableVarReferenceIds: ESTree.Identifier[] = [] + const scriptElements: AST.SvelteScriptElement[] = [] + let inSvelteReactiveStatement = false + + // Extract references to mutable variables in the root scope. + for (const scope of [ + sourceCode.scopeManager.globalScope, + sourceCode.scopeManager.globalScope?.childScopes.find( + (scope) => scope.type === "module", + ), + ]) { + if (!scope) continue + for (const variable of scope.variables) { + if ( + variable.defs.some( + (def) => + def.type === "Variable" && + (def.parent.kind === "var" || def.parent.kind === "let"), + ) + ) { + for (const reference of variable.references) { + mutableVarReferenceIds.push(reference.identifier) + } + } + } + } + + // Extract diff --git a/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/binary-expression01-output.svelte b/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/binary-expression01-output.svelte new file mode 100644 index 000000000..fb58d3623 --- /dev/null +++ b/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/binary-expression01-output.svelte @@ -0,0 +1,6 @@ + diff --git a/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/nullish-coalescing01-errors.yaml b/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/nullish-coalescing01-errors.yaml new file mode 100644 index 000000000..1f9a0fec4 --- /dev/null +++ b/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/nullish-coalescing01-errors.yaml @@ -0,0 +1,5 @@ +- message: Unnecessary conditional, left-hand side of `??` operator is always + `null` or `undefined`. + line: 5 + column: 13 + suggestions: null diff --git a/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/nullish-coalescing01-input.svelte b/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/nullish-coalescing01-input.svelte new file mode 100644 index 000000000..5988e2af9 --- /dev/null +++ b/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/nullish-coalescing01-input.svelte @@ -0,0 +1,7 @@ + diff --git a/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/nullish-coalescing01-output.svelte b/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/nullish-coalescing01-output.svelte new file mode 100644 index 000000000..5988e2af9 --- /dev/null +++ b/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/nullish-coalescing01-output.svelte @@ -0,0 +1,7 @@ + diff --git a/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/optional-chaining01-errors.yaml b/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/optional-chaining01-errors.yaml new file mode 100644 index 000000000..a1c26eb0d --- /dev/null +++ b/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/optional-chaining01-errors.yaml @@ -0,0 +1,4 @@ +- message: Unnecessary optional chain on a non-nullish value. + line: 4 + column: 16 + suggestions: null diff --git a/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/optional-chaining01-input.svelte b/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/optional-chaining01-input.svelte new file mode 100644 index 000000000..516d3d9fa --- /dev/null +++ b/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/optional-chaining01-input.svelte @@ -0,0 +1,6 @@ + diff --git a/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/optional-chaining01-output.svelte b/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/optional-chaining01-output.svelte new file mode 100644 index 000000000..78f2f5ae6 --- /dev/null +++ b/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/optional-chaining01-output.svelte @@ -0,0 +1,6 @@ + diff --git a/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/test01-errors.yaml b/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/test01-errors.yaml new file mode 100644 index 000000000..5dfc72094 --- /dev/null +++ b/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/test01-errors.yaml @@ -0,0 +1,4 @@ +- message: Unnecessary conditional, value is always truthy. + line: 5 + column: 14 + suggestions: null diff --git a/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/test01-input.svelte b/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/test01-input.svelte new file mode 100644 index 000000000..c052b5103 --- /dev/null +++ b/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/test01-input.svelte @@ -0,0 +1,6 @@ + diff --git a/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/test01-output.svelte b/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/test01-output.svelte new file mode 100644 index 000000000..c052b5103 --- /dev/null +++ b/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/invalid/test01-output.svelte @@ -0,0 +1,6 @@ + diff --git a/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/valid/reactive-statement01-input.svelte b/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/valid/reactive-statement01-input.svelte new file mode 100644 index 000000000..8fe05cfc7 --- /dev/null +++ b/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/valid/reactive-statement01-input.svelte @@ -0,0 +1,6 @@ + diff --git a/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/valid/template01-input.svelte b/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/valid/template01-input.svelte new file mode 100644 index 000000000..444277909 --- /dev/null +++ b/tests/fixtures/rules/@typescript-eslint/no-unnecessary-condition/valid/template01-input.svelte @@ -0,0 +1,15 @@ + + +