11/* eslint no-nested-ternary: off */
2+ import type { Rule } from "eslint" ;
3+ import type * as ESTree from "estree" ;
24import browserslist from "browserslist" ;
3- import {
4- AstMetadataApiWithTargetsResolver ,
5- ESLintNode ,
6- BrowserListConfig ,
7- Target ,
8- HandleFailingRule ,
9- Context ,
10- BrowsersListOpts ,
11- } from "./types" ;
5+ import { BrowserListConfig , Target , BrowsersListOpts } from "./types" ;
126import { TargetNameMappings } from "./constants" ;
137
148/*
@@ -20,147 +14,145 @@ import { TargetNameMappings } from "./constants";
2014 - All of the rules have compatibility info attached to them
2115- Each API is given to versioning.ts with compatibility info
2216*/
23- function isInsideIfStatement ( context : Context ) {
24- return context . getAncestors ( ) . some ( ( ancestor ) => {
25- return ancestor . type === "IfStatement" ;
26- } ) ;
27- }
2817
29- function checkNotInsideIfStatementAndReport (
30- context : Context ,
31- handleFailingRule : HandleFailingRule ,
32- failingRule : AstMetadataApiWithTargetsResolver ,
33- node : ESLintNode
34- ) {
35- if ( ! isInsideIfStatement ( context ) ) {
36- handleFailingRule ( failingRule , node ) ;
37- }
18+ enum GuardType {
19+ // Guard is true if the browser supports the API
20+ POSITIVE ,
21+ // Guard is false if the browser supports the API
22+ NEGATIVE ,
3823}
3924
40- export function lintCallExpression (
41- context : Context ,
42- handleFailingRule : HandleFailingRule ,
43- rules : AstMetadataApiWithTargetsResolver [ ] ,
44- node : ESLintNode
45- ) {
46- if ( ! node . callee ) return ;
47- const calleeName = node . callee . name ;
48- const failingRule = rules . find ( ( rule ) => rule . object === calleeName ) ;
49- if ( failingRule )
50- checkNotInsideIfStatementAndReport (
51- context ,
52- handleFailingRule ,
53- failingRule ,
54- node
55- ) ;
25+ export interface GuardedScope {
26+ scope : ESTree . Node & Rule . NodeParentExtension ;
27+ index : number ;
5628}
5729
58- export function lintNewExpression (
59- context : Context ,
60- handleFailingRule : HandleFailingRule ,
61- rules : Array < AstMetadataApiWithTargetsResolver > ,
62- node : ESLintNode
63- ) {
64- if ( ! node . callee ) return ;
65- const calleeName = node . callee . name ;
66- const failingRule = rules . find ( ( rule ) => rule . object === calleeName ) ;
67- if ( failingRule )
68- checkNotInsideIfStatementAndReport (
69- context ,
70- handleFailingRule ,
71- failingRule ,
72- node
73- ) ;
74- }
30+ /**
31+ * Checks if the given node is used in a guard, and whether it's used to guard for or against the
32+ * block statement. Should be called with either a bare Identifier, or a MemberExpression.
33+ */
34+ export function determineGuardedScope (
35+ node : ( ESTree . Identifier | ESTree . MemberExpression ) & Rule . NodeParentExtension
36+ ) : GuardedScope | null {
37+ const result = getIfStatementAndGuardType ( node ) ;
38+ if ( ! result ) return null ;
7539
76- export function lintExpressionStatement (
77- context : Context ,
78- handleFailingRule : HandleFailingRule ,
79- rules : AstMetadataApiWithTargetsResolver [ ] ,
80- node : ESLintNode
81- ) {
82- if ( ! node ?. expression ?. name ) return ;
83- const failingRule = rules . find (
84- ( rule ) => rule . object === node ?. expression ?. name
85- ) ;
86- if ( failingRule )
87- checkNotInsideIfStatementAndReport (
88- context ,
89- handleFailingRule ,
90- failingRule ,
91- node
92- ) ;
40+ const [ ifStatement , guardType ] = result ;
41+
42+ if ( guardType === GuardType . POSITIVE ) {
43+ return { scope : ifStatement . consequent as GuardedScope [ "scope" ] , index : 0 } ;
44+ }
45+
46+ // guardType is NEGATIVE
47+
48+ if (
49+ ifStatementHasEarlyReturn ( ifStatement ) &&
50+ isBlockOrProgram ( ifStatement . parent )
51+ ) {
52+ const scope = ifStatement . parent ;
53+ const index = scope . body . indexOf ( ifStatement ) + 1 ;
54+ return { scope, index } ;
55+ }
56+
57+ return null ;
9358}
9459
95- function isStringLiteral ( node : ESLintNode ) : boolean {
96- return node . type === "Literal" && typeof node . value === "string" ;
60+ export function isBlockOrProgram (
61+ node : Rule . Node
62+ ) : node is ( ESTree . Program | ESTree . BlockStatement ) & Rule . NodeParentExtension {
63+ return node . type === "Program" || node . type === "BlockStatement" ;
9764}
9865
99- function protoChainFromMemberExpression ( node : ESLintNode ) : string [ ] {
100- if ( ! node . object ) return [ node . name ] ;
101- const protoChain = ( ( ) => {
66+ function getIfStatementAndGuardType (
67+ node : ( ESTree . Identifier | ESTree . MemberExpression ) & Rule . NodeParentExtension
68+ ) : [ ESTree . IfStatement & Rule . NodeParentExtension , GuardType ] | null {
69+ let guardType = GuardType . POSITIVE ;
70+ let expression : ESTree . Node & Rule . NodeParentExtension = node ;
71+
72+ if (
73+ node . parent ?. type === "UnaryExpression" &&
74+ node . parent . operator === "typeof"
75+ ) {
76+ // unused typeof check
77+ if ( node . parent . parent ?. type !== "BinaryExpression" ) return null ;
78+
79+ expression = node . parent . parent ;
80+ // unexpected comparison
81+ if ( ! isStringLiteral ( expression . right ) ) return null ;
82+
83+ const operator = expression . operator ;
84+ const right = expression . right . value ;
85+
86+ const operatorIsPositive = operator === "===" || operator === "==" ;
87+ const rightIsPositive = right !== "undefined" ;
88+
10289 if (
103- node . object . type === "NewExpression" ||
104- node . object . type === "CallExpression"
90+ ( operatorIsPositive && rightIsPositive ) ||
91+ ( ! operatorIsPositive && ! rightIsPositive )
10592 ) {
106- return protoChainFromMemberExpression ( node . object . callee ! ) ;
107- } else if ( node . object . type === "ArrayExpression" ) {
108- return [ "Array" ] ;
109- } else if ( isStringLiteral ( node . object ) ) {
110- return [ "String" ] ;
93+ // typeof foo === "function"
94+ // typeof foo !== "undefined"
95+ guardType = GuardType . POSITIVE ;
11196 } else {
112- return protoChainFromMemberExpression ( node . object ) ;
97+ // typeof foo !== "function"
98+ // typepf foo === "undefined"
99+ guardType = GuardType . NEGATIVE ;
113100 }
114- } ) ( ) ;
115- return [ ...protoChain , node . property ! . name ] ;
116- }
101+ }
117102
118- export function lintMemberExpression (
119- context : Context ,
120- handleFailingRule : HandleFailingRule ,
121- rules : Array < AstMetadataApiWithTargetsResolver > ,
122- node : ESLintNode
123- ) {
124- if ( ! node . object || ! node . property ) return ;
125- if (
126- ! node . object . name ||
127- node . object . name === "window" ||
128- node . object . name === "globalThis"
103+ // !window.fetch
104+ // !!window.fetch
105+ // !!!!!!window.fetch
106+ // !(typeof fetch === "undefined")
107+ while (
108+ expression . parent ?. type === "UnaryExpression" &&
109+ expression . parent . operator === "!"
129110 ) {
130- const rawProtoChain = protoChainFromMemberExpression ( node ) ;
131- const [ firstObj ] = rawProtoChain ;
132- const protoChain =
133- firstObj === "window" || firstObj === "globalThis"
134- ? rawProtoChain . slice ( 1 )
135- : rawProtoChain ;
136- const protoChainId = protoChain . join ( "." ) ;
137- const failingRule = rules . find (
138- ( rule ) => rule . protoChainId === protoChainId
139- ) ;
140- if ( failingRule ) {
141- checkNotInsideIfStatementAndReport (
142- context ,
143- handleFailingRule ,
144- failingRule ,
145- node
146- ) ;
111+ expression = expression . parent ;
112+ if ( guardType === GuardType . POSITIVE ) {
113+ guardType = GuardType . NEGATIVE ;
114+ } else {
115+ guardType = GuardType . POSITIVE ;
147116 }
148- } else {
149- const objectName = node . object . name ;
150- const propertyName = node . property . name ;
151- const failingRule = rules . find (
152- ( rule ) =>
153- rule . object === objectName &&
154- ( rule . property == null || rule . property === propertyName )
155- ) ;
156- if ( failingRule )
157- checkNotInsideIfStatementAndReport (
158- context ,
159- handleFailingRule ,
160- failingRule ,
161- node
162- ) ;
163117 }
118+
119+ // skip over && and || expressions
120+ while ( expression . parent ?. type === "LogicalExpression" ) {
121+ expression = expression . parent ;
122+ }
123+
124+ if ( expression . parent ?. type === "IfStatement" ) {
125+ return [ expression . parent , guardType ] ;
126+ }
127+
128+ return null ;
129+ }
130+
131+ function ifStatementHasEarlyReturn (
132+ node : ESTree . IfStatement & Rule . NodeParentExtension
133+ ) {
134+ return (
135+ node . consequent . type === "ReturnStatement" ||
136+ node . consequent . type === "ThrowStatement" ||
137+ ( node . consequent . type === "BlockStatement" &&
138+ node . consequent . body . some (
139+ ( statement ) =>
140+ statement . type === "ReturnStatement" ||
141+ statement . type === "ThrowStatement"
142+ ) )
143+ ) ;
144+ }
145+
146+ export function isInsideTypeofCheck (
147+ node : ESTree . Identifier & Rule . NodeParentExtension
148+ ) {
149+ return (
150+ node . parent . type === "UnaryExpression" && node . parent . operator === "typeof"
151+ ) ;
152+ }
153+
154+ function isStringLiteral ( node : ESTree . Node ) : node is ESTree . Literal {
155+ return node . type === "Literal" && typeof node . value === "string" ;
164156}
165157
166158export function reverseTargetMappings < K extends string , V extends string > (
0 commit comments