Follow package-local forwarding modules in Typert references
Typert 引用追踪包内转发模块
`WorkspaceAnalyzer` resolves every type reference to its original declaration before classifying it, then reads only the referencing file's own `import` statement to decide whether the reference crossed a package through a public export. A package that re-exports another package's type from one of its own modules, and imports that module by relative path elsewhere, therefore fails with `crosses a package without an e
English
Problem
WorkspaceAnalyzer resolves every type reference to its original declaration before classifying it, then reads only the referencing file's own import statement to decide whether the reference crossed a package through a public export. A package that re-exports another package's type from one of its own modules, and imports that module by relative path elsewhere, therefore fails with crosses a package without an explicit package import although the package import exists one hop away. The failure is deterministic for every batch size and package order; it surfaces in whichever analysis selects the referencing package as a root, which is why issue 3525 observed it as batch-dependent.
Decision
targetForReference resolves a relative specifier through the face's shared compiler host and module-resolution cache and follows it only while the resolved file stays inside the referencing package. In each forwarding module it collects the export edges that carry the requested name: a named re-export with a specifier, an export { local } backed by that module's import, and star re-exports whose module exports the same symbol. Explicit edges are tried before star edges, matching TypeScript's shadowing of star exports, and each resolved module and requested export-name pair is entered once, so circular star re-exports terminate while distinct renamed routes through one module remain available. The walk stops at the first package specifier and feeds that identity and export name to the existing packageExportName check, so a forwarded type must still be public at the package subpath the forwarding module names, and a package name without a registration is refused there. The reference model is unchanged: the target remains declaration for a same-face owner and cross-face for another face.
The walk yields no package import, and the reference fails as before, when a relative specifier resolves outside the referencing package, when the only edge carrying the name is a namespace re-export or a re-exported namespace import, or when every edge loops back to a module and requested-name pair already entered.
Alternatives considered
Treat a relative import whose alias chain ends in another package as implicitly public. Rejected: it would accept ../../other/src/file.ts and any forwarding module that itself reaches the other package by relative path, removing the public-export check the generated Remote declarations rely on to name an importable subpath.
Record the forwarding module as the reference target. Rejected: emitters and cross-face links need the original declaration's package and public subpath; a package-local module has no public identity of its own.
Select edges in source order without symbol checks. Rejected: a star re-export that loops back to an earlier module can precede the explicit re-export that actually carries the type, and TypeScript itself lets explicit exports shadow star exports; ordering explicit edges first and continuing past an entered module and requested-name pair keeps such modules accepted without an unbounded walk.
Make batched and whole-workspace analysis select the same roots. Rejected as a fix: root selection does not change the verdict on a reference, only whether the reference is visited, so aligning the callers would hide the incorrect classification rather than remove it.
Consequences
Packages may keep one forwarding module for foreign types and import it relatively, matching how their own modules are organized. Each cross-package relative reference costs one module resolution per hop through the face's shared resolution cache; reachableFiles now resolves through the same cache. type-model.spec.ts pins named, renamed multi-hop, import-then-export, star, and namespace-import forwarding, an explicit re-export beside a looping star edge, distinct renamed routes through one shared module, a forwarded private export, a forwarding module that crosses by relative path, a cycle whose only exit crosses by relative path, a namespace re-export, a re-exported namespace import, cross-face forwarding, and equality of whole and batched analysis for the forwarding fixture across batch sizes and package orders.
中文
Problem
WorkspaceAnalyzer 先把每个类型引用解析到原始声明再分类,然后只读引用所在文件自己的 import 语句来判断该引用是否经由公开导出跨包。一个包若在自己的某个模块里重新导出另一个包的类型,并在别处用相对路径导入该模块,就会报 crosses a package without an explicit package import,尽管包导入只隔一跳。这个失败在任何批次大小和包顺序下都会稳定出现;它出现在哪次分析里,取决于哪次分析把引用方的包选为根,因此 issue 3525 观察到的现象像是与批次相关。
Decision
targetForReference 通过该 face 共享的编译器宿主及其模块解析缓存来解析相对说明符,且只在解析到的文件仍位于引用方包内时继续追踪。在每个转发模块里,它收集承载所请求名字的 export 边:带说明符的具名重新导出、由该模块自身 import 支撑的 export { local },以及导出同一符号的星号重新导出。显式边先于星号边尝试,与 TypeScript 中显式导出遮蔽星号导出的规则一致;解析后的模块与请求导出名组成的每个组合只进入一次,因此循环的星号重新导出能够终止,经同一模块转发的不同改名路径仍可继续尝试。追踪在遇到第一个包说明符时停止,并把该包身份和导出名交给现有的 packageExportName 检查,因此被转发的类型仍必须在转发模块所写的包子路径上公开,没有登记的包名也在此被拒绝。引用模型不变:同 face 的所有者仍是 declaration,另一 face 仍是 cross-face。
当相对说明符解析到引用方包之外、承载该名字的唯一边是命名空间重新导出或被重新导出的命名空间导入,或所有边都回到已进入的模块与请求名组合时,追踪得不到包导入,引用照旧失败。
Alternatives considered
把别名链终点在另一个包的相对导入视为隐式公开。 已拒绝:这会接受 ../../other/src/file.ts,也会接受自身用相对路径抵达另一个包的转发模块,从而取消公开导出检查,而生成的 Remote 声明依赖该检查来命名可导入的子路径。
把转发模块记为引用目标。 已拒绝:发射器和跨 face 链接需要原始声明的包和公开子路径,包内模块没有自己的公开身份。
按源码顺序选边且不校验符号。 已拒绝:回到更早模块的星号重新导出可能排在真正承载该类型的显式重新导出之前,而 TypeScript 本身允许显式导出遮蔽星号导出;显式边优先并跳过已进入的模块与请求名组合,既能接受这类模块,又不会无限追踪。
让分批分析与全工作区分析选择相同的根。 作为修复方案已拒绝:根的选择不改变对一个引用的判定,只决定该引用是否被访问,对齐调用方只会掩盖错误分类,不能消除它。
Consequences
包可以为外部类型保留一个转发模块并用相对路径导入它,与自身模块的组织方式一致。每个跨包相对引用每跳付出一次经该 face 共享解析缓存的模块解析;reachableFiles 现在也通过同一缓存解析。type-model.spec.ts 固定了具名、改名多跳、先导入再导出、星号和命名空间导入这几种转发,与回环星号边并存的显式重新导出,经同一模块转发的不同改名路径,被转发的私有导出,用相对路径跨包的转发模块,唯一出口用相对路径跨包的循环,命名空间重新导出,被重新导出的命名空间导入,跨 face 转发,以及转发 fixture 在不同批次大小和包顺序下全量分析与分批分析相等。