Skip to content

Commit 4c8fa11

Browse files
committed
WIP
1 parent 24e6fa9 commit 4c8fa11

File tree

7 files changed

+296
-246
lines changed

7 files changed

+296
-246
lines changed

src/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const STANDARD_TARGET_NAME_MAPPING = {
2727

2828
export type TargetNameMappings = typeof STANDARD_TARGET_NAME_MAPPING;
2929

30-
export enum AstNodeTypes {
30+
export enum AstNodeType {
3131
MemberExpression = "MemberExpression",
3232
CallExpression = "CallExpression",
3333
NewExpression = "NewExpression",

src/helpers.ts

Lines changed: 117 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,11 @@
11
import fs from "fs";
22

33
import browserslist from "browserslist";
4-
import { Rule } from "eslint";
4+
import type { Rule } from "eslint";
55
import type * as ESTree from "estree";
66
import 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";
169
import { TargetNameMappings } from "./constants";
1710

1811
const 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

188164
export function reverseTargetMappings<K extends string, V extends string>(

0 commit comments

Comments
 (0)