diff --git a/package.json b/package.json index 297c0bcdf..a6f08c991 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eslint-react/monorepo", - "version": "0.7.9", + "version": "0.8.0", "description": "ESLint plugin for React function components with TypeScript, built (mostly) from scratch.", "keywords": [ "eslint", diff --git a/packages/ast/package.json b/packages/ast/package.json index 6724e4232..e9c19b5ef 100644 --- a/packages/ast/package.json +++ b/packages/ast/package.json @@ -1,6 +1,6 @@ { "name": "@eslint-react/ast", - "version": "0.7.9", + "version": "0.8.0", "description": "AST Utility Module for Static Analysis of TypeScript", "homepage": "https://github.com/eslint-react/eslint-react", "bugs": { diff --git a/packages/ast/src/unary.ts b/packages/ast/src/unary.ts index e7569da99..2e6df8734 100644 --- a/packages/ast/src/unary.ts +++ b/packages/ast/src/unary.ts @@ -2,16 +2,18 @@ import type { TSESTree } from "@typescript-eslint/types"; import { NodeType } from "./node-type"; -export function getNestedUnaryOperators( - node: TSESTree.UnaryExpression, - seen = [], -): TSESTree.UnaryExpression["operator"][] { +/** + * Get all unary operators in a nested unary expression. + * @param node The node to get the operators from. + * @returns All unary operators in a nested unary expression. + */ +export function getNestedUnaryOperators(node: TSESTree.UnaryExpression): TSESTree.UnaryExpression["operator"][] { const { operator } = node; const { argument } = node; if (argument.type === NodeType.UnaryExpression) { - return [...seen, operator, ...getNestedUnaryOperators(argument, seen)]; + return [operator, ...getNestedUnaryOperators(argument)]; } - return [...seen, operator]; + return [operator]; } diff --git a/packages/core/package.json b/packages/core/package.json index c491c1304..e002ccc03 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@eslint-react/core", - "version": "0.7.9", + "version": "0.8.0", "description": "AST Utility Module for Static Analysis of React core API and Patterns.", "homepage": "https://github.com/eslint-react/eslint-react", "bugs": { diff --git a/packages/eslint-plugin-debug/package.json b/packages/eslint-plugin-debug/package.json index 90868e827..c5a5e4e16 100644 --- a/packages/eslint-plugin-debug/package.json +++ b/packages/eslint-plugin-debug/package.json @@ -1,6 +1,6 @@ { "name": "@eslint-react/eslint-plugin-debug", - "version": "0.7.9", + "version": "0.8.0", "description": "Debug specific rules for @eslint-react/eslint-plugin", "homepage": "https://github.com/eslint-react/eslint-react", "bugs": { diff --git a/packages/eslint-plugin-hooks/package.json b/packages/eslint-plugin-hooks/package.json index da138ce49..08178651d 100644 --- a/packages/eslint-plugin-hooks/package.json +++ b/packages/eslint-plugin-hooks/package.json @@ -1,6 +1,6 @@ { "name": "@eslint-react/eslint-plugin-hooks", - "version": "0.7.9", + "version": "0.8.0", "description": "Hooks specific rules for @eslint-react/eslint-plugin", "homepage": "https://github.com/eslint-react/eslint-react", "bugs": { diff --git a/packages/eslint-plugin-jsx/package.json b/packages/eslint-plugin-jsx/package.json index ae13ea726..b050b37b0 100644 --- a/packages/eslint-plugin-jsx/package.json +++ b/packages/eslint-plugin-jsx/package.json @@ -1,6 +1,6 @@ { "name": "@eslint-react/eslint-plugin-jsx", - "version": "0.7.9", + "version": "0.8.0", "description": "JSX specific rules for @eslint-react/eslint-plugin", "homepage": "https://github.com/eslint-react/eslint-react", "bugs": { diff --git a/packages/eslint-plugin-jsx/src/rules/no-leaked-conditional-rendering.spec.ts b/packages/eslint-plugin-jsx/src/rules/no-leaked-conditional-rendering.spec.ts index d3e8cc065..8146aa1c1 100644 --- a/packages/eslint-plugin-jsx/src/rules/no-leaked-conditional-rendering.spec.ts +++ b/packages/eslint-plugin-jsx/src/rules/no-leaked-conditional-rendering.spec.ts @@ -281,7 +281,7 @@ ruleTester.run(RULE_NAME, rule, { } `, dedent` - const someCondition1 = 0; + const someCondition = 0; const SomeComponent = () =>
; const App = () => { @@ -293,7 +293,7 @@ ruleTester.run(RULE_NAME, rule, { prop1={val1} prop2={val2} />) - : someCondition1 ? null :
+ : someCondition ? null :
} ) @@ -316,6 +316,28 @@ ruleTester.run(RULE_NAME, rule, { ) } `, + dedent` + const someCondition = 0; + const SomeComponent = () =>
; + + const App = () => { + return ( + <> + {!!someCondition + ? ( + ) + : someCondition ? Date() + : someCondition && someCondition + ?
+ : null + } + + ) + } + `, ], invalid: [ { @@ -489,7 +511,7 @@ ruleTester.run(RULE_NAME, rule, { }, { code: dedent` - const someCondition1 = 0; + const someCondition = 0; const SomeComponent = () =>
; const App = () => { @@ -515,7 +537,7 @@ ruleTester.run(RULE_NAME, rule, { }, { code: dedent` - const someCondition1 = 0; + const someCondition = 0; const SomeComponent = () =>
; const App = () => { @@ -527,7 +549,7 @@ ruleTester.run(RULE_NAME, rule, { prop1={val1} prop2={val2} />) - : someCondition1 &&
+ : someCondition &&
} ) @@ -541,7 +563,7 @@ ruleTester.run(RULE_NAME, rule, { }, { code: dedent` - const someCondition1 = 0; + const someCondition = 1; const SomeComponent = () =>
; const App = () => { @@ -553,7 +575,7 @@ ruleTester.run(RULE_NAME, rule, { prop1={val1} prop2={val2} />) - : someCondition1 ? Date() :
+ : someCondition ? Date() :
} ) @@ -585,5 +607,32 @@ ruleTester.run(RULE_NAME, rule, { }, ], }, + { + code: dedent` + const someCondition = 1; + const SomeComponent = () =>
; + + const App = () => { + return ( + <> + {!!someCondition + ? ( + ) + : someCondition ? window + : someCondition && someCondition + ?
+ : null + } + + ) + } + `, + errors: [ + { messageId: "NO_LEAKED_CONDITIONAL_RENDERING" }, + ], + }, ], }); diff --git a/packages/eslint-plugin-jsx/src/rules/no-leaked-conditional-rendering.ts b/packages/eslint-plugin-jsx/src/rules/no-leaked-conditional-rendering.ts index bb6e67b29..5d7669308 100644 --- a/packages/eslint-plugin-jsx/src/rules/no-leaked-conditional-rendering.ts +++ b/packages/eslint-plugin-jsx/src/rules/no-leaked-conditional-rendering.ts @@ -1,5 +1,5 @@ /* eslint-disable sonarjs/no-duplicate-string */ -import { getNestedUnaryOperators, isJSX, isNodeEqual, NodeType } from "@eslint-react/ast"; +import { getNestedUnaryOperators, isJSX, isNodeEqual, isOneOf, NodeType } from "@eslint-react/ast"; import { isJSXValue, JSXValueCheckHint } from "@eslint-react/jsx"; import { F } from "@eslint-react/tools"; import { getConstrainedTypeAtLocation } from "@typescript-eslint/type-utils"; @@ -34,6 +34,13 @@ type VariantType = | "truthy number" | "truthy string"; +const falsyTypes = [ + "nullish", + "falsy boolean", + "falsy number", + "falsy string", +] as const satisfies VariantType[]; + // Allowed un-guarded type variants const allowTypes = [ "boolean", @@ -54,7 +61,7 @@ const allowGuardedConsequentTypes = [ "truthy boolean", "truthy number", "truthy string", -] as const; +] as const satisfies VariantType[]; // Allowed guarded alternate type variants const allowGuardedAlternateTypes = [ @@ -66,7 +73,7 @@ const allowGuardedAlternateTypes = [ "falsy boolean", "falsy number", "falsy string", -] as const; +] as const satisfies VariantType[]; // Allowed guarded logical right type variants const allowGuardedUnaryNotTypes = [ @@ -82,7 +89,7 @@ const allowGuardedUnaryNotTypes = [ "falsy boolean", "falsy number", "falsy string", -] as const; +] as const satisfies VariantType[]; /** * Ported from https://github.com/typescript-eslint/typescript-eslint/blob/eb736bbfc22554694400e6a4f97051d845d32e0b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts#L826 @@ -220,6 +227,7 @@ export default createRule<[], MessageID>({ }, }, defaultOptions: [], + // eslint-disable-next-line sonarjs/cognitive-complexity create(context) { const hint = JSXValueCheckHint.StrictArray | JSXValueCheckHint.StrictLogical @@ -252,24 +260,24 @@ export default createRule<[], MessageID>({ return true; } - const isLeftHasUnaryNot = left.type === NodeType.UnaryExpression - && getNestedUnaryOperators(left).some(op => op === "!"); + const isLeftUnaryNot = left.type === NodeType.UnaryExpression + && left.operator === "!"; - if (isLeftHasUnaryNot) { + if (isLeftUnaryNot) { if (isJSX(right)) { return true; } const rightType = getConstrainedTypeAtLocation(services, right); - const types = inspectVariantTypes(tsutils.unionTypeParts(rightType)); + const rightTypeVariants = inspectVariantTypes(tsutils.unionTypeParts(rightType)); - return types.every(type => allowGuardedUnaryNotTypes.includes(type as never)); + return rightTypeVariants.every(type => allowGuardedUnaryNotTypes.includes(type as never)); } const leftType = getConstrainedTypeAtLocation(services, left); - const types = inspectVariantTypes(tsutils.unionTypeParts(leftType)); + const leftTypeVariants = inspectVariantTypes(tsutils.unionTypeParts(leftType)); - return types.every(type => allowTypes.includes(type as never)); + return leftTypeVariants.every(type => allowTypes.includes(type as never)); } function isValidConditionalExpression( @@ -279,27 +287,45 @@ export default createRule<[], MessageID>({ const isConsequentGuarded = isNodeEqual(consequent, test); const testType = getConstrainedTypeAtLocation(services, test); - const types = inspectVariantTypes(tsutils.unionTypeParts(testType)); + const testTypeVariants = inspectVariantTypes(tsutils.unionTypeParts(testType)); if ( isConsequentGuarded - && types.every(type => allowGuardedConsequentTypes.includes(type as never)) + && testTypeVariants.every(type => allowGuardedConsequentTypes.includes(type as never)) ) { return true; } - const unaryOperatorsInTest = test.type === NodeType.UnaryExpression - ? getNestedUnaryOperators(test) - : []; + if (test.type === NodeType.UnaryExpression) { + const unaryNotOperatorsInTest = getNestedUnaryOperators(test); + const testIsFalsy = testTypeVariants.every(type => falsyTypes.includes(type as never)); + const isAlternateGuarded = testIsFalsy + // Check for `!!` or `!!!!` etc in the test + && unaryNotOperatorsInTest.every(op => op === "!") + && unaryNotOperatorsInTest.length % 2 === 0; - const isAlternateGuarded = unaryOperatorsInTest.every(op => op === "!") - && unaryOperatorsInTest.length % 2 === 1; + if (isAlternateGuarded && testTypeVariants.every(type => allowGuardedAlternateTypes.includes(type as never))) { + return isValidInnerExpression(alternate); + } + } - if (isAlternateGuarded && types.every(type => allowGuardedAlternateTypes.includes(type as never))) { - return true; + if (test.type === NodeType.Identifier) { + const isAlternateGuarded = testTypeVariants.every(type => falsyTypes.includes(type as never)); + + if (isAlternateGuarded) { + if (isJSX(alternate)) { + return true; + } + + if (isOneOf([NodeType.LogicalExpression, NodeType.ConditionalExpression])(alternate)) { + return isValidInnerExpression(alternate); + } + + return true; + } } - return isValidInnerExpression(alternate) && isValidInnerExpression(consequent); + return isValidInnerExpression(consequent) && isValidInnerExpression(alternate); } return { diff --git a/packages/eslint-plugin-naming-convention/package.json b/packages/eslint-plugin-naming-convention/package.json index 795409071..4a72e7338 100644 --- a/packages/eslint-plugin-naming-convention/package.json +++ b/packages/eslint-plugin-naming-convention/package.json @@ -1,6 +1,6 @@ { "name": "@eslint-react/eslint-plugin-naming-convention", - "version": "0.7.9", + "version": "0.8.0", "description": "Naming convention specific rules for ESLint-plugin-React", "homepage": "https://github.com/eslint-react/eslint-react", "bugs": { diff --git a/packages/eslint-plugin-react/package.json b/packages/eslint-plugin-react/package.json index e3fd03537..0d146753a 100644 --- a/packages/eslint-plugin-react/package.json +++ b/packages/eslint-plugin-react/package.json @@ -1,6 +1,6 @@ { "name": "@eslint-react/eslint-plugin-react", - "version": "0.7.9", + "version": "0.8.0", "description": "React specific rules for ESLint", "homepage": "https://github.com/eslint-react/eslint-react", "bugs": { diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index ea866d824..72d677fdc 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@eslint-react/eslint-plugin", - "version": "0.7.9", + "version": "0.8.0", "description": "ESLint plugin for React function components with TypeScript, built (mostly) from scratch.", "keywords": [ "eslint", diff --git a/packages/jsx/package.json b/packages/jsx/package.json index a03167581..9896c5664 100644 --- a/packages/jsx/package.json +++ b/packages/jsx/package.json @@ -1,6 +1,6 @@ { "name": "@eslint-react/jsx", - "version": "0.7.9", + "version": "0.8.0", "description": "AST Utility Module for Static Analysis of JSX", "homepage": "https://github.com/eslint-react/eslint-react", "bugs": { diff --git a/packages/shared/package.json b/packages/shared/package.json index 5e55ed1e8..518fe0a0b 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@eslint-react/shared", - "version": "0.7.9", + "version": "0.8.0", "description": "Shared constants and utilities for @eslint-react's packages", "homepage": "https://github.com/eslint-react/eslint-react", "bugs": { diff --git a/packages/tools/package.json b/packages/tools/package.json index 72ea0b3c9..22712f19a 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,6 +1,6 @@ { "name": "@eslint-react/tools", - "version": "0.7.9", + "version": "0.8.0", "description": "Primitive tools for @eslint-react's packages", "homepage": "https://github.com/eslint-react/eslint-react", "bugs": { diff --git a/packages/types/package.json b/packages/types/package.json index 48abb4eda..b432134a7 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@eslint-react/types", - "version": "0.7.9", + "version": "0.8.0", "description": "Type definitions for @eslint-react", "homepage": "https://github.com/eslint-react/eslint-react", "bugs": {