shared/analyzers/location.analyzer.ts
shared/analyzers/location.analyzer.ts is a file in GovLab Extension Host. 178 lines of code and 32 definitions.
import {
calleeName,
isType,
literalString,
nameOf,
nodeAt,
nodesAt,
recordAt,
stringIn,
} from "../selectors/syntax.selector.ts";
import { keyForLocation, locationForKey } from "../registries/location.registry.ts";
import type { AstNode } from "../../types/syntax.types.ts";
import type { ComposedKey } from "../../types/location.types.ts";
import { existsSync } from "node:fs";
import { normalizePath } from "../resolvers/anchor.resolver.ts";
const PATH_CALLEES = new Set([
"join",
"resolve",
"relative",
"readFileSync",
"writeFileSync",
"existsSync",
"readdirSync",
"statSync",
"mkdirSync",
"rmSync",
"unlinkSync",
"readFile",
"writeFile",
"readdir",
"stat",
"mkdir",
]);
const CJS_ANCHORS = new Set(["__dirname", "__filename"]);
const BUILDERS = new Set(["join", "resolve"]);
export const cookedOf = function cookedOf(node: AstNode | null): string {
return stringIn(recordAt(node, "value"), "cooked");
};
const trimSlashes = function trimSlashes(value: string): string {
let start = 0;
let end = value.length;
while (start < end && value[start] === "/") {
start += 1;
}
while (end > start && value[end - 1] === "/") {
end -= 1;
}
return value.slice(start, end);
};
export const composedKeyOf = function composedKeyOf(node: AstNode, ssotNames: ReadonlySet<string>): ComposedKey | null {
const parent = nodeAt(node, "parent");
if (!isType(parent, "TemplateLiteral")) {
return null;
}
const [anchor] = nodesAt(parent, "expressions");
if (anchor?.type !== "CallExpression" || !ssotNames.has(calleeName(anchor))) {
return null;
}
const keyValue = literalString(nodesAt(anchor, "arguments")[0] ?? null);
if (keyValue === null) {
return null;
}
const tail = trimSlashes(cookedOf(node));
if (tail.length === 0 || tail.includes("$")) {
return null;
}
const anchorLocation = locationForKey(keyValue);
if (anchorLocation === null) {
return null;
}
const key = keyForLocation(`${anchorLocation}/${tail}`);
return key === null ? null : { key, tail };
};
export const holdsMeta = function holdsMeta(node: AstNode | null): boolean {
if (node === null) {
return false;
}
if (node.type === "MetaProperty") {
return true;
}
if (node.type === "Identifier" && CJS_ANCHORS.has(nameOf(node))) {
return true;
}
if (node.type === "MemberExpression") {
return holdsMeta(nodeAt(node, "object"));
}
if (node.type === "CallExpression") {
return nodesAt(node, "arguments").some((arg) => holdsMeta(arg));
}
return false;
};
const climbCountOf = function climbCountOf(value: string): number {
let count = 0;
for (const segment of normalizePath(value).split("/")) {
if (segment === "..") {
count += 1;
}
}
return count;
};
export const depthBelowPackage = function depthBelowPackage(filename: string): number {
const posix = normalizePath(filename);
const segments = posix.split("/");
for (let cut = segments.length - 1; cut > 0; cut -= 1) {
const dir = segments.slice(0, cut).join("/");
if (existsSync(`${dir}/package.json`)) {
return segments.length - 1 - cut;
}
}
return 0;
};
export const isFileDir = function isFileDir(node: AstNode | null, dirNames: ReadonlySet<string>): boolean {
if (node === null) {
return false;
}
if (node.type === "Identifier") {
const name = nameOf(node);
return CJS_ANCHORS.has(name) || dirNames.has(name);
}
if (node.type === "MemberExpression") {
return isType(nodeAt(node, "object"), "MetaProperty") && nameOf(nodeAt(node, "property")) === "dirname";
}
if (node.type === "CallExpression" && calleeName(node) === "dirname") {
return nodesAt(node, "arguments").some((arg) => holdsMeta(arg));
}
return false;
};
export const staticTargetOf = function staticTargetOf(
node: AstNode,
dirNames: ReadonlySet<string>,
fromDir: string,
): string | null {
if (node.type !== "CallExpression" || !BUILDERS.has(calleeName(node))) {
return null;
}
const segments: string[] = [];
let sawFileDir = false;
for (const arg of nodesAt(node, "arguments")) {
if (isFileDir(arg, dirNames)) {
sawFileDir = true;
continue;
}
const literal = literalString(arg);
if (literal === null) {
return null;
}
segments.push(literal);
}
if (!sawFileDir || segments.length === 0) {
return null;
}
return normalizePath([fromDir, ...segments].join("/"));
};
export const boundNameOf = function boundNameOf(node: AstNode): string | null {
const parent = nodeAt(node, "parent");
if (!isType(parent, "VariableDeclarator")) {
return null;
}
const id = nodeAt(parent, "id");
return isType(id, "Identifier") ? nameOf(id) : null;
};
export const isMetaClimb = function isMetaClimb(node: AstNode, metaNames: ReadonlySet<string>, depth: number): boolean {
if (node.type !== "CallExpression" || !PATH_CALLEES.has(calleeName(node))) {
return false;
}
let sawMeta = false;
let climbs = 0;
for (const arg of nodesAt(node, "arguments")) {
if (holdsMeta(arg) || (arg.type === "Identifier" && metaNames.has(nameOf(arg)))) {
sawMeta = true;
}
const literal = literalString(arg);
if (literal !== null) {
climbs += climbCountOf(literal);
}
}
return sawMeta && climbs > depth;
};