fix(dead-code): a type that declare module or declare global merges outward is not dead (#2418)

The viewer's Dead code screen listed TypeScript interfaces that augment a
type declared outside the project: chart.js's `PluginOptionsByType` in
`declare module 'chart.js' { … }` (ghostfolio), `interface Window` in
`declare global { … }` (angular-realworld). The library or the runtime
reads them through the type they merge into, so nothing in the repository
names them, and the index recorded them as unexported file-level
interfaces.

TypeScript exports what such a body declares without the keyword. The
binder makes an ambient module body an export context unless it holds an
export declaration (`export {}`, `export =`, `export default x`), and a
namespace nested in one is ambient too. Both extractors now set isExported
that way: isAmbientExport in languages/typescript.ts, shared by TSX and
ArkTS, and is_ambient_export in the kernel's tsjs walker. The exported rule
then drops them, counted under `exported`, and that count's label now
names them. `declare namespace X` is unchanged: in a module file X belongs
to that file.

The resolver sees them as exported too. A file holding `declare global`
was already treated as exporting, so nothing moves there. A `declare
module` member becomes reachable across files by name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-07 08:21:08 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent d8a7f868b3
commit e0bbb6620f
9 changed files with 412 additions and 9 deletions
+3 -2
View File
@@ -1373,8 +1373,9 @@ const HAS_IMPORT_STATEMENT = /^[ \t]*import[\s{*'"]/m;
* CommonJS shapes cover files that never use ESM syntax at all, in both the dot
* and the bracket form; and `declare global` contributes names to every file
* whether or not the module exports anything of its own. Kept as a source test
* rather than a node scan precisely because `isExported` is set only from an
* `export_statement` ancestor, so `const x = …; export { x }` and
* rather than a node scan precisely because `isExported` is set only where a
* declaration is written (an `export_statement` around it, or a `declare
* module` / `declare global` body), so `const x = …; export { x }` and
* `module.exports = { x }` both read as unexported on the node.
*/
const HAS_ESM_EXPORT = /^[ \t]*export[\s{*]|^[ \t]*declare\s+global\b/m;