Skip to content

Commit 008dfc6

Browse files
committed
WIP
1 parent 6a50593 commit 008dfc6

File tree

8 files changed

+340
-279
lines changed

8 files changed

+340
-279
lines changed

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,13 @@
6161
],
6262
"rules": {
6363
"import/extensions": "off",
64-
"import/no-extraneous-dependencies": "off"
64+
"import/no-extraneous-dependencies": "off",
65+
"@typescript-eslint/no-use-before-define": [
66+
"error",
67+
{
68+
"functions": false
69+
}
70+
]
6571
},
6672
"root": true
6773
},

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: 124 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
11
/* eslint no-nested-ternary: off */
2+
import type { Rule } from "eslint";
3+
import type * as ESTree from "estree";
24
import 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";
126
import { 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

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

0 commit comments

Comments
 (0)