mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-16 20:53:57 +08:00
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:
@@ -1030,7 +1030,8 @@ impl<'t> Walker<'t> {
|
||||
None
|
||||
}
|
||||
|
||||
/// isExported: walk the parent chain for an export_statement.
|
||||
/// isExported: walk the parent chain for an export_statement, then ask
|
||||
/// whether a `declare module '…'` / `declare global` body exports it.
|
||||
fn is_exported(&self, node: Node) -> bool {
|
||||
let mut cur = node.parent();
|
||||
while let Some(p) = cur {
|
||||
@@ -1039,7 +1040,7 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
cur = p.parent();
|
||||
}
|
||||
false
|
||||
is_ambient_export(node)
|
||||
}
|
||||
|
||||
fn has_keyword_child(&self, node: Node, kw: &str) -> bool {
|
||||
@@ -1136,6 +1137,48 @@ fn body_of(node: Node) -> Option<Node> {
|
||||
resolve_field_body(node).or_else(|| node.child_by_field_name("body"))
|
||||
}
|
||||
|
||||
/// isAmbientExport (languages/typescript.ts): declared in a `declare module
|
||||
/// 'x' { … }` or `declare global { … }` body, which TypeScript exports without
|
||||
/// an `export` keyword unless the body holds an export declaration of its own.
|
||||
fn is_ambient_export(node: Node) -> bool {
|
||||
let mut cur = node.parent();
|
||||
while let Some(p) = cur {
|
||||
if p.kind() == "statement_block" && has_export_declaration(p) {
|
||||
return false;
|
||||
}
|
||||
if p.kind() == "ambient_declaration" {
|
||||
// `declare global` is the keyword and a bare block, with no field names.
|
||||
for i in 0..p.child_count() {
|
||||
if p.child(i).is_some_and(|c| c.kind() == "global") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if let Some(declared) = p.named_child(0) {
|
||||
if declared.kind() == "module"
|
||||
&& declared.child_by_field_name("name").is_some_and(|n| n.kind() == "string")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
cur = p.parent();
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// hasExportDeclaration: a statement that exports by name or by assignment
|
||||
/// (`export {…}`, `export =`, `export default x`), not by declaring.
|
||||
fn has_export_declaration(block: Node) -> bool {
|
||||
for i in 0..block.named_child_count() {
|
||||
if let Some(statement) = block.named_child(i) {
|
||||
if statement.kind() == "export_statement" && statement.child_by_field_name("declaration").is_none() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn opt_str(arena: &mut Arena, s: Option<&str>) -> StrRef {
|
||||
match s {
|
||||
Some(s) => arena.put(s),
|
||||
|
||||
Reference in New Issue
Block a user