1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
//! Import declaration handling for JS/TS AST analysis.
//!
//! This module handles parsing of import declarations including:
//! - Static imports: `import { foo } from './bar'`
//! - Default imports: `import Default from './bar'`
//! - Namespace imports: `import * as NS from './bar'`
//! - Type imports: `import type { Foo } from './bar'`
//! - Side-effect imports: `import './styles.css'`
//!
//! Also handles namespace member access tracking for accurate symbol usage.
//!
//! VibeCrafted with AI Agents (c)2026 Loctree Team
use oxc_ast::ast::*;
use crate::types::{ImportEntry, ImportKind, ImportSymbol};
use super::visitor::JsVisitor;
impl<'a> JsVisitor<'a> {
/// Handle import declaration, extracting symbols and resolving paths.
pub(super) fn handle_import_declaration(&mut self, decl: &ImportDeclaration<'a>) {
let source = decl.source.value.to_string();
let mut entry = ImportEntry::new(source.clone(), ImportKind::Static);
entry.line = Some(self.get_line(decl.span));
entry.resolved_path = self.resolve_path(&source);
entry.is_bare = !source.starts_with('.') && !source.starts_with('/');
if matches!(decl.import_kind, ImportOrExportKind::Type) {
entry.kind = ImportKind::Type;
}
if let Some(specifiers) = &decl.specifiers {
for spec in specifiers {
match spec {
ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => {
entry.symbols.push(ImportSymbol {
name: s.local.name.to_string(),
alias: None,
is_default: true,
});
}
ImportDeclarationSpecifier::ImportSpecifier(s) => {
let name = match &s.imported {
ModuleExportName::IdentifierName(id) => id.name.to_string(),
ModuleExportName::IdentifierReference(id) => id.name.to_string(),
ModuleExportName::StringLiteral(str) => str.value.to_string(),
};
// Fix cmp_owned: compare &str directly
let alias = if *s.local.name != *name {
Some(s.local.name.to_string())
} else {
None
};
entry.symbols.push(ImportSymbol {
name,
alias,
is_default: false,
});
}
ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => {
let alias = s.local.name.to_string();
entry.symbols.push(ImportSymbol {
name: "*".to_string(),
alias: Some(alias.clone()),
is_default: false,
});
// Track namespace import for member expression resolution
self.namespace_imports
.insert(alias, (source.clone(), entry.resolved_path.clone()));
}
}
}
} else {
// Side-effect import
entry.kind = ImportKind::SideEffect;
}
self.analysis.imports.push(entry);
}
/// Handle member expression to track namespace member access.
///
/// This fixes Issue #5 - namespace member access not tracked.
/// When using `NS.member` where NS is from `import * as NS`, we track
/// which members are actually used.
pub(super) fn handle_member_expression(&mut self, member: &MemberExpression<'a>) {
if let MemberExpression::StaticMemberExpression(static_member) = member {
// Check if the object is an identifier (e.g., `NS` in `NS.transform`)
if let Expression::Identifier(obj_ident) = &static_member.object {
let namespace_name = obj_ident.name.to_string();
let member_name = static_member.property.name.to_string();
// Check if this identifier is a namespace import
if let Some((source, _resolved_path)) = self.namespace_imports.get(&namespace_name)
{
// Find the import entry and add this member as a used symbol
for imp in &mut self.analysis.imports {
if &imp.source == source {
// Check if we already have this member symbol
let already_has_member =
imp.symbols.iter().any(|s| s.name == member_name);
if !already_has_member {
// Add the accessed member as an import symbol
imp.symbols.push(ImportSymbol {
name: member_name.clone(),
alias: None,
is_default: false,
});
}
break;
}
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::super::*;
use std::path::Path;
/// Test import alias tracking - the original export name should be in `name`,
/// and the local alias should be in `alias` field.
#[test]
fn test_import_alias_tracking() {
let content = r#"
// Test various import alias patterns
import { Component as MyComponent } from 'react';
import { useState as useStateHook, useEffect } from 'react';
import DefaultExport from './module';
import { originalName as renamedImport } from './utils';
import { default as DefaultWithAlias } from './other';
// Use the imports (to avoid them being marked as unused in other analyses)
MyComponent();
useStateHook();
useEffect();
DefaultExport();
renamedImport();
DefaultWithAlias();
"#;
let analysis = analyze_js_file_ast(
content,
Path::new("src/test.tsx"),
Path::new("src"),
None,
None,
"test.tsx".to_string(),
&CommandDetectionConfig::default(),
);
// Find ALL react imports - they may be separate or merged depending on implementation
let react_imports: Vec<_> = analysis
.imports
.iter()
.filter(|i| i.source == "react")
.collect();
// Collect all react symbols across all import entries
let all_react_symbols: Vec<_> = react_imports
.iter()
.flat_map(|i| i.symbols.iter())
.collect();
// We should have 3 symbols from react: Component, useState, useEffect
assert_eq!(
all_react_symbols.len(),
3,
"Should have 3 symbols from react total"
);
// Check Component as MyComponent
let component_sym = all_react_symbols
.iter()
.find(|s| s.name == "Component")
.expect("Should find Component symbol");
assert_eq!(
component_sym.alias.as_deref(),
Some("MyComponent"),
"Alias should be 'MyComponent'"
);
assert!(
!component_sym.is_default,
"Component is not a default export"
);
// Check useState as useStateHook
let usestate_sym = all_react_symbols
.iter()
.find(|s| s.name == "useState")
.expect("Should find useState symbol");
assert_eq!(
usestate_sym.name, "useState",
"Original export name should be 'useState'"
);
assert_eq!(
usestate_sym.alias.as_deref(),
Some("useStateHook"),
"Alias should be 'useStateHook'"
);
// Check useEffect (no alias)
let useeffect_sym = all_react_symbols
.iter()
.find(|s| s.name == "useEffect")
.expect("Should find useEffect symbol");
assert_eq!(
useeffect_sym.name, "useEffect",
"Name should be 'useEffect'"
);
assert_eq!(useeffect_sym.alias, None, "Should have no alias");
// Check utils import
let utils_import = analysis
.imports
.iter()
.find(|i| i.source == "./utils")
.expect("Should find ./utils import");
let original_sym = utils_import
.symbols
.iter()
.find(|s| s.name == "originalName")
.expect("Should find originalName symbol");
assert_eq!(
original_sym.name, "originalName",
"Original name should be preserved"
);
assert_eq!(
original_sym.alias.as_deref(),
Some("renamedImport"),
"Alias should be 'renamedImport'"
);
// Check { default as DefaultWithAlias } pattern
let other_import = analysis
.imports
.iter()
.find(|i| i.source == "./other")
.expect("Should find ./other import");
let default_alias_sym = &other_import.symbols[0];
assert_eq!(
default_alias_sym.name, "default",
"Should track 'default' as the original export name"
);
assert_eq!(
default_alias_sym.alias.as_deref(),
Some("DefaultWithAlias"),
"Alias should be 'DefaultWithAlias'"
);
assert!(
!default_alias_sym.is_default,
"This is NOT a default import (uses named import syntax)"
);
}
/// Test for Issue #5: Namespace member access should be tracked
/// When using `import * as namespace`, accessing `namespace.member` should be detected
/// as usage of the `member` export from the imported module.
#[test]
fn test_namespace_member_access_tracking() {
let content = r#"
import * as amp from '@sveltejs/amp';
import * as utils from './utils';
// These member accesses should be tracked as using 'transform' and 'helper'
const result = amp.transform(buffer);
utils.helper();
const value = utils.CONSTANT;
"#;
let analysis = analyze_js_file_ast(
content,
Path::new("src/app.ts"),
Path::new("src"),
None,
None,
"app.ts".to_string(),
&CommandDetectionConfig::default(),
);
// Should have 2 imports
assert_eq!(analysis.imports.len(), 2);
// Check the amp import
let amp_import = analysis
.imports
.iter()
.find(|i| i.source == "@sveltejs/amp")
.expect("Should find @sveltejs/amp import");
// Should have both the namespace symbol (*) and the accessed member (transform)
assert_eq!(
amp_import.symbols.len(),
2,
"amp import should have 2 symbols: * and transform"
);
assert!(
amp_import.symbols.iter().any(|s| s.name == "*"),
"amp import should have namespace symbol"
);
assert!(
amp_import.symbols.iter().any(|s| s.name == "transform"),
"amp import should track 'transform' member access"
);
// Check the utils import
let utils_import = analysis
.imports
.iter()
.find(|i| i.source == "./utils")
.expect("Should find ./utils import");
// Should have namespace symbol (*), helper, and CONSTANT
assert_eq!(
utils_import.symbols.len(),
3,
"utils import should have 3 symbols: *, helper, and CONSTANT"
);
assert!(
utils_import.symbols.iter().any(|s| s.name == "*"),
"utils import should have namespace symbol"
);
assert!(
utils_import.symbols.iter().any(|s| s.name == "helper"),
"utils import should track 'helper' member access"
);
assert!(
utils_import.symbols.iter().any(|s| s.name == "CONSTANT"),
"utils import should track 'CONSTANT' member access"
);
}
}