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