Skip to content

Commit ce1c17f

Browse files
committed
a few more changes
1 parent 4c8fa11 commit ce1c17f

File tree

3 files changed

+162
-58
lines changed

3 files changed

+162
-58
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"lint": "eslint --ignore-path .gitignore --ext .js,.ts .",
3636
"spec": "jest --testPathIgnorePatterns test/e2e-repo.spec.ts /benchmarks-tmp",
3737
"spec:e2e": "jest test/e2e-repo.spec.ts",
38+
"spec:watch": "npm run spec -- --watch",
3839
"test": "npm run lint && npm run build && npm run spec",
3940
"tsc": "tsc",
4041
"version": "npm run build"

src/helpers.ts

Lines changed: 120 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ const BABEL_CONFIGS = [
1818
".babelrc.cjs",
1919
];
2020

21+
export const GLOBALS = ["window", "globalThis"];
22+
2123
/*
2224
3) Figures out which browsers user is targeting
2325
@@ -38,9 +40,7 @@ export interface GuardedScope {
3840
* scope that the guard applies to and after which index it applies.
3941
* Should be called with either a bare Identifier or a MemberExpression.
4042
*/
41-
export function determineGuardedScope(
42-
node: (ESTree.Identifier | ESTree.MemberExpression) & Rule.NodeParentExtension
43-
): GuardedScope | null {
43+
export function determineGuardedScope(node: Rule.Node): GuardedScope | null {
4444
const result = getIfStatementAndGuardType(node);
4545
if (!result) return null;
4646

@@ -71,15 +71,12 @@ export function isBlockOrProgram(
7171
}
7272

7373
function getIfStatementAndGuardType(
74-
node: (ESTree.Identifier | ESTree.MemberExpression) & Rule.NodeParentExtension
74+
node: Rule.Node
7575
): [ESTree.IfStatement & Rule.NodeParentExtension, boolean] | null {
7676
let positiveGuard = true;
7777
let expression: Rule.Node = node;
7878

79-
if (
80-
node.parent?.type === "UnaryExpression" &&
81-
node.parent.operator === "typeof"
82-
) {
79+
if (isUnaryExpression(node.parent, "typeof")) {
8380
// unused typeof check
8481
if (node.parent.parent?.type !== "BinaryExpression") return null;
8582

@@ -105,16 +102,21 @@ function getIfStatementAndGuardType(
105102
// typepf foo === "undefined"
106103
positiveGuard = false;
107104
}
105+
} else if (isBinaryExpression(expression.parent, "in")) {
106+
expression = expression.parent;
107+
} else if (isBinaryExpression(expression.parent)) {
108+
if (
109+
expression.parent.right.type === "Identifier" &&
110+
expression.parent.right.name === "undefined"
111+
) {
112+
}
108113
}
109114

110115
// !window.fetch
111116
// !!window.fetch
112117
// !!!!!!window.fetch
113118
// !(typeof fetch === "undefined")
114-
while (
115-
expression.parent?.type === "UnaryExpression" &&
116-
expression.parent.operator === "!"
117-
) {
119+
while (isUnaryExpression(expression.parent, "!")) {
118120
expression = expression.parent;
119121
positiveGuard = !positiveGuard;
120122
}
@@ -147,20 +149,37 @@ function ifStatementHasEarlyReturn(
147149
);
148150
}
149151

150-
export function isInsideTypeofCheck(
151-
node: ESTree.Identifier & Rule.NodeParentExtension
152-
) {
152+
export function isInsideTypeofCheck(node: Rule.Node) {
153153
return (
154154
node.parent.type === "UnaryExpression" && node.parent.operator === "typeof"
155155
);
156156
}
157157

158158
function isStringLiteral(
159-
node: ESTree.Node
160-
): node is ESTree.SimpleLiteral & { value: string } {
159+
node: Rule.Node
160+
): node is ESTree.SimpleLiteral & { value: string } & Rule.NodeParentExtension {
161161
return node.type === "Literal" && typeof node.value === "string";
162162
}
163163

164+
function isBinaryExpression(
165+
node: Rule.Node,
166+
operator?: string
167+
): node is ESTree.BinaryExpression & Rule.NodeParentExtension {
168+
return (
169+
node.type === "BinaryExpression" &&
170+
(!operator || node.operator === operator)
171+
);
172+
}
173+
174+
function isUnaryExpression(
175+
node: Rule.Node,
176+
operator?: string
177+
): node is ESTree.UnaryExpression & Rule.NodeParentExtension {
178+
return (
179+
node.type === "UnaryExpression" && (!operator || node.operator === operator)
180+
);
181+
}
182+
164183
export function reverseTargetMappings<K extends string, V extends string>(
165184
targetMappings: Record<K, V>
166185
): Record<V, K> {
@@ -170,6 +189,90 @@ export function reverseTargetMappings<K extends string, V extends string>(
170189
return Object.fromEntries(reversedEntries);
171190
}
172191

192+
export function topmostIdentifierOrMemberExpression(
193+
node: Rule.Node
194+
):
195+
| ((ESTree.Identifier | ESTree.MemberExpression) & Rule.NodeParentExtension)
196+
| null {
197+
switch (node.type) {
198+
case "Identifier":
199+
case "MemberExpression": {
200+
if (node.parent.type !== "MemberExpression") return node;
201+
202+
let expression = node.parent;
203+
while (expression.parent.type === "MemberExpression") {
204+
expression = expression.parent;
205+
}
206+
207+
return expression;
208+
}
209+
210+
default:
211+
return null;
212+
}
213+
}
214+
215+
interface IdentifierProtoChain {
216+
protoChain: string[];
217+
expression: Rule.Node;
218+
}
219+
220+
export function identifierProtoChain(
221+
node: ESTree.Identifier & Rule.NodeParentExtension
222+
): null | IdentifierProtoChain {
223+
const result = identifierProtoChainHelper(node);
224+
if (!result) return null;
225+
226+
const { expression, protoChain } = result;
227+
228+
if (
229+
isBinaryExpression(expression, "in") &&
230+
isStringLiteral(expression.left)
231+
) {
232+
// e.g. `if ("fetch" in window) {}`
233+
protoChain.push(expression.left.value);
234+
}
235+
236+
return { expression, protoChain };
237+
}
238+
239+
/**
240+
* Returns an array of property names from the given identifier, without any leading
241+
* window or globalThis.
242+
*/
243+
function identifierProtoChainHelper(
244+
node: ESTree.Identifier & Rule.NodeParentExtension
245+
): null | IdentifierProtoChain {
246+
const expression = topmostIdentifierOrMemberExpression(node);
247+
if (!expression) return null;
248+
249+
const protoChain: string[] = [];
250+
251+
const protoChainFromMemberExpressionObject = (obj: ESTree.Node) => {
252+
if (obj.type === "Identifier") {
253+
protoChain.push(obj.name);
254+
return true;
255+
}
256+
257+
if (obj.type === "MemberExpression") {
258+
if (obj.property.type !== "Identifier") return false;
259+
if (!protoChainFromMemberExpressionObject(obj.object)) return false;
260+
protoChain.push(obj.property.name);
261+
return true;
262+
}
263+
264+
return false;
265+
};
266+
267+
if (!protoChainFromMemberExpressionObject(expression)) return null;
268+
269+
if (GLOBALS.includes(protoChain[0])) {
270+
protoChain.shift();
271+
}
272+
273+
return { expression, protoChain };
274+
}
275+
173276
/**
174277
* Determine the settings to run this plugin with, including the browserslist targets and
175278
* whether to lint all ES APIs.

src/rules/compat.ts

Lines changed: 41 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
determineGuardedScope,
1515
GuardedScope,
1616
isBlockOrProgram,
17+
identifierProtoChain,
18+
GLOBALS,
1719
} from "../helpers"; // will be deprecated and introduced to this file
1820
import {
1921
AstMetadataApiWithTargetsResolver,
@@ -22,6 +24,19 @@ import {
2224
} from "../types";
2325
import { nodes } from "../providers";
2426

27+
function getErrorNode(node: Rule.Node): Rule.Node {
28+
switch (node.type) {
29+
case "MemberExpression": {
30+
return node.property;
31+
}
32+
case "Identifier": {
33+
return node;
34+
}
35+
default:
36+
throw new Error("not found");
37+
}
38+
}
39+
2540
function getName(node: Rule.Node): string {
2641
switch (node.type) {
2742
case "Identifier": {
@@ -71,12 +86,14 @@ const getRulesForTargets = memoize(
7186

7287
const result: Record<string, AstMetadataApiWithTargetsResolver> = {};
7388

74-
nodes
75-
.filter((node) => (lintAllEsApis ? true : node.kind !== "es"))
76-
.forEach((node) => {
77-
if (!node.getUnsupportedTargets(targets).length) return;
78-
result[node.property || node.object] = node;
79-
});
89+
for (const node of nodes) {
90+
if (
91+
(lintAllEsApis || node.kind !== "es") &&
92+
node.getUnsupportedTargets(targets).length > 0
93+
) {
94+
result[node.protoChainId] = node;
95+
}
96+
}
8097

8198
return result;
8299
}
@@ -114,8 +131,12 @@ const ruleModule: Rule.RuleModule = {
114131
eslintNode: Rule.Node
115132
) => {
116133
if (isPolyfilled(context, node)) return;
134+
135+
// TODO: name should never include leading "window" or "globalThis"
136+
// and location should also not include it, we have to compute location ourselves
137+
117138
errors.push({
118-
node: eslintNode,
139+
node: getErrorNode(eslintNode),
119140
message: [
120141
generateErrorName(node),
121142
"is not supported in",
@@ -165,49 +186,28 @@ const ruleModule: Rule.RuleModule = {
165186
}
166187
}
167188

189+
// Check if identifier is one we care about
190+
const result = identifierProtoChain(node);
191+
if (!result) return;
192+
168193
// Check if the identifier name matches any of the ones in our list
169-
const name = node.name;
170-
const rule = targetedRules[name];
194+
const protoChainId = result.protoChain.join(".");
195+
const rule = targetedRules[protoChainId];
171196
if (!rule) return;
172197

198+
// If this a bare Identifier, not window / globalThis, and not used in a `typeof`
199+
// expression, then we can error immediately
173200
if (
174-
node.parent.type !== "MemberExpression" &&
175-
!isInsideTypeofCheck(node)
201+
result.expression.type === "Identifier" &&
202+
!GLOBALS.includes(result.expression.name) &&
203+
!isInsideTypeofCheck(result.expression)
176204
) {
177-
// This will always produce a ReferenceError in unsupported browsers
205+
// This will always produce a `ReferenceError` in unsupported browsers
178206
handleFailingRule(rule, node);
179207
return;
180208
}
181209

182-
let object = "window";
183-
184-
let expression: Rule.Node = node;
185-
if (expression.parent.type === "MemberExpression") {
186-
expression = expression.parent;
187-
if (expression.object.type === "MemberExpression") {
188-
// thing.Promise.all, thing.thing.Promise.all, ignore this
189-
if (
190-
expression.object.object.type !== "Literal" ||
191-
expression.object.object.value !== "window" ||
192-
expression.object.property.type !== "Identifier"
193-
) {
194-
return;
195-
}
196-
197-
// window.Promise.all, for example
198-
object = expression.object.property.name;
199-
} else if (expression.object.type === "Identifier") {
200-
object = expression.object.name;
201-
} else {
202-
// unrecognized syntax
203-
return;
204-
}
205-
}
206-
207-
// This doesn't actually match our rule, like `Thing.all`
208-
if (object !== "window" && rule.object !== object) return;
209-
210-
const scope = determineGuardedScope(expression);
210+
const scope = determineGuardedScope(result.expression);
211211
if (!scope) {
212212
// this node isn't guarding an if statement, so we can report an error
213213
handleFailingRule(rule, expression);

0 commit comments

Comments
 (0)