/** * @license * Copyright 2026 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Disallows runtime (value) imports from `packages/core/src/utils/` * production modules into modules outside the utils/ directory. Type-only * imports are permitted because they are erased at compile time and therefore * introduce no runtime upward dependency. * * The goal is a leaf utils/ layer: every runtime dependency of a utils module * must be a sibling utils module (or an external/npm package). A small * allowlist carries the deferred inversions that cannot be leafed without a * larger refactor (`Storage` and `getTraceContext` are stateful and live * behind `debugLogger`). */ import { readFileSync } from 'node:fs'; import path from 'node:path'; import { URL } from 'node:url'; const CORE_SRC_MARKER = 'packages/core/src/'; const UTILS_SRC_MARKER = 'packages/core/src/utils/'; const CORE_PACKAGE_SPECIFIER = '@qwen-code/qwen-code-core'; const CORE_PACKAGE_SRC_PREFIX = `${CORE_PACKAGE_SPECIFIER}/src/`; const CORE_PACKAGE_DIST_PREFIX = `${CORE_PACKAGE_SPECIFIER}/dist/`; const CORE_PACKAGE_SUBPATH_PREFIX = `${CORE_PACKAGE_SPECIFIER}/`; // Resolve named self-reference subpaths from the package contract so this // boundary cannot drift from packages/core/package.json. const CORE_PACKAGE_EXPORTS = JSON.parse( readFileSync( new URL('../packages/core/package.json', import.meta.url), 'utf8', ), ).exports; // Deferred inversions, keyed by the utils-relative importer. Targets are // relative to packages/core/src and omit their extension. See the file header // for why these are tolerated rather than moved. const ALLOWED_UPWARD_IMPORTS = new Map([ ['debugLogger.ts', new Set(['config/storage', 'telemetry/trace-context'])], ]); function isUtilsProductionFile(filename) { if (!filename || filename === '' || filename === '') { return false; } const normalized = path.normalize(filename).replaceAll('\\', '/'); const start = normalized.lastIndexOf(UTILS_SRC_MARKER); if (start < 0) return false; const relativePath = normalized.slice(start + UTILS_SRC_MARKER.length); return !/\.(test|spec)\.[cm]?[jt]sx?$/.test(relativePath); } function coreSrcAbs(filename) { const normalized = path.normalize(filename).replaceAll('\\', '/'); const start = normalized.lastIndexOf(CORE_SRC_MARKER); if (start < 0) return null; return path.resolve(normalized.slice(0, start + CORE_SRC_MARKER.length)); } function stripExtension(rel) { return rel.replace(/\.(js|ts|tsx|mjs|cjs)$/, ''); } function exportTargetOf(entry) { return typeof entry === 'string' ? entry : entry?.import; } // Node resolves a subpath against `exports` by exact key first, then by // pattern: among keys containing a single `*`, the one with the longest // literal prefix wins; ties prefer the longer full key. What `*` captured is // substituted into the target. // The rule has to follow the same order, because `packages/core/package.json` // carries a `./*` catch-all — without pattern matching every deep specifier // misses the exact lookup, resolves fine at runtime, and is reported by // nothing. export function resolveExportTarget( exportKey, packageExports = CORE_PACKAGE_EXPORTS, ) { const exact = exportTargetOf(packageExports[exportKey]); if (typeof exact === 'string') return exact; let best = null; for (const [pattern, entry] of Object.entries(packageExports)) { const star = pattern.indexOf('*'); if (star < 0 || pattern.indexOf('*', star + 1) >= 0) continue; const prefix = pattern.slice(0, star); const suffix = pattern.slice(star + 1); if (!exportKey.startsWith(prefix)) continue; if (!exportKey.endsWith(suffix)) continue; if (exportKey.length < prefix.length + suffix.length) continue; if ( best && (best.prefix.length > prefix.length || (best.prefix.length === prefix.length && best.pattern.length >= pattern.length)) ) { continue; } best = { pattern, prefix, suffix, entry }; } if (!best) return null; const target = exportTargetOf(best.entry); if (typeof target !== 'string' || !target.includes('*')) return null; const captured = exportKey.slice( best.prefix.length, exportKey.length - best.suffix.length, ); return target.replace('*', captured); } function corePackageSourcePath(importedPath) { if (importedPath.startsWith(CORE_PACKAGE_SRC_PREFIX)) { return importedPath.slice(CORE_PACKAGE_SRC_PREFIX.length); } if (importedPath.startsWith(CORE_PACKAGE_DIST_PREFIX)) { const distRelative = importedPath.slice(CORE_PACKAGE_DIST_PREFIX.length); return distRelative.startsWith('src/') ? distRelative.slice('src/'.length) : distRelative; } if (!importedPath.startsWith(CORE_PACKAGE_SUBPATH_PREFIX)) { return null; } const exportKey = `./${importedPath.slice(CORE_PACKAGE_SUBPATH_PREFIX.length)}`; const exportTarget = resolveExportTarget(exportKey); if (typeof exportTarget !== 'string') { return null; } if (exportTarget.startsWith('./dist/src/')) { return exportTarget.slice('./dist/src/'.length); } if (exportTarget.startsWith('./src/')) { return exportTarget.slice('./src/'.length); } return null; } export default { meta: { type: 'problem', docs: { description: 'Disallow runtime upward imports from core utils modules.', }, schema: [], messages: { noCoreUtilsUpwardImport: "Core utils module '{{file}}' imports runtime value '{{importedPath}}' from outside utils/. Move the value into utils/ (or re-export it from its owner module) so utils/ stays a leaf layer. Type-only imports are allowed.", }, }, create(context) { const filename = context.filename; if (!isUtilsProductionFile(filename)) { return {}; } const srcRoot = coreSrcAbs(filename); if (!srcRoot) { return {}; } const utilsRoot = path.join(srcRoot, 'utils'); const importer = path .relative(utilsRoot, path.resolve(filename)) .replaceAll('\\', '/'); function reportIfUpward(sourceNode, importedPath) { if (typeof importedPath !== 'string') { return; } let resolved; if (importedPath.startsWith('.')) { resolved = path.resolve(path.dirname(filename), importedPath); } else { const sourcePath = corePackageSourcePath(importedPath); if (!sourcePath) { return; } resolved = path.resolve(srcRoot, sourcePath); } // Leave cross-package relative imports to no-relative-cross-package-imports. const relToCore = path.relative(srcRoot, resolved).replaceAll('\\', '/'); if (relToCore.startsWith('..') || path.isAbsolute(relToCore)) { return; } if ( ALLOWED_UPWARD_IMPORTS.get(importer)?.has(stripExtension(relToCore)) ) { return; } const relToUtils = path.relative(utilsRoot, resolved); if (relToUtils.startsWith('..') || path.isAbsolute(relToUtils)) { context.report({ node: sourceNode, messageId: 'noCoreUtilsUpwardImport', data: { file: path.relative(utilsRoot, filename), importedPath, }, }); } } function checkSource(node) { if (node.source && typeof node.source.value === 'string') { reportIfUpward(node.source, node.source.value); } } function checkDynamicImport(node) { const source = node.source; if (source.type === 'Literal') { reportIfUpward(source, source.value); } else if ( source.type === 'TemplateLiteral' && source.quasis.length === 1 ) { reportIfUpward(source, source.quasis[0].value.cooked); } } return { ImportDeclaration(node) { if (node.importKind === 'type') return; checkSource(node); }, ExportNamedDeclaration(node) { if (node.exportKind === 'type') return; checkSource(node); }, ExportAllDeclaration(node) { if (node.exportKind === 'type') return; checkSource(node); }, ImportExpression: checkDynamicImport, }; }, };