1
- /* eslint-disable unicorn/no-typeof-undefined */
2
- import { isOneOf , NodeType } from "@eslint-react/ast " ;
1
+ import { isJSXValue , JSXValueCheckHint } from "@ eslint-react/jsx" ;
2
+ import { getConstrainedTypeAtLocation } from "@typescript-eslint/type-utils " ;
3
3
import { type TSESTree } from "@typescript-eslint/types" ;
4
- import type { ESLintUtils } from "@typescript-eslint/utils" ;
4
+ import { ESLintUtils } from "@typescript-eslint/utils" ;
5
5
import type { ConstantCase } from "string-ts" ;
6
+ import * as tsutils from "ts-api-utils" ;
7
+ import * as ts from "typescript" ;
6
8
7
9
import { createRule } from "../utils" ;
8
10
9
11
export const RULE_NAME = "no-leaked-conditional-rendering" ;
10
12
11
13
export type MessageID = ConstantCase < typeof RULE_NAME > ;
12
14
13
- type TernaryAlternateValue = RegExp | bigint | boolean | null | number | string ;
15
+ const allowTypes = [
16
+ "boolean" ,
17
+ "string" ,
14
18
15
- const COERCE_STRATEGY = "coerce" ;
16
- const TERNARY_STRATEGY = "ternary" ;
17
- const DEFAULT_VALID_STRATEGIES = [ TERNARY_STRATEGY , COERCE_STRATEGY ] as const ;
18
- const COERCE_VALID_LEFT_SIDE_EXPRESSIONS = [
19
- NodeType . UnaryExpression ,
20
- NodeType . BinaryExpression ,
21
- NodeType . CallExpression ,
19
+ "truthy boolean" ,
20
+ "truthy string" ,
22
21
] as const ;
23
22
24
- const TERNARY_INVALID_ALTERNATE_VALUES = new Set < TernaryAlternateValue > ( [ null , false ] ) ;
23
+ /** The types we care about */
24
+ type VariantType =
25
+ | "any"
26
+ | "boolean"
27
+ | "enum"
28
+ | "never"
29
+ | "nullish"
30
+ | "number"
31
+ | "object"
32
+ | "string"
33
+ | "truthy boolean"
34
+ | "truthy number"
35
+ | "truthy string" ;
25
36
26
- function getIsCoerceValidNestedLogicalExpression ( node : TSESTree . Node ) : boolean {
27
- if ( node . type === NodeType . LogicalExpression ) {
28
- return getIsCoerceValidNestedLogicalExpression ( node . left )
29
- && getIsCoerceValidNestedLogicalExpression ( node . right ) ;
37
+ /**
38
+ * Ported from https://github.com/typescript-eslint/typescript-eslint/blob/eb736bbfc22554694400e6a4f97051d845d32e0b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts#L826
39
+ * Check union variants for the types we care about
40
+ * @param types
41
+ */
42
+ function inspectVariantTypes ( types : ts . Type [ ] ) {
43
+ const variantTypes = new Set < VariantType > ( ) ;
44
+
45
+ if (
46
+ types . some ( type =>
47
+ tsutils . isTypeFlagSet (
48
+ type ,
49
+ ts . TypeFlags . Null | ts . TypeFlags . Undefined | ts . TypeFlags . VoidLike ,
50
+ )
51
+ )
52
+ ) {
53
+ variantTypes . add ( "nullish" ) ;
30
54
}
55
+ const booleans = types . filter ( type => tsutils . isTypeFlagSet ( type , ts . TypeFlags . BooleanLike ) ) ;
31
56
32
- return isOneOf ( COERCE_VALID_LEFT_SIDE_EXPRESSIONS ) ( node ) ;
33
- }
57
+ // If incoming type is either "true" or "false", there will be one type
58
+ // object with intrinsicName set accordingly
59
+ // If incoming type is boolean, there will be two type objects with
60
+ // intrinsicName set "true" and "false" each because of ts-api-utils.unionTypeParts()
61
+ // eslint-disable-next-line no-restricted-syntax
62
+ if ( booleans . length === 1 && booleans [ 0 ] ) {
63
+ tsutils . isTrueLiteralType ( booleans [ 0 ] )
64
+ ? variantTypes . add ( "truthy boolean" )
65
+ : variantTypes . add ( "boolean" ) ;
66
+ } else if ( booleans . length === 2 ) {
67
+ variantTypes . add ( "boolean" ) ;
68
+ }
69
+
70
+ const strings = types . filter ( type => tsutils . isTypeFlagSet ( type , ts . TypeFlags . StringLike ) ) ;
71
+
72
+ if ( strings . length > 0 ) {
73
+ // eslint-disable-next-line no-restricted-syntax
74
+ if (
75
+ strings . every ( type => type . isStringLiteral ( ) && type . value !== "" )
76
+ ) {
77
+ variantTypes . add ( "truthy string" ) ;
78
+ } else {
79
+ variantTypes . add ( "string" ) ;
80
+ }
81
+ }
82
+
83
+ const numbers = types . filter ( type =>
84
+ tsutils . isTypeFlagSet (
85
+ type ,
86
+ ts . TypeFlags . NumberLike | ts . TypeFlags . BigIntLike ,
87
+ )
88
+ ) ;
89
+
90
+ if ( numbers . length > 0 ) {
91
+ // eslint-disable-next-line no-restricted-syntax
92
+ if ( numbers . every ( type => type . isNumberLiteral ( ) && type . value !== 0 ) ) {
93
+ variantTypes . add ( "truthy number" ) ;
94
+ } else {
95
+ variantTypes . add ( "number" ) ;
96
+ }
97
+ }
98
+
99
+ if (
100
+ types . some ( type => tsutils . isTypeFlagSet ( type , ts . TypeFlags . EnumLike ) )
101
+ ) {
102
+ variantTypes . add ( "enum" ) ;
103
+ }
104
+
105
+ if (
106
+ types . some (
107
+ type =>
108
+ ! tsutils . isTypeFlagSet (
109
+ type ,
110
+ ts . TypeFlags . Null
111
+ | ts . TypeFlags . Undefined
112
+ | ts . TypeFlags . VoidLike
113
+ | ts . TypeFlags . BooleanLike
114
+ | ts . TypeFlags . StringLike
115
+ | ts . TypeFlags . NumberLike
116
+ | ts . TypeFlags . BigIntLike
117
+ | ts . TypeFlags . TypeParameter
118
+ | ts . TypeFlags . Any
119
+ | ts . TypeFlags . Unknown
120
+ | ts . TypeFlags . Never ,
121
+ ) ,
122
+ )
123
+ ) {
124
+ variantTypes . add ( "object" ) ;
125
+ }
34
126
35
- function isValidTernaryAlternate ( node : TSESTree . ConditionalExpression ) {
36
- if ( ! ( "alternate" in node && "value" in node . alternate ) ) {
37
- return true ;
127
+ if (
128
+ types . some ( type =>
129
+ tsutils . isTypeFlagSet (
130
+ type ,
131
+ ts . TypeFlags . TypeParameter
132
+ | ts . TypeFlags . Any
133
+ | ts . TypeFlags . Unknown ,
134
+ )
135
+ )
136
+ ) {
137
+ variantTypes . add ( "any" ) ;
38
138
}
39
139
40
- if ( typeof node . alternate . value === "undefined" ) {
41
- return false ;
140
+ if ( types . some ( type => tsutils . isTypeFlagSet ( type , ts . TypeFlags . Never ) ) ) {
141
+ variantTypes . add ( "never" ) ;
42
142
}
43
143
44
- return ! TERNARY_INVALID_ALTERNATE_VALUES . has ( node . alternate . value ) ;
144
+ return [ ... variantTypes ] ;
45
145
}
46
146
47
147
export default createRule < [ ] , MessageID > ( {
@@ -61,34 +161,41 @@ export default createRule<[], MessageID>({
61
161
} ,
62
162
defaultOptions : [ ] ,
63
163
create ( context ) {
164
+ const services = ESLintUtils . getParserServices ( context ) ;
165
+
166
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/strict-boolean-expressions
167
+ if ( ! services . program ) {
168
+ throw new Error ( "see https://typescript-eslint.io/docs/linting/type-linting" ) ;
169
+ }
170
+
64
171
return {
65
172
"JSXExpressionContainer > ConditionalExpression" ( node : TSESTree . ConditionalExpression ) {
66
- if ( DEFAULT_VALID_STRATEGIES . includes ( TERNARY_STRATEGY ) ) {
67
- return ;
68
- }
69
- const isJSXElementAlternate = node . alternate . type === NodeType . JSXElement ;
70
- if ( isValidTernaryAlternate ( node ) || isJSXElementAlternate ) {
173
+ const hint = JSXValueCheckHint . SkipNumberLiteral
174
+ | JSXValueCheckHint . StrictArray
175
+ | JSXValueCheckHint . StrictLogical
176
+ | JSXValueCheckHint . StrictConditional ;
177
+
178
+ if ( ! isJSXValue ( node , context , hint ) ) {
71
179
context . report ( {
72
180
messageId : "NO_LEAKED_CONDITIONAL_RENDERING" ,
73
- node : node . alternate ,
181
+ node,
74
182
} ) ;
75
183
}
76
184
} ,
77
185
'JSXExpressionContainer > LogicalExpression[operator="&&"]' ( node : TSESTree . LogicalExpression ) {
78
- const leftSide = node . left ;
79
- const isCoerceValidLeftSide = isOneOf ( COERCE_VALID_LEFT_SIDE_EXPRESSIONS ) ( leftSide ) ;
80
- if (
81
- DEFAULT_VALID_STRATEGIES . includes ( COERCE_STRATEGY )
82
- && ( isCoerceValidLeftSide || getIsCoerceValidNestedLogicalExpression ( leftSide ) )
83
- ) {
84
- return ;
85
- }
86
- if ( leftSide . type === NodeType . Literal && leftSide . value === "" ) {
186
+ const { left } = node ;
187
+
188
+ const leftType = getConstrainedTypeAtLocation ( services , left ) ;
189
+
190
+ const types = inspectVariantTypes ( [ leftType ] ) ;
191
+
192
+ if ( types . every ( type => allowTypes . includes ( type as never ) ) ) {
87
193
return ;
88
194
}
195
+
89
196
context . report ( {
90
197
messageId : "NO_LEAKED_CONDITIONAL_RENDERING" ,
91
- node : leftSide ,
198
+ node : left ,
92
199
} ) ;
93
200
} ,
94
201
} ;
0 commit comments