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