Skip to main content

aft/imports/
mod.rs

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