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