core/quality/eslint/no-upward-dependency.eslint.rule.ts
core/quality/eslint/no-upward-dependency.eslint.rule.ts is a file in GovLab Quality. 110 lines of code and 25 definitions.
import type { Rule } from "eslint";
import { govlabMeta } from "#core/factories/eslint.factory";
import { govlabSettings } from "#core/selectors/eslint.selector";
interface AstNode {
type: string;
source?: { value?: unknown };
}
const DOTDOT = "../";
const isAstNode = function isAstNode(value: unknown): value is AstNode {
return value !== null && typeof value === "object" && "type" in value;
};
const asNode = function asNode(value: unknown): AstNode | null {
return isAstNode(value) ? value : null;
};
const moduleOf = function moduleOf(filePath: string, marker: string): string | null {
const normalized = filePath.replaceAll("\\", "/");
const idx = normalized.indexOf(marker);
if (idx === -1) {
return null;
}
const after = normalized.slice(idx + marker.length);
const slash = after.indexOf("/");
return slash === -1 ? after.replace(".ts", "") : after.slice(0, slash);
};
const importModuleOf = function importModuleOf(importPath: string): string {
let cleaned = importPath.replaceAll("\\", "/");
while (cleaned.startsWith(DOTDOT)) {
cleaned = cleaned.slice(DOTDOT.length);
}
const slash = cleaned.indexOf("/");
return slash === -1 ? cleaned.replace(".ts", "") : cleaned.slice(0, slash);
};
interface ModuleContext {
levels: Record<string, number>;
sourceModule: string;
sourceLevel: number;
}
const resolveModuleContext = function resolveModuleContext(context: Rule.RuleContext): ModuleContext | null {
const settings = govlabSettings(context);
const pathMarker = settings.scopes?.backend;
const levels = settings.hostPolicy?.moduleLevels;
if (typeof pathMarker !== "string" || !levels) {
return null;
}
const sourceModule = moduleOf(context.filename, pathMarker);
if (sourceModule === null || !Object.hasOwn(levels, sourceModule)) {
return null;
}
return { levels, sourceLevel: levels[sourceModule] ?? 0, sourceModule };
};
const checkImport = function checkImport(context: Rule.RuleContext, ctx: ModuleContext, node: Rule.Node): void {
const importPath = asNode(node)?.source?.value;
if (typeof importPath !== "string" || !importPath.startsWith(".")) {
return;
}
const targetModule = importModuleOf(importPath);
if (targetModule === ctx.sourceModule || !Object.hasOwn(ctx.levels, targetModule)) {
return;
}
const targetLevel = ctx.levels[targetModule];
if (typeof targetLevel === "number" && targetLevel > ctx.sourceLevel) {
context.report({
data: {
sourceLevel: String(ctx.sourceLevel),
sourceModule: ctx.sourceModule,
targetLevel: String(targetLevel),
targetModule,
},
messageId: "noUpwardDependency",
node,
});
}
};
const onExportNamed = function onExportNamed(context: Rule.RuleContext, ctx: ModuleContext, node: Rule.Node): void {
if (asNode(node)?.source) {
checkImport(context, ctx, node);
}
};
export default {
create(context: Rule.RuleContext): Rule.RuleListener {
const ctx = resolveModuleContext(context);
if (ctx === null) {
return {};
}
const handlers: [string, (node: Rule.Node) => void][] = [
[
"ExportNamedDeclaration",
(node): void => {
onExportNamed(context, ctx, node);
},
],
[
"ImportDeclaration",
(node): void => {
checkImport(context, ctx, node);
},
],
];
return Object.fromEntries(handlers);
},
meta: govlabMeta({
canonical: ["module-boundaries", "layering"],
description: "Disallow imports from higher-level modules into lower-level modules (dependency inversion)",
messages: {
noUpwardDependency:
"Module '{{sourceModule}}' (level {{sourceLevel}}) must not import from '{{targetModule}}' (level {{targetLevel}}). Dependencies flow downward: constants/database → core → services → web/dashboard → server.",
},
ruleId: "no_upward_dependency",
}),
} satisfies Rule.RuleModule;