1import { defineRule, type ESTree } from "@oxlint/plugins";
2
3// Add global APIs by dotted path, or instance methods by name. Values explain the replacement.
4const UNSUPPORTED_GLOBAL_APIS = new Map([
5 [
6 "Intl.Segmenter",
7 "Use a portable implementation or a simpler character-counting approximation.",
8 ],
9]);
10
11const UNSUPPORTED_METHODS = new Map([
12 [
13 "toSorted",
14 "Hermes does not implement Array#toSorted. Copy the array first: [...array].sort(...).",
15 ],
16 [
17 "toReversed",
18 "Hermes does not implement Array#toReversed. Copy the array first: [...array].reverse().",
19 ],
20 // splice returns the removed elements, so the copy itself is the result.
21 [
22 "toSpliced",
23 "Hermes does not implement Array#toSpliced. Copy the array first: const copy = [...array]; copy.splice(...); use copy.",
24 ],
25]);
26
27function memberName(node: ESTree.MemberExpression): string | null {
28 const { property } = node;
29 if (!node.computed && property.type === "Identifier") return property.name;
30 if (property.type === "Literal" && typeof property.value === "string") return property.value;
31 if (property.type === "TemplateLiteral" && property.expressions.length === 0)
32 return property.quasis[0]?.value.cooked ?? null;
33 return null;
34}
35
36function globalApiPath(node: ESTree.Node): string | null {
37 if (node.type === "Identifier") return node.name;
38 if (node.type !== "MemberExpression") return null;
39 const object = globalApiPath(node.object);
40 const property = memberName(node);
41 if (object === null || property === null) return null;
42 return ["globalThis", "global", "window"].includes(object) ? property : `${object}.${property}`;
43}
44
45export default defineRule({
46 meta: {
47 type: "problem",
48 docs: {
49 description: "Disallow APIs that Hermes does not implement in mobile and shared client code.",
50 },
51 },
52 create(context) {
53 function checkApi(node: ESTree.CallExpression | ESTree.NewExpression) {
54 const path = globalApiPath(node.callee);
55 const replacement = path === null ? undefined : UNSUPPORTED_GLOBAL_APIS.get(path);
56 if (replacement !== undefined) {
57 context.report({
58 node: node.callee,
59 message: `Hermes does not implement ${path}. ${replacement}`,
60 });
61 return;
62 }
63 if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") return;
64 const name = memberName(node.callee);
65 const message = name === null ? undefined : UNSUPPORTED_METHODS.get(name);
66 if (message !== undefined) context.report({ node: node.callee.property, message });
67 }
68 return { NewExpression: checkApi, CallExpression: checkApi };
69 },
70});