Skip to main content

aft/imports/
mod.rs

1//! Import analysis engine: parsing, grouping, deduplication, and insertion.
2//!
3//! Per-language behavior is provided by [`ImportSyntax`] implementations,
4//! resolved through the [`syntax_for`] registry. Each engine extracts imports
5//! from tree-sitter ASTs, classifies them into groups, and generates import
6//! text. A single import's structured shape is carried by [`ImportForm`].
7//!
8//! Currently supports: TypeScript, TSX, JavaScript, Python, Rust, Go.
9
10use std::ops::Range;
11
12use tree_sitter::{Node, Parser, Tree};
13
14use crate::parser::{grammar_for, LangId};
15
16mod c;
17pub(crate) use c::{classify_group_c_import_kind, normalize_include_module};
18mod csharp;
19mod java;
20mod kotlin;
21mod lua;
22pub(crate) mod perl;
23mod php;
24pub(crate) use php::{
25    php_grouped_use_matches_module, php_grouped_use_shares_prefix, php_import_matches_module,
26    rewrite_php_import_without_module,
27};
28pub(crate) mod ruby;
29mod scala;
30pub(crate) use scala::scala_block_uses_scala2_dialect;
31mod swift;
32
33// ---------------------------------------------------------------------------
34// Shared types
35// ---------------------------------------------------------------------------
36
37/// What kind of import this is.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum ImportKind {
40    /// `import { X } from 'y'` or `import X from 'y'`
41    Value,
42    /// `import type { X } from 'y'`
43    Type,
44    /// `import './side-effect'`
45    SideEffect,
46}
47
48/// Which logical group an import belongs to (language-specific).
49///
50/// Ordering matches conventional import group sorting:
51///   Stdlib (first) < External < Internal (last)
52///
53/// Language mapping:
54///   - TS/JS/TSX: External (no `.` prefix), Internal (`.`/`..` prefix)
55///   - Python:    Stdlib, External (third-party), Internal (relative `.`/`..`)
56///   - Rust:      Stdlib (std/core/alloc), External (crates), Internal (crate/self/super)
57///   - Go:        Stdlib (no dots in path), External (dots in path)
58#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
59pub enum ImportGroup {
60    /// Standard library (Python stdlib, Rust std/core/alloc, Go stdlib).
61    /// TS/JS don't use this group.
62    Stdlib,
63    /// External/third-party packages.
64    External,
65    /// Internal/relative imports (TS relative, Python local, Rust crate/self/super).
66    Internal,
67}
68
69impl ImportGroup {
70    /// Human-readable label for the group.
71    pub fn label(&self) -> &'static str {
72        match self {
73            ImportGroup::Stdlib => "stdlib",
74            ImportGroup::External => "external",
75            ImportGroup::Internal => "internal",
76        }
77    }
78}
79
80/// One clause in a PHP `use` declaration.
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct PhpImportClause {
83    /// Fully qualified imported symbol for this clause.
84    pub module_path: String,
85    /// Local alias introduced by `as`, when present.
86    pub alias: Option<String>,
87    /// PHP import namespace (`function` or `const`), when present.
88    pub import_kind: Option<String>,
89}
90
91/// Structured, language-honest representation of a single import's shape.
92///
93/// This is the migration target that replaces the TS-shaped flat fields
94/// (`names`/`default_import`/`namespace_import`) and their per-language
95/// overloads (Rust packs `"pub"` into `default_import`; Go packs the alias
96/// there). It is introduced additively alongside the flat fields (Stream M of
97/// the imports-refactor plan): parsers populate BOTH, readers migrate onto
98/// `form` one at a time behind the golden-parity gate, and the flat fields are
99/// removed once no reader depends on them. New-language variants (Static,
100/// Include, RuntimeRequire, …) are added when their engines land — only the
101/// variants the existing engines produce exist today.
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub enum ImportForm {
104    /// ES modules: TypeScript, TSX, JavaScript.
105    /// `named` holds verbatim specifiers (`"useState"`, `"stdin as input"`,
106    /// `"type Foo"`, `"type Foo as Bar"`) — see [`specifier_imported_name`].
107    Es {
108        default_import: Option<String>,
109        namespace_import: Option<String>,
110        named: Vec<String>,
111        /// Statement-level `import type { ... }`.
112        type_only: bool,
113        /// Side-effect-only `import "mod"` (no bindings).
114        side_effect: bool,
115        /// Verbatim `with { ... }` or legacy `assert { ... }` clause.
116        /// Import attributes select the module type, so dropping this clause can
117        /// change a valid import into one the runtime refuses to load.
118        attribute_clause: Option<String>,
119        /// Decoded value of the clause's `type` attribute, when parsed.
120        attribute_type: Option<String>,
121    },
122    /// Python `import module` (`from_import = false`) or
123    /// `from module import a, b` (`from_import = true`).
124    Python {
125        from_import: bool,
126        named: Vec<String>,
127    },
128    /// Rust `use path;` / `pub use path;`. `visibility` replaces the
129    /// `default_import == "pub"` overload (`Some("pub")`, `Some("pub(crate)")`,
130    /// …). The brace/use-tree text remains carried by `module_path` per the
131    /// lossless-round-trip decision; `named` holds extracted use-list names.
132    RustUse {
133        visibility: Option<String>,
134        named: Vec<String>,
135    },
136    /// Go import. `alias` replaces the `default_import` overload, including the
137    /// blank (`_`) and dot (`.`) import bindings.
138    Go { alias: Option<String> },
139    /// Solidity import, in one of four forms:
140    /// - side-effect: `import "x";` (all empty)
141    /// - named: `import { A, B as C } from "x";` (`named`)
142    /// - namespace: `import * as A from "x";` (`namespace`)
143    /// - whole-file alias: `import "x" as A;` (`alias`)
144    ///
145    /// `named` holds verbatim specifiers (`"A"`, `"B as C"`) like the ES form,
146    /// so [`specifier_imported_name`] / [`specifier_local_name`] apply.
147    Solidity {
148        named: Vec<String>,
149        namespace: Option<String>,
150        alias: Option<String>,
151    },
152    /// One physical PHP `use` declaration. A declaration may contain multiple
153    /// comma-separated clauses, each with its own path, alias, and import kind.
154    Php { clauses: Vec<PhpImportClause> },
155    /// Generic structured form shared by Java, C#, grouped PHP uses, Kotlin,
156    /// Scala, Swift, and other engines that use this common schema. Includes all
157    /// schema fields so a new engine does not need its own enum variant;
158    /// `module_path` on the parent `ImportStatement` stores the full path or
159    /// fully qualified name, while `named` stores each specifier exactly as parsed.
160    Structured {
161        named: Vec<String>,
162        namespace: Option<String>,
163        alias: Option<String>,
164        modifiers: Vec<String>,
165        import_kind: Option<String>,
166    },
167}
168
169/// Structured request to generate a single import line. Superset of the fields
170/// the public `aft_import` schema exposes; each engine reads only the subset it
171/// supports. New languages add fields here rather than growing positional
172/// parameters on every generator signature.
173#[derive(Debug, Clone)]
174pub struct ImportRequest<'a> {
175    pub module_path: &'a str,
176    pub names: &'a [String],
177    pub default_import: Option<&'a str>,
178    /// ES `* as ns` / Solidity `* as A`.
179    pub namespace: Option<&'a str>,
180    /// Whole-module local alias (Solidity `import "x" as A`).
181    pub alias: Option<&'a str>,
182    pub type_only: bool,
183    /// Statement-level modifier tokens (Java/C# `static`, C# `global`/`unsafe`,
184    /// `wildcard`, Swift `@testable`, …). Empty for the legacy engines.
185    pub modifiers: &'a [String],
186    /// Symbol-kind-specific import (PHP `function`/`const`, Swift `struct`/…,
187    /// Scala `given`). Absent for the legacy engines.
188    pub import_kind: Option<&'a str>,
189}
190
191/// Empty default for the `modifiers` slice so legacy callers need not allocate.
192const NO_MODIFIERS: &[String] = &[];
193
194impl<'a> ImportRequest<'a> {
195    /// Construct a request carrying only the legacy positional fields; the
196    /// structured fields (alias/modifiers/import_kind) default to absent. Used
197    /// by the back-compat free-function wrappers.
198    pub fn legacy(
199        module_path: &'a str,
200        names: &'a [String],
201        default_import: Option<&'a str>,
202        namespace: Option<&'a str>,
203        type_only: bool,
204    ) -> Self {
205        ImportRequest {
206            module_path,
207            names,
208            default_import,
209            namespace,
210            alias: None,
211            type_only,
212            modifiers: NO_MODIFIERS,
213            import_kind: None,
214        }
215    }
216}
217
218/// A single parsed import statement.
219#[derive(Debug, Clone, PartialEq, Eq)]
220pub struct ImportStatement {
221    /// The module path (e.g., `react`, `./utils`, `../config`).
222    pub module_path: String,
223    /// Named imports (e.g., `["useState", "useEffect"]`).
224    pub names: Vec<String>,
225    /// Default import name (e.g., `React` from `import React from 'react'`).
226    pub default_import: Option<String>,
227    /// Namespace import name (e.g., `path` from `import * as path from 'path'`).
228    pub namespace_import: Option<String>,
229    /// What kind: value, type, or side-effect.
230    pub kind: ImportKind,
231    /// Which group this import belongs to.
232    pub group: ImportGroup,
233    /// Byte range in the original source.
234    pub byte_range: Range<usize>,
235    /// Raw text of the import statement.
236    pub raw_text: String,
237    /// Structured, de-overloaded representation (Stream M migration target).
238    /// Populated by every parser alongside the flat fields above; readers
239    /// migrate onto this incrementally behind the golden-parity gate.
240    pub form: ImportForm,
241}
242
243/// A block of parsed imports from a file.
244#[derive(Debug, Clone, PartialEq, Eq)]
245pub struct ImportBlock {
246    /// All parsed import statements, in source order.
247    pub imports: Vec<ImportStatement>,
248    /// Overall byte range covering all import statements (start of first to end of last).
249    /// `None` if no imports found.
250    pub byte_range: Option<Range<usize>>,
251}
252
253impl ImportBlock {
254    pub fn empty() -> Self {
255        ImportBlock {
256            imports: Vec::new(),
257            byte_range: None,
258        }
259    }
260}
261
262pub(crate) fn import_byte_range(imports: &[ImportStatement]) -> Option<Range<usize>> {
263    imports.first().zip(imports.last()).map(|(first, last)| {
264        let start = first.byte_range.start;
265        let end = last.byte_range.end;
266        start..end
267    })
268}
269
270// ---------------------------------------------------------------------------
271// Specifier helpers (TS/JS verbatim-string format)
272// ---------------------------------------------------------------------------
273
274/// Return the local binding name for a TS/JS named-import specifier stored in
275/// `ImportStatement::names`. Specifiers are stored verbatim — e.g.
276/// `"stdin as input"`, `"type Foo"`, `"type Foo as Bar"`, `"useState"` — so
277/// callers that want the name actually introduced into scope must strip the
278/// optional `type ` prefix and prefer the post-`as` identifier when present.
279///
280/// Examples:
281///   `"useState"`            → `"useState"`
282///   `"stdin as input"`      → `"input"`
283///   `"type Foo"`            → `"Foo"`
284///   `"type Foo as Bar"`     → `"Bar"`
285pub fn specifier_local_name(spec: &str) -> &str {
286    let trimmed = spec.trim();
287    let after_type = trimmed
288        .strip_prefix("type ")
289        .unwrap_or(trimmed)
290        .trim_start();
291    if let Some(idx) = after_type.find(" as ") {
292        after_type[idx + 4..].trim()
293    } else {
294        after_type
295    }
296}
297
298/// Return the imported (pre-`as`) name for a TS/JS named-import specifier.
299/// Used by dedup, remove, and any caller that needs the source-side name.
300///
301/// Examples:
302///   `"useState"`            → `"useState"`
303///   `"stdin as input"`      → `"stdin"`
304///   `"type Foo"`            → `"Foo"`
305///   `"type Foo as Bar"`     → `"Foo"`
306pub fn specifier_imported_name(spec: &str) -> &str {
307    let trimmed = spec.trim();
308    let after_type = trimmed
309        .strip_prefix("type ")
310        .unwrap_or(trimmed)
311        .trim_start();
312    after_type
313        .find(" as ")
314        .map(|idx| after_type[..idx].trim())
315        .unwrap_or(after_type)
316}
317
318/// Whether a stored specifier matches a target name. Matches against either
319/// the imported name or the local binding so callers can pass whichever name
320/// they observed in source. Useful for `remove_import` where the agent may
321/// reference an aliased import by either name.
322pub fn specifier_matches(spec: &str, target: &str) -> bool {
323    specifier_imported_name(spec) == target || specifier_local_name(spec) == target
324}
325
326// ---------------------------------------------------------------------------
327// Per-language engine: the ImportSyntax trait + registry
328// ---------------------------------------------------------------------------
329
330/// Per-language import engine. One impl per supported language; [`syntax_for`]
331/// maps a [`LangId`] to its `&'static dyn ImportSyntax`. This is the single
332/// plug-in point that replaces the scattered `match lang` dispatch in
333/// `parse_imports` / `generate_import_line_with_namespace` / `classify_group` /
334/// `is_supported`. Adding a language is a new impl + one registry arm.
335///
336/// The existing engines are thin wrappers over the free functions they already
337/// used, so routing through the trait is behavior-preserving (golden-gated).
338pub trait ImportSyntax: Sync {
339    /// Parse all imports from a file's already-parsed tree.
340    fn parse(&self, source: &str, tree: &Tree) -> ImportBlock;
341
342    /// Generate a single import line from a structured [`ImportRequest`].
343    /// Engines read only the fields they support and ignore the rest.
344    fn generate_line(&self, req: &ImportRequest) -> String;
345
346    /// Classify a module path into stdlib / external / internal.
347    fn classify_group(&self, module_path: &str) -> ImportGroup;
348}
349
350/// ES modules engine: TypeScript, TSX, JavaScript.
351struct EsSyntax;
352impl ImportSyntax for EsSyntax {
353    fn parse(&self, source: &str, tree: &Tree) -> ImportBlock {
354        parse_ts_imports(source, tree)
355    }
356    fn generate_line(&self, req: &ImportRequest) -> String {
357        generate_ts_import_line(
358            req.module_path,
359            req.names,
360            req.default_import,
361            req.namespace,
362            req.type_only,
363        )
364    }
365    fn classify_group(&self, module_path: &str) -> ImportGroup {
366        classify_group_ts(module_path)
367    }
368}
369
370struct PythonSyntax;
371impl ImportSyntax for PythonSyntax {
372    fn parse(&self, source: &str, tree: &Tree) -> ImportBlock {
373        parse_py_imports(source, tree)
374    }
375    fn generate_line(&self, req: &ImportRequest) -> String {
376        generate_py_import_line(req.module_path, req.names, req.default_import)
377    }
378    fn classify_group(&self, module_path: &str) -> ImportGroup {
379        classify_group_py(module_path)
380    }
381}
382
383struct RustSyntax;
384impl ImportSyntax for RustSyntax {
385    fn parse(&self, source: &str, tree: &Tree) -> ImportBlock {
386        parse_rs_imports(source, tree)
387    }
388    fn generate_line(&self, req: &ImportRequest) -> String {
389        generate_rs_import_line(req.module_path, req.names, req.type_only)
390    }
391    fn classify_group(&self, module_path: &str) -> ImportGroup {
392        classify_group_rs(module_path)
393    }
394}
395
396struct GoSyntax;
397impl ImportSyntax for GoSyntax {
398    fn parse(&self, source: &str, tree: &Tree) -> ImportBlock {
399        parse_go_imports(source, tree)
400    }
401    fn generate_line(&self, req: &ImportRequest) -> String {
402        generate_go_import_line(req.module_path, req.default_import, false)
403    }
404    fn classify_group(&self, module_path: &str) -> ImportGroup {
405        classify_group_go(module_path)
406    }
407}
408
409/// Solidity import engine. Supports named / namespace / whole-file-alias /
410/// side-effect forms.
411struct SoliditySyntax;
412impl ImportSyntax for SoliditySyntax {
413    fn parse(&self, source: &str, tree: &Tree) -> ImportBlock {
414        parse_solidity_imports(source, tree)
415    }
416    fn generate_line(&self, req: &ImportRequest) -> String {
417        generate_solidity_import_line(req)
418    }
419    fn classify_group(&self, module_path: &str) -> ImportGroup {
420        classify_group_solidity(module_path)
421    }
422}
423
424#[derive(Debug, Clone, Copy, PartialEq, Eq)]
425pub(crate) enum VueScriptRangeError {
426    MissingScript,
427    MultipleScripts,
428}
429
430impl VueScriptRangeError {
431    pub(crate) fn code(self) -> &'static str {
432        match self {
433            VueScriptRangeError::MissingScript => "missing_vue_script",
434            VueScriptRangeError::MultipleScripts => "ambiguous_vue_script",
435        }
436    }
437
438    pub(crate) fn message(self, command: &str) -> String {
439        match self {
440            VueScriptRangeError::MissingScript => format!(
441                "{command}: Vue import management requires exactly one <script> block; found none"
442            ),
443            VueScriptRangeError::MultipleScripts => format!(
444                "{command}: Vue import management requires exactly one <script> block; found multiple"
445            ),
446        }
447    }
448}
449
450/// Locate the byte range of the single `<script>` block's inner content in a
451/// Vue Single-File Component. tree-sitter-vue exposes the script body as a
452/// single `raw_text` node; this returns `(start, end)` of that node, or — for an
453/// empty `<script></script>` with no `raw_text` child — a zero-width range right
454/// after the start tag. Multiple scripts are ambiguous for byte-level edits and
455/// no-script SFCs have no safe insertion region, so callers should surface the
456/// returned error instead of silently editing byte 0 or the first script.
457pub(crate) fn vue_single_script_content_range(
458    tree: &Tree,
459) -> Result<(usize, usize), VueScriptRangeError> {
460    let root = tree.root_node();
461    let mut ranges = Vec::new();
462    let mut cursor = root.walk();
463    for child in root.named_children(&mut cursor) {
464        if child.kind() == "script_element" {
465            ranges.push(vue_script_element_content_range(&child));
466        }
467    }
468
469    match ranges.len() {
470        0 => Err(VueScriptRangeError::MissingScript),
471        1 => Ok(ranges[0]),
472        _ => Err(VueScriptRangeError::MultipleScripts),
473    }
474}
475
476/// Back-compat convenience wrapper for callers that only need the safe single
477/// script range and intentionally treat missing/ambiguous scripts as absent.
478pub(crate) fn vue_script_content_range(tree: &Tree) -> Option<(usize, usize)> {
479    vue_single_script_content_range(tree).ok()
480}
481
482fn vue_script_element_content_range(child: &Node) -> (usize, usize) {
483    let mut inner = child.walk();
484    for sub in child.named_children(&mut inner) {
485        if sub.kind() == "raw_text" {
486            return (sub.start_byte(), sub.end_byte());
487        }
488    }
489
490    // Empty `<script></script>`: insert right after the start tag.
491    let mut inner2 = child.walk();
492    for sub in child.named_children(&mut inner2) {
493        if sub.kind() == "start_tag" {
494            return (sub.end_byte(), sub.end_byte());
495        }
496    }
497
498    (child.end_byte(), child.end_byte())
499}
500
501/// Parse imports from a Vue SFC `<script>` block. The script body is re-parsed
502/// with the TypeScript grammar (which covers both `lang="ts"` and plain JS
503/// import syntax), then every byte offset is remapped from script-relative to
504/// whole-file positions so insertion, removal, and organize operate correctly.
505fn parse_vue_imports(source: &str, tree: &Tree) -> ImportBlock {
506    let Ok((start, end)) = vue_single_script_content_range(tree) else {
507        return ImportBlock {
508            imports: Vec::new(),
509            byte_range: None,
510        };
511    };
512    let inner = &source[start..end];
513    let mut parser = Parser::new();
514    if parser
515        .set_language(&grammar_for(LangId::TypeScript))
516        .is_err()
517    {
518        return ImportBlock {
519            imports: Vec::new(),
520            byte_range: None,
521        };
522    }
523    let Some(inner_tree) = parser.parse(inner, None) else {
524        return ImportBlock {
525            imports: Vec::new(),
526            byte_range: None,
527        };
528    };
529    let mut block = parse_ts_imports(inner, &inner_tree);
530    for imp in &mut block.imports {
531        imp.byte_range = (imp.byte_range.start + start)..(imp.byte_range.end + start);
532    }
533    block.byte_range = block.byte_range.map(|r| (r.start + start)..(r.end + start));
534    block
535}
536
537/// Vue Single-File Component import engine. The `<script>` body is exposed by
538/// tree-sitter-vue as a single `raw_text` node, so we re-parse it with the
539/// TypeScript grammar and remap the resulting byte offsets back to whole-file
540/// positions. Generation and grouping reuse the ES (TS/JS) engine, since Vue
541/// script imports are TypeScript/JavaScript.
542struct VueSyntax;
543impl ImportSyntax for VueSyntax {
544    fn parse(&self, source: &str, tree: &Tree) -> ImportBlock {
545        parse_vue_imports(source, tree)
546    }
547    fn generate_line(&self, req: &ImportRequest) -> String {
548        generate_ts_import_line(
549            req.module_path,
550            req.names,
551            req.default_import,
552            req.namespace,
553            req.type_only,
554        )
555    }
556    fn classify_group(&self, module_path: &str) -> ImportGroup {
557        classify_group_ts(module_path)
558    }
559}
560
561static ES_SYNTAX: EsSyntax = EsSyntax;
562static PYTHON_SYNTAX: PythonSyntax = PythonSyntax;
563static RUST_SYNTAX: RustSyntax = RustSyntax;
564static GO_SYNTAX: GoSyntax = GoSyntax;
565static SOLIDITY_SYNTAX: SoliditySyntax = SoliditySyntax;
566static VUE_SYNTAX: VueSyntax = VueSyntax;
567
568/// Map a language to its import engine, or `None` when imports are unsupported.
569pub fn syntax_for(lang: LangId) -> Option<&'static dyn ImportSyntax> {
570    match lang {
571        LangId::TypeScript | LangId::Tsx | LangId::JavaScript => Some(&ES_SYNTAX),
572        LangId::Python => Some(&PYTHON_SYNTAX),
573        LangId::Rust => Some(&RUST_SYNTAX),
574        LangId::Go => Some(&GO_SYNTAX),
575        LangId::Solidity => Some(&SOLIDITY_SYNTAX),
576        LangId::Vue => Some(&VUE_SYNTAX),
577        LangId::C => Some(&c::C_SYNTAX),
578        LangId::Cpp => Some(&c::C_SYNTAX),
579        LangId::Java => Some(&java::JAVA_SYNTAX),
580        LangId::Kotlin => Some(&kotlin::KOTLIN_SYNTAX),
581        LangId::Lua => Some(&lua::LUA_SYNTAX),
582        LangId::CSharp => Some(&csharp::CSHARP_SYNTAX),
583        LangId::Php => Some(&php::PHP_SYNTAX),
584        LangId::Perl => Some(&perl::PERL_SYNTAX),
585        LangId::Ruby => Some(&ruby::RUBY_SYNTAX),
586        LangId::Scala => Some(&scala::SCALA_SYNTAX),
587        LangId::Swift => Some(&swift::SWIFT_SYNTAX),
588        LangId::Zig
589        | LangId::Bash
590        | LangId::Scss
591        | LangId::Json
592        | LangId::Html
593        | LangId::Markdown
594        | LangId::Yaml
595        | LangId::Pascal
596        | LangId::R
597        | LangId::Groovy
598        | LangId::ObjC => None,
599    }
600}
601
602// ---------------------------------------------------------------------------
603// Core API
604// ---------------------------------------------------------------------------
605
606/// Parse imports from source using the provided tree-sitter tree.
607pub fn parse_imports(source: &str, tree: &Tree, lang: LangId) -> ImportBlock {
608    match syntax_for(lang) {
609        Some(engine) => engine.parse(source, tree),
610        None => ImportBlock::empty(),
611    }
612}
613
614/// Check if an import with the given module + name combination already exists.
615///
616/// For dedup: same module path and matching binding shape. Side-effect imports
617/// are only duplicates of side-effect imports; namespace imports are distinct
618/// from side-effect imports and from other namespace aliases.
619pub fn is_duplicate(
620    block: &ImportBlock,
621    module_path: &str,
622    names: &[String],
623    default_import: Option<&str>,
624    type_only: bool,
625) -> bool {
626    is_duplicate_with_namespace(block, module_path, names, default_import, None, type_only)
627}
628
629/// Check if an import with the given module + complete binding shape already exists.
630pub fn is_duplicate_with_namespace(
631    block: &ImportBlock,
632    module_path: &str,
633    names: &[String],
634    default_import: Option<&str>,
635    namespace_import: Option<&str>,
636    type_only: bool,
637) -> bool {
638    let target_kind = if type_only {
639        ImportKind::Type
640    } else {
641        ImportKind::Value
642    };
643
644    for imp in &block.imports {
645        if imp.module_path != module_path {
646            continue;
647        }
648
649        // For side-effect imports (no names/default/namespace): module path
650        // match is sufficient only when the existing import is also a
651        // side-effect import. Namespace imports like `import * as fs from 'fs'`
652        // are distinct local bindings and must not be conflated with
653        // `import 'fs'`.
654        if names.is_empty()
655            && default_import.is_none()
656            && namespace_import.is_none()
657            && imp.names.is_empty()
658            && imp.default_import.is_none()
659            && imp.namespace_import.is_none()
660        {
661            return true;
662        }
663
664        // For side-effect imports specifically (TS/JS): module match is enough
665        if names.is_empty()
666            && default_import.is_none()
667            && namespace_import.is_none()
668            && imp.kind == ImportKind::SideEffect
669        {
670            return true;
671        }
672
673        // Kind must match for dedup (value imports don't dedup against type imports)
674        if imp.kind != target_kind && imp.kind != ImportKind::SideEffect {
675            continue;
676        }
677
678        // Default+namespace imports are one ES binding shape. A plain default
679        // import must not satisfy a request for `default, * as ns`, and a
680        // different namespace alias must not either.
681        if let (Some(def), Some(namespace)) = (default_import, namespace_import) {
682            if imp.default_import.as_deref() == Some(def)
683                && imp.namespace_import.as_deref() == Some(namespace)
684                && names
685                    .iter()
686                    .all(|n| imp.names.iter().any(|stored| specifier_matches(stored, n)))
687            {
688                return true;
689            }
690            continue;
691        }
692
693        // Namespace-only requests are satisfied by any existing same-module
694        // import that already binds that namespace alias, even if it also has a
695        // default binding.
696        if names.is_empty()
697            && default_import.is_none()
698            && namespace_import.is_some()
699            && imp.namespace_import.as_deref() == namespace_import
700        {
701            return true;
702        }
703
704        // Check default import match. This branch only handles requests that do
705        // not also ask for a namespace; that combined shape is checked above.
706        if let Some(def) = default_import {
707            if namespace_import.is_none() && imp.default_import.as_deref() == Some(def) {
708                return true;
709            }
710        }
711
712        // Check named imports — if ALL requested names already exist.
713        // Compare on the imported (pre-`as`) name so adding `Foo` is a
714        // no-op when `Foo as Bar` is already imported, but adding
715        // `Foo as Bar` is NOT a duplicate of bare `Foo` (different
716        // local bindings).
717        if !names.is_empty()
718            && names
719                .iter()
720                .all(|n| imp.names.iter().any(|stored| specifier_matches(stored, n)))
721        {
722            return true;
723        }
724    }
725
726    false
727}
728
729/// Check whether a fully structured add-import request is already present.
730///
731/// Legacy ES/Python/Rust/Go callers intentionally keep the historical
732/// subset/dominance semantics (`import { a, b }` satisfies adding `{ a }`). The
733/// newer engines carry language-specific shape in `ImportForm::Structured` (or
734/// Solidity's dedicated form), where module path alone is not enough: include
735/// delimiters, statement kinds, modifiers, aliases, and runtime import flavors
736/// all affect the generated source. Those languages deduplicate on a canonical
737/// full-form key so `#include <x>` does not block `#include "x"`, `load` does
738/// not block `require`, and side-effect Solidity imports do not block aliases.
739pub(crate) fn is_duplicate_import_request(
740    lang: LangId,
741    block: &ImportBlock,
742    req: &ImportRequest<'_>,
743) -> bool {
744    if lang == LangId::Php
745        && block
746            .imports
747            .iter()
748            .any(|imp| php::php_import_satisfies_request(imp, req))
749    {
750        return true;
751    }
752
753    if lang == LangId::Python && req.names.is_empty() && req.default_import.is_none() {
754        return block.imports.iter().any(|imp| {
755            matches!(
756                &imp.form,
757                ImportForm::Python {
758                    from_import: false,
759                    named,
760                } if named.iter().any(|specifier| {
761                    specifier_imported_name(specifier) == req.module_path
762                })
763            )
764        });
765    }
766
767    if !uses_form_aware_dedup(lang) {
768        return is_duplicate_with_namespace(
769            block,
770            req.module_path,
771            req.names,
772            req.default_import,
773            req.namespace,
774            req.type_only,
775        );
776    }
777
778    let target = request_dedup_key(lang, req);
779    block
780        .imports
781        .iter()
782        .map(|imp| statement_dedup_key(lang, imp))
783        .any(|key| key == target)
784}
785
786fn uses_form_aware_dedup(lang: LangId) -> bool {
787    matches!(
788        lang,
789        LangId::Solidity
790            | LangId::C
791            | LangId::Cpp
792            | LangId::Java
793            | LangId::CSharp
794            | LangId::Php
795            | LangId::Kotlin
796            | LangId::Scala
797            | LangId::Swift
798            | LangId::Ruby
799            | LangId::Lua
800            | LangId::Perl
801    )
802}
803
804#[derive(Debug, Clone, PartialEq, Eq)]
805struct ImportDedupKey {
806    module_path: String,
807    kind: ImportKind,
808    form: ImportForm,
809}
810
811fn statement_dedup_key(lang: LangId, imp: &ImportStatement) -> ImportDedupKey {
812    canonical_dedup_key(
813        lang,
814        ImportDedupKey {
815            module_path: imp.module_path.clone(),
816            kind: imp.kind,
817            form: imp.form.clone(),
818        },
819    )
820}
821
822fn request_dedup_key(lang: LangId, req: &ImportRequest<'_>) -> ImportDedupKey {
823    let key = match lang {
824        LangId::Solidity => {
825            let kind = if req.names.is_empty() && req.namespace.is_none() && req.alias.is_none() {
826                ImportKind::SideEffect
827            } else {
828                ImportKind::Value
829            };
830            ImportDedupKey {
831                module_path: req.module_path.to_string(),
832                kind,
833                form: ImportForm::Solidity {
834                    named: req.names.to_vec(),
835                    namespace: req.namespace.map(str::to_string),
836                    alias: req.alias.map(str::to_string),
837                },
838            }
839        }
840        LangId::C | LangId::Cpp => structured_dedup_key(
841            req.module_path,
842            ImportKind::SideEffect,
843            &[],
844            None,
845            None,
846            &[],
847            Some(req.import_kind.or(req.default_import).unwrap_or("system")),
848        ),
849        LangId::Java => {
850            let (mut module_path, modifiers) = wildcard_suffix_request(
851                req.module_path,
852                req.modifiers,
853                req.default_import == Some("*"),
854            );
855            let mut names = req.names.to_vec();
856            normalize_java_static_member_key(&mut module_path, &modifiers, &mut names);
857            structured_dedup_key(
858                &module_path,
859                ImportKind::Value,
860                &names,
861                None,
862                None,
863                &modifiers,
864                None,
865            )
866        }
867        LangId::CSharp => structured_dedup_key(
868            req.module_path,
869            ImportKind::Value,
870            &[],
871            None,
872            req.alias,
873            req.modifiers,
874            None,
875        ),
876        LangId::Php => structured_dedup_key(
877            req.module_path,
878            ImportKind::Value,
879            &[],
880            None,
881            req.alias,
882            req.modifiers,
883            req.import_kind,
884        ),
885        LangId::Kotlin => {
886            let wildcard = req.default_import == Some("*") || req.module_path.ends_with(".*");
887            let (module_path, modifiers) =
888                wildcard_suffix_request(req.module_path, req.modifiers, wildcard);
889            let alias = req
890                .alias
891                .or(req.default_import.filter(|value| *value != "*"));
892            structured_dedup_key(
893                &module_path,
894                ImportKind::Value,
895                &[],
896                None,
897                alias,
898                &modifiers,
899                None,
900            )
901        }
902        LangId::Scala => scala_request_dedup_key(req),
903        LangId::Swift => structured_dedup_key(
904            req.module_path,
905            ImportKind::Value,
906            &[],
907            None,
908            None,
909            req.modifiers,
910            req.import_kind,
911        ),
912        LangId::Ruby => {
913            let mut modifiers = req.modifiers.to_vec();
914            if !modifiers
915                .iter()
916                .any(|modifier| modifier == "quote:single" || modifier == "quote:double")
917            {
918                modifiers.push("quote:single".to_string());
919            }
920            structured_dedup_key(
921                req.module_path,
922                ImportKind::SideEffect,
923                &[],
924                None,
925                None,
926                &modifiers,
927                Some(req.import_kind.unwrap_or("require")),
928            )
929        }
930        LangId::Lua => {
931            let alias = req.default_import.or(req.alias);
932            let kind = if alias.is_some() {
933                ImportKind::Value
934            } else {
935                ImportKind::SideEffect
936            };
937            structured_dedup_key(req.module_path, kind, &[], None, alias, req.modifiers, None)
938        }
939        LangId::Perl => structured_dedup_key(
940            req.module_path,
941            ImportKind::SideEffect,
942            &[],
943            None,
944            None,
945            req.modifiers,
946            Some(req.import_kind.unwrap_or("use")),
947        ),
948        _ => structured_dedup_key(
949            req.module_path,
950            if req.type_only {
951                ImportKind::Type
952            } else {
953                ImportKind::Value
954            },
955            req.names,
956            req.namespace,
957            req.alias,
958            req.modifiers,
959            req.import_kind,
960        ),
961    };
962
963    canonical_dedup_key(lang, key)
964}
965
966fn structured_dedup_key(
967    module_path: &str,
968    kind: ImportKind,
969    named: &[String],
970    namespace: Option<&str>,
971    alias: Option<&str>,
972    modifiers: &[String],
973    import_kind: Option<&str>,
974) -> ImportDedupKey {
975    ImportDedupKey {
976        module_path: module_path.to_string(),
977        kind,
978        form: ImportForm::Structured {
979            named: named.to_vec(),
980            namespace: namespace.map(str::to_string),
981            alias: alias.map(str::to_string),
982            modifiers: modifiers.to_vec(),
983            import_kind: import_kind.map(str::to_string),
984        },
985    }
986}
987
988fn wildcard_suffix_request(
989    module_path: &str,
990    modifiers: &[String],
991    wildcard: bool,
992) -> (String, Vec<String>) {
993    let stripped = module_path.strip_suffix(".*").unwrap_or(module_path);
994    let mut modifiers = modifiers.to_vec();
995    if (wildcard || stripped.len() != module_path.len())
996        && !modifiers.iter().any(|modifier| modifier == "wildcard")
997    {
998        modifiers.push("wildcard".to_string());
999    }
1000    (stripped.to_string(), modifiers)
1001}
1002
1003fn normalize_java_static_member_key(
1004    module_path: &mut String,
1005    modifiers: &[String],
1006    names: &mut Vec<String>,
1007) {
1008    let is_static = modifiers.iter().any(|modifier| modifier == "static");
1009    let is_wildcard = modifiers.iter().any(|modifier| modifier == "wildcard");
1010    if !is_static || is_wildcard || !names.is_empty() {
1011        return;
1012    }
1013
1014    if let Some((prefix, member)) = module_path.rsplit_once('.') {
1015        if !prefix.is_empty() && !member.is_empty() {
1016            names.push(member.to_string());
1017            *module_path = prefix.to_string();
1018        }
1019    }
1020}
1021
1022fn scala_request_dedup_key(req: &ImportRequest<'_>) -> ImportDedupKey {
1023    let mut module_path = req.module_path.to_string();
1024    let mut names: Vec<String> = req
1025        .names
1026        .iter()
1027        .map(|name| normalize_scala_selector_for_dedup(name))
1028        .collect();
1029    let mut identity_modifiers: Vec<String> = req
1030        .modifiers
1031        .iter()
1032        .filter(|modifier| modifier.as_str() != "scala2")
1033        .cloned()
1034        .collect();
1035    let mut import_kind = req.import_kind.map(str::to_string);
1036
1037    if req.default_import == Some("given") || module_path.ends_with(".given") {
1038        import_kind.get_or_insert_with(|| "given".to_string());
1039        if let Some(stripped) = module_path.strip_suffix(".given") {
1040            module_path = stripped.to_string();
1041        }
1042    }
1043
1044    if matches!(req.default_import, Some("*") | Some("_"))
1045        || matches!(req.namespace, Some("*") | Some("_"))
1046        || module_path.ends_with(".*")
1047        || module_path.ends_with("._")
1048    {
1049        if !identity_modifiers
1050            .iter()
1051            .any(|modifier| modifier == "wildcard")
1052        {
1053            identity_modifiers.push("wildcard".to_string());
1054        }
1055        module_path = module_path
1056            .strip_suffix(".*")
1057            .or_else(|| module_path.strip_suffix("._"))
1058            .unwrap_or(&module_path)
1059            .to_string();
1060    }
1061
1062    if names.is_empty() {
1063        if let Some(alias) = req.alias.filter(|alias| !alias.is_empty()) {
1064            if let Some((prefix, leaf)) = module_path.rsplit_once('.') {
1065                names.push(format!("{leaf} as {alias}"));
1066                module_path = prefix.to_string();
1067            }
1068        }
1069    }
1070
1071    structured_dedup_key(
1072        &module_path,
1073        ImportKind::Value,
1074        &names,
1075        None,
1076        None,
1077        &identity_modifiers,
1078        import_kind.as_deref(),
1079    )
1080}
1081
1082fn normalize_scala_selector_for_dedup(name: &str) -> String {
1083    let trimmed = name.trim();
1084    if let Some((from, to)) = trimmed.split_once("=>") {
1085        format!("{} as {}", from.trim(), to.trim())
1086    } else {
1087        trimmed.to_string()
1088    }
1089}
1090
1091fn canonical_dedup_key(lang: LangId, mut key: ImportDedupKey) -> ImportDedupKey {
1092    match &mut key.form {
1093        ImportForm::Structured { named, .. } | ImportForm::Solidity { named, .. } => {
1094            sort_named_specifiers(named);
1095        }
1096        ImportForm::Es { named, .. } | ImportForm::Python { named, .. } => {
1097            sort_named_specifiers(named);
1098        }
1099        ImportForm::RustUse { named, .. } => {
1100            sort_named_specifiers(named);
1101        }
1102        ImportForm::Go { .. } | ImportForm::Php { .. } => {}
1103    }
1104
1105    if matches!(lang, LangId::Java | LangId::Kotlin) {
1106        if let Some(stripped) = key.module_path.strip_suffix(".*") {
1107            key.module_path = stripped.to_string();
1108        }
1109        if matches!(lang, LangId::Java) {
1110            if let ImportForm::Structured {
1111                named, modifiers, ..
1112            } = &mut key.form
1113            {
1114                normalize_java_static_member_key(&mut key.module_path, modifiers, named);
1115            }
1116        }
1117    } else if matches!(lang, LangId::Scala) {
1118        key.module_path = key
1119            .module_path
1120            .strip_suffix(".given")
1121            .or_else(|| key.module_path.strip_suffix(".*"))
1122            .or_else(|| key.module_path.strip_suffix("._"))
1123            .unwrap_or(&key.module_path)
1124            .to_string();
1125    }
1126
1127    key
1128}
1129
1130fn sort_named_specifiers(names: &mut [String]) {
1131    names.sort_by(|a, b| {
1132        specifier_imported_name(a)
1133            .cmp(specifier_imported_name(b))
1134            .then_with(|| a.cmp(b))
1135    });
1136}
1137
1138/// Find the byte offset where a new import should be inserted.
1139///
1140/// Strategy:
1141/// - Find all existing imports in the same group.
1142/// - Within that group, find the alphabetical position by module path.
1143/// - Type imports sort after value imports within the same group and module-sort position.
1144/// - If no imports exist in the target group, insert after the last import of the
1145///   nearest preceding group (or before the first import of the nearest following
1146///   group, or at file start if no groups exist).
1147/// - Returns (byte_offset, needs_newline_before, needs_newline_after)
1148pub fn find_insertion_point(
1149    source: &str,
1150    block: &ImportBlock,
1151    group: ImportGroup,
1152    module_path: &str,
1153    type_only: bool,
1154) -> (usize, bool, bool) {
1155    if block.imports.is_empty() {
1156        // No imports at all — insert at start of file
1157        return (0, false, source.is_empty().then_some(false).unwrap_or(true));
1158    }
1159
1160    let target_kind = if type_only {
1161        ImportKind::Type
1162    } else {
1163        ImportKind::Value
1164    };
1165
1166    // Collect imports in the target group
1167    let group_imports: Vec<&ImportStatement> =
1168        block.imports.iter().filter(|i| i.group == group).collect();
1169
1170    if group_imports.is_empty() {
1171        // No imports in this group yet — find nearest neighbor group
1172        // Try preceding groups (lower ordinal) first
1173        let preceding_last = block.imports.iter().filter(|i| i.group < group).last();
1174
1175        if let Some(last) = preceding_last {
1176            let end = last.byte_range.end;
1177            let insert_at = skip_newline(source, end);
1178            return (insert_at, true, true);
1179        }
1180
1181        // No preceding group — try following groups (higher ordinal)
1182        let following_first = block.imports.iter().find(|i| i.group > group);
1183
1184        if let Some(first) = following_first {
1185            return (first.byte_range.start, false, true);
1186        }
1187
1188        // Shouldn't reach here if block is non-empty, but handle gracefully
1189        let first_byte = import_byte_range(&block.imports)
1190            .map(|range| range.start)
1191            .unwrap_or(0);
1192        return (first_byte, false, true);
1193    }
1194
1195    // Find position within the group (alphabetical by module path, type after value)
1196    for imp in &group_imports {
1197        let cmp = module_path.cmp(&imp.module_path);
1198        match cmp {
1199            std::cmp::Ordering::Less => {
1200                // Insert before this import
1201                return (imp.byte_range.start, false, false);
1202            }
1203            std::cmp::Ordering::Equal => {
1204                // Same module — type imports go after value imports
1205                if target_kind == ImportKind::Type && imp.kind == ImportKind::Value {
1206                    // Insert after this value import
1207                    let end = imp.byte_range.end;
1208                    let insert_at = skip_newline(source, end);
1209                    return (insert_at, false, false);
1210                }
1211                // Insert before (or it's a duplicate, caller should have checked)
1212                return (imp.byte_range.start, false, false);
1213            }
1214            std::cmp::Ordering::Greater => continue,
1215        }
1216    }
1217
1218    // Module path sorts after all existing imports in this group — insert at end
1219    let Some(last) = group_imports.last() else {
1220        return (
1221            import_byte_range(&block.imports)
1222                .map(|range| range.end)
1223                .unwrap_or(0),
1224            false,
1225            false,
1226        );
1227    };
1228    let end = last.byte_range.end;
1229    let insert_at = skip_newline(source, end);
1230    (insert_at, false, false)
1231}
1232
1233/// Generate a single import line from a structured [`ImportRequest`]. The full
1234/// entry point — engines read the fields they support; unsupported languages
1235/// yield an empty string.
1236pub fn generate_import(lang: LangId, req: &ImportRequest) -> String {
1237    match syntax_for(lang) {
1238        Some(engine) => engine.generate_line(req),
1239        None => String::new(),
1240    }
1241}
1242
1243/// Generate an import line for the given language. Back-compat wrapper over
1244/// [`generate_import`] for callers that pass only the legacy positional fields.
1245pub fn generate_import_line(
1246    lang: LangId,
1247    module_path: &str,
1248    names: &[String],
1249    default_import: Option<&str>,
1250    type_only: bool,
1251) -> String {
1252    generate_import(
1253        lang,
1254        &ImportRequest::legacy(module_path, names, default_import, None, type_only),
1255    )
1256}
1257
1258/// Generate an import line including namespace imports
1259/// (`import * as ns from 'mod'`). Back-compat wrapper over [`generate_import`].
1260pub fn generate_import_line_with_namespace(
1261    lang: LangId,
1262    module_path: &str,
1263    names: &[String],
1264    default_import: Option<&str>,
1265    namespace_import: Option<&str>,
1266    type_only: bool,
1267) -> String {
1268    generate_import_line_with_namespace_and_attribute_clause(
1269        lang,
1270        module_path,
1271        names,
1272        default_import,
1273        namespace_import,
1274        type_only,
1275        None,
1276    )
1277}
1278
1279/// Generate a line while retaining an existing ES import-attribute clause.
1280/// Other language engines ignore the clause because it has no meaning there.
1281pub(crate) fn generate_import_line_with_namespace_and_attribute_clause(
1282    lang: LangId,
1283    module_path: &str,
1284    names: &[String],
1285    default_import: Option<&str>,
1286    namespace_import: Option<&str>,
1287    type_only: bool,
1288    attribute_clause: Option<&str>,
1289) -> String {
1290    if matches!(
1291        lang,
1292        LangId::TypeScript | LangId::Tsx | LangId::JavaScript | LangId::Vue
1293    ) {
1294        return generate_ts_import_line_with_attribute_clause(
1295            module_path,
1296            names,
1297            default_import,
1298            namespace_import,
1299            type_only,
1300            attribute_clause,
1301        );
1302    }
1303
1304    generate_import(
1305        lang,
1306        &ImportRequest::legacy(
1307            module_path,
1308            names,
1309            default_import,
1310            namespace_import,
1311            type_only,
1312        ),
1313    )
1314}
1315
1316/// Check if the given language is supported by the import engine.
1317pub fn is_supported(lang: LangId) -> bool {
1318    syntax_for(lang).is_some()
1319}
1320
1321/// Classify a module path into a group for TS/JS/TSX.
1322pub fn classify_group_ts(module_path: &str) -> ImportGroup {
1323    if module_path.starts_with('.') {
1324        ImportGroup::Internal
1325    } else {
1326        ImportGroup::External
1327    }
1328}
1329
1330/// Classify a module path into a group for the given language.
1331pub fn classify_group(lang: LangId, module_path: &str) -> ImportGroup {
1332    match syntax_for(lang) {
1333        Some(engine) => engine.classify_group(module_path),
1334        // Unsupported languages have no grouping policy; External is the
1335        // historical neutral default.
1336        None => ImportGroup::External,
1337    }
1338}
1339
1340/// Parse a file from disk and return its import block.
1341/// Convenience wrapper that handles parsing.
1342pub fn parse_file_imports(
1343    path: &std::path::Path,
1344    lang: LangId,
1345) -> Result<(String, Tree, ImportBlock), crate::error::AftError> {
1346    let source =
1347        std::fs::read_to_string(path).map_err(|e| crate::error::AftError::FileNotFound {
1348            path: format!("{}: {}", path.display(), e),
1349        })?;
1350
1351    let grammar = grammar_for(lang);
1352    let mut parser = Parser::new();
1353    parser
1354        .set_language(&grammar)
1355        .map_err(|e| crate::error::AftError::ParseError {
1356            message: format!("grammar init failed for {:?}: {}", lang, e),
1357        })?;
1358
1359    let tree = parser
1360        .parse(&source, None)
1361        .ok_or_else(|| crate::error::AftError::ParseError {
1362            message: format!("tree-sitter parse returned None for {}", path.display()),
1363        })?;
1364
1365    let block = parse_imports(&source, &tree, lang);
1366    Ok((source, tree, block))
1367}
1368
1369// ---------------------------------------------------------------------------
1370// TS/JS/TSX implementation
1371// ---------------------------------------------------------------------------
1372
1373/// Parse imports from a TS/JS/TSX file.
1374///
1375/// Walks the AST root's direct children looking for `import_statement` nodes (D041).
1376fn parse_ts_imports(source: &str, tree: &Tree) -> ImportBlock {
1377    let root = tree.root_node();
1378    let mut imports = Vec::new();
1379
1380    let mut cursor = root.walk();
1381    if !cursor.goto_first_child() {
1382        return ImportBlock::empty();
1383    }
1384
1385    loop {
1386        let node = cursor.node();
1387        if node.kind() == "import_statement" {
1388            if let Some(imp) = parse_single_ts_import(source, &node) {
1389                imports.push(imp);
1390            }
1391        }
1392        if !cursor.goto_next_sibling() {
1393            break;
1394        }
1395    }
1396
1397    let byte_range = import_byte_range(&imports);
1398
1399    ImportBlock {
1400        imports,
1401        byte_range,
1402    }
1403}
1404
1405/// Parse a single `import_statement` node into an `ImportStatement`.
1406fn parse_single_ts_import(source: &str, node: &Node) -> Option<ImportStatement> {
1407    let raw_text = source[node.byte_range()].to_string();
1408    let byte_range = node.byte_range();
1409
1410    // Find the source module (string/string_fragment child of the import)
1411    let module_path = extract_module_path(source, node)?;
1412
1413    // Determine if this is a type-only import: `import type ...`
1414    let is_type_only = has_type_keyword(node);
1415
1416    // Extract import clause details
1417    let mut names = Vec::new();
1418    let mut default_import = None;
1419    let mut namespace_import = None;
1420
1421    let mut child_cursor = node.walk();
1422    if child_cursor.goto_first_child() {
1423        loop {
1424            let child = child_cursor.node();
1425            match child.kind() {
1426                "import_clause" => {
1427                    extract_import_clause(
1428                        source,
1429                        &child,
1430                        &mut names,
1431                        &mut default_import,
1432                        &mut namespace_import,
1433                    );
1434                }
1435                // In some grammars, the default import is a direct identifier child
1436                "identifier" => {
1437                    let text = &source[child.byte_range()];
1438                    if text != "import" && text != "from" && text != "type" {
1439                        default_import = Some(text.to_string());
1440                    }
1441                }
1442                _ => {}
1443            }
1444            if !child_cursor.goto_next_sibling() {
1445                break;
1446            }
1447        }
1448    }
1449
1450    // Classify kind
1451    let kind = if names.is_empty() && default_import.is_none() && namespace_import.is_none() {
1452        ImportKind::SideEffect
1453    } else if is_type_only {
1454        ImportKind::Type
1455    } else {
1456        ImportKind::Value
1457    };
1458
1459    let group = classify_group_ts(&module_path);
1460
1461    let attribute_clause = extract_es_import_attribute_clause(source, node);
1462    let attribute_type = attribute_clause
1463        .as_deref()
1464        .and_then(parse_es_import_attribute_type);
1465    let form = ImportForm::Es {
1466        default_import: default_import.clone(),
1467        namespace_import: namespace_import.clone(),
1468        named: names.clone(),
1469        type_only: is_type_only,
1470        side_effect: matches!(kind, ImportKind::SideEffect),
1471        attribute_clause,
1472        attribute_type,
1473    };
1474
1475    Some(ImportStatement {
1476        module_path,
1477        names,
1478        default_import,
1479        namespace_import,
1480        kind,
1481        group,
1482        byte_range,
1483        raw_text,
1484        form,
1485    })
1486}
1487
1488/// Capture the exact source spelling because `with` and legacy `assert`
1489/// clauses have the same semantics but must not be normalized during a rewrite.
1490fn extract_es_import_attribute_clause(source: &str, node: &Node) -> Option<String> {
1491    let mut cursor = node.walk();
1492    if !cursor.goto_first_child() {
1493        return None;
1494    }
1495
1496    let module_end = loop {
1497        let child = cursor.node();
1498        if child.kind() == "string" {
1499            break child.end_byte();
1500        }
1501        if !cursor.goto_next_sibling() {
1502            return None;
1503        }
1504    };
1505
1506    let tail = source[module_end..node.end_byte()].trim();
1507    let clause = tail.strip_suffix(';').unwrap_or(tail).trim();
1508    (clause.starts_with("with") || clause.starts_with("assert")).then(|| clause.to_string())
1509}
1510
1511/// Return the semantic import-attribute clause attached to an ES import.
1512pub(crate) fn es_import_attribute_clause(imp: &ImportStatement) -> Option<&str> {
1513    match &imp.form {
1514        ImportForm::Es {
1515            attribute_clause, ..
1516        } => attribute_clause.as_deref(),
1517        _ => None,
1518    }
1519}
1520
1521/// Return the semantic value of an ES import's `type` attribute.
1522pub(crate) fn es_import_attribute_type(imp: &ImportStatement) -> Option<&str> {
1523    match &imp.form {
1524        ImportForm::Es { attribute_type, .. } => attribute_type.as_deref(),
1525        _ => None,
1526    }
1527}
1528
1529/// Decode the module type through one canonical parse. Legacy `assert` and current
1530/// `with` clauses carry the same attribute semantics even though their spelling
1531/// must remain unchanged when the original import is rewritten.
1532fn parse_es_import_attribute_type(clause: &str) -> Option<String> {
1533    let body = ["with", "assert"].into_iter().find_map(|keyword| {
1534        let rest = clause.strip_prefix(keyword)?;
1535        rest.chars()
1536            .next()
1537            .is_some_and(|ch| ch.is_whitespace() || ch == '{' || ch == '/')
1538            .then_some(rest)
1539    })?;
1540    let synthetic = format!("import 'module' with{body};");
1541    let mut parser = Parser::new();
1542    parser.set_language(&grammar_for(LangId::TypeScript)).ok()?;
1543    let tree = parser.parse(&synthetic, None)?;
1544    (!tree.root_node().has_error())
1545        .then(|| find_type_attribute(&synthetic, tree.root_node()))
1546        .flatten()
1547}
1548
1549fn find_type_attribute(source: &str, node: Node<'_>) -> Option<String> {
1550    if node.kind() == "pair" {
1551        let key = node
1552            .child_by_field_name("key")
1553            .or_else(|| node.named_child(0))?;
1554        let value = node
1555            .child_by_field_name("value")
1556            .or_else(|| node.named_child(1))?;
1557        if decode_js_attribute_atom(source.get(key.byte_range())?)?.as_str() == "type" {
1558            return decode_js_attribute_atom(source.get(value.byte_range())?);
1559        }
1560    }
1561
1562    let mut cursor = node.walk();
1563    let found = node
1564        .children(&mut cursor)
1565        .find_map(|child| find_type_attribute(source, child));
1566    found
1567}
1568
1569fn decode_js_attribute_atom(text: &str) -> Option<String> {
1570    let text = text.trim();
1571    let quote = text.as_bytes().first().copied();
1572    if !matches!(quote, Some(b'\'') | Some(b'"')) {
1573        return Some(text.to_string());
1574    }
1575    if text.as_bytes().last().copied() != quote || text.len() < 2 {
1576        return None;
1577    }
1578
1579    let mut chars = text[1..text.len() - 1].chars();
1580    let mut decoded = String::new();
1581    while let Some(ch) = chars.next() {
1582        if ch != '\\' {
1583            decoded.push(ch);
1584            continue;
1585        }
1586
1587        match chars.next()? {
1588            '\n' => {}
1589            '\r' => {
1590                if chars.clone().next() == Some('\n') {
1591                    chars.next();
1592                }
1593            }
1594            'b' => decoded.push('\u{0008}'),
1595            'f' => decoded.push('\u{000c}'),
1596            'n' => decoded.push('\n'),
1597            'r' => decoded.push('\r'),
1598            't' => decoded.push('\t'),
1599            'v' => decoded.push('\u{000b}'),
1600            '0' => decoded.push('\0'),
1601            'x' => decoded.push(decode_hex_escape(&mut chars, 2)?),
1602            'u' => {
1603                let digits = if chars.clone().next() == Some('{') {
1604                    chars.next();
1605                    let mut digits = String::new();
1606                    loop {
1607                        let ch = chars.next()?;
1608                        if ch == '}' {
1609                            break;
1610                        }
1611                        digits.push(ch);
1612                    }
1613                    digits
1614                } else {
1615                    chars.by_ref().take(4).collect()
1616                };
1617                decoded.push(char::from_u32(u32::from_str_radix(&digits, 16).ok()?)?);
1618            }
1619            escaped => decoded.push(escaped),
1620        }
1621    }
1622    Some(decoded)
1623}
1624
1625fn decode_hex_escape(chars: &mut impl Iterator<Item = char>, count: usize) -> Option<char> {
1626    let digits: String = chars.take(count).collect();
1627    (digits.len() == count)
1628        .then(|| u32::from_str_radix(&digits, 16).ok())
1629        .flatten()
1630        .and_then(char::from_u32)
1631}
1632
1633/// Extract the module path string from an import_statement node.
1634///
1635/// Looks for a `string` child node and extracts the content without quotes.
1636fn extract_module_path(source: &str, node: &Node) -> Option<String> {
1637    let mut cursor = node.walk();
1638    if !cursor.goto_first_child() {
1639        return None;
1640    }
1641
1642    loop {
1643        let child = cursor.node();
1644        if child.kind() == "string" {
1645            // Get the text and strip quotes
1646            let text = &source[child.byte_range()];
1647            let stripped = text
1648                .trim_start_matches(|c| c == '\'' || c == '"')
1649                .trim_end_matches(|c| c == '\'' || c == '"');
1650            return Some(stripped.to_string());
1651        }
1652        if !cursor.goto_next_sibling() {
1653            break;
1654        }
1655    }
1656    None
1657}
1658
1659/// Check if the import_statement has a `type` keyword (import type ...).
1660///
1661/// In tree-sitter-typescript, `import type { X } from 'y'` produces a `type`
1662/// node as a direct child of `import_statement`, between `import` and `import_clause`.
1663fn has_type_keyword(node: &Node) -> bool {
1664    let mut cursor = node.walk();
1665    if !cursor.goto_first_child() {
1666        return false;
1667    }
1668
1669    loop {
1670        let child = cursor.node();
1671        if child.kind() == "type" {
1672            return true;
1673        }
1674        if !cursor.goto_next_sibling() {
1675            break;
1676        }
1677    }
1678
1679    false
1680}
1681
1682/// Extract named imports, default import, and namespace import from an import_clause.
1683fn extract_import_clause(
1684    source: &str,
1685    node: &Node,
1686    names: &mut Vec<String>,
1687    default_import: &mut Option<String>,
1688    namespace_import: &mut Option<String>,
1689) {
1690    let mut cursor = node.walk();
1691    if !cursor.goto_first_child() {
1692        return;
1693    }
1694
1695    loop {
1696        let child = cursor.node();
1697        match child.kind() {
1698            "identifier" => {
1699                // This is a default import: `import Foo from 'bar'`
1700                let text = &source[child.byte_range()];
1701                if text != "type" {
1702                    *default_import = Some(text.to_string());
1703                }
1704            }
1705            "named_imports" => {
1706                // `{ name1, name2 }`
1707                extract_named_imports(source, &child, names);
1708            }
1709            "namespace_import" => {
1710                // `* as name`
1711                extract_namespace_import(source, &child, namespace_import);
1712            }
1713            _ => {}
1714        }
1715        if !cursor.goto_next_sibling() {
1716            break;
1717        }
1718    }
1719}
1720
1721/// Extract individual names from a named_imports node (`{ a, b, c }`).
1722///
1723/// Each name is stored verbatim including any alias and per-name `type`
1724/// modifier so the regenerator can round-trip them losslessly. Examples of
1725/// captured forms:
1726///
1727/// - `useState`               (plain)
1728/// - `stdin as input`         (renamed)
1729/// - `type Foo`               (per-specifier type-only)
1730/// - `type Foo as Bar`        (per-specifier type-only with rename)
1731///
1732/// **Why verbatim strings instead of a struct field per attribute:** dedup,
1733/// sort, dropping a single import, and the regenerator are all driven by
1734/// `Vec<String>` today. Encoding the alias inside the string preserves the
1735/// shape so the rest of the pipeline (organize, remove_import, move_symbol)
1736/// keeps working without a workspace-wide refactor. The cost is that callers
1737/// who want the canonical name (e.g. dedup) must compare on the leading
1738/// identifier only — see `extract_canonical_name` if you need that.
1739fn extract_named_imports(source: &str, node: &Node, names: &mut Vec<String>) {
1740    let mut cursor = node.walk();
1741    if !cursor.goto_first_child() {
1742        return;
1743    }
1744
1745    loop {
1746        let child = cursor.node();
1747        if child.kind() == "import_specifier" {
1748            // Capture the full text of the specifier so per-name `type` markers
1749            // and `as alias` clauses are preserved across organize/regenerate
1750            // round-trips. Falls back to the imported name if the specifier
1751            // text is empty for any reason.
1752            let raw = source[child.byte_range()].trim().to_string();
1753            if !raw.is_empty() {
1754                names.push(raw);
1755            } else if let Some(name_node) = child.child_by_field_name("name") {
1756                names.push(source[name_node.byte_range()].to_string());
1757            }
1758        }
1759        if !cursor.goto_next_sibling() {
1760            break;
1761        }
1762    }
1763}
1764
1765/// Extract the alias name from a namespace_import node (`* as name`).
1766fn extract_namespace_import(source: &str, node: &Node, namespace_import: &mut Option<String>) {
1767    let mut cursor = node.walk();
1768    if !cursor.goto_first_child() {
1769        return;
1770    }
1771
1772    loop {
1773        let child = cursor.node();
1774        if child.kind() == "identifier" {
1775            *namespace_import = Some(source[child.byte_range()].to_string());
1776            return;
1777        }
1778        if !cursor.goto_next_sibling() {
1779            break;
1780        }
1781    }
1782}
1783
1784/// Generate an import line for TS/JS/TSX.
1785fn generate_ts_import_line(
1786    module_path: &str,
1787    names: &[String],
1788    default_import: Option<&str>,
1789    namespace_import: Option<&str>,
1790    type_only: bool,
1791) -> String {
1792    generate_ts_import_line_with_attribute_clause(
1793        module_path,
1794        names,
1795        default_import,
1796        namespace_import,
1797        type_only,
1798        None,
1799    )
1800}
1801
1802fn generate_ts_import_line_with_attribute_clause(
1803    module_path: &str,
1804    names: &[String],
1805    default_import: Option<&str>,
1806    namespace_import: Option<&str>,
1807    type_only: bool,
1808    attribute_clause: Option<&str>,
1809) -> String {
1810    let line = generate_ts_import_line_base(
1811        module_path,
1812        names,
1813        default_import,
1814        namespace_import,
1815        type_only,
1816    );
1817    let Some(attribute_clause) = attribute_clause else {
1818        return line;
1819    };
1820
1821    let line = line.strip_suffix(';').unwrap_or(&line);
1822    format!("{line} {};", attribute_clause.trim())
1823}
1824
1825fn generate_ts_import_line_base(
1826    module_path: &str,
1827    names: &[String],
1828    default_import: Option<&str>,
1829    namespace_import: Option<&str>,
1830    type_only: bool,
1831) -> String {
1832    let type_prefix = if type_only { "type " } else { "" };
1833
1834    // Side-effect import
1835    if names.is_empty() && default_import.is_none() && namespace_import.is_none() {
1836        return format!("import '{module_path}';");
1837    }
1838
1839    // Namespace import only
1840    if names.is_empty() && default_import.is_none() {
1841        if let Some(namespace) = namespace_import {
1842            return format!("import {type_prefix}* as {namespace} from '{module_path}';");
1843        }
1844    }
1845
1846    // Default + namespace import
1847    if names.is_empty() {
1848        if let (Some(def), Some(namespace)) = (default_import, namespace_import) {
1849            return format!("import {type_prefix}{def}, * as {namespace} from '{module_path}';");
1850        }
1851    }
1852
1853    // Default import only
1854    if names.is_empty() && namespace_import.is_none() {
1855        if let Some(def) = default_import {
1856            return format!("import {type_prefix}{def} from '{module_path}';");
1857        }
1858    }
1859
1860    // Named imports only
1861    if default_import.is_none() && namespace_import.is_none() {
1862        let mut sorted_names = names.to_vec();
1863        sort_named_specifiers(&mut sorted_names);
1864        let names_str = sorted_names.join(", ");
1865        return format!("import {type_prefix}{{ {names_str} }} from '{module_path}';");
1866    }
1867
1868    // Namespace + named imports
1869    if default_import.is_none() {
1870        if let Some(namespace) = namespace_import {
1871            let mut sorted_names = names.to_vec();
1872            sort_named_specifiers(&mut sorted_names);
1873            let names_str = sorted_names.join(", ");
1874            return format!(
1875                "import {type_prefix}{{ {names_str} }}, * as {namespace} from '{module_path}';"
1876            );
1877        }
1878    }
1879
1880    // Default + named + namespace imports
1881    if let (Some(def), Some(namespace)) = (default_import, namespace_import) {
1882        let mut sorted_names = names.to_vec();
1883        sort_named_specifiers(&mut sorted_names);
1884        let names_str = sorted_names.join(", ");
1885        return format!(
1886            "import {type_prefix}{def}, {{ {names_str} }}, * as {namespace} from '{module_path}';"
1887        );
1888    }
1889
1890    // Both default and named imports
1891    if let Some(def) = default_import {
1892        let mut sorted_names = names.to_vec();
1893        sort_named_specifiers(&mut sorted_names);
1894        let names_str = sorted_names.join(", ");
1895        return format!("import {type_prefix}{def}, {{ {names_str} }} from '{module_path}';");
1896    }
1897
1898    // Shouldn't reach here, but handle gracefully
1899    format!("import '{module_path}';")
1900}
1901
1902// ---------------------------------------------------------------------------
1903// Python implementation
1904// ---------------------------------------------------------------------------
1905
1906/// Python 3.x standard library module names (top-level modules).
1907/// Used for import group classification. Covers the commonly-used modules;
1908/// unknown modules are assumed third-party.
1909const PYTHON_STDLIB: &[&str] = &[
1910    "__future__",
1911    "_thread",
1912    "abc",
1913    "aifc",
1914    "argparse",
1915    "array",
1916    "ast",
1917    "asynchat",
1918    "asyncio",
1919    "asyncore",
1920    "atexit",
1921    "audioop",
1922    "base64",
1923    "bdb",
1924    "binascii",
1925    "bisect",
1926    "builtins",
1927    "bz2",
1928    "calendar",
1929    "cgi",
1930    "cgitb",
1931    "chunk",
1932    "cmath",
1933    "cmd",
1934    "code",
1935    "codecs",
1936    "codeop",
1937    "collections",
1938    "colorsys",
1939    "compileall",
1940    "concurrent",
1941    "configparser",
1942    "contextlib",
1943    "contextvars",
1944    "copy",
1945    "copyreg",
1946    "cProfile",
1947    "crypt",
1948    "csv",
1949    "ctypes",
1950    "curses",
1951    "dataclasses",
1952    "datetime",
1953    "dbm",
1954    "decimal",
1955    "difflib",
1956    "dis",
1957    "distutils",
1958    "doctest",
1959    "email",
1960    "encodings",
1961    "enum",
1962    "errno",
1963    "faulthandler",
1964    "fcntl",
1965    "filecmp",
1966    "fileinput",
1967    "fnmatch",
1968    "fractions",
1969    "ftplib",
1970    "functools",
1971    "gc",
1972    "getopt",
1973    "getpass",
1974    "gettext",
1975    "glob",
1976    "grp",
1977    "gzip",
1978    "hashlib",
1979    "heapq",
1980    "hmac",
1981    "html",
1982    "http",
1983    "idlelib",
1984    "imaplib",
1985    "imghdr",
1986    "importlib",
1987    "inspect",
1988    "io",
1989    "ipaddress",
1990    "itertools",
1991    "json",
1992    "keyword",
1993    "lib2to3",
1994    "linecache",
1995    "locale",
1996    "logging",
1997    "lzma",
1998    "mailbox",
1999    "mailcap",
2000    "marshal",
2001    "math",
2002    "mimetypes",
2003    "mmap",
2004    "modulefinder",
2005    "multiprocessing",
2006    "netrc",
2007    "numbers",
2008    "operator",
2009    "optparse",
2010    "os",
2011    "pathlib",
2012    "pdb",
2013    "pickle",
2014    "pickletools",
2015    "pipes",
2016    "pkgutil",
2017    "platform",
2018    "plistlib",
2019    "poplib",
2020    "posixpath",
2021    "pprint",
2022    "profile",
2023    "pstats",
2024    "pty",
2025    "pwd",
2026    "py_compile",
2027    "pyclbr",
2028    "pydoc",
2029    "queue",
2030    "quopri",
2031    "random",
2032    "re",
2033    "readline",
2034    "reprlib",
2035    "resource",
2036    "rlcompleter",
2037    "runpy",
2038    "sched",
2039    "secrets",
2040    "select",
2041    "selectors",
2042    "shelve",
2043    "shlex",
2044    "shutil",
2045    "signal",
2046    "site",
2047    "smtplib",
2048    "sndhdr",
2049    "socket",
2050    "socketserver",
2051    "sqlite3",
2052    "ssl",
2053    "stat",
2054    "statistics",
2055    "string",
2056    "stringprep",
2057    "struct",
2058    "subprocess",
2059    "symtable",
2060    "sys",
2061    "sysconfig",
2062    "syslog",
2063    "tabnanny",
2064    "tarfile",
2065    "tempfile",
2066    "termios",
2067    "textwrap",
2068    "threading",
2069    "time",
2070    "timeit",
2071    "tkinter",
2072    "token",
2073    "tokenize",
2074    "tomllib",
2075    "trace",
2076    "traceback",
2077    "tracemalloc",
2078    "tty",
2079    "turtle",
2080    "types",
2081    "typing",
2082    "unicodedata",
2083    "unittest",
2084    "urllib",
2085    "uuid",
2086    "venv",
2087    "warnings",
2088    "wave",
2089    "weakref",
2090    "webbrowser",
2091    "wsgiref",
2092    "xml",
2093    "xmlrpc",
2094    "zipapp",
2095    "zipfile",
2096    "zipimport",
2097    "zlib",
2098];
2099
2100/// Classify a Python import into a group.
2101pub fn classify_group_py(module_path: &str) -> ImportGroup {
2102    // Relative imports start with '.'
2103    if module_path.starts_with('.') {
2104        return ImportGroup::Internal;
2105    }
2106    // Check stdlib: use the top-level module name (before first '.')
2107    let top_module = module_path.split('.').next().unwrap_or(module_path);
2108    if PYTHON_STDLIB.contains(&top_module) {
2109        ImportGroup::Stdlib
2110    } else {
2111        ImportGroup::External
2112    }
2113}
2114
2115/// Parse imports from a Python file.
2116fn parse_py_imports(source: &str, tree: &Tree) -> ImportBlock {
2117    let root = tree.root_node();
2118    let mut imports = Vec::new();
2119
2120    let mut cursor = root.walk();
2121    if !cursor.goto_first_child() {
2122        return ImportBlock::empty();
2123    }
2124
2125    loop {
2126        let node = cursor.node();
2127        match node.kind() {
2128            "import_statement" => {
2129                if let Some(imp) = parse_py_import_statement(source, &node) {
2130                    imports.push(imp);
2131                }
2132            }
2133            "import_from_statement" => {
2134                if let Some(imp) = parse_py_import_from_statement(source, &node) {
2135                    imports.push(imp);
2136                }
2137            }
2138            _ => {}
2139        }
2140        if !cursor.goto_next_sibling() {
2141            break;
2142        }
2143    }
2144
2145    let byte_range = import_byte_range(&imports);
2146
2147    ImportBlock {
2148        imports,
2149        byte_range,
2150    }
2151}
2152
2153/// Parse `import X` or `import X.Y` Python statements.
2154fn parse_py_import_statement(source: &str, node: &Node) -> Option<ImportStatement> {
2155    let raw_text = source[node.byte_range()].to_string();
2156    let byte_range = node.byte_range();
2157
2158    // Keep every module specifier verbatim. A single Python import statement can
2159    // bind several modules, and aliases must remain in the model so removing or
2160    // merging one binding cannot silently delete its siblings.
2161    let mut specifiers = Vec::new();
2162    let mut c = node.walk();
2163    if c.goto_first_child() {
2164        loop {
2165            let child = c.node();
2166            if matches!(child.kind(), "dotted_name" | "aliased_import") {
2167                specifiers.push(source[child.byte_range()].trim().to_string());
2168            }
2169            if !c.goto_next_sibling() {
2170                break;
2171            }
2172        }
2173    }
2174    let module_path = specifiers
2175        .first()
2176        .map(|specifier| specifier_imported_name(specifier).to_string())?;
2177
2178    let group = classify_group_py(&module_path);
2179
2180    Some(ImportStatement {
2181        module_path,
2182        names: Vec::new(),
2183        default_import: None,
2184        namespace_import: None,
2185        kind: ImportKind::Value,
2186        group,
2187        byte_range,
2188        raw_text,
2189        form: ImportForm::Python {
2190            from_import: false,
2191            named: specifiers,
2192        },
2193    })
2194}
2195
2196/// Parse `from X import Y, Z` or `from . import Y` Python statements.
2197fn parse_py_import_from_statement(source: &str, node: &Node) -> Option<ImportStatement> {
2198    let raw_text = source[node.byte_range()].to_string();
2199    let byte_range = node.byte_range();
2200
2201    let mut module_path = String::new();
2202    let mut names = Vec::new();
2203
2204    let mut c = node.walk();
2205    if c.goto_first_child() {
2206        loop {
2207            let child = c.node();
2208            match child.kind() {
2209                "dotted_name" => {
2210                    // Could be the module name or an imported name
2211                    // The module name comes right after `from`, imported names come after `import`
2212                    // Use position: if we haven't set module_path yet and this comes
2213                    // before the `import` keyword, it's the module.
2214                    if module_path.is_empty()
2215                        && !has_seen_import_keyword(source, node, child.start_byte())
2216                    {
2217                        module_path = source[child.byte_range()].to_string();
2218                    } else {
2219                        // It's an imported name
2220                        names.push(source[child.byte_range()].to_string());
2221                    }
2222                }
2223                "relative_import" => {
2224                    // from . import X or from ..module import X
2225                    module_path = source[child.byte_range()].to_string();
2226                }
2227                "aliased_import" => {
2228                    // Store the full `source as local` spelling so later edits
2229                    // can match either name and reproduce the alias unchanged.
2230                    names.push(source[child.byte_range()].trim().to_string());
2231                }
2232                _ => {}
2233            }
2234            if !c.goto_next_sibling() {
2235                break;
2236            }
2237        }
2238    }
2239
2240    // module_path must be non-empty for a valid import
2241    if module_path.is_empty() {
2242        return None;
2243    }
2244
2245    let group = classify_group_py(&module_path);
2246
2247    Some(ImportStatement {
2248        module_path,
2249        names: names.clone(),
2250        default_import: None,
2251        namespace_import: None,
2252        kind: ImportKind::Value,
2253        group,
2254        byte_range,
2255        raw_text,
2256        form: ImportForm::Python {
2257            from_import: true,
2258            named: names,
2259        },
2260    })
2261}
2262
2263/// Check if the `import` keyword appears before the given byte position in a from...import node.
2264fn has_seen_import_keyword(_source: &str, parent: &Node, before_byte: usize) -> bool {
2265    let mut c = parent.walk();
2266    if c.goto_first_child() {
2267        loop {
2268            let child = c.node();
2269            if child.kind() == "import" && child.start_byte() < before_byte {
2270                return true;
2271            }
2272            if child.start_byte() >= before_byte {
2273                return false;
2274            }
2275            if !c.goto_next_sibling() {
2276                break;
2277            }
2278        }
2279    }
2280    false
2281}
2282
2283/// Generate a Python import line.
2284fn generate_py_import_line(
2285    module_path: &str,
2286    names: &[String],
2287    _default_import: Option<&str>,
2288) -> String {
2289    if names.is_empty() {
2290        // `import module`
2291        format!("import {module_path}")
2292    } else {
2293        // `from module import name1, name2`
2294        let mut sorted = names.to_vec();
2295        sorted.sort();
2296        let names_str = sorted.join(", ");
2297        format!("from {module_path} import {names_str}")
2298    }
2299}
2300
2301// ---------------------------------------------------------------------------
2302// Rust implementation
2303// ---------------------------------------------------------------------------
2304
2305/// Classify a Rust use path into a group.
2306pub fn classify_group_rs(module_path: &str) -> ImportGroup {
2307    // Extract the first path segment (before ::)
2308    let first_seg = module_path.split("::").next().unwrap_or(module_path);
2309    match first_seg {
2310        "std" | "core" | "alloc" => ImportGroup::Stdlib,
2311        "crate" | "self" | "super" => ImportGroup::Internal,
2312        _ => ImportGroup::External,
2313    }
2314}
2315
2316/// Parse imports from a Rust file.
2317fn parse_rs_imports(source: &str, tree: &Tree) -> ImportBlock {
2318    let root = tree.root_node();
2319    let mut imports = Vec::new();
2320
2321    let mut cursor = root.walk();
2322    if !cursor.goto_first_child() {
2323        return ImportBlock::empty();
2324    }
2325
2326    loop {
2327        let node = cursor.node();
2328        if node.kind() == "use_declaration" {
2329            if let Some(imp) = parse_rs_use_declaration(source, &node) {
2330                imports.push(imp);
2331            }
2332        }
2333        if !cursor.goto_next_sibling() {
2334            break;
2335        }
2336    }
2337
2338    let byte_range = import_byte_range(&imports);
2339
2340    ImportBlock {
2341        imports,
2342        byte_range,
2343    }
2344}
2345
2346/// Parse a single `use` declaration from Rust.
2347fn parse_rs_use_declaration(source: &str, node: &Node) -> Option<ImportStatement> {
2348    let raw_text = source[node.byte_range()].to_string();
2349    let byte_range = node.byte_range();
2350
2351    // Capture the EXACT visibility modifier text (`pub`, `pub(crate)`,
2352    // `pub(super)`, `pub(in path)`) so organize re-emits it faithfully instead
2353    // of widening every restricted visibility to a bare `pub`.
2354    let mut visibility: Option<String> = None;
2355    let mut use_path = String::new();
2356    let mut names = Vec::new();
2357
2358    let mut c = node.walk();
2359    if c.goto_first_child() {
2360        loop {
2361            let child = c.node();
2362            match child.kind() {
2363                "visibility_modifier" => {
2364                    visibility = Some(source[child.byte_range()].to_string());
2365                }
2366                "scoped_identifier" | "identifier" | "use_as_clause" => {
2367                    // Full path like `std::collections::HashMap` or just `serde`
2368                    use_path = source[child.byte_range()].to_string();
2369                }
2370                "scoped_use_list" => {
2371                    // e.g. `serde::{Deserialize, Serialize}`
2372                    use_path = source[child.byte_range()].to_string();
2373                    // Also extract the individual names from the use_list
2374                    extract_rs_use_list_names(source, &child, &mut names);
2375                }
2376                _ => {}
2377            }
2378            if !c.goto_next_sibling() {
2379                break;
2380            }
2381        }
2382    }
2383
2384    if use_path.is_empty() {
2385        return None;
2386    }
2387
2388    let group = classify_group_rs(&use_path);
2389
2390    Some(ImportStatement {
2391        module_path: use_path,
2392        names: names.clone(),
2393        // `default_import` carries the visibility text for the Rust engine
2394        // (e.g. "pub", "pub(crate)"). organize re-emits it verbatim.
2395        default_import: visibility.clone(),
2396        namespace_import: None,
2397        kind: ImportKind::Value,
2398        group,
2399        byte_range,
2400        raw_text,
2401        form: ImportForm::RustUse {
2402            visibility,
2403            named: names,
2404        },
2405    })
2406}
2407
2408/// Extract individual names from a Rust `scoped_use_list` node.
2409fn extract_rs_use_list_names(source: &str, node: &Node, names: &mut Vec<String>) {
2410    let mut c = node.walk();
2411    if c.goto_first_child() {
2412        loop {
2413            let child = c.node();
2414            if child.kind() == "use_list" {
2415                // Walk into the use_list to find identifiers
2416                let mut lc = child.walk();
2417                if lc.goto_first_child() {
2418                    loop {
2419                        let lchild = lc.node();
2420                        if lchild.kind() == "identifier" || lchild.kind() == "scoped_identifier" {
2421                            names.push(source[lchild.byte_range()].to_string());
2422                        }
2423                        if !lc.goto_next_sibling() {
2424                            break;
2425                        }
2426                    }
2427                }
2428            }
2429            if !c.goto_next_sibling() {
2430                break;
2431            }
2432        }
2433    }
2434}
2435
2436/// Generate a Rust import line.
2437fn generate_rs_import_line(module_path: &str, names: &[String], _type_only: bool) -> String {
2438    if names.is_empty() {
2439        format!("use {module_path};")
2440    } else {
2441        let mut sorted_names = names.to_vec();
2442        sort_named_specifiers(&mut sorted_names);
2443        format!("use {module_path}::{{{}}};", sorted_names.join(", "))
2444    }
2445}
2446
2447// ---------------------------------------------------------------------------
2448// Go implementation
2449// ---------------------------------------------------------------------------
2450
2451/// Classify a Go import path into a group.
2452pub fn classify_group_go(module_path: &str) -> ImportGroup {
2453    // stdlib paths don't contain dots (e.g., "fmt", "os", "net/http")
2454    // external paths contain dots (e.g., "github.com/pkg/errors")
2455    if module_path.contains('.') {
2456        ImportGroup::External
2457    } else {
2458        ImportGroup::Stdlib
2459    }
2460}
2461
2462/// Parse imports from a Go file.
2463fn parse_go_imports(source: &str, tree: &Tree) -> ImportBlock {
2464    let root = tree.root_node();
2465    let mut imports = Vec::new();
2466
2467    let mut cursor = root.walk();
2468    if !cursor.goto_first_child() {
2469        return ImportBlock::empty();
2470    }
2471
2472    loop {
2473        let node = cursor.node();
2474        if node.kind() == "import_declaration" {
2475            parse_go_import_declaration(source, &node, &mut imports);
2476        }
2477        if !cursor.goto_next_sibling() {
2478            break;
2479        }
2480    }
2481
2482    let byte_range = import_byte_range(&imports);
2483
2484    ImportBlock {
2485        imports,
2486        byte_range,
2487    }
2488}
2489
2490/// Parse a single Go import_declaration (may contain one or multiple specs).
2491fn parse_go_import_declaration(source: &str, node: &Node, imports: &mut Vec<ImportStatement>) {
2492    let mut c = node.walk();
2493    if c.goto_first_child() {
2494        loop {
2495            let child = c.node();
2496            match child.kind() {
2497                "import_spec" => {
2498                    if let Some(imp) = parse_go_import_spec(source, &child) {
2499                        imports.push(imp);
2500                    }
2501                }
2502                "import_spec_list" => {
2503                    // Grouped imports: walk into the list
2504                    let mut lc = child.walk();
2505                    if lc.goto_first_child() {
2506                        loop {
2507                            if lc.node().kind() == "import_spec" {
2508                                if let Some(imp) = parse_go_import_spec(source, &lc.node()) {
2509                                    imports.push(imp);
2510                                }
2511                            }
2512                            if !lc.goto_next_sibling() {
2513                                break;
2514                            }
2515                        }
2516                    }
2517                }
2518                _ => {}
2519            }
2520            if !c.goto_next_sibling() {
2521                break;
2522            }
2523        }
2524    }
2525}
2526
2527/// Parse a single Go import_spec node.
2528fn parse_go_import_spec(source: &str, node: &Node) -> Option<ImportStatement> {
2529    let raw_text = source[node.byte_range()].to_string();
2530    let byte_range = node.byte_range();
2531
2532    let mut import_path = String::new();
2533    let mut alias = None;
2534
2535    let mut c = node.walk();
2536    if c.goto_first_child() {
2537        loop {
2538            let child = c.node();
2539            match child.kind() {
2540                "interpreted_string_literal" => {
2541                    // Extract the path without quotes
2542                    let text = source[child.byte_range()].to_string();
2543                    import_path = text.trim_matches('"').to_string();
2544                }
2545                "identifier" | "blank_identifier" | "dot" => {
2546                    // This is an alias (e.g., `alias "path"` or `. "path"` or `_ "path"`)
2547                    alias = Some(source[child.byte_range()].to_string());
2548                }
2549                _ => {}
2550            }
2551            if !c.goto_next_sibling() {
2552                break;
2553            }
2554        }
2555    }
2556
2557    if import_path.is_empty() {
2558        return None;
2559    }
2560
2561    let group = classify_group_go(&import_path);
2562
2563    Some(ImportStatement {
2564        module_path: import_path,
2565        names: Vec::new(),
2566        default_import: alias.clone(),
2567        namespace_import: None,
2568        kind: ImportKind::Value,
2569        group,
2570        byte_range,
2571        raw_text,
2572        form: ImportForm::Go { alias },
2573    })
2574}
2575
2576/// Public API for Go import line generation (used by add_import handler).
2577pub fn generate_go_import_line_pub(
2578    module_path: &str,
2579    alias: Option<&str>,
2580    in_group: bool,
2581) -> String {
2582    generate_go_import_line(module_path, alias, in_group)
2583}
2584
2585/// Generate a Go import line (public API for command handler).
2586///
2587/// `in_group` controls whether to generate a spec for insertion into an
2588/// existing grouped import (`\t"path"`) or a standalone import (`import "path"`).
2589fn generate_go_import_line(module_path: &str, alias: Option<&str>, in_group: bool) -> String {
2590    if in_group {
2591        // Spec for grouped import block
2592        match alias {
2593            Some(a) => format!("\t{a} \"{module_path}\""),
2594            None => format!("\t\"{module_path}\""),
2595        }
2596    } else {
2597        // Standalone import
2598        match alias {
2599            Some(a) => format!("import {a} \"{module_path}\""),
2600            None => format!("import \"{module_path}\""),
2601        }
2602    }
2603}
2604
2605/// Check if a Go import block has a grouped import declaration.
2606/// Returns the byte range of the full import_declaration if found.
2607pub fn go_has_grouped_import(_source: &str, tree: &Tree) -> Option<Range<usize>> {
2608    let root = tree.root_node();
2609    let mut cursor = root.walk();
2610    if !cursor.goto_first_child() {
2611        return None;
2612    }
2613
2614    loop {
2615        let node = cursor.node();
2616        if node.kind() == "import_declaration" && go_import_declaration_is_grouped(&node) {
2617            return Some(node.byte_range());
2618        }
2619        if !cursor.goto_next_sibling() {
2620            break;
2621        }
2622    }
2623    None
2624}
2625
2626pub fn go_import_declarations_range(_source: &str, tree: &Tree) -> Option<Range<usize>> {
2627    let root = tree.root_node();
2628    let mut cursor = root.walk();
2629    let mut range: Option<Range<usize>> = None;
2630    if !cursor.goto_first_child() {
2631        return None;
2632    }
2633
2634    loop {
2635        let node = cursor.node();
2636        if node.kind() == "import_declaration" {
2637            let node_range = node.byte_range();
2638            range = Some(match range {
2639                Some(existing) => {
2640                    existing.start.min(node_range.start)..existing.end.max(node_range.end)
2641                }
2642                None => node_range,
2643            });
2644        }
2645        if !cursor.goto_next_sibling() {
2646            break;
2647        }
2648    }
2649
2650    range
2651}
2652
2653pub fn go_offset_is_in_grouped_import(_source: &str, tree: &Tree, offset: usize) -> bool {
2654    let root = tree.root_node();
2655    let mut cursor = root.walk();
2656    if !cursor.goto_first_child() {
2657        return false;
2658    }
2659
2660    loop {
2661        let node = cursor.node();
2662        if node.kind() == "import_declaration"
2663            && node.start_byte() < offset
2664            && offset < node.end_byte()
2665            && go_import_declaration_is_grouped(&node)
2666        {
2667            return true;
2668        }
2669        if !cursor.goto_next_sibling() {
2670            break;
2671        }
2672    }
2673
2674    false
2675}
2676
2677fn go_import_declaration_is_grouped(node: &Node) -> bool {
2678    let mut c = node.walk();
2679    if c.goto_first_child() {
2680        loop {
2681            if c.node().kind() == "import_spec_list" {
2682                return true;
2683            }
2684            if !c.goto_next_sibling() {
2685                break;
2686            }
2687        }
2688    }
2689    false
2690}
2691
2692// ---------------------------------------------------------------------------
2693// Solidity implementation
2694// ---------------------------------------------------------------------------
2695
2696/// Classify a Solidity import path: relative (`./`, `../`) is internal,
2697/// everything else (remappings, `@scope/...`, bare) is external. No stdlib.
2698pub fn classify_group_solidity(module_path: &str) -> ImportGroup {
2699    if module_path.starts_with('.') {
2700        ImportGroup::Internal
2701    } else {
2702        ImportGroup::External
2703    }
2704}
2705
2706fn parse_solidity_imports(source: &str, tree: &Tree) -> ImportBlock {
2707    let root = tree.root_node();
2708    let mut imports = Vec::new();
2709    let mut cursor = root.walk();
2710    if cursor.goto_first_child() {
2711        loop {
2712            let node = cursor.node();
2713            if node.kind() == "import_directive" {
2714                if let Some(imp) = parse_solidity_import_directive(source, &node) {
2715                    imports.push(imp);
2716                }
2717            }
2718            if !cursor.goto_next_sibling() {
2719                break;
2720            }
2721        }
2722    }
2723    let byte_range = import_byte_range(&imports);
2724    ImportBlock {
2725        imports,
2726        byte_range,
2727    }
2728}
2729
2730fn strip_one_matching_solidity_quote_pair(value: &str) -> &str {
2731    if value.len() < 2 {
2732        return value;
2733    }
2734
2735    let bytes = value.as_bytes();
2736    let quote = bytes[0];
2737    if matches!(quote, b'\'' | b'"') && bytes.last() == Some(&quote) {
2738        &value[1..value.len() - 1]
2739    } else {
2740        value
2741    }
2742}
2743
2744/// Parse one `import_directive`. The Solidity grammar emits a flat token
2745/// sequence (verified by grammar fixture test), so the four forms are
2746/// distinguished by the presence of `{` (named), `*` (namespace), a trailing
2747/// `as` (whole-file alias), or none (side-effect).
2748fn parse_solidity_import_directive(source: &str, node: &Node) -> Option<ImportStatement> {
2749    let raw_text = source[node.byte_range()].to_string();
2750    let byte_range = node.byte_range();
2751
2752    let mut children: Vec<(String, String)> = Vec::new();
2753    let mut c = node.walk();
2754    if c.goto_first_child() {
2755        loop {
2756            let ch = c.node();
2757            children.push((ch.kind().to_string(), source[ch.byte_range()].to_string()));
2758            if !c.goto_next_sibling() {
2759                break;
2760            }
2761        }
2762    }
2763
2764    // Every form carries exactly one string literal: the imported file path.
2765    let module_path = children
2766        .iter()
2767        .find(|(k, _)| k == "string")
2768        .map(|(_, text)| strip_one_matching_solidity_quote_pair(text).to_string())?;
2769    if module_path.is_empty() {
2770        return None;
2771    }
2772
2773    let has_brace = children.iter().any(|(k, _)| k == "{");
2774    let has_star = children.iter().any(|(k, _)| k == "*");
2775
2776    let mut named: Vec<String> = Vec::new();
2777    let mut namespace: Option<String> = None;
2778    let mut alias: Option<String> = None;
2779
2780    if has_brace {
2781        named = parse_solidity_named_specifiers(&children);
2782    } else if has_star {
2783        namespace = solidity_identifier_after_as(&children);
2784    } else {
2785        // No `{`, no `*`: a trailing `as IDENT` is a whole-file alias;
2786        // otherwise a bare side-effect import.
2787        alias = solidity_identifier_after_as(&children);
2788    }
2789
2790    let kind = if named.is_empty() && namespace.is_none() && alias.is_none() {
2791        ImportKind::SideEffect
2792    } else {
2793        ImportKind::Value
2794    };
2795    let group = classify_group_solidity(&module_path);
2796
2797    Some(ImportStatement {
2798        module_path,
2799        names: named.clone(),
2800        default_import: None,
2801        // Namespace maps to the flat slot so existing readers (dedup) see it;
2802        // the whole-file alias has no flat slot and lives only in `form`.
2803        namespace_import: namespace.clone(),
2804        kind,
2805        group,
2806        byte_range,
2807        raw_text,
2808        form: ImportForm::Solidity {
2809            named,
2810            namespace,
2811            alias,
2812        },
2813    })
2814}
2815
2816/// Return the `identifier` token immediately following the first `as`.
2817fn solidity_identifier_after_as(children: &[(String, String)]) -> Option<String> {
2818    let as_pos = children.iter().position(|(k, _)| k == "as")?;
2819    children[as_pos + 1..]
2820        .iter()
2821        .find(|(k, _)| k == "identifier")
2822        .map(|(_, t)| t.clone())
2823}
2824
2825/// Collect named specifiers between `{` and `}` into verbatim strings,
2826/// combining `A as B` into `"A as B"` to match the ES specifier convention.
2827fn parse_solidity_named_specifiers(children: &[(String, String)]) -> Vec<String> {
2828    let mut names = Vec::new();
2829    let mut in_braces = false;
2830    let mut current: Option<String> = None;
2831    let mut expect_alias = false;
2832    for (k, t) in children {
2833        match k.as_str() {
2834            "{" => in_braces = true,
2835            "}" => {
2836                if let Some(n) = current.take() {
2837                    names.push(n);
2838                }
2839                in_braces = false;
2840            }
2841            _ if !in_braces => {}
2842            "identifier" => {
2843                if expect_alias {
2844                    if let Some(n) = current.take() {
2845                        names.push(format!("{n} as {t}"));
2846                    }
2847                    expect_alias = false;
2848                } else {
2849                    if let Some(n) = current.take() {
2850                        names.push(n);
2851                    }
2852                    current = Some(t.clone());
2853                }
2854            }
2855            "as" => expect_alias = true,
2856            "," => {
2857                if let Some(n) = current.take() {
2858                    names.push(n);
2859                }
2860                expect_alias = false;
2861            }
2862            _ => {}
2863        }
2864    }
2865    names
2866}
2867
2868/// Generate a Solidity import line in the appropriate form.
2869fn generate_solidity_import_line(req: &ImportRequest) -> String {
2870    if !req.names.is_empty() {
2871        format!(
2872            "import {{ {} }} from \"{}\";",
2873            req.names.join(", "),
2874            req.module_path
2875        )
2876    } else if let Some(ns) = req.namespace {
2877        format!("import * as {} from \"{}\";", ns, req.module_path)
2878    } else if let Some(al) = req.alias {
2879        format!("import \"{}\" as {};", req.module_path, al)
2880    } else {
2881        format!("import \"{}\";", req.module_path)
2882    }
2883}
2884
2885/// Skip past a newline character at the given position.
2886fn skip_newline(source: &str, pos: usize) -> usize {
2887    if pos < source.len() {
2888        let bytes = source.as_bytes();
2889        if bytes[pos] == b'\n' {
2890            return pos + 1;
2891        }
2892        if bytes[pos] == b'\r' {
2893            if pos + 1 < source.len() && bytes[pos + 1] == b'\n' {
2894                return pos + 2;
2895            }
2896            return pos + 1;
2897        }
2898    }
2899    pos
2900}
2901
2902// ---------------------------------------------------------------------------
2903// Unit tests
2904// ---------------------------------------------------------------------------
2905
2906#[cfg(test)]
2907mod tests {
2908    use super::*;
2909
2910    // --- ImportForm field-mapping contract (Stream M) ---
2911    //
2912    // These assert the additive `form` field faithfully mirrors the flat
2913    // fields each parser populates. They are the executable field-mapping
2914    // contract from the migration plan: when a reader is moved off a flat
2915    // field onto `form`, these guarantee no information was lost in the
2916    // de-overloading (Rust `pub`, Go alias) or restructuring.
2917
2918    #[test]
2919    fn form_es_mirrors_flat_fields() {
2920        let (_, block) = parse_ts(
2921            "import Default, { a, b as c } from \"ext\";\nimport type { T } from \"./t\";\nimport \"./side\";\nimport * as ns from \"nspkg\";\n",
2922        );
2923        // import Default, { a, b as c } from "ext"
2924        match &block.imports[0].form {
2925            ImportForm::Es {
2926                default_import,
2927                namespace_import,
2928                named,
2929                type_only,
2930                side_effect,
2931                attribute_clause,
2932                attribute_type,
2933            } => {
2934                assert_eq!(default_import.as_deref(), Some("Default"));
2935                assert_eq!(namespace_import, &None);
2936                assert_eq!(named, &block.imports[0].names);
2937                assert!(!type_only);
2938                assert!(!side_effect);
2939                assert_eq!(attribute_clause, &None);
2940                assert_eq!(attribute_type, &None);
2941            }
2942            other => panic!("expected Es, got {other:?}"),
2943        }
2944        // import type { T } from "./t"
2945        match &block.imports[1].form {
2946            ImportForm::Es {
2947                type_only, named, ..
2948            } => {
2949                assert!(type_only);
2950                assert_eq!(named, &block.imports[1].names);
2951            }
2952            other => panic!("expected Es type-only, got {other:?}"),
2953        }
2954        // import "./side"
2955        match &block.imports[2].form {
2956            ImportForm::Es { side_effect, .. } => assert!(side_effect),
2957            other => panic!("expected Es side-effect, got {other:?}"),
2958        }
2959        // import * as ns from "nspkg"
2960        match &block.imports[3].form {
2961            ImportForm::Es {
2962                namespace_import, ..
2963            } => assert_eq!(namespace_import.as_deref(), Some("ns")),
2964            other => panic!("expected Es namespace, got {other:?}"),
2965        }
2966    }
2967
2968    #[test]
2969    fn form_python_mirrors_flat_fields() {
2970        let (_, block) = parse_py("import os\nfrom sys import argv, path\n");
2971        match &block.imports[0].form {
2972            ImportForm::Python { from_import, named } => {
2973                assert!(!from_import, "`import os` is not a from-import");
2974                assert_eq!(named, &["os"]);
2975            }
2976            other => panic!("expected Python import, got {other:?}"),
2977        }
2978        match &block.imports[1].form {
2979            ImportForm::Python { from_import, named } => {
2980                assert!(from_import, "`from sys import ...` is a from-import");
2981                assert_eq!(named, &block.imports[1].names);
2982            }
2983            other => panic!("expected Python from-import, got {other:?}"),
2984        }
2985    }
2986
2987    #[test]
2988    fn form_rust_de_overloads_pub_from_default_import() {
2989        let (_, block) = parse_rust("pub use crate::a::Exported;\nuse std::fmt::Debug;\n");
2990        // pub use -> visibility=Some("pub"); flat field still carries the "pub" hack.
2991        match &block.imports[0].form {
2992            ImportForm::RustUse { visibility, named } => {
2993                assert_eq!(visibility.as_deref(), Some("pub"));
2994                assert_eq!(named, &block.imports[0].names);
2995            }
2996            other => panic!("expected RustUse, got {other:?}"),
2997        }
2998        assert_eq!(
2999            block.imports[0].default_import.as_deref(),
3000            Some("pub"),
3001            "flat field unchanged during additive migration"
3002        );
3003        // plain use -> visibility=None
3004        match &block.imports[1].form {
3005            ImportForm::RustUse { visibility, .. } => assert_eq!(visibility, &None),
3006            other => panic!("expected RustUse, got {other:?}"),
3007        }
3008        assert_eq!(block.imports[1].default_import, None);
3009    }
3010
3011    #[test]
3012    fn form_go_de_overloads_alias_from_default_import() {
3013        // The current Go parser only captures blank (`_`) / dot bindings as the
3014        // alias (regular package aliases like `al "path"` are a pre-existing
3015        // parser gap — not extracted into `default_import` today). The contract
3016        // locked here is that `form.alias` mirrors `default_import` exactly,
3017        // whatever the parser captures, so the de-overload is information-faithful.
3018        let (_, block) =
3019            parse_go("package main\n\nimport (\n\t_ \"github.com/x/y\"\n\t\"fmt\"\n)\n");
3020        let blank = block
3021            .imports
3022            .iter()
3023            .find(|i| i.module_path == "github.com/x/y")
3024            .expect("blank import parsed");
3025        match &blank.form {
3026            ImportForm::Go { alias } => assert_eq!(alias.as_deref(), Some("_")),
3027            other => panic!("expected Go blank-aliased, got {other:?}"),
3028        }
3029        assert_eq!(
3030            blank.default_import.as_deref(),
3031            Some("_"),
3032            "form.alias mirrors the flat default_import field exactly"
3033        );
3034        let plain = block
3035            .imports
3036            .iter()
3037            .find(|i| i.module_path == "fmt")
3038            .expect("plain import parsed");
3039        match &plain.form {
3040            ImportForm::Go { alias } => assert_eq!(alias, &None),
3041            other => panic!("expected Go plain, got {other:?}"),
3042        }
3043        assert_eq!(plain.default_import, None);
3044    }
3045
3046    fn parse_ts(source: &str) -> (Tree, ImportBlock) {
3047        let grammar = grammar_for(LangId::TypeScript);
3048        let mut parser = Parser::new();
3049        parser.set_language(&grammar).unwrap();
3050        let tree = parser.parse(source, None).unwrap();
3051        let block = parse_imports(source, &tree, LangId::TypeScript);
3052        (tree, block)
3053    }
3054
3055    fn parse_js(source: &str) -> (Tree, ImportBlock) {
3056        let grammar = grammar_for(LangId::JavaScript);
3057        let mut parser = Parser::new();
3058        parser.set_language(&grammar).unwrap();
3059        let tree = parser.parse(source, None).unwrap();
3060        let block = parse_imports(source, &tree, LangId::JavaScript);
3061        (tree, block)
3062    }
3063
3064    fn parse_vue(source: &str) -> (Tree, ImportBlock) {
3065        let grammar = grammar_for(LangId::Vue);
3066        let mut parser = Parser::new();
3067        parser.set_language(&grammar).unwrap();
3068        let tree = parser.parse(source, None).unwrap();
3069        let block = parse_imports(source, &tree, LangId::Vue);
3070        (tree, block)
3071    }
3072
3073    /// Locks the tree-sitter-vue node kinds the Vue engine depends on: the
3074    /// `<script>` body is exposed as a single `raw_text` node inside a
3075    /// `script_element`. If a grammar bump changes this, the engine breaks
3076    /// silently, so assert it here.
3077    #[test]
3078    fn vue_grammar_node_kinds_are_stable() {
3079        let src = "<template>\n  <div />\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from 'vue'\n</script>\n";
3080        let grammar = grammar_for(LangId::Vue);
3081        let mut parser = Parser::new();
3082        parser.set_language(&grammar).unwrap();
3083        let tree = parser.parse(src, None).unwrap();
3084        let root = tree.root_node();
3085        let mut cursor = root.walk();
3086        let script = root
3087            .named_children(&mut cursor)
3088            .find(|n| n.kind() == "script_element")
3089            .expect("expected a script_element node");
3090        let mut inner = script.walk();
3091        assert!(
3092            script
3093                .named_children(&mut inner)
3094                .any(|n| n.kind() == "raw_text"),
3095            "expected script body exposed as raw_text"
3096        );
3097    }
3098
3099    #[test]
3100    fn vue_parses_script_imports_with_whole_file_offsets() {
3101        let src = "<template>\n  <div />\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport Foo from './Foo.vue'\nconst x = ref(0)\n</script>\n";
3102        let (_tree, block) = parse_vue(src);
3103        assert_eq!(block.imports.len(), 2, "should find both script imports");
3104        // Byte ranges must be whole-file (inside the <script> block), not
3105        // script-relative — verify the raw slice round-trips.
3106        for imp in &block.imports {
3107            assert_eq!(&src[imp.byte_range.clone()], imp.raw_text);
3108            assert!(
3109                imp.byte_range.start > src.find("<script").unwrap(),
3110                "import offset must fall inside the script block"
3111            );
3112        }
3113        assert_eq!(block.imports[0].module_path, "vue");
3114        assert_eq!(block.imports[1].module_path, "./Foo.vue");
3115    }
3116
3117    #[test]
3118    fn vue_without_script_block_has_no_imports() {
3119        let src = "<template>\n  <div />\n</template>\n\n<style>.x{}</style>\n";
3120        let (_tree, block) = parse_vue(src);
3121        assert!(block.imports.is_empty());
3122        assert!(block.byte_range.is_none());
3123    }
3124
3125    // --- Basic parsing ---
3126
3127    #[test]
3128    fn parse_ts_named_imports() {
3129        let source = "import { useState, useEffect } from 'react';\n";
3130        let (_, block) = parse_ts(source);
3131        assert_eq!(block.imports.len(), 1);
3132        let imp = &block.imports[0];
3133        assert_eq!(imp.module_path, "react");
3134        assert!(imp.names.contains(&"useState".to_string()));
3135        assert!(imp.names.contains(&"useEffect".to_string()));
3136        assert_eq!(imp.kind, ImportKind::Value);
3137        assert_eq!(imp.group, ImportGroup::External);
3138    }
3139
3140    #[test]
3141    fn parse_ts_default_import() {
3142        let source = "import React from 'react';\n";
3143        let (_, block) = parse_ts(source);
3144        assert_eq!(block.imports.len(), 1);
3145        let imp = &block.imports[0];
3146        assert_eq!(imp.default_import.as_deref(), Some("React"));
3147        assert_eq!(imp.kind, ImportKind::Value);
3148    }
3149
3150    #[test]
3151    fn parse_ts_side_effect_import() {
3152        let source = "import './styles.css';\n";
3153        let (_, block) = parse_ts(source);
3154        assert_eq!(block.imports.len(), 1);
3155        assert_eq!(block.imports[0].kind, ImportKind::SideEffect);
3156        assert_eq!(block.imports[0].module_path, "./styles.css");
3157    }
3158
3159    #[test]
3160    fn parse_ts_relative_import() {
3161        let source = "import { helper } from './utils';\n";
3162        let (_, block) = parse_ts(source);
3163        assert_eq!(block.imports.len(), 1);
3164        assert_eq!(block.imports[0].group, ImportGroup::Internal);
3165    }
3166
3167    #[test]
3168    fn parse_ts_multiple_groups() {
3169        let source = "\
3170import React from 'react';
3171import { useState } from 'react';
3172import { helper } from './utils';
3173import { Config } from '../config';
3174";
3175        let (_, block) = parse_ts(source);
3176        assert_eq!(block.imports.len(), 4);
3177
3178        let external: Vec<_> = block
3179            .imports
3180            .iter()
3181            .filter(|i| i.group == ImportGroup::External)
3182            .collect();
3183        let relative: Vec<_> = block
3184            .imports
3185            .iter()
3186            .filter(|i| i.group == ImportGroup::Internal)
3187            .collect();
3188        assert_eq!(external.len(), 2);
3189        assert_eq!(relative.len(), 2);
3190    }
3191
3192    #[test]
3193    fn parse_ts_namespace_import() {
3194        let source = "import * as path from 'path';\n";
3195        let (_, block) = parse_ts(source);
3196        assert_eq!(block.imports.len(), 1);
3197        let imp = &block.imports[0];
3198        assert_eq!(imp.namespace_import.as_deref(), Some("path"));
3199        assert_eq!(imp.kind, ImportKind::Value);
3200    }
3201
3202    #[test]
3203    fn parse_js_imports() {
3204        let source = "import { readFile } from 'fs';\nimport { helper } from './helper';\n";
3205        let (_, block) = parse_js(source);
3206        assert_eq!(block.imports.len(), 2);
3207        assert_eq!(block.imports[0].group, ImportGroup::External);
3208        assert_eq!(block.imports[1].group, ImportGroup::Internal);
3209    }
3210
3211    // --- Group classification ---
3212
3213    #[test]
3214    fn classify_external() {
3215        assert_eq!(classify_group_ts("react"), ImportGroup::External);
3216        assert_eq!(classify_group_ts("@scope/pkg"), ImportGroup::External);
3217        assert_eq!(classify_group_ts("lodash/map"), ImportGroup::External);
3218    }
3219
3220    #[test]
3221    fn classify_relative() {
3222        assert_eq!(classify_group_ts("./utils"), ImportGroup::Internal);
3223        assert_eq!(classify_group_ts("../config"), ImportGroup::Internal);
3224        assert_eq!(classify_group_ts("./"), ImportGroup::Internal);
3225    }
3226
3227    // --- Dedup ---
3228
3229    #[test]
3230    fn dedup_detects_same_named_import() {
3231        let source = "import { useState } from 'react';\n";
3232        let (_, block) = parse_ts(source);
3233        assert!(is_duplicate(
3234            &block,
3235            "react",
3236            &["useState".to_string()],
3237            None,
3238            false
3239        ));
3240    }
3241
3242    #[test]
3243    fn dedup_misses_different_name() {
3244        let source = "import { useState } from 'react';\n";
3245        let (_, block) = parse_ts(source);
3246        assert!(!is_duplicate(
3247            &block,
3248            "react",
3249            &["useEffect".to_string()],
3250            None,
3251            false
3252        ));
3253    }
3254
3255    #[test]
3256    fn dedup_detects_default_import() {
3257        let source = "import React from 'react';\n";
3258        let (_, block) = parse_ts(source);
3259        assert!(is_duplicate(&block, "react", &[], Some("React"), false));
3260    }
3261
3262    #[test]
3263    fn dedup_side_effect() {
3264        let source = "import './styles.css';\n";
3265        let (_, block) = parse_ts(source);
3266        assert!(is_duplicate(&block, "./styles.css", &[], None, false));
3267    }
3268
3269    #[test]
3270    fn dedup_namespace_import_distinct_from_side_effect_import() {
3271        let side_effect_source = "import 'fs';\n";
3272        let (_, side_effect_block) = parse_ts(side_effect_source);
3273        assert!(!is_duplicate_with_namespace(
3274            &side_effect_block,
3275            "fs",
3276            &[],
3277            None,
3278            Some("fs"),
3279            false
3280        ));
3281
3282        let namespace_source = "import * as fs from 'fs';\n";
3283        let (_, namespace_block) = parse_ts(namespace_source);
3284        assert!(!is_duplicate(&namespace_block, "fs", &[], None, false));
3285        assert!(is_duplicate_with_namespace(
3286            &namespace_block,
3287            "fs",
3288            &[],
3289            None,
3290            Some("fs"),
3291            false
3292        ));
3293        assert!(!is_duplicate_with_namespace(
3294            &namespace_block,
3295            "fs",
3296            &[],
3297            None,
3298            Some("other"),
3299            false
3300        ));
3301    }
3302
3303    #[test]
3304    fn dedup_type_vs_value() {
3305        let source = "import { FC } from 'react';\n";
3306        let (_, block) = parse_ts(source);
3307        // Type import should NOT match a value import of the same name
3308        assert!(!is_duplicate(
3309            &block,
3310            "react",
3311            &["FC".to_string()],
3312            None,
3313            true
3314        ));
3315    }
3316
3317    // --- Generation ---
3318
3319    #[test]
3320    fn generate_named_import() {
3321        let line = generate_import_line(
3322            LangId::TypeScript,
3323            "react",
3324            &["useState".to_string(), "useEffect".to_string()],
3325            None,
3326            false,
3327        );
3328        assert_eq!(line, "import { useEffect, useState } from 'react';");
3329    }
3330
3331    #[test]
3332    fn generate_named_import_sorts_by_imported_name() {
3333        let line = generate_import_line(
3334            LangId::TypeScript,
3335            "x",
3336            &[
3337                "useState".to_string(),
3338                "type Foo".to_string(),
3339                "stdin as input".to_string(),
3340                "type Bar".to_string(),
3341            ],
3342            None,
3343            false,
3344        );
3345        assert_eq!(
3346            line,
3347            "import { type Bar, type Foo, stdin as input, useState } from 'x';"
3348        );
3349    }
3350
3351    #[test]
3352    fn generate_default_import() {
3353        let line = generate_import_line(LangId::TypeScript, "react", &[], Some("React"), false);
3354        assert_eq!(line, "import React from 'react';");
3355    }
3356
3357    #[test]
3358    fn generate_type_import() {
3359        let line =
3360            generate_import_line(LangId::TypeScript, "react", &["FC".to_string()], None, true);
3361        assert_eq!(line, "import type { FC } from 'react';");
3362    }
3363
3364    #[test]
3365    fn generate_side_effect_import() {
3366        let line = generate_import_line(LangId::TypeScript, "./styles.css", &[], None, false);
3367        assert_eq!(line, "import './styles.css';");
3368    }
3369
3370    #[test]
3371    fn generate_default_and_named() {
3372        let line = generate_import_line(
3373            LangId::TypeScript,
3374            "react",
3375            &["useState".to_string()],
3376            Some("React"),
3377            false,
3378        );
3379        assert_eq!(line, "import React, { useState } from 'react';");
3380    }
3381
3382    #[test]
3383    fn parse_es_import_attributes_preserve_standard_and_legacy_spellings() {
3384        let source = r#"import data from './data.json' with { type: 'json' };
3385import legacy from './legacy.json' assert { type: 'json' };
3386import escaped from './escaped.json' with { "t\u0079pe": "j\u0073on" };
3387"#;
3388        let (_, block) = parse_js(source);
3389
3390        assert_eq!(
3391            es_import_attribute_clause(&block.imports[0]),
3392            Some("with { type: 'json' }")
3393        );
3394        assert_eq!(
3395            es_import_attribute_clause(&block.imports[1]),
3396            Some("assert { type: 'json' }")
3397        );
3398        assert_eq!(es_import_attribute_type(&block.imports[0]), Some("json"));
3399        assert_eq!(es_import_attribute_type(&block.imports[1]), Some("json"));
3400        assert_eq!(es_import_attribute_type(&block.imports[2]), Some("json"));
3401    }
3402
3403    #[test]
3404    fn parse_ts_type_import() {
3405        let source = "import type { FC } from 'react';\n";
3406        let (_, block) = parse_ts(source);
3407        assert_eq!(block.imports.len(), 1);
3408        let imp = &block.imports[0];
3409        assert_eq!(imp.kind, ImportKind::Type);
3410        assert!(imp.names.contains(&"FC".to_string()));
3411        assert_eq!(imp.group, ImportGroup::External);
3412    }
3413
3414    // --- Insertion point ---
3415
3416    #[test]
3417    fn insertion_empty_file() {
3418        let source = "";
3419        let (_, block) = parse_ts(source);
3420        let (offset, _, _) =
3421            find_insertion_point(source, &block, ImportGroup::External, "react", false);
3422        assert_eq!(offset, 0);
3423    }
3424
3425    #[test]
3426    fn insertion_alphabetical_within_group() {
3427        let source = "\
3428import { a } from 'alpha';
3429import { c } from 'charlie';
3430";
3431        let (_, block) = parse_ts(source);
3432        let (offset, _, _) =
3433            find_insertion_point(source, &block, ImportGroup::External, "bravo", false);
3434        // Should insert before 'charlie' (which starts at line 2)
3435        let before_charlie = source.find("import { c }").unwrap();
3436        assert_eq!(offset, before_charlie);
3437    }
3438
3439    // --- Python parsing ---
3440
3441    fn parse_py(source: &str) -> (Tree, ImportBlock) {
3442        let grammar = grammar_for(LangId::Python);
3443        let mut parser = Parser::new();
3444        parser.set_language(&grammar).unwrap();
3445        let tree = parser.parse(source, None).unwrap();
3446        let block = parse_imports(source, &tree, LangId::Python);
3447        (tree, block)
3448    }
3449
3450    #[test]
3451    fn parse_py_import_statement() {
3452        let source = "import os\nimport sys\n";
3453        let (_, block) = parse_py(source);
3454        assert_eq!(block.imports.len(), 2);
3455        assert_eq!(block.imports[0].module_path, "os");
3456        assert_eq!(block.imports[1].module_path, "sys");
3457        assert_eq!(block.imports[0].group, ImportGroup::Stdlib);
3458    }
3459
3460    #[test]
3461    fn parse_py_import_statement_preserves_aliases_and_siblings() {
3462        let source = "import alpha, beta as local_beta\n";
3463        let (_, block) = parse_py(source);
3464        let imp = &block.imports[0];
3465        assert_eq!(imp.module_path, "alpha");
3466        assert!(imp.names.is_empty());
3467        match &imp.form {
3468            ImportForm::Python { from_import, named } => {
3469                assert!(!from_import);
3470                assert_eq!(named, &["alpha", "beta as local_beta"]);
3471                assert!(specifier_matches(&named[1], "beta"));
3472                assert!(specifier_matches(&named[1], "local_beta"));
3473            }
3474            other => panic!("expected Python import, got {other:?}"),
3475        }
3476    }
3477
3478    #[test]
3479    fn parse_py_from_import() {
3480        let source = "from collections import OrderedDict\nfrom typing import List, Optional\n";
3481        let (_, block) = parse_py(source);
3482        assert_eq!(block.imports.len(), 2);
3483        assert_eq!(block.imports[0].module_path, "collections");
3484        assert!(block.imports[0].names.contains(&"OrderedDict".to_string()));
3485        assert_eq!(block.imports[0].group, ImportGroup::Stdlib);
3486        assert_eq!(block.imports[1].module_path, "typing");
3487        assert!(block.imports[1].names.contains(&"List".to_string()));
3488        assert!(block.imports[1].names.contains(&"Optional".to_string()));
3489    }
3490
3491    #[test]
3492    fn parse_py_from_import_preserves_aliases() {
3493        let source = "from module import alpha, beta as local_beta\n";
3494        let (_, block) = parse_py(source);
3495        let imp = &block.imports[0];
3496        assert_eq!(imp.names, ["alpha", "beta as local_beta"]);
3497        assert!(specifier_matches(&imp.names[1], "beta"));
3498        assert!(specifier_matches(&imp.names[1], "local_beta"));
3499        assert_eq!(
3500            imp.form,
3501            ImportForm::Python {
3502                from_import: true,
3503                named: vec!["alpha".to_string(), "beta as local_beta".to_string()],
3504            }
3505        );
3506    }
3507
3508    #[test]
3509    fn parse_py_relative_import() {
3510        let source = "from . import utils\nfrom ..config import Settings\n";
3511        let (_, block) = parse_py(source);
3512        assert_eq!(block.imports.len(), 2);
3513        assert_eq!(block.imports[0].module_path, ".");
3514        assert!(block.imports[0].names.contains(&"utils".to_string()));
3515        assert_eq!(block.imports[0].group, ImportGroup::Internal);
3516        assert_eq!(block.imports[1].module_path, "..config");
3517        assert_eq!(block.imports[1].group, ImportGroup::Internal);
3518    }
3519
3520    #[test]
3521    fn classify_py_groups() {
3522        assert_eq!(classify_group_py("os"), ImportGroup::Stdlib);
3523        assert_eq!(classify_group_py("sys"), ImportGroup::Stdlib);
3524        assert_eq!(classify_group_py("json"), ImportGroup::Stdlib);
3525        assert_eq!(classify_group_py("collections"), ImportGroup::Stdlib);
3526        assert_eq!(classify_group_py("os.path"), ImportGroup::Stdlib);
3527        assert_eq!(classify_group_py("requests"), ImportGroup::External);
3528        assert_eq!(classify_group_py("flask"), ImportGroup::External);
3529        assert_eq!(classify_group_py("."), ImportGroup::Internal);
3530        assert_eq!(classify_group_py("..config"), ImportGroup::Internal);
3531        assert_eq!(classify_group_py(".utils"), ImportGroup::Internal);
3532    }
3533
3534    #[test]
3535    fn parse_py_three_groups() {
3536        let source = "import os\nimport sys\n\nimport requests\n\nfrom . import utils\n";
3537        let (_, block) = parse_py(source);
3538        let stdlib: Vec<_> = block
3539            .imports
3540            .iter()
3541            .filter(|i| i.group == ImportGroup::Stdlib)
3542            .collect();
3543        let external: Vec<_> = block
3544            .imports
3545            .iter()
3546            .filter(|i| i.group == ImportGroup::External)
3547            .collect();
3548        let internal: Vec<_> = block
3549            .imports
3550            .iter()
3551            .filter(|i| i.group == ImportGroup::Internal)
3552            .collect();
3553        assert_eq!(stdlib.len(), 2);
3554        assert_eq!(external.len(), 1);
3555        assert_eq!(internal.len(), 1);
3556    }
3557
3558    #[test]
3559    fn generate_py_import() {
3560        let line = generate_import_line(LangId::Python, "os", &[], None, false);
3561        assert_eq!(line, "import os");
3562    }
3563
3564    #[test]
3565    fn generate_py_from_import() {
3566        let line = generate_import_line(
3567            LangId::Python,
3568            "collections",
3569            &["OrderedDict".to_string()],
3570            None,
3571            false,
3572        );
3573        assert_eq!(line, "from collections import OrderedDict");
3574    }
3575
3576    #[test]
3577    fn generate_py_from_import_multiple() {
3578        let line = generate_import_line(
3579            LangId::Python,
3580            "typing",
3581            &["Optional".to_string(), "List".to_string()],
3582            None,
3583            false,
3584        );
3585        assert_eq!(line, "from typing import List, Optional");
3586    }
3587
3588    // --- Rust parsing ---
3589
3590    fn parse_rust(source: &str) -> (Tree, ImportBlock) {
3591        let grammar = grammar_for(LangId::Rust);
3592        let mut parser = Parser::new();
3593        parser.set_language(&grammar).unwrap();
3594        let tree = parser.parse(source, None).unwrap();
3595        let block = parse_imports(source, &tree, LangId::Rust);
3596        (tree, block)
3597    }
3598
3599    #[test]
3600    fn parse_rs_use_std() {
3601        let source = "use std::collections::HashMap;\nuse std::io::Read;\n";
3602        let (_, block) = parse_rust(source);
3603        assert_eq!(block.imports.len(), 2);
3604        assert_eq!(block.imports[0].module_path, "std::collections::HashMap");
3605        assert_eq!(block.imports[0].group, ImportGroup::Stdlib);
3606        assert_eq!(block.imports[1].group, ImportGroup::Stdlib);
3607    }
3608
3609    #[test]
3610    fn parse_rs_use_external() {
3611        let source = "use serde::{Deserialize, Serialize};\n";
3612        let (_, block) = parse_rust(source);
3613        assert_eq!(block.imports.len(), 1);
3614        assert_eq!(block.imports[0].group, ImportGroup::External);
3615        assert!(block.imports[0].names.contains(&"Deserialize".to_string()));
3616        assert!(block.imports[0].names.contains(&"Serialize".to_string()));
3617    }
3618
3619    #[test]
3620    fn parse_rs_use_crate() {
3621        let source = "use crate::config::Settings;\nuse super::parent::Thing;\n";
3622        let (_, block) = parse_rust(source);
3623        assert_eq!(block.imports.len(), 2);
3624        assert_eq!(block.imports[0].group, ImportGroup::Internal);
3625        assert_eq!(block.imports[1].group, ImportGroup::Internal);
3626    }
3627
3628    #[test]
3629    fn parse_rs_pub_use() {
3630        let source = "pub use super::parent::Thing;\n";
3631        let (_, block) = parse_rust(source);
3632        assert_eq!(block.imports.len(), 1);
3633        // `pub` is stored in default_import as a marker
3634        assert_eq!(block.imports[0].default_import.as_deref(), Some("pub"));
3635    }
3636
3637    #[test]
3638    fn classify_rs_groups() {
3639        assert_eq!(
3640            classify_group_rs("std::collections::HashMap"),
3641            ImportGroup::Stdlib
3642        );
3643        assert_eq!(classify_group_rs("core::mem"), ImportGroup::Stdlib);
3644        assert_eq!(classify_group_rs("alloc::vec"), ImportGroup::Stdlib);
3645        assert_eq!(
3646            classify_group_rs("serde::Deserialize"),
3647            ImportGroup::External
3648        );
3649        assert_eq!(classify_group_rs("tokio::runtime"), ImportGroup::External);
3650        assert_eq!(classify_group_rs("crate::config"), ImportGroup::Internal);
3651        assert_eq!(classify_group_rs("self::utils"), ImportGroup::Internal);
3652        assert_eq!(classify_group_rs("super::parent"), ImportGroup::Internal);
3653    }
3654
3655    #[test]
3656    fn generate_rs_use() {
3657        let line = generate_import_line(LangId::Rust, "std::fmt::Display", &[], None, false);
3658        assert_eq!(line, "use std::fmt::Display;");
3659    }
3660
3661    // --- Go parsing ---
3662
3663    fn parse_go(source: &str) -> (Tree, ImportBlock) {
3664        let grammar = grammar_for(LangId::Go);
3665        let mut parser = Parser::new();
3666        parser.set_language(&grammar).unwrap();
3667        let tree = parser.parse(source, None).unwrap();
3668        let block = parse_imports(source, &tree, LangId::Go);
3669        (tree, block)
3670    }
3671
3672    #[test]
3673    fn parse_go_single_import() {
3674        let source = "package main\n\nimport \"fmt\"\n";
3675        let (_, block) = parse_go(source);
3676        assert_eq!(block.imports.len(), 1);
3677        assert_eq!(block.imports[0].module_path, "fmt");
3678        assert_eq!(block.imports[0].group, ImportGroup::Stdlib);
3679    }
3680
3681    #[test]
3682    fn parse_go_grouped_import() {
3683        let source =
3684            "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/pkg/errors\"\n)\n";
3685        let (_, block) = parse_go(source);
3686        assert_eq!(block.imports.len(), 3);
3687        assert_eq!(block.imports[0].module_path, "fmt");
3688        assert_eq!(block.imports[0].group, ImportGroup::Stdlib);
3689        assert_eq!(block.imports[1].module_path, "os");
3690        assert_eq!(block.imports[1].group, ImportGroup::Stdlib);
3691        assert_eq!(block.imports[2].module_path, "github.com/pkg/errors");
3692        assert_eq!(block.imports[2].group, ImportGroup::External);
3693    }
3694
3695    #[test]
3696    fn parse_go_mixed_imports() {
3697        // Single + grouped
3698        let source = "package main\n\nimport \"fmt\"\n\nimport (\n\t\"os\"\n\t\"github.com/pkg/errors\"\n)\n";
3699        let (_, block) = parse_go(source);
3700        assert_eq!(block.imports.len(), 3);
3701    }
3702
3703    #[test]
3704    fn classify_go_groups() {
3705        assert_eq!(classify_group_go("fmt"), ImportGroup::Stdlib);
3706        assert_eq!(classify_group_go("os"), ImportGroup::Stdlib);
3707        assert_eq!(classify_group_go("net/http"), ImportGroup::Stdlib);
3708        assert_eq!(classify_group_go("encoding/json"), ImportGroup::Stdlib);
3709        assert_eq!(
3710            classify_group_go("github.com/pkg/errors"),
3711            ImportGroup::External
3712        );
3713        assert_eq!(
3714            classify_group_go("golang.org/x/tools"),
3715            ImportGroup::External
3716        );
3717    }
3718
3719    #[test]
3720    fn generate_go_standalone() {
3721        let line = generate_go_import_line("fmt", None, false);
3722        assert_eq!(line, "import \"fmt\"");
3723    }
3724
3725    #[test]
3726    fn generate_go_grouped_spec() {
3727        let line = generate_go_import_line("fmt", None, true);
3728        assert_eq!(line, "\t\"fmt\"");
3729    }
3730
3731    #[test]
3732    fn generate_go_with_alias() {
3733        let line = generate_go_import_line("github.com/pkg/errors", Some("errs"), false);
3734        assert_eq!(line, "import errs \"github.com/pkg/errors\"");
3735    }
3736
3737    // --- Solidity ---
3738
3739    fn parse_solidity(source: &str) -> (Tree, ImportBlock) {
3740        let grammar = grammar_for(LangId::Solidity);
3741        let mut parser = Parser::new();
3742        parser.set_language(&grammar).unwrap();
3743        let tree = parser.parse(source, None).unwrap();
3744        let block = parse_imports(source, &tree, LangId::Solidity);
3745        (tree, block)
3746    }
3747
3748    /// Grammar fixture (council #6): lock the tree-sitter-solidity node kinds the
3749    /// parser depends on. If the grammar updates and renames these, this test
3750    /// fails loudly before the parser silently mis-parses.
3751    #[test]
3752    fn solidity_grammar_node_kinds_are_stable() {
3753        let grammar = grammar_for(LangId::Solidity);
3754        let mut parser = Parser::new();
3755        parser.set_language(&grammar).unwrap();
3756        let src = "import { Foo, Bar as Baz } from \"./A.sol\";\nimport * as N from \"./B.sol\";\nimport \"./C.sol\" as C;\nimport \"./D.sol\";\n";
3757        let tree = parser.parse(src, None).unwrap();
3758        let mut kinds: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
3759        fn walk(node: tree_sitter::Node, kinds: &mut std::collections::BTreeSet<String>) {
3760            kinds.insert(node.kind().to_string());
3761            let mut c = node.walk();
3762            if c.goto_first_child() {
3763                loop {
3764                    walk(c.node(), kinds);
3765                    if !c.goto_next_sibling() {
3766                        break;
3767                    }
3768                }
3769            }
3770        }
3771        walk(tree.root_node(), &mut kinds);
3772        for required in [
3773            "import_directive",
3774            "string",
3775            "identifier",
3776            "as",
3777            "from",
3778            "*",
3779            "{",
3780            "}",
3781        ] {
3782            assert!(
3783                kinds.contains(required),
3784                "solidity grammar missing node kind {required:?}; present: {kinds:?}"
3785            );
3786        }
3787    }
3788
3789    #[test]
3790    fn solidity_string_path_strips_one_matching_quote_pair() {
3791        assert_eq!(
3792            strip_one_matching_solidity_quote_pair("\"./A.sol\""),
3793            "./A.sol"
3794        );
3795        assert_eq!(
3796            strip_one_matching_solidity_quote_pair("'./A.sol'"),
3797            "./A.sol"
3798        );
3799        assert_eq!(
3800            strip_one_matching_solidity_quote_pair("\"./a'b.sol\""),
3801            "./a'b.sol"
3802        );
3803        assert_eq!(
3804            strip_one_matching_solidity_quote_pair("\"\"./A.sol\"\""),
3805            "\"./A.sol\""
3806        );
3807        assert_eq!(
3808            strip_one_matching_solidity_quote_pair("'./A.sol\""),
3809            "'./A.sol\""
3810        );
3811        assert_eq!(
3812            strip_one_matching_solidity_quote_pair("\"./A.sol"),
3813            "\"./A.sol"
3814        );
3815        assert_eq!(strip_one_matching_solidity_quote_pair("\""), "\"");
3816    }
3817
3818    #[test]
3819    fn parse_solidity_all_four_forms() {
3820        let (_, block) = parse_solidity(
3821            "import \"./A.sol\";\nimport \"./B.sol\" as B;\nimport * as C from \"./C.sol\";\nimport { Foo, Bar as Baz } from \"./D.sol\";\nimport './E.sol';\nimport './F.sol' as F;\nimport * as G from './G.sol';\nimport { Quux, Corge as Grault } from './H.sol';\n",
3822        );
3823        assert_eq!(block.imports.len(), 8);
3824
3825        // side-effect
3826        assert_eq!(block.imports[0].module_path, "./A.sol");
3827        assert_eq!(block.imports[0].kind, ImportKind::SideEffect);
3828        assert_eq!(
3829            block.imports[0].form,
3830            ImportForm::Solidity {
3831                named: vec![],
3832                namespace: None,
3833                alias: None
3834            }
3835        );
3836
3837        // whole-file alias
3838        assert_eq!(
3839            block.imports[1].form,
3840            ImportForm::Solidity {
3841                named: vec![],
3842                namespace: None,
3843                alias: Some("B".to_string())
3844            }
3845        );
3846
3847        // namespace
3848        match &block.imports[2].form {
3849            ImportForm::Solidity { namespace, .. } => assert_eq!(namespace.as_deref(), Some("C")),
3850            other => panic!("expected Solidity namespace, got {other:?}"),
3851        }
3852        assert_eq!(block.imports[2].namespace_import.as_deref(), Some("C"));
3853
3854        // named with alias (verbatim specifier convention)
3855        match &block.imports[3].form {
3856            ImportForm::Solidity { named, .. } => {
3857                assert_eq!(named, &vec!["Foo".to_string(), "Bar as Baz".to_string()]);
3858            }
3859            other => panic!("expected Solidity named, got {other:?}"),
3860        }
3861        assert_eq!(
3862            block.imports[3].names,
3863            vec!["Foo".to_string(), "Bar as Baz".to_string()]
3864        );
3865
3866        assert_eq!(
3867            block.imports[4..]
3868                .iter()
3869                .map(|import| import.module_path.as_str())
3870                .collect::<Vec<_>>(),
3871            ["./E.sol", "./F.sol", "./G.sol", "./H.sol"]
3872        );
3873        assert_eq!(block.imports[4].kind, ImportKind::SideEffect);
3874        assert!(matches!(
3875            &block.imports[5].form,
3876            ImportForm::Solidity {
3877                alias: Some(alias),
3878                ..
3879            } if alias == "F"
3880        ));
3881        assert_eq!(block.imports[6].namespace_import.as_deref(), Some("G"));
3882        assert_eq!(
3883            block.imports[7].names,
3884            vec!["Quux".to_string(), "Corge as Grault".to_string()]
3885        );
3886    }
3887
3888    #[test]
3889    fn generate_solidity_all_forms() {
3890        // side-effect
3891        assert_eq!(
3892            generate_import(
3893                LangId::Solidity,
3894                &ImportRequest::legacy("./A.sol", &[], None, None, false)
3895            ),
3896            "import \"./A.sol\";"
3897        );
3898        // named
3899        let names = vec!["Foo".to_string(), "Bar as Baz".to_string()];
3900        assert_eq!(
3901            generate_import(
3902                LangId::Solidity,
3903                &ImportRequest::legacy("./D.sol", &names, None, None, false)
3904            ),
3905            "import { Foo, Bar as Baz } from \"./D.sol\";"
3906        );
3907        // namespace
3908        assert_eq!(
3909            generate_import(
3910                LangId::Solidity,
3911                &ImportRequest::legacy("./C.sol", &[], None, Some("C"), false)
3912            ),
3913            "import * as C from \"./C.sol\";"
3914        );
3915        // whole-file alias
3916        assert_eq!(
3917            generate_import(
3918                LangId::Solidity,
3919                &ImportRequest {
3920                    module_path: "./B.sol",
3921                    names: &[],
3922                    default_import: None,
3923                    namespace: None,
3924                    alias: Some("B"),
3925                    type_only: false,
3926                    modifiers: &[],
3927                    import_kind: None,
3928                }
3929            ),
3930            "import \"./B.sol\" as B;"
3931        );
3932    }
3933
3934    #[test]
3935    fn solidity_round_trips_through_parse_generate() {
3936        // Every generated form must parse back to the same structured shape.
3937        for src in [
3938            "import \"./A.sol\";",
3939            "import \"./B.sol\" as B;",
3940            "import * as C from \"./C.sol\";",
3941            "import { Foo, Bar as Baz } from \"./D.sol\";",
3942        ] {
3943            let (_, block) = parse_solidity(src);
3944            assert_eq!(block.imports.len(), 1, "parse {src:?}");
3945            let imp = &block.imports[0];
3946            let (namespace, alias) = match &imp.form {
3947                ImportForm::Solidity {
3948                    namespace, alias, ..
3949                } => (namespace.as_deref(), alias.as_deref()),
3950                other => panic!("expected Solidity, got {other:?}"),
3951            };
3952            let regenerated = generate_import(
3953                LangId::Solidity,
3954                &ImportRequest {
3955                    module_path: &imp.module_path,
3956                    names: &imp.names,
3957                    default_import: None,
3958                    namespace,
3959                    alias,
3960                    type_only: false,
3961                    modifiers: &[],
3962                    import_kind: None,
3963                },
3964            );
3965            assert_eq!(regenerated, src, "round-trip mismatch for {src:?}");
3966        }
3967    }
3968
3969    #[test]
3970    fn classify_group_solidity_relative_vs_external() {
3971        assert_eq!(classify_group_solidity("./A.sol"), ImportGroup::Internal);
3972        assert_eq!(
3973            classify_group_solidity("../lib/B.sol"),
3974            ImportGroup::Internal
3975        );
3976        assert_eq!(
3977            classify_group_solidity("@openzeppelin/contracts/token/ERC20/ERC20.sol"),
3978            ImportGroup::External
3979        );
3980    }
3981}