11import fs from "fs" ;
22
33import browserslist from "browserslist" ;
4- import { Rule } from "eslint" ;
4+ import type { Rule } from "eslint" ;
55import type * as ESTree from "estree" ;
66import findUp from "find-up" ;
77
8- import {
9- AstMetadataApiWithTargetsResolver ,
10- BrowserListConfig ,
11- Target ,
12- HandleFailingRule ,
13- Context ,
14- BrowsersListOpts ,
15- } from "./types" ;
8+ import { BrowserListConfig , Target , Context , BrowsersListOpts } from "./types" ;
169import { TargetNameMappings } from "./constants" ;
1710
1811const BABEL_CONFIGS = [
@@ -34,155 +27,138 @@ const BABEL_CONFIGS = [
3427 - All of the rules have compatibility info attached to them
3528- Each API is given to versioning.ts with compatibility info
3629*/
37- function isInsideIfStatement ( context : Context ) {
38- return context . getAncestors ( ) . some ( ( ancestor ) => {
39- return ancestor . type === "IfStatement" ;
40- } ) ;
30+
31+ export interface GuardedScope {
32+ scope : Rule . Node ;
33+ index : number ;
4134}
4235
43- function checkNotInsideIfStatementAndReport (
44- context : Context ,
45- handleFailingRule : HandleFailingRule ,
46- failingRule : AstMetadataApiWithTargetsResolver ,
47- node : Rule . Node
48- ) {
49- if ( ! isInsideIfStatement ( context ) ) {
50- handleFailingRule ( failingRule , node ) ;
36+ /**
37+ * Checks if the given node is used in an if statement, and if it is, returns the
38+ * scope that the guard applies to and after which index it applies.
39+ * Should be called with either a bare Identifier or a MemberExpression.
40+ */
41+ export function determineGuardedScope (
42+ node : ( ESTree . Identifier | ESTree . MemberExpression ) & Rule . NodeParentExtension
43+ ) : GuardedScope | null {
44+ const result = getIfStatementAndGuardType ( node ) ;
45+ if ( ! result ) return null ;
46+
47+ const [ ifStatement , positiveGuard ] = result ;
48+
49+ if ( positiveGuard ) {
50+ // It's okay to use the identifier inside of the if statement
51+ return { scope : ifStatement . consequent as Rule . Node , index : 0 } ;
5152 }
52- }
5353
54- export function lintCallExpression (
55- context : Context ,
56- handleFailingRule : HandleFailingRule ,
57- rules : AstMetadataApiWithTargetsResolver [ ] ,
58- node : ESTree . CallExpression & Rule . NodeParentExtension
59- ) {
60- if ( ! node . callee ) return ;
61- const calleeName = ( node . callee as any ) . name ;
62- const failingRule = rules . find ( ( rule ) => rule . object === calleeName ) ;
63- if ( failingRule )
64- checkNotInsideIfStatementAndReport (
65- context ,
66- handleFailingRule ,
67- failingRule ,
68- node
69- ) ;
70- }
54+ if (
55+ ifStatementHasEarlyReturn ( ifStatement ) &&
56+ isBlockOrProgram ( ifStatement . parent )
57+ ) {
58+ // It's okay to use the identifier after the if statement
59+ const scope = ifStatement . parent ;
60+ const index = scope . body . indexOf ( ifStatement ) + 1 ;
61+ return { scope, index } ;
62+ }
7163
72- export function lintNewExpression (
73- context : Context ,
74- handleFailingRule : HandleFailingRule ,
75- rules : Array < AstMetadataApiWithTargetsResolver > ,
76- node : ESTree . NewExpression & Rule . NodeParentExtension
77- ) {
78- if ( ! node . callee ) return ;
79- const calleeName = ( node . callee as any ) . name ;
80- const failingRule = rules . find ( ( rule ) => rule . object === calleeName ) ;
81- if ( failingRule )
82- checkNotInsideIfStatementAndReport (
83- context ,
84- handleFailingRule ,
85- failingRule ,
86- node
87- ) ;
64+ return null ;
8865}
8966
90- export function lintExpressionStatement (
91- context : Context ,
92- handleFailingRule : HandleFailingRule ,
93- rules : AstMetadataApiWithTargetsResolver [ ] ,
94- node : ESTree . ExpressionStatement & Rule . NodeParentExtension
95- ) {
96- if ( ! ( node ?. expression as any ) ?. name ) return ;
97- const failingRule = rules . find (
98- ( rule ) => rule . object === ( node ?. expression as any ) ?. name
99- ) ;
100- if ( failingRule )
101- checkNotInsideIfStatementAndReport (
102- context ,
103- handleFailingRule ,
104- failingRule ,
105- node
106- ) ;
67+ export function isBlockOrProgram (
68+ node : Rule . Node
69+ ) : node is ( ESTree . Program | ESTree . BlockStatement ) & Rule . NodeParentExtension {
70+ return node . type === "Program" || node . type === "BlockStatement" ;
10771}
10872
109- function isStringLiteral (
110- node : ESTree . Node
111- ) : node is Omit < ESTree . SimpleLiteral , "value" > & { value : string } {
112- return node . type === "Literal" && typeof node . value === "string" ;
113- }
73+ function getIfStatementAndGuardType (
74+ node : ( ESTree . Identifier | ESTree . MemberExpression ) & Rule . NodeParentExtension
75+ ) : [ ESTree . IfStatement & Rule . NodeParentExtension , boolean ] | null {
76+ let positiveGuard = true ;
77+ let expression : Rule . Node = node ;
78+
79+ if (
80+ node . parent ?. type === "UnaryExpression" &&
81+ node . parent . operator === "typeof"
82+ ) {
83+ // unused typeof check
84+ if ( node . parent . parent ?. type !== "BinaryExpression" ) return null ;
85+
86+ expression = node . parent . parent ;
87+ // unexpected comparison
88+ if ( ! isStringLiteral ( expression . right ) ) return null ;
89+
90+ const operator = expression . operator ;
91+ const right = expression . right . value ;
92+
93+ const operatorIsPositive = operator === "===" || operator === "==" ;
94+ const rightIsPositive = right !== "undefined" ;
11495
115- function protoChainFromMemberExpression (
116- node : ESTree . MemberExpression
117- ) : string [ ] {
118- if ( ! node . object ) return [ ( node as any ) . name ] ;
119- const protoChain = ( ( ) => {
12096 if (
121- node . object . type === "NewExpression" ||
122- node . object . type === "CallExpression"
97+ ( operatorIsPositive && rightIsPositive ) ||
98+ ( ! operatorIsPositive && ! rightIsPositive )
12399 ) {
124- return protoChainFromMemberExpression (
125- node . object . callee ! as ESTree . MemberExpression
126- ) ;
127- } else if ( node . object . type === "ArrayExpression" ) {
128- return [ "Array" ] ;
129- } else if ( isStringLiteral ( node . object ) ) {
130- return [ "String" ] ;
100+ // typeof foo === "function"
101+ // typeof foo !== "undefined"
102+ positiveGuard = true ;
131103 } else {
132- return protoChainFromMemberExpression (
133- node . object as ESTree . MemberExpression
134- ) ;
104+ // typeof foo !== "function"
105+ // typepf foo === "undefined"
106+ positiveGuard = false ;
135107 }
136- } ) ( ) ;
137- return [ ...protoChain , ( node . property as any ) . name ] ;
138- }
108+ }
139109
140- export function lintMemberExpression (
141- context : Context ,
142- handleFailingRule : HandleFailingRule ,
143- rules : Array < AstMetadataApiWithTargetsResolver > ,
144- node : ESTree . MemberExpression & Rule . NodeParentExtension
145- ) {
146- if ( ! node . object || ! node . property ) return ;
147- if (
148- ! ( node . object as any ) . name ||
149- ( node . object as any ) . name === "window" ||
150- ( node . object as any ) . name === "globalThis"
110+ // !window.fetch
111+ // !!window.fetch
112+ // !!!!!!window.fetch
113+ // !(typeof fetch === "undefined")
114+ while (
115+ expression . parent ?. type === "UnaryExpression" &&
116+ expression . parent . operator === "!"
151117 ) {
152- const rawProtoChain = protoChainFromMemberExpression ( node ) ;
153- const [ firstObj ] = rawProtoChain ;
154- const protoChain =
155- firstObj === "window" || firstObj === "globalThis"
156- ? rawProtoChain . slice ( 1 )
157- : rawProtoChain ;
158- const protoChainId = protoChain . join ( "." ) ;
159- const failingRule = rules . find (
160- ( rule ) => rule . protoChainId === protoChainId
161- ) ;
162- if ( failingRule ) {
163- checkNotInsideIfStatementAndReport (
164- context ,
165- handleFailingRule ,
166- failingRule ,
167- node
168- ) ;
169- }
170- } else {
171- const objectName = ( node . object as any ) . name ;
172- const propertyName = ( node . property as any ) . name ;
173- const failingRule = rules . find (
174- ( rule ) =>
175- rule . object === objectName &&
176- ( rule . property == null || rule . property === propertyName )
177- ) ;
178- if ( failingRule )
179- checkNotInsideIfStatementAndReport (
180- context ,
181- handleFailingRule ,
182- failingRule ,
183- node
184- ) ;
118+ expression = expression . parent ;
119+ positiveGuard = ! positiveGuard ;
120+ }
121+
122+ // TODO: allow && checks, but || checks aren't really safe
123+ // skip over && and || expressions
124+ while ( expression . parent ?. type === "LogicalExpression" ) {
125+ expression = expression . parent ;
126+ }
127+
128+ if ( expression . parent ?. type === "IfStatement" ) {
129+ return [ expression . parent , positiveGuard ] ;
185130 }
131+
132+ return null ;
133+ }
134+
135+ function ifStatementHasEarlyReturn (
136+ node : ESTree . IfStatement & Rule . NodeParentExtension
137+ ) {
138+ return (
139+ node . consequent . type === "ReturnStatement" ||
140+ node . consequent . type === "ThrowStatement" ||
141+ ( node . consequent . type === "BlockStatement" &&
142+ node . consequent . body . some (
143+ ( statement ) =>
144+ statement . type === "ReturnStatement" ||
145+ statement . type === "ThrowStatement"
146+ ) )
147+ ) ;
148+ }
149+
150+ export function isInsideTypeofCheck (
151+ node : ESTree . Identifier & Rule . NodeParentExtension
152+ ) {
153+ return (
154+ node . parent . type === "UnaryExpression" && node . parent . operator === "typeof"
155+ ) ;
156+ }
157+
158+ function isStringLiteral (
159+ node : ESTree . Node
160+ ) : node is ESTree . SimpleLiteral & { value : string } {
161+ return node . type === "Literal" && typeof node . value === "string" ;
186162}
187163
188164export function reverseTargetMappings < K extends string , V extends string > (
0 commit comments