125 lines · 4.5 KB · TypeScript
RawDownload
import { defineRule } from "@oxlint/plugins";import * as Option from "effect/Option"; import { getPropertyName, isIdentifier, unwrapExpression } from "../utils.ts"; const RUNTIME_PROPERTIES = new Set(["platform", "arch"]);const NODE_OS_MODULES = new Set(["node:os", "os"]); const isGlobalProcessObject = (node: unknown): boolean => {  const expression = unwrapExpression(node);  if (isIdentifier(expression, "process")) return true;  if (Option.isNone(expression) || expression.value.type !== "MemberExpression") return false;   const object = unwrapExpression(expression.value.object);  const property = getPropertyName(expression.value.property);  return (    isIdentifier(object, "globalThis") && Option.isSome(property) && property.value === "process"  );}; const message = (property: string) =>  `Use HostProcess${property === "arch" ? "Architecture" : "Platform"} instead of process.${property}; inject the runtime reference in Effect code and provide it explicitly in tests.`; const getLiteralStringValue = (node: unknown): Option.Option<string> => {  if (typeof node !== "object" || node === null) return Option.none();  if (!("type" in node) || node.type !== "Literal") return Option.none();  if (!("value" in node) || typeof node.value !== "string") return Option.none();  return Option.some(node.value);}; export default defineRule({  meta: {    type: "problem",    docs: {      description:        "Disallow direct host runtime platform/architecture reads; the shared host process references are exempted in the lint config.",    },  },  createOnce(context) {    const nodeOsNamespaces = new Set<string>();    const nodeOsRuntimeImports = new Map<string, string>();     const resetBindings = () => {      nodeOsNamespaces.clear();      nodeOsRuntimeImports.clear();    };     const trackImportDeclaration = (node: unknown) => {      if (typeof node !== "object" || node === null) return;      if (!("source" in node)) return;       const source = getLiteralStringValue(node.source);      if (Option.isNone(source) || !NODE_OS_MODULES.has(source.value)) return;      if (!("specifiers" in node) || !Array.isArray(node.specifiers)) return;       for (const specifier of node.specifiers) {        if (typeof specifier !== "object" || specifier === null) continue;        if (!("local" in specifier)) continue;         const local = unwrapExpression(specifier.local);        if (Option.isNone(local) || local.value.type !== "Identifier") continue;        const localName = local.value.name;         if (          specifier.type === "ImportNamespaceSpecifier" ||          specifier.type === "ImportDefaultSpecifier"        ) {          nodeOsNamespaces.add(localName);          continue;        }         if (specifier.type !== "ImportSpecifier" || !("imported" in specifier)) continue;         const imported = getPropertyName(specifier.imported);        if (Option.isSome(imported) && RUNTIME_PROPERTIES.has(imported.value)) {          nodeOsRuntimeImports.set(localName, imported.value);        }      }    };     const getNodeOsRuntimeCall = (callee: unknown): Option.Option<string> => {      const expression = unwrapExpression(callee);      if (Option.isNone(expression)) return Option.none();       if (expression.value.type === "Identifier") {        const property = nodeOsRuntimeImports.get(expression.value.name);        return property === undefined ? Option.none() : Option.some(property);      }       if (expression.value.type !== "MemberExpression") return Option.none();       const object = unwrapExpression(expression.value.object);      if (Option.isNone(object) || object.value.type !== "Identifier") return Option.none();      if (!nodeOsNamespaces.has(object.value.name)) return Option.none();       return Option.filter(getPropertyName(expression.value.property), (property) =>        RUNTIME_PROPERTIES.has(property),      );    };     return {      before: resetBindings,      ImportDeclaration: trackImportDeclaration,      MemberExpression(node) {        const property = getPropertyName(node.property);        if (Option.isNone(property) || !RUNTIME_PROPERTIES.has(property.value)) return;        if (!isGlobalProcessObject(node.object)) return;         context.report({          node,          message: message(property.value),        });      },      CallExpression(node) {        const property = getNodeOsRuntimeCall(node.callee);        if (Option.isNone(property)) return;         context.report({          node,          message: message(property.value),        });      },    };  },});