mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
feat(scripts): report aliasing re-exports (#121849)
This commit is contained in:
committed by
GitHub
parent
df3e111c91
commit
98b222ded8
@@ -10,6 +10,7 @@ import {
|
|||||||
isTestLikeTypeScriptFile,
|
isTestLikeTypeScriptFile,
|
||||||
resolveSourceRoots,
|
resolveSourceRoots,
|
||||||
runAsScript,
|
runAsScript,
|
||||||
|
toLine,
|
||||||
unwrapExpression,
|
unwrapExpression,
|
||||||
} from "./lib/ts-guard-utils.mts";
|
} from "./lib/ts-guard-utils.mts";
|
||||||
|
|
||||||
@@ -37,12 +38,18 @@ export type NamedReExport = {
|
|||||||
moduleSpecifier: string;
|
moduleSpecifier: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AliasingReExport = NamedReExport & {
|
||||||
|
line: number;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type ExportedValueDefinition = {
|
export type ExportedValueDefinition = {
|
||||||
importedReferences: ImportedSymbolReference[];
|
importedReferences: ImportedSymbolReference[];
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ModuleExports = {
|
export type ModuleExports = {
|
||||||
|
aliasingReExports: Array<Omit<AliasingReExport, "path">>;
|
||||||
definitions: Set<string>;
|
definitions: Set<string>;
|
||||||
exportedNames: Set<string>;
|
exportedNames: Set<string>;
|
||||||
namedReExports: NamedReExport[];
|
namedReExports: NamedReExport[];
|
||||||
@@ -330,6 +337,7 @@ export function collectModuleExportNames(content: string, fileName = "source.ts"
|
|||||||
const directlyExportedNames = new Set<string>();
|
const directlyExportedNames = new Set<string>();
|
||||||
const locallyExportedNames = new Set<string>();
|
const locallyExportedNames = new Set<string>();
|
||||||
const exportedNames = new Set<string>();
|
const exportedNames = new Set<string>();
|
||||||
|
const aliasingReExports: Array<Omit<AliasingReExport, "path">> = [];
|
||||||
const namedReExports: NamedReExport[] = [];
|
const namedReExports: NamedReExport[] = [];
|
||||||
const pendingLocalReExports: Array<{ exportedName: string; localName: string }> = [];
|
const pendingLocalReExports: Array<{ exportedName: string; localName: string }> = [];
|
||||||
const starExportSpecifiers: string[] = [];
|
const starExportSpecifiers: string[] = [];
|
||||||
@@ -442,15 +450,18 @@ export function collectModuleExportNames(content: string, fileName = "source.ts"
|
|||||||
}
|
}
|
||||||
const localName = specifier.propertyName?.text ?? specifier.name.text;
|
const localName = specifier.propertyName?.text ?? specifier.name.text;
|
||||||
if (statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)) {
|
if (statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)) {
|
||||||
namedReExports.push({
|
const reExport = {
|
||||||
exportedName: specifier.name.text,
|
exportedName: specifier.name.text,
|
||||||
importedName: localName,
|
importedName: localName,
|
||||||
moduleSpecifier: statement.moduleSpecifier.text,
|
moduleSpecifier: statement.moduleSpecifier.text,
|
||||||
});
|
};
|
||||||
|
namedReExports.push(reExport);
|
||||||
|
if (reExport.exportedName !== reExport.importedName) {
|
||||||
|
aliasingReExports.push({ ...reExport, line: toLine(sourceFile, specifier) });
|
||||||
|
}
|
||||||
} else if (!statement.moduleSpecifier) {
|
} else if (!statement.moduleSpecifier) {
|
||||||
pendingLocalReExports.push({ exportedName: specifier.name.text, localName });
|
pendingLocalReExports.push({ exportedName: specifier.name.text, localName });
|
||||||
}
|
}
|
||||||
// Renamed re-exports are deliberately outside this guard's first slice.
|
|
||||||
if (specifier.name.text !== localName) {
|
if (specifier.name.text !== localName) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -544,6 +555,7 @@ export function collectModuleExportNames(content: string, fileName = "source.ts"
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
aliasingReExports,
|
||||||
definitions,
|
definitions,
|
||||||
exportedNames,
|
exportedNames,
|
||||||
namedReExports: namedReExports.toSorted((left, right) =>
|
namedReExports: namedReExports.toSorted((left, right) =>
|
||||||
@@ -612,8 +624,8 @@ function collectTransitiveExportNames(
|
|||||||
// exact module. Flagging them would push burn-down work to "fix" a deliberate idiom.
|
// exact module. Flagging them would push burn-down work to "fix" a deliberate idiom.
|
||||||
const intentionalSameNameFamilies = new Set(["testing", "testApi"]);
|
const intentionalSameNameFamilies = new Set(["testing", "testApi"]);
|
||||||
|
|
||||||
/** Finds duplicate exported function/const definitions across source modules. */
|
function analyzeExportNames(modules: SourceModule[]) {
|
||||||
export function findExportNameCollisions(modules: SourceModule[]): ExportNameCollision[] {
|
const aliasingReExports: AliasingReExport[] = [];
|
||||||
const filesByName = new Map<string, Set<string>>();
|
const filesByName = new Map<string, Set<string>>();
|
||||||
const sdkExportNames = new Set<string>();
|
const sdkExportNames = new Set<string>();
|
||||||
const modulesByPath = new Map<string, ModuleExports>();
|
const modulesByPath = new Map<string, ModuleExports>();
|
||||||
@@ -623,6 +635,14 @@ export function findExportNameCollisions(modules: SourceModule[]): ExportNameCol
|
|||||||
const relativePath = normalizeRelativePath(sourceModule.path);
|
const relativePath = normalizeRelativePath(sourceModule.path);
|
||||||
const moduleExports = collectModuleExportNames(sourceModule.content, relativePath);
|
const moduleExports = collectModuleExportNames(sourceModule.content, relativePath);
|
||||||
modulesByPath.set(relativePath, moduleExports);
|
modulesByPath.set(relativePath, moduleExports);
|
||||||
|
if (sourceModule.includeDefinitions !== false && !relativePath.startsWith("src/plugin-sdk/")) {
|
||||||
|
aliasingReExports.push(
|
||||||
|
...moduleExports.aliasingReExports.map((reExport) => ({
|
||||||
|
...reExport,
|
||||||
|
path: relativePath,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
if (sourceModule.includeDefinitions !== false) {
|
if (sourceModule.includeDefinitions !== false) {
|
||||||
for (const name of moduleExports.definitions) {
|
for (const name of moduleExports.definitions) {
|
||||||
const files = filesByName.get(name) ?? new Set<string>();
|
const files = filesByName.get(name) ?? new Set<string>();
|
||||||
@@ -654,7 +674,25 @@ export function findExportNameCollisions(modules: SourceModule[]): ExportNameCol
|
|||||||
}
|
}
|
||||||
collisions.push(collision);
|
collisions.push(collision);
|
||||||
}
|
}
|
||||||
return collisions.toSorted((left, right) => left.name.localeCompare(right.name));
|
return {
|
||||||
|
aliasingReExports: aliasingReExports.toSorted(
|
||||||
|
(left, right) =>
|
||||||
|
left.path.localeCompare(right.path) ||
|
||||||
|
left.line - right.line ||
|
||||||
|
left.exportedName.localeCompare(right.exportedName),
|
||||||
|
),
|
||||||
|
collisions: collisions.toSorted((left, right) => left.name.localeCompare(right.name)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Finds direct renamed re-exports outside the Plugin SDK boundary. */
|
||||||
|
export function findAliasingReExports(modules: SourceModule[]) {
|
||||||
|
return analyzeExportNames(modules).aliasingReExports;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Finds duplicate exported function/const definitions across source modules. */
|
||||||
|
export function findExportNameCollisions(modules: SourceModule[]): ExportNameCollision[] {
|
||||||
|
return analyzeExportNames(modules).collisions;
|
||||||
}
|
}
|
||||||
|
|
||||||
type CollisionChange = {
|
type CollisionChange = {
|
||||||
@@ -702,7 +740,7 @@ function resolveBaselinePath(repoRoot: string) {
|
|||||||
return path.join(repoRoot, ...baselineRelativePath.split("/"));
|
return path.join(repoRoot, ...baselineRelativePath.split("/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function collectRepositoryCollisions(repoRoot: string) {
|
async function collectRepositoryModules(repoRoot: string) {
|
||||||
const sourceCollectOptions = {
|
const sourceCollectOptions = {
|
||||||
fileExtensions: [".ts", ".mts", ".js", ".mjs"],
|
fileExtensions: [".ts", ".mts", ".js", ".mjs"],
|
||||||
includeTests: true,
|
includeTests: true,
|
||||||
@@ -735,7 +773,15 @@ export async function collectRepositoryCollisions(repoRoot: string) {
|
|||||||
path: normalizeRelativePath(path.relative(repoRoot, filePath)),
|
path: normalizeRelativePath(path.relative(repoRoot, filePath)),
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
return findExportNameCollisions(modules);
|
return modules;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectRepositoryExportAnalysis(repoRoot: string) {
|
||||||
|
return analyzeExportNames(await collectRepositoryModules(repoRoot));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function collectRepositoryCollisions(repoRoot: string) {
|
||||||
|
return (await collectRepositoryExportAnalysis(repoRoot)).collisions;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readBaseline(repoRoot: string) {
|
async function readBaseline(repoRoot: string) {
|
||||||
@@ -751,8 +797,7 @@ async function readBaseline(repoRoot: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function writeBaseline(repoRoot: string) {
|
async function writeBaseline(repoRoot: string, collisions: ExportNameCollision[]) {
|
||||||
const collisions = await collectRepositoryCollisions(repoRoot);
|
|
||||||
await fs.writeFile(resolveBaselinePath(repoRoot), `${JSON.stringify(collisions, null, 2)}\n`);
|
await fs.writeFile(resolveBaselinePath(repoRoot), `${JSON.stringify(collisions, null, 2)}\n`);
|
||||||
return collisions.length;
|
return collisions.length;
|
||||||
}
|
}
|
||||||
@@ -761,11 +806,25 @@ function formatCollision(collision: ExportNameCollision | undefined) {
|
|||||||
return JSON.stringify(collision);
|
return JSON.stringify(collision);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function printAliasingReExports(reExports: AliasingReExport[]) {
|
||||||
|
if (reExports.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("Aliasing re-exports outside src/plugin-sdk/ (informational):");
|
||||||
|
for (const reExport of reExports) {
|
||||||
|
console.log(
|
||||||
|
`- ${reExport.path}:${reExport.line}: ${reExport.importedName} as ${reExport.exportedName} from ${JSON.stringify(reExport.moduleSpecifier)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function main() {
|
export async function main() {
|
||||||
const repoRoot = resolveRepoRoot(import.meta.url);
|
const repoRoot = resolveRepoRoot(import.meta.url);
|
||||||
if (process.argv.includes("--update-debt-baseline")) {
|
if (process.argv.includes("--update-debt-baseline")) {
|
||||||
const count = await writeBaseline(repoRoot);
|
const analysis = await collectRepositoryExportAnalysis(repoRoot);
|
||||||
|
const count = await writeBaseline(repoRoot, analysis.collisions);
|
||||||
console.log(`Wrote ${baselineRelativePath} (${count} entries)`);
|
console.log(`Wrote ${baselineRelativePath} (${count} entries)`);
|
||||||
|
printAliasingReExports(analysis.aliasingReExports);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -776,8 +835,9 @@ export async function main() {
|
|||||||
);
|
);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const current = await collectRepositoryCollisions(repoRoot);
|
const analysis = await collectRepositoryExportAnalysis(repoRoot);
|
||||||
const debt = compareExportNameCollisionDebt(current, baseline);
|
const debt = compareExportNameCollisionDebt(analysis.collisions, baseline);
|
||||||
|
printAliasingReExports(analysis.aliasingReExports);
|
||||||
if (debt.regressions.length === 0 && debt.improvements.length === 0) {
|
if (debt.regressions.length === 0 && debt.improvements.length === 0) {
|
||||||
console.log("export name collision guard passed.");
|
console.log("export name collision guard passed.");
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
collectModuleExportNames,
|
collectModuleExportNames,
|
||||||
collectRepositoryCollisions,
|
collectRepositoryCollisions,
|
||||||
compareExportNameCollisionDebt,
|
compareExportNameCollisionDebt,
|
||||||
|
findAliasingReExports,
|
||||||
findExportNameCollisions,
|
findExportNameCollisions,
|
||||||
isExcludedExportCollisionSource,
|
isExcludedExportCollisionSource,
|
||||||
} from "../../scripts/check-export-name-collisions.mts";
|
} from "../../scripts/check-export-name-collisions.mts";
|
||||||
@@ -64,6 +65,45 @@ describe("export name collision guard", () => {
|
|||||||
expect([...result.exportedNames]).toEqual(["importedValue", "remoteValue"]);
|
expect([...result.exportedNames]).toEqual(["importedValue", "remoteValue"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("reports direct aliasing re-exports only outside the Plugin SDK", () => {
|
||||||
|
expect(
|
||||||
|
findAliasingReExports([
|
||||||
|
{
|
||||||
|
path: "src/alias.ts",
|
||||||
|
content: `
|
||||||
|
export { original } from "./source.js";
|
||||||
|
export type { OriginalType as RenamedType } from "./source.js";
|
||||||
|
export { original as renamed } from "./source.js";
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "src/local-alias.ts",
|
||||||
|
content: `
|
||||||
|
import { original } from "./source.js";
|
||||||
|
export { original as locallyRenamed };
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "src/plugin-sdk/alias.ts",
|
||||||
|
content: 'export { original as sanctioned } from "../source.js";',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "packages/support.ts",
|
||||||
|
content: 'export { original as packageAlias } from "./source.js";',
|
||||||
|
includeDefinitions: false,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
exportedName: "renamed",
|
||||||
|
importedName: "original",
|
||||||
|
line: 4,
|
||||||
|
moduleSpecifier: "./source.js",
|
||||||
|
path: "src/alias.ts",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("exempts exact function and const same-name forwarders", () => {
|
it("exempts exact function and const same-name forwarders", () => {
|
||||||
const forwarders = [
|
const forwarders = [
|
||||||
`
|
`
|
||||||
|
|||||||
Reference in New Issue
Block a user