0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-07 13:19:19 +08:00
discourse/frontend/lint-rules/no-cross-group-internals.mjs
Sérgio Saquetim 72d0b4b455
DEV: Catch internals reach-ins behind an interpolated import suffix
An interpolated dynamic import whose fixed prefix already names an
`-internals` path proves the violation before any expression runs, so
the rule now judges such prefixes by their complete segments and applies
the ordinary containment check; a specifier whose group is itself
interpolated remains beyond static analysis, and the policy doc states
that exact boundary.
2026-08-05 12:52:02 -03:00

150 lines
4.2 KiB
JavaScript
Vendored

import { posix as path } from "node:path";
const INTERNALS_SEGMENT = "-internals";
function isWithin(candidate, directory) {
return candidate === directory || candidate.startsWith(`${directory}/`);
}
function normalizedFilename(context) {
return path.normalize(context.filename.replaceAll("\\", "/"));
}
function moduleIdFor(filename) {
const segments = filename.split("/");
const appIndex = segments.lastIndexOf("app");
if (appIndex === -1 || appIndex === segments.length - 1) {
return;
}
return ["discourse", ...segments.slice(appIndex + 1)].join("/");
}
function mayImportInternals(filename, specifier) {
const specifierSegments = specifier.split("/");
// Locate the internals segment on the NORMALIZED target, never on the raw
// specifier: `./-internals/../../other-group/-internals/x` names this
// group's internals as a prefix while resolving into another group's.
if (specifierSegments[0] === "." || specifierSegments[0] === "..") {
const importerPath = path.resolve(filename);
const resolved = path.resolve(path.dirname(importerPath), specifier);
const segments = resolved.split("/");
const internalsIndex = segments.indexOf(INTERNALS_SEGMENT);
if (internalsIndex === -1) {
return true;
}
const groupDirectory = segments.slice(0, internalsIndex).join("/");
if (!groupDirectory.split("/").includes("ui-kit")) {
return true;
}
return isWithin(importerPath, groupDirectory);
}
const segments = path.normalize(specifier).split("/");
const internalsIndex = segments.indexOf(INTERNALS_SEGMENT);
if (internalsIndex === -1) {
return true;
}
const groupId = segments.slice(0, internalsIndex).join("/");
if (!groupId.split("/").includes("ui-kit")) {
return true;
}
const importerModuleId = moduleIdFor(filename);
return Boolean(importerModuleId) && isWithin(importerModuleId, groupId);
}
/**
* The statically known module specifier of an import/export source, covering
* string literals and no-substitution template literals.
*
* An interpolated template has no full static value, but its FIXED PREFIX can
* already prove an internals reach-in — `.../-internals/${name}` names the
* group before any expression runs. In that case the prefix is returned with
* a placeholder leaf, so the ordinary containment check applies. A specifier
* whose group is itself interpolated stays undecidable and returns undefined;
* that limit is deliberate and pinned by the tests.
*/
function staticSpecifierOf(source) {
if (!source) {
return;
}
if (typeof source.value === "string") {
return source.value;
}
if (source.type !== "TemplateLiteral") {
return;
}
if (source.expressions.length === 0 && source.quasis.length === 1) {
return source.quasis[0].value.cooked;
}
const prefix = source.quasis[0]?.value.cooked ?? "";
// Only complete segments count: a prefix ending mid-segment ("-inter…")
// proves nothing about where the specifier lands.
const completeSegments = prefix.split("/").slice(0, -1);
if (completeSegments.includes(INTERNALS_SEGMENT)) {
return [...completeSegments, "dynamic-leaf"].join("/");
}
}
export default {
meta: {
type: "problem",
messages: {
crossGroupInternals:
"Do not import another group's -internals modules; use its public component.",
},
schema: [],
},
create(context) {
const filename = normalizedFilename(context);
if (filename.split("/").includes("tests")) {
return {};
}
function checkSource(node) {
const specifier = staticSpecifierOf(node.source);
if (typeof specifier !== "string") {
return;
}
// A target can only land inside an internals directory by naming the
// segment, so a specifier without it needs no resolution at all.
if (
!specifier.split("/").includes(INTERNALS_SEGMENT) ||
mayImportInternals(filename, specifier)
) {
return;
}
context.report({
node: node.source,
messageId: "crossGroupInternals",
});
}
return {
ExportAllDeclaration: checkSource,
ExportNamedDeclaration: checkSource,
ImportDeclaration: checkSource,
ImportExpression: checkSource,
};
},
};