rules/eslint/no-undeclared-dependency.eslint.rule.ts
rules/eslint/no-undeclared-dependency.eslint.rule.ts is a file in GovLab Extension Host. 176 lines of code and 43 definitions.
import type { Rule } from "eslint";
import { builtinModules } from "node:module";
defineCheck({ detects: [], enforces: ["architecture:explicit-contracts"] });
import { defineCheck } from "@govlab/context/check";
import fs from "node:fs";
import { jsonRecordAt } from "../../shared/loaders/manifest.loader.ts";
import path from "node:path";
interface AstNode {
type: string;
name?: string;
value?: unknown;
callee?: AstNode;
arguments?: AstNode[];
source?: AstNode;
}
const BUILTINS = new Set(builtinModules);
const DEP_FIELDS = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
const MESSAGE =
"'{{pkg}}' is imported but declared in no package.json (dependencies/devDependencies/peer/optional) up the tree. An undeclared external package is not installed by `npm install`, so the import throws ERR_MODULE_NOT_FOUND at runtime — even when an ambient `declare module` shim hides it from the type-checker. Add '{{pkg}}' to the owning package.json (and install it), or remove the import. [no_undeclared_dependency]";
const isNode = function isNode(value: unknown): value is AstNode {
return value !== null && typeof value === "object" && "type" in value;
};
const asNode = function asNode(value: unknown): AstNode | null {
return isNode(value) ? value : null;
};
const isReportNode = function isReportNode(value: unknown): value is Rule.Node {
return value !== null && typeof value === "object" && "type" in value;
};
const asReportNode = function asReportNode(value: unknown): Rule.Node | null {
return isReportNode(value) ? value : null;
};
const normalize = function normalize(filename: string): string {
return filename.split("\\").join("/");
};
const isRecord = function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
};
const packageRoot = function packageRoot(specifier: string): string {
const bare = specifier.startsWith("node:") ? specifier.slice("node:".length) : specifier;
if (bare.startsWith("@")) {
const firstSlash = bare.indexOf("/");
if (firstSlash === -1) {
return bare;
}
const secondSlash = bare.indexOf("/", firstSlash + 1);
return secondSlash === -1 ? bare : bare.slice(0, secondSlash);
}
const slash = bare.indexOf("/");
return slash === -1 ? bare : bare.slice(0, slash);
};
const SELF_DECLARING_SCHEMES = ["data:", "file:", "npm:", "jsr:", "http:", "https:"];
const isExternal = function isExternal(specifier: string): boolean {
if (specifier.length === 0) {
return false;
}
const [first] = specifier;
if (first === "." || first === "/" || first === "#") {
return false;
}
return !SELF_DECLARING_SCHEMES.some((scheme) => specifier.startsWith(scheme));
};
const declaredCache = new Map<string, Set<string>>();
const addDepsFrom = function addDepsFrom(pkgPath: string, declared: Set<string>): void {
if (!fs.existsSync(pkgPath)) {
return;
}
const parsed = jsonRecordAt(pkgPath);
for (const field of DEP_FIELDS) {
const deps = parsed[field];
if (isRecord(deps)) {
for (const name of Object.keys(deps)) {
declared.add(name);
}
}
}
};
const collectDeclared = function collectDeclared(startDir: string): Set<string> {
const cached = declaredCache.get(startDir);
if (cached) {
return cached;
}
const declared = new Set<string>();
let dir = startDir;
for (;;) {
addDepsFrom(path.join(dir, "package.json"), declared);
const parent = path.dirname(dir);
if (parent === dir) {
break;
}
dir = parent;
}
declaredCache.set(startDir, declared);
return declared;
};
const isAllowed = function isAllowed(pkg: string, fileDir: string): boolean {
const bare = pkg.startsWith("node:") ? pkg.slice("node:".length) : pkg;
if (BUILTINS.has(bare) || BUILTINS.has(`node:${bare}`)) {
return true;
}
return collectDeclared(fileDir).has(pkg);
};
const reportUndeclared = function reportUndeclared(
context: Rule.RuleContext,
fileDir: string,
target: { node: AstNode; specifier: unknown },
): void {
const { node, specifier } = target;
if (typeof specifier !== "string" || !isExternal(specifier)) {
return;
}
const pkg = packageRoot(specifier);
if (isAllowed(pkg, fileDir)) {
return;
}
const reportNode = asReportNode(node);
if (reportNode !== null) {
context.report({ data: { pkg }, messageId: "undeclared", node: reportNode });
}
};
const makeFromSource = function makeFromSource(context: Rule.RuleContext, fileDir: string): (node: Rule.Node) => void {
return function fromSource(node: Rule.Node): void {
const source = asNode(node)?.source;
if (isRecord(source)) {
reportUndeclared(context, fileDir, { node: source, specifier: source.value });
}
};
};
const makeOnCall = function makeOnCall(context: Rule.RuleContext, fileDir: string): (node: Rule.Node) => void {
return function onCall(node: Rule.Node): void {
const call = asNode(node);
if (call?.callee?.type === "Identifier" && call.callee.name === "require") {
const first = call.arguments?.[0];
if (first?.type === "Literal") {
reportUndeclared(context, fileDir, { node: first, specifier: first.value });
}
}
};
};
const makeOnImportExpression = function makeOnImportExpression(
context: Rule.RuleContext,
fileDir: string,
): (node: Rule.Node) => void {
return function onImportExpression(node: Rule.Node): void {
const source = asNode(node)?.source;
if (source?.type === "Literal") {
reportUndeclared(context, fileDir, { node: source, specifier: source.value });
}
};
};
const noUndeclaredDependency: Rule.RuleModule = {
create(context: Rule.RuleContext): Rule.RuleListener {
const fileDir = path.dirname(normalize(context.filename));
const fromSource = makeFromSource(context, fileDir);
const handlers: [string, (node: Rule.Node) => void][] = [
["CallExpression", makeOnCall(context, fileDir)],
["ExportAllDeclaration", fromSource],
["ExportNamedDeclaration", fromSource],
["ImportDeclaration", fromSource],
["ImportExpression", makeOnImportExpression(context, fileDir)],
];
return Object.fromEntries(handlers);
},
meta: {
docs: {
description:
"Every imported package is declared by a manifest up the tree. An undeclared import can still resolve — through a hoisted transitive install, a workspace symlink, or an ambient shim — so it type-checks and runs locally while a clean install has nothing to resolve it against.",
},
messages: { undeclared: MESSAGE },
schema: [],
type: "problem",
},
};
export default {
plugins: { "govlab-deps": { rules: { "no-undeclared-dependency": noUndeclaredDependency } } },
tool: "eslint",
};