Skip to main content

aft/imports/
mod.rs

1//! Import analysis engine: parsing, grouping, deduplication, and insertion.
2//!
3//! Per-language behavior is provided by [`ImportSyntax`] implementations,
4//! resolved through the [`syntax_for`] registry. Each engine extracts imports
5//! from tree-sitter ASTs, classifies them into groups, and generates import
6//! text. A single import's structured shape is carried by [`ImportForm`].
7//!
8//! Currently supports: TypeScript, TSX, JavaScript, Python, Rust, Go.
9
10use std::ops::Range;
11
12use tree_sitter::{Node, Parser, Tree};
13
14use crate::parser::{grammar_for, LangId};
15
16mod c;
17pub(crate) use c::{classify_group_c_import_kind, normalize_include_module};
18mod csharp;
19mod java;
20mod kotlin;
21mod lua;
22mod perl;
23mod php;
24pub(crate) use php::{php_grouped_use_matches_module, php_grouped_use_shares_prefix};
25mod ruby;
26mod scala;
27pub(crate) use scala::scala_block_uses_scala2_dialect;
28mod swift;
29
30// ---------------------------------------------------------------------------
31// Shared types
32// ---------------------------------------------------------------------------
33
34/// What kind of import this is.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum ImportKind {
37    /// `import { X } from 'y'` or `import X from 'y'`
38    Value,
39    /// `import type { X } from 'y'`
40    Type,
41    /// `import './side-effect'`
42    SideEffect,
43}
44
45/// Which logical group an import belongs to (language-specific).
46///
47/// Ordering matches conventional import group sorting:
48///   Stdlib (first) < External < Internal (last)
49///
50/// Language mapping:
51///   - TS/JS/TSX: External (no `.` prefix), Internal (`.`/`..` prefix)
52///   - Python:    Stdlib, External (third-party), Internal (relative `.`/`..`)
53///   - Rust:      Stdlib (std/core/alloc), External (crates), Internal (crate/self/super)
54///   - Go:        Stdlib (no dots in path), External (dots in path)
55#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
56pub enum ImportGroup {
57    /// Standard library (Python stdlib, Rust std/core/alloc, Go stdlib).
58    /// TS/JS don't use this group.
59    Stdlib,
60    /// External/third-party packages.
61    External,
62    /// Internal/relative imports (TS relative, Python local, Rust crate/self/super).
63    Internal,
64}
65
66impl ImportGroup {
67    /// Human-readable label for the group.
68    pub fn label(&self) -> &'static str {
69        match self {
70            ImportGroup::Stdlib => "stdlib",
71            ImportGroup::External => "external",
72            ImportGroup::Internal => "internal",
73        }
74    }
75}
76
77/// Structured, language-honest representation of a single import's shape.
78///
79/// This is the migration target that replaces the TS-shaped flat fields
80/// (`names`/`default_import`/`namespace_import`) and their per-language
81/// overloads (Rust packs `"pub"` into `default_import`; Go packs the alias
82/// there). It is introduced additively alongside the flat fields (Stream M of
83/// the imports-refactor plan): parsers populate BOTH, readers migrate onto
84/// `form` one at a time behind the golden-parity gate, and the flat fields are
85/// removed once no reader depends on them. New-language variants (Static,
86/// Include, RuntimeRequire, …) are added when their engines land — only the
87/// variants the existing engines produce exist today.
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub enum ImportForm {
90    /// ES modules: TypeScript, TSX, JavaScript.
91    /// `named` holds verbatim specifiers (`"useState"`, `"stdin as input"`,
92    /// `"type Foo"`, `"type Foo as Bar"`) — see [`specifier_imported_name`].
93    Es {
94        default_import: Option<String>,
95        namespace_import: Option<String>,
96        named: Vec<String>,
97        /// Statement-level `import type { ... }`.
98        type_only: bool,
99        /// Side-effect-only `import "mod"` (no bindings).
100        side_effect: bool,
101    },
102    /// Python `import module` (`from_import = false`) or
103    /// `from module import a, b` (`from_import = true`).
104    Python {
105        from_import: bool,
106        named: Vec<String>,
107    },
108    /// Rust `use path;` / `pub use path;`. `visibility` replaces the
109    /// `default_import == "pub"` overload (`Some("pub")`, `Some("pub(crate)")`,
110    /// …). The brace/use-tree text remains carried by `module_path` per the
111    /// lossless-round-trip decision; `named` holds extracted use-list names.
112    RustUse {
113        visibility: Option<String>,
114        named: Vec<String>,
115    },
116    /// Go import. `alias` replaces the `default_import` overload, including the
117    /// blank (`_`) and dot (`.`) import bindings.
118    Go { alias: Option<String> },
119    /// Solidity import, in one of four forms:
120    /// - side-effect: `import "x";` (all empty)
121    /// - named: `import { A, B as C } from "x";` (`named`)
122    /// - namespace: `import * as A from "x";` (`namespace`)
123    /// - whole-file alias: `import "x" as A;` (`alias`)
124    ///
125    /// `named` holds verbatim specifiers (`"A"`, `"B as C"`) like the ES form,
126    /// so [`specifier_imported_name`] / [`specifier_local_name`] apply.
127    Solidity {
128        named: Vec<String>,
129        namespace: Option<String>,
130        alias: Option<String>,
131    },
132    /// Generic structured form shared by the Phase-1 engines (Java, C#, PHP,
133    /// Kotlin, Scala, Swift, …). Carries the full schema field set so a new
134    /// engine does not need its own enum variant; `module_path` (on the parent
135    /// `ImportStatement`) holds the path/FQN. `named` uses the verbatim
136    /// specifier convention.
137    Structured {
138        named: Vec<String>,
139        namespace: Option<String>,
140        alias: Option<String>,
141        modifiers: Vec<String>,
142        import_kind: Option<String>,
143    },
144}
145
146/// Structured request to generate a single import line. Superset of the fields
147/// the public `aft_import` schema exposes; each engine reads only the subset it
148/// supports. New languages add fields here rather than growing positional
149/// parameters on every generator signature.
150#[derive(Debug, Clone)]
151pub struct ImportRequest<'a> {
152    pub module_path: &'a str,
153    pub names: &'a [String],
154    pub default_import: Option<&'a str>,
155    /// ES `* as ns` / Solidity `* as A`.
156    pub namespace: Option<&'a str>,
157    /// Whole-module local alias (Solidity `import "x" as A`).
158    pub alias: Option<&'a str>,
159    pub type_only: bool,
160    /// Statement-level modifier tokens (Java/C# `static`, C# `global`/`unsafe`,
161    /// `wildcard`, Swift `@testable`, …). Empty for the legacy engines.
162    pub modifiers: &'a [String],
163    /// Symbol-kind-specific import (PHP `function`/`const`, Swift `struct`/…,
164    /// Scala `given`). Absent for the legacy engines.
165    pub import_kind: Option<&'a str>,
166}
167
168/// Empty default for the `modifiers` slice so legacy callers need not allocate.
169const NO_MODIFIERS: &[String] = &[];
170
171impl<'a> ImportRequest<'a> {
172    /// Construct a request carrying only the legacy positional fields; the
173    /// structured fields (alias/modifiers/import_kind) default to absent. Used
174    /// by the back-compat free-function wrappers.
175    pub fn legacy(
176        module_path: &'a str,
177        names: &'a [String],
178        default_import: Option<&'a str>,
179        namespace: Option<&'a str>,
180        type_only: bool,
181    ) -> Self {
182        ImportRequest {
183            module_path,
184            names,
185            default_import,
186            namespace,
187            alias: None,
188            type_only,
189            modifiers: NO_MODIFIERS,
190            import_kind: None,
191        }
192    }
193}
194
195/// A single parsed import statement.
196#[derive(Debug, Clone)]
197pub struct ImportStatement {
198    /// The module path (e.g., `react`, `./utils`, `../config`).
199    pub module_path: String,
200    /// Named imports (e.g., `["useState", "useEffect"]`).
201    pub names: Vec<String>,
202    /// Default import name (e.g., `React` from `import React from 'react'`).
203    pub default_import: Option<String>,
204    /// Namespace import name (e.g., `path` from `import * as path from 'path'`).
205    pub namespace_import: Option<String>,
206    /// What kind: value, type, or side-effect.
207    pub kind: ImportKind,
208    /// Which group this import belongs to.
209    pub group: ImportGroup,
210    /// Byte range in the original source.
211    pub byte_range: Range<usize>,
212    /// Raw text of the import statement.
213    pub raw_text: String,
214    /// Structured, de-overloaded representation (Stream M migration target).
215    /// Populated by every parser alongside the flat fields above; readers
216    /// migrate onto this incrementally behind the golden-parity gate.
217    pub form: ImportForm,
218}
219
220/// A block of parsed imports from a file.
221#[derive(Debug, Clone)]
222pub struct ImportBlock {
223    /// All parsed import statements, in source order.
224    pub imports: Vec<ImportStatement>,
225    /// Overall byte range covering all import statements (start of first to end of last).
226    /// `None` if no imports found.
227    pub byte_range: Option<Range<usize>>,
228}
229
230impl ImportBlock {
231    pub fn empty() -> Self {
232        ImportBlock {
233            imports: Vec::new(),
234            byte_range: None,
235        }
236    }
237}
238
239pub(crate) fn import_byte_range(imports: &[ImportStatement]) -> Option<Range<usize>> {
240    imports.first().zip(imports.last()).map(|(first, last)| {
241        let start = first.byte_range.start;
242        let end = last.byte_range.end;
243        start..end
244    })
245}
246
247// ---------------------------------------------------------------------------
248// Specifier helpers (TS/JS verbatim-string format)
249// ---------------------------------------------------------------------------
250
251/// Return the local binding name for a TS/JS named-import specifier stored in
252/// `ImportStatement::names`. Specifiers are stored verbatim — e.g.
253/// `"stdin as input"`, `"type Foo"`, `"type Foo as Bar"`, `"useState"` — so
254/// callers that want the name actually introduced into scope must strip the
255/// optional `type ` prefix and prefer the post-`as` identifier when present.
256///
257/// Examples:
258///   `"useState"`            → `"useState"`
259///   `"stdin as input"`      → `"input"`
260///   `"type Foo"`            → `"Foo"`
261///   `"type Foo as Bar"`     → `"Bar"`
262pub fn specifier_local_name(spec: &str) -> &str {
263    let trimmed = spec.trim();
264    let after_type = trimmed
265        .strip_prefix("type ")
266        .unwrap_or(trimmed)
267        .trim_start();
268    if let Some(idx) = after_type.find(" as ") {
269        after_type[idx + 4..].trim()
270    } else {
271        after_type
272    }
273}
274
275/// Return the imported (pre-`as`) name for a TS/JS named-import specifier.
276/// Used by dedup, remove, and any caller that needs the source-side name.
277///
278/// Examples:
279///   `"useState"`            → `"useState"`
280///   `"stdin as input"`      → `"stdin"`
281///   `"type Foo"`            → `"Foo"`
282///   `"type Foo as Bar"`     → `"Foo"`
283pub fn specifier_imported_name(spec: &str) -> &str {
284    let trimmed = spec.trim();
285    let after_type = trimmed
286        .strip_prefix("type ")
287        .unwrap_or(trimmed)
288        .trim_start();
289    after_type
290        .find(" as ")
291        .map(|idx| after_type[..idx].trim())
292        .unwrap_or(after_type)
293}
294
295/// Whether a stored specifier matches a target name. Matches against either
296/// the imported name or the local binding so callers can pass whichever name
297/// they observed in source. Useful for `remove_import` where the agent may
298/// reference an aliased import by either name.
299pub fn specifier_matches(spec: &str, target: &str) -> bool {
300    specifier_imported_name(spec) == target || specifier_local_name(spec) == target
301}
302
303// ---------------------------------------------------------------------------
304// Per-language engine: the ImportSyntax trait + registry
305// ---------------------------------------------------------------------------
306
307/// Per-language import engine. One impl per supported language; [`syntax_for`]
308/// maps a [`LangId`] to its `&'static dyn ImportSyntax`. This is the single
309/// plug-in point that replaces the scattered `match lang` dispatch in
310/// `parse_imports` / `generate_import_line_with_namespace` / `classify_group` /
311/// `is_supported`. Adding a language is a new impl + one registry arm.
312///
313/// The existing engines are thin wrappers over the free functions they already
314/// used, so routing through the trait is behavior-preserving (golden-gated).
315pub trait ImportSyntax: Sync {
316    /// Parse all imports from a file's already-parsed tree.
317    fn parse(&self, source: &str, tree: &Tree) -> ImportBlock;
318
319    /// Generate a single import line from a structured [`ImportRequest`].
320    /// Engines read only the fields they support and ignore the rest.
321    fn generate_line(&self, req: &ImportRequest) -> String;
322
323    /// Classify a module path into stdlib / external / internal.
324    fn classify_group(&self, module_path: &str) -> ImportGroup;
325}
326
327/// ES modules engine: TypeScript, TSX, JavaScript.
328struct EsSyntax;
329impl ImportSyntax for EsSyntax {
330    fn parse(&self, source: &str, tree: &Tree) -> ImportBlock {
331        parse_ts_imports(source, tree)
332    }
333    fn generate_line(&self, req: &ImportRequest) -> String {
334        generate_ts_import_line(
335            req.module_path,
336            req.names,
337            req.default_import,
338            req.namespace,
339            req.type_only,
340        )
341    }
342    fn classify_group(&self, module_path: &str) -> ImportGroup {
343        classify_group_ts(module_path)
344    }
345}
346
347struct PythonSyntax;
348impl ImportSyntax for PythonSyntax {
349    fn parse(&self, source: &str, tree: &Tree) -> ImportBlock {
350        parse_py_imports(source, tree)
351    }
352    fn generate_line(&self, req: &ImportRequest) -> String {
353        generate_py_import_line(req.module_path, req.names, req.default_import)
354    }
355    fn classify_group(&self, module_path: &str) -> ImportGroup {
356        classify_group_py(module_path)
357    }
358}
359
360struct RustSyntax;
361impl ImportSyntax for RustSyntax {
362    fn parse(&self, source: &str, tree: &Tree) -> ImportBlock {
363        parse_rs_imports(source, tree)
364    }
365    fn generate_line(&self, req: &ImportRequest) -> String {
366        generate_rs_import_line(req.module_path, req.names, req.type_only)
367    }
368    fn classify_group(&self, module_path: &str) -> ImportGroup {
369        classify_group_rs(module_path)
370    }
371}
372
373struct GoSyntax;
374impl ImportSyntax for GoSyntax {
375    fn parse(&self, source: &str, tree: &Tree) -> ImportBlock {
376        parse_go_imports(source, tree)
377    }
378    fn generate_line(&self, req: &ImportRequest) -> String {
379        generate_go_import_line(req.module_path, req.default_import, false)
380    }
381    fn classify_group(&self, module_path: &str) -> ImportGroup {
382        classify_group_go(module_path)
383    }
384}
385
386/// Solidity import engine. Supports named / namespace / whole-file-alias /
387/// side-effect forms (Phase 1: first new language onto the registry).
388struct SoliditySyntax;
389impl ImportSyntax for SoliditySyntax {
390    fn parse(&self, source: &str, tree: &Tree) -> ImportBlock {
391        parse_solidity_imports(source, tree)
392    }
393    fn generate_line(&self, req: &ImportRequest) -> String {
394        generate_solidity_import_line(req)
395    }
396    fn classify_group(&self, module_path: &str) -> ImportGroup {
397        classify_group_solidity(module_path)
398    }
399}
400
401#[derive(Debug, Clone, Copy, PartialEq, Eq)]
402pub(crate) enum VueScriptRangeError {
403    MissingScript,
404    MultipleScripts,
405}
406
407impl VueScriptRangeError {
408    pub(crate) fn code(self) -> &'static str {
409        match self {
410            VueScriptRangeError::MissingScript => "missing_vue_script",
411            VueScriptRangeError::MultipleScripts => "ambiguous_vue_script",
412        }
413    }
414
415    pub(crate) fn message(self, command: &str) -> String {
416        match self {
417            VueScriptRangeError::MissingScript => format!(
418                "{command}: Vue import management requires exactly one <script> block; found none"
419            ),
420            VueScriptRangeError::MultipleScripts => format!(
421                "{command}: Vue import management requires exactly one <script> block; found multiple"
422            ),
423        }
424    }
425}
426
427/// Locate the byte range of the single `<script>` block's inner content in a
428/// Vue Single-File Component. tree-sitter-vue exposes the script body as a
429/// single `raw_text` node; this returns `(start, end)` of that node, or — for an
430/// empty `<script></script>` with no `raw_text` child — a zero-width range right
431/// after the start tag. Multiple scripts are ambiguous for byte-level edits and
432/// no-script SFCs have no safe insertion region, so callers should surface the
433/// returned error instead of silently editing byte 0 or the first script.
434pub(crate) fn vue_single_script_content_range(
435    tree: &Tree,
436) -> Result<(usize, usize), VueScriptRangeError> {
437    let root = tree.root_node();
438    let mut ranges = Vec::new();
439    let mut cursor = root.walk();
440    for child in root.named_children(&mut cursor) {
441        if child.kind() == "script_element" {
442            ranges.push(vue_script_element_content_range(&child));
443        }
444    }
445
446    match ranges.len() {
447        0 => Err(VueScriptRangeError::MissingScript),
448        1 => Ok(ranges[0]),
449        _ => Err(VueScriptRangeError::MultipleScripts),
450    }
451}
452
453/// Back-compat convenience wrapper for callers that only need the safe single
454/// script range and intentionally treat missing/ambiguous scripts as absent.
455pub(crate) fn vue_script_content_range(tree: &Tree) -> Option<(usize, usize)> {
456    vue_single_script_content_range(tree).ok()
457}
458
459fn vue_script_element_content_range(child: &Node) -> (usize, usize) {
460    let mut inner = child.walk();
461    for sub in child.named_children(&mut inner) {
462        if sub.kind() == "raw_text" {
463            return (sub.start_byte(), sub.end_byte());
464        }
465    }
466
467    // Empty `<script></script>`: insert right after the start tag.
468    let mut inner2 = child.walk();
469    for sub in child.named_children(&mut inner2) {
470        if sub.kind() == "start_tag" {
471            return (sub.end_byte(), sub.end_byte());
472        }
473    }
474
475    (child.end_byte(), child.end_byte())
476}
477
478/// Parse imports from a Vue SFC `<script>` block. The script body is re-parsed
479/// with the TypeScript grammar (which covers both `lang="ts"` and plain JS
480/// import syntax), then every byte offset is remapped from script-relative to
481/// whole-file positions so insertion, removal, and organize operate correctly.
482fn parse_vue_imports(source: &str, tree: &Tree) -> ImportBlock {
483    let Ok((start, end)) = vue_single_script_content_range(tree) else {
484        return ImportBlock {
485            imports: Vec::new(),
486            byte_range: None,
487        };
488    };
489    let inner = &source[start..end];
490    let mut parser = Parser::new();
491    if parser
492        .set_language(&grammar_for(LangId::TypeScript))
493        .is_err()
494    {
495        return ImportBlock {
496            imports: Vec::new(),
497            byte_range: None,
498        };
499    }
500    let Some(inner_tree) = parser.parse(inner, None) else {
501        return ImportBlock {
502            imports: Vec::new(),
503            byte_range: None,
504        };
505    };
506    let mut block = parse_ts_imports(inner, &inner_tree);
507    for imp in &mut block.imports {
508        imp.byte_range = (imp.byte_range.start + start)..(imp.byte_range.end + start);
509    }
510    block.byte_range = block.byte_range.map(|r| (r.start + start)..(r.end + start));
511    block
512}
513
514/// Vue Single-File Component import engine. The `<script>` body is exposed by
515/// tree-sitter-vue as a single `raw_text` node, so we re-parse it with the
516/// TypeScript grammar and remap the resulting byte offsets back to whole-file
517/// positions. Generation and grouping reuse the ES (TS/JS) engine, since Vue
518/// script imports are TypeScript/JavaScript.
519struct VueSyntax;
520impl ImportSyntax for VueSyntax {
521    fn parse(&self, source: &str, tree: &Tree) -> ImportBlock {
522        parse_vue_imports(source, tree)
523    }
524    fn generate_line(&self, req: &ImportRequest) -> String {
525        generate_ts_import_line(
526            req.module_path,
527            req.names,
528            req.default_import,
529            req.namespace,
530            req.type_only,
531        )
532    }
533    fn classify_group(&self, module_path: &str) -> ImportGroup {
534        classify_group_ts(module_path)
535    }
536}
537
538static ES_SYNTAX: EsSyntax = EsSyntax;
539static PYTHON_SYNTAX: PythonSyntax = PythonSyntax;
540static RUST_SYNTAX: RustSyntax = RustSyntax;
541static GO_SYNTAX: GoSyntax = GoSyntax;
542static SOLIDITY_SYNTAX: SoliditySyntax = SoliditySyntax;
543static VUE_SYNTAX: VueSyntax = VueSyntax;
544
545/// Map a language to its import engine, or `None` when imports are unsupported.
546pub fn syntax_for(lang: LangId) -> Option<&'static dyn ImportSyntax> {
547    match lang {
548        LangId::TypeScript | LangId::Tsx | LangId::JavaScript => Some(&ES_SYNTAX),
549        LangId::Python => Some(&PYTHON_SYNTAX),
550        LangId::Rust => Some(&RUST_SYNTAX),
551        LangId::Go => Some(&GO_SYNTAX),
552        LangId::Solidity => Some(&SOLIDITY_SYNTAX),
553        LangId::Vue => Some(&VUE_SYNTAX),
554        LangId::C => Some(&c::C_SYNTAX),
555        LangId::Cpp => Some(&c::C_SYNTAX),
556        LangId::Java => Some(&java::JAVA_SYNTAX),
557        LangId::Kotlin => Some(&kotlin::KOTLIN_SYNTAX),
558        LangId::Lua => Some(&lua::LUA_SYNTAX),
559        LangId::CSharp => Some(&csharp::CSHARP_SYNTAX),
560        LangId::Php => Some(&php::PHP_SYNTAX),
561        LangId::Perl => Some(&perl::PERL_SYNTAX),
562        LangId::Ruby => Some(&ruby::RUBY_SYNTAX),
563        LangId::Scala => Some(&scala::SCALA_SYNTAX),
564        LangId::Swift => Some(&swift::SWIFT_SYNTAX),
565        LangId::Zig
566        | LangId::Bash
567        | LangId::Scss
568        | LangId::Json
569        | LangId::Html
570        | LangId::Markdown
571        | LangId::Yaml => None,
572    }
573}
574
575// ---------------------------------------------------------------------------
576// Core API
577// ---------------------------------------------------------------------------
578
579/// Parse imports from source using the provided tree-sitter tree.
580pub fn parse_imports(source: &str, tree: &Tree, lang: LangId) -> ImportBlock {
581    match syntax_for(lang) {
582        Some(engine) => engine.parse(source, tree),
583        None => ImportBlock::empty(),
584    }
585}
586
587/// Check if an import with the given module + name combination already exists.
588///
589/// For dedup: same module path and matching binding shape. Side-effect imports
590/// are only duplicates of side-effect imports; namespace imports are distinct
591/// from side-effect imports and from other namespace aliases.
592pub fn is_duplicate(
593    block: &ImportBlock,
594    module_path: &str,
595    names: &[String],
596    default_import: Option<&str>,
597    type_only: bool,
598) -> bool {
599    is_duplicate_with_namespace(block, module_path, names, default_import, None, type_only)
600}
601
602/// Check if an import with the given module + complete binding shape already exists.
603pub fn is_duplicate_with_namespace(
604    block: &ImportBlock,
605    module_path: &str,
606    names: &[String],
607    default_import: Option<&str>,
608    namespace_import: Option<&str>,
609    type_only: bool,
610) -> bool {
611    let target_kind = if type_only {
612        ImportKind::Type
613    } else {
614        ImportKind::Value
615    };
616
617    for imp in &block.imports {
618        if imp.module_path != module_path {
619            continue;
620        }
621
622        // For side-effect imports (no names/default/namespace): module path
623        // match is sufficient only when the existing import is also a
624        // side-effect import. Namespace imports like `import * as fs from 'fs'`
625        // are distinct local bindings and must not be conflated with
626        // `import 'fs'`.
627        if names.is_empty()
628            && default_import.is_none()
629            && namespace_import.is_none()
630            && imp.names.is_empty()
631            && imp.default_import.is_none()
632            && imp.namespace_import.is_none()
633        {
634            return true;
635        }
636
637        // For side-effect imports specifically (TS/JS): module match is enough
638        if names.is_empty()
639            && default_import.is_none()
640            && namespace_import.is_none()
641            && imp.kind == ImportKind::SideEffect
642        {
643            return true;
644        }
645
646        // Kind must match for dedup (value imports don't dedup against type imports)
647        if imp.kind != target_kind && imp.kind != ImportKind::SideEffect {
648            continue;
649        }
650
651        // Default+namespace imports are one ES binding shape. A plain default
652        // import must not satisfy a request for `default, * as ns`, and a
653        // different namespace alias must not either.
654        if let (Some(def), Some(namespace)) = (default_import, namespace_import) {
655            if imp.default_import.as_deref() == Some(def)
656                && imp.namespace_import.as_deref() == Some(namespace)
657                && names
658                    .iter()
659                    .all(|n| imp.names.iter().any(|stored| specifier_matches(stored, n)))
660            {
661                return true;
662            }
663            continue;
664        }
665
666        // Namespace-only requests are satisfied by any existing same-module
667        // import that already binds that namespace alias, even if it also has a
668        // default binding.
669        if names.is_empty()
670            && default_import.is_none()
671            && namespace_import.is_some()
672            && imp.namespace_import.as_deref() == namespace_import
673        {
674            return true;
675        }
676
677        // Check default import match. This branch only handles requests that do
678        // not also ask for a namespace; that combined shape is checked above.
679        if let Some(def) = default_import {
680            if namespace_import.is_none() && imp.default_import.as_deref() == Some(def) {
681                return true;
682            }
683        }
684
685        // Check named imports — if ALL requested names already exist.
686        // Compare on the imported (pre-`as`) name so adding `Foo` is a
687        // no-op when `Foo as Bar` is already imported, but adding
688        // `Foo as Bar` is NOT a duplicate of bare `Foo` (different
689        // local bindings).
690        if !names.is_empty()
691            && names
692                .iter()
693                .all(|n| imp.names.iter().any(|stored| specifier_matches(stored, n)))
694        {
695            return true;
696        }
697    }
698
699    false
700}
701
702/// Check whether a fully structured add-import request is already present.
703///
704/// Legacy ES/Python/Rust/Go callers intentionally keep the historical
705/// subset/dominance semantics (`import { a, b }` satisfies adding `{ a }`). The
706/// newer engines carry language-specific shape in `ImportForm::Structured` (or
707/// Solidity's dedicated form), where module path alone is not enough: include
708/// delimiters, statement kinds, modifiers, aliases, and runtime import flavors
709/// all affect the generated source. Those languages deduplicate on a canonical
710/// full-form key so `#include <x>` does not block `#include "x"`, `load` does
711/// not block `require`, and side-effect Solidity imports do not block aliases.
712pub(crate) fn is_duplicate_import_request(
713    lang: LangId,
714    block: &ImportBlock,
715    req: &ImportRequest<'_>,
716) -> bool {
717    if !uses_form_aware_dedup(lang) {
718        return is_duplicate_with_namespace(
719            block,
720            req.module_path,
721            req.names,
722            req.default_import,
723            req.namespace,
724            req.type_only,
725        );
726    }
727
728    let target = request_dedup_key(lang, req);
729    block
730        .imports
731        .iter()
732        .map(|imp| statement_dedup_key(lang, imp))
733        .any(|key| key == target)
734}
735
736fn uses_form_aware_dedup(lang: LangId) -> bool {
737    matches!(
738        lang,
739        LangId::Solidity
740            | LangId::C
741            | LangId::Cpp
742            | LangId::Java
743            | LangId::CSharp
744            | LangId::Php
745            | LangId::Kotlin
746            | LangId::Scala
747            | LangId::Swift
748            | LangId::Ruby
749            | LangId::Lua
750            | LangId::Perl
751    )
752}
753
754#[derive(Debug, Clone, PartialEq, Eq)]
755struct ImportDedupKey {
756    module_path: String,
757    kind: ImportKind,
758    form: ImportForm,
759}
760
761fn statement_dedup_key(lang: LangId, imp: &ImportStatement) -> ImportDedupKey {
762    canonical_dedup_key(
763        lang,
764        ImportDedupKey {
765            module_path: imp.module_path.clone(),
766            kind: imp.kind,
767            form: imp.form.clone(),
768        },
769    )
770}
771
772fn request_dedup_key(lang: LangId, req: &ImportRequest<'_>) -> ImportDedupKey {
773    let key = match lang {
774        LangId::Solidity => {
775            let kind = if req.names.is_empty() && req.namespace.is_none() && req.alias.is_none() {
776                ImportKind::SideEffect
777            } else {
778                ImportKind::Value
779            };
780            ImportDedupKey {
781                module_path: req.module_path.to_string(),
782                kind,
783                form: ImportForm::Solidity {
784                    named: req.names.to_vec(),
785                    namespace: req.namespace.map(str::to_string),
786                    alias: req.alias.map(str::to_string),
787                },
788            }
789        }
790        LangId::C | LangId::Cpp => structured_dedup_key(
791            req.module_path,
792            ImportKind::SideEffect,
793            &[],
794            None,
795            None,
796            &[],
797            Some(req.import_kind.or(req.default_import).unwrap_or("system")),
798        ),
799        LangId::Java => {
800            let (mut module_path, modifiers) = wildcard_suffix_request(
801                req.module_path,
802                req.modifiers,
803                req.default_import == Some("*"),
804            );
805            let mut names = req.names.to_vec();
806            normalize_java_static_member_key(&mut module_path, &modifiers, &mut names);
807            structured_dedup_key(
808                &module_path,
809                ImportKind::Value,
810                &names,
811                None,
812                None,
813                &modifiers,
814                None,
815            )
816        }
817        LangId::CSharp => structured_dedup_key(
818            req.module_path,
819            ImportKind::Value,
820            &[],
821            None,
822            req.alias,
823            req.modifiers,
824            None,
825        ),
826        LangId::Php => structured_dedup_key(
827            req.module_path,
828            ImportKind::Value,
829            &[],
830            None,
831            req.alias,
832            req.modifiers,
833            req.import_kind,
834        ),
835        LangId::Kotlin => {
836            let wildcard = req.default_import == Some("*") || req.module_path.ends_with(".*");
837            let (module_path, modifiers) =
838                wildcard_suffix_request(req.module_path, req.modifiers, wildcard);
839            let alias = req
840                .alias
841                .or(req.default_import.filter(|value| *value != "*"));
842            structured_dedup_key(
843                &module_path,
844                ImportKind::Value,
845                &[],
846                None,
847                alias,
848                &modifiers,
849                None,
850            )
851        }
852        LangId::Scala => scala_request_dedup_key(req),
853        LangId::Swift => structured_dedup_key(
854            req.module_path,
855            ImportKind::Value,
856            &[],
857            None,
858            None,
859            req.modifiers,
860            req.import_kind,
861        ),
862        LangId::Ruby => {
863            let mut modifiers = req.modifiers.to_vec();
864            if !modifiers
865                .iter()
866                .any(|modifier| modifier == "quote:single" || modifier == "quote:double")
867            {
868                modifiers.push("quote:single".to_string());
869            }
870            structured_dedup_key(
871                req.module_path,
872                ImportKind::SideEffect,
873                &[],
874                None,
875                None,
876                &modifiers,
877                Some(req.import_kind.unwrap_or("require")),
878            )
879        }
880        LangId::Lua => {
881            let alias = req.default_import.or(req.alias);
882            let kind = if alias.is_some() {
883                ImportKind::Value
884            } else {
885                ImportKind::SideEffect
886            };
887            structured_dedup_key(req.module_path, kind, &[], None, alias, req.modifiers, None)
888        }
889        LangId::Perl => structured_dedup_key(
890            req.module_path,
891            ImportKind::SideEffect,
892            &[],
893            None,
894            None,
895            req.modifiers,
896            Some(req.import_kind.unwrap_or("use")),
897        ),
898        _ => structured_dedup_key(
899            req.module_path,
900            if req.type_only {
901                ImportKind::Type
902            } else {
903                ImportKind::Value
904            },
905            req.names,
906            req.namespace,
907            req.alias,
908            req.modifiers,
909            req.import_kind,
910        ),
911    };
912
913    canonical_dedup_key(lang, key)
914}
915
916fn structured_dedup_key(
917    module_path: &str,
918    kind: ImportKind,
919    named: &[String],
920    namespace: Option<&str>,
921    alias: Option<&str>,
922    modifiers: &[String],
923    import_kind: Option<&str>,
924) -> ImportDedupKey {
925    ImportDedupKey {
926        module_path: module_path.to_string(),
927        kind,
928        form: ImportForm::Structured {
929            named: named.to_vec(),
930            namespace: namespace.map(str::to_string),
931            alias: alias.map(str::to_string),
932            modifiers: modifiers.to_vec(),
933            import_kind: import_kind.map(str::to_string),
934        },
935    }
936}
937
938fn wildcard_suffix_request(
939    module_path: &str,
940    modifiers: &[String],
941    wildcard: bool,
942) -> (String, Vec<String>) {
943    let stripped = module_path.strip_suffix(".*").unwrap_or(module_path);
944    let mut modifiers = modifiers.to_vec();
945    if (wildcard || stripped.len() != module_path.len())
946        && !modifiers.iter().any(|modifier| modifier == "wildcard")
947    {
948        modifiers.push("wildcard".to_string());
949    }
950    (stripped.to_string(), modifiers)
951}
952
953fn normalize_java_static_member_key(
954    module_path: &mut String,
955    modifiers: &[String],
956    names: &mut Vec<String>,
957) {
958    let is_static = modifiers.iter().any(|modifier| modifier == "static");
959    let is_wildcard = modifiers.iter().any(|modifier| modifier == "wildcard");
960    if !is_static || is_wildcard || !names.is_empty() {
961        return;
962    }
963
964    if let Some((prefix, member)) = module_path.rsplit_once('.') {
965        if !prefix.is_empty() && !member.is_empty() {
966            names.push(member.to_string());
967            *module_path = prefix.to_string();
968        }
969    }
970}
971
972fn scala_request_dedup_key(req: &ImportRequest<'_>) -> ImportDedupKey {
973    let mut module_path = req.module_path.to_string();
974    let mut names: Vec<String> = req
975        .names
976        .iter()
977        .map(|name| normalize_scala_selector_for_dedup(name))
978        .collect();
979    let mut modifiers = req.modifiers.to_vec();
980    let mut import_kind = req.import_kind.map(str::to_string);
981
982    if req.default_import == Some("given") || module_path.ends_with(".given") {
983        import_kind.get_or_insert_with(|| "given".to_string());
984        if let Some(stripped) = module_path.strip_suffix(".given") {
985            module_path = stripped.to_string();
986        }
987    }
988
989    if matches!(req.default_import, Some("*") | Some("_"))
990        || matches!(req.namespace, Some("*") | Some("_"))
991        || module_path.ends_with(".*")
992        || module_path.ends_with("._")
993    {
994        if !modifiers.iter().any(|modifier| modifier == "wildcard") {
995            modifiers.push("wildcard".to_string());
996        }
997        module_path = module_path
998            .strip_suffix(".*")
999            .or_else(|| module_path.strip_suffix("._"))
1000            .unwrap_or(&module_path)
1001            .to_string();
1002    }
1003
1004    if names.is_empty() {
1005        if let Some(alias) = req.alias.filter(|alias| !alias.is_empty()) {
1006            if let Some((prefix, leaf)) = module_path.rsplit_once('.') {
1007                names.push(format!("{leaf} as {alias}"));
1008                module_path = prefix.to_string();
1009            }
1010        }
1011    }
1012
1013    structured_dedup_key(
1014        &module_path,
1015        ImportKind::Value,
1016        &names,
1017        None,
1018        None,
1019        &modifiers,
1020        import_kind.as_deref(),
1021    )
1022}
1023
1024fn normalize_scala_selector_for_dedup(name: &str) -> String {
1025    let trimmed = name.trim();
1026    if let Some((from, to)) = trimmed.split_once("=>") {
1027        format!("{} as {}", from.trim(), to.trim())
1028    } else {
1029        trimmed.to_string()
1030    }
1031}
1032
1033fn canonical_dedup_key(lang: LangId, mut key: ImportDedupKey) -> ImportDedupKey {
1034    match &mut key.form {
1035        ImportForm::Structured { named, .. } | ImportForm::Solidity { named, .. } => {
1036            sort_named_specifiers(named);
1037        }
1038        ImportForm::Es { named, .. } | ImportForm::Python { named, .. } => {
1039            sort_named_specifiers(named);
1040        }
1041        ImportForm::RustUse { named, .. } => {
1042            sort_named_specifiers(named);
1043        }
1044        ImportForm::Go { .. } => {}
1045    }
1046
1047    if matches!(lang, LangId::Java | LangId::Kotlin) {
1048        if let Some(stripped) = key.module_path.strip_suffix(".*") {
1049            key.module_path = stripped.to_string();
1050        }
1051        if matches!(lang, LangId::Java) {
1052            if let ImportForm::Structured {
1053                named, modifiers, ..
1054            } = &mut key.form
1055            {
1056                normalize_java_static_member_key(&mut key.module_path, modifiers, named);
1057            }
1058        }
1059    } else if matches!(lang, LangId::Scala) {
1060        key.module_path = key
1061            .module_path
1062            .strip_suffix(".given")
1063            .or_else(|| key.module_path.strip_suffix(".*"))
1064            .or_else(|| key.module_path.strip_suffix("._"))
1065            .unwrap_or(&key.module_path)
1066            .to_string();
1067    }
1068
1069    key
1070}
1071
1072fn sort_named_specifiers(names: &mut [String]) {
1073    names.sort_by(|a, b| {
1074        specifier_imported_name(a)
1075            .cmp(specifier_imported_name(b))
1076            .then_with(|| a.cmp(b))
1077    });
1078}
1079
1080/// Find the byte offset where a new import should be inserted.
1081///
1082/// Strategy:
1083/// - Find all existing imports in the same group.
1084/// - Within that group, find the alphabetical position by module path.
1085/// - Type imports sort after value imports within the same group and module-sort position.
1086/// - If no imports exist in the target group, insert after the last import of the
1087///   nearest preceding group (or before the first import of the nearest following
1088///   group, or at file start if no groups exist).
1089/// - Returns (byte_offset, needs_newline_before, needs_newline_after)
1090pub fn find_insertion_point(
1091    source: &str,
1092    block: &ImportBlock,
1093    group: ImportGroup,
1094    module_path: &str,
1095    type_only: bool,
1096) -> (usize, bool, bool) {
1097    if block.imports.is_empty() {
1098        // No imports at all — insert at start of file
1099        return (0, false, source.is_empty().then_some(false).unwrap_or(true));
1100    }
1101
1102    let target_kind = if type_only {
1103        ImportKind::Type
1104    } else {
1105        ImportKind::Value
1106    };
1107
1108    // Collect imports in the target group
1109    let group_imports: Vec<&ImportStatement> =
1110        block.imports.iter().filter(|i| i.group == group).collect();
1111
1112    if group_imports.is_empty() {
1113        // No imports in this group yet — find nearest neighbor group
1114        // Try preceding groups (lower ordinal) first
1115        let preceding_last = block.imports.iter().filter(|i| i.group < group).last();
1116
1117        if let Some(last) = preceding_last {
1118            let end = last.byte_range.end;
1119            let insert_at = skip_newline(source, end);
1120            return (insert_at, true, true);
1121        }
1122
1123        // No preceding group — try following groups (higher ordinal)
1124        let following_first = block.imports.iter().find(|i| i.group > group);
1125
1126        if let Some(first) = following_first {
1127            return (first.byte_range.start, false, true);
1128        }
1129
1130        // Shouldn't reach here if block is non-empty, but handle gracefully
1131        let first_byte = import_byte_range(&block.imports)
1132            .map(|range| range.start)
1133            .unwrap_or(0);
1134        return (first_byte, false, true);
1135    }
1136
1137    // Find position within the group (alphabetical by module path, type after value)
1138    for imp in &group_imports {
1139        let cmp = module_path.cmp(&imp.module_path);
1140        match cmp {
1141            std::cmp::Ordering::Less => {
1142                // Insert before this import
1143                return (imp.byte_range.start, false, false);
1144            }
1145            std::cmp::Ordering::Equal => {
1146                // Same module — type imports go after value imports
1147                if target_kind == ImportKind::Type && imp.kind == ImportKind::Value {
1148                    // Insert after this value import
1149                    let end = imp.byte_range.end;
1150                    let insert_at = skip_newline(source, end);
1151                    return (insert_at, false, false);
1152                }
1153                // Insert before (or it's a duplicate, caller should have checked)
1154                return (imp.byte_range.start, false, false);
1155            }
1156            std::cmp::Ordering::Greater => continue,
1157        }
1158    }
1159
1160    // Module path sorts after all existing imports in this group — insert at end
1161    let Some(last) = group_imports.last() else {
1162        return (
1163            import_byte_range(&block.imports)
1164                .map(|range| range.end)
1165                .unwrap_or(0),
1166            false,
1167            false,
1168        );
1169    };
1170    let end = last.byte_range.end;
1171    let insert_at = skip_newline(source, end);
1172    (insert_at, false, false)
1173}
1174
1175/// Generate a single import line from a structured [`ImportRequest`]. The full
1176/// entry point — engines read the fields they support; unsupported languages
1177/// yield an empty string.
1178pub fn generate_import(lang: LangId, req: &ImportRequest) -> String {
1179    match syntax_for(lang) {
1180        Some(engine) => engine.generate_line(req),
1181        None => String::new(),
1182    }
1183}
1184
1185/// Generate an import line for the given language. Back-compat wrapper over
1186/// [`generate_import`] for callers that pass only the legacy positional fields.
1187pub fn generate_import_line(
1188    lang: LangId,
1189    module_path: &str,
1190    names: &[String],
1191    default_import: Option<&str>,
1192    type_only: bool,
1193) -> String {
1194    generate_import(
1195        lang,
1196        &ImportRequest::legacy(module_path, names, default_import, None, type_only),
1197    )
1198}
1199
1200/// Generate an import line including namespace imports
1201/// (`import * as ns from 'mod'`). Back-compat wrapper over [`generate_import`].
1202pub fn generate_import_line_with_namespace(
1203    lang: LangId,
1204    module_path: &str,
1205    names: &[String],
1206    default_import: Option<&str>,
1207    namespace_import: Option<&str>,
1208    type_only: bool,
1209) -> String {
1210    generate_import(
1211        lang,
1212        &ImportRequest::legacy(
1213            module_path,
1214            names,
1215            default_import,
1216            namespace_import,
1217            type_only,
1218        ),
1219    )
1220}
1221
1222/// Check if the given language is supported by the import engine.
1223pub fn is_supported(lang: LangId) -> bool {
1224    syntax_for(lang).is_some()
1225}
1226
1227/// Classify a module path into a group for TS/JS/TSX.
1228pub fn classify_group_ts(module_path: &str) -> ImportGroup {
1229    if module_path.starts_with('.') {
1230        ImportGroup::Internal
1231    } else {
1232        ImportGroup::External
1233    }
1234}
1235
1236/// Classify a module path into a group for the given language.
1237pub fn classify_group(lang: LangId, module_path: &str) -> ImportGroup {
1238    match syntax_for(lang) {
1239        Some(engine) => engine.classify_group(module_path),
1240        // Unsupported languages have no grouping policy; External is the
1241        // historical neutral default.
1242        None => ImportGroup::External,
1243    }
1244}
1245
1246/// Parse a file from disk and return its import block.
1247/// Convenience wrapper that handles parsing.
1248pub fn parse_file_imports(
1249    path: &std::path::Path,
1250    lang: LangId,
1251) -> Result<(String, Tree, ImportBlock), crate::error::AftError> {
1252    let source =
1253        std::fs::read_to_string(path).map_err(|e| crate::error::AftError::FileNotFound {
1254            path: format!("{}: {}", path.display(), e),
1255        })?;
1256
1257    let grammar = grammar_for(lang);
1258    let mut parser = Parser::new();
1259    parser
1260        .set_language(&grammar)
1261        .map_err(|e| crate::error::AftError::ParseError {
1262            message: format!("grammar init failed for {:?}: {}", lang, e),
1263        })?;
1264
1265    let tree = parser
1266        .parse(&source, None)
1267        .ok_or_else(|| crate::error::AftError::ParseError {
1268            message: format!("tree-sitter parse returned None for {}", path.display()),
1269        })?;
1270
1271    let block = parse_imports(&source, &tree, lang);
1272    Ok((source, tree, block))
1273}
1274
1275// ---------------------------------------------------------------------------
1276// TS/JS/TSX implementation
1277// ---------------------------------------------------------------------------
1278
1279/// Parse imports from a TS/JS/TSX file.
1280///
1281/// Walks the AST root's direct children looking for `import_statement` nodes (D041).
1282fn parse_ts_imports(source: &str, tree: &Tree) -> ImportBlock {
1283    let root = tree.root_node();
1284    let mut imports = Vec::new();
1285
1286    let mut cursor = root.walk();
1287    if !cursor.goto_first_child() {
1288        return ImportBlock::empty();
1289    }
1290
1291    loop {
1292        let node = cursor.node();
1293        if node.kind() == "import_statement" {
1294            if let Some(imp) = parse_single_ts_import(source, &node) {
1295                imports.push(imp);
1296            }
1297        }
1298        if !cursor.goto_next_sibling() {
1299            break;
1300        }
1301    }
1302
1303    let byte_range = import_byte_range(&imports);
1304
1305    ImportBlock {
1306        imports,
1307        byte_range,
1308    }
1309}
1310
1311/// Parse a single `import_statement` node into an `ImportStatement`.
1312fn parse_single_ts_import(source: &str, node: &Node) -> Option<ImportStatement> {
1313    let raw_text = source[node.byte_range()].to_string();
1314    let byte_range = node.byte_range();
1315
1316    // Find the source module (string/string_fragment child of the import)
1317    let module_path = extract_module_path(source, node)?;
1318
1319    // Determine if this is a type-only import: `import type ...`
1320    let is_type_only = has_type_keyword(node);
1321
1322    // Extract import clause details
1323    let mut names = Vec::new();
1324    let mut default_import = None;
1325    let mut namespace_import = None;
1326
1327    let mut child_cursor = node.walk();
1328    if child_cursor.goto_first_child() {
1329        loop {
1330            let child = child_cursor.node();
1331            match child.kind() {
1332                "import_clause" => {
1333                    extract_import_clause(
1334                        source,
1335                        &child,
1336                        &mut names,
1337                        &mut default_import,
1338                        &mut namespace_import,
1339                    );
1340                }
1341                // In some grammars, the default import is a direct identifier child
1342                "identifier" => {
1343                    let text = &source[child.byte_range()];
1344                    if text != "import" && text != "from" && text != "type" {
1345                        default_import = Some(text.to_string());
1346                    }
1347                }
1348                _ => {}
1349            }
1350            if !child_cursor.goto_next_sibling() {
1351                break;
1352            }
1353        }
1354    }
1355
1356    // Classify kind
1357    let kind = if names.is_empty() && default_import.is_none() && namespace_import.is_none() {
1358        ImportKind::SideEffect
1359    } else if is_type_only {
1360        ImportKind::Type
1361    } else {
1362        ImportKind::Value
1363    };
1364
1365    let group = classify_group_ts(&module_path);
1366
1367    let form = ImportForm::Es {
1368        default_import: default_import.clone(),
1369        namespace_import: namespace_import.clone(),
1370        named: names.clone(),
1371        type_only: is_type_only,
1372        side_effect: matches!(kind, ImportKind::SideEffect),
1373    };
1374
1375    Some(ImportStatement {
1376        module_path,
1377        names,
1378        default_import,
1379        namespace_import,
1380        kind,
1381        group,
1382        byte_range,
1383        raw_text,
1384        form,
1385    })
1386}
1387
1388/// Extract the module path string from an import_statement node.
1389///
1390/// Looks for a `string` child node and extracts the content without quotes.
1391fn extract_module_path(source: &str, node: &Node) -> Option<String> {
1392    let mut cursor = node.walk();
1393    if !cursor.goto_first_child() {
1394        return None;
1395    }
1396
1397    loop {
1398        let child = cursor.node();
1399        if child.kind() == "string" {
1400            // Get the text and strip quotes
1401            let text = &source[child.byte_range()];
1402            let stripped = text
1403                .trim_start_matches(|c| c == '\'' || c == '"')
1404                .trim_end_matches(|c| c == '\'' || c == '"');
1405            return Some(stripped.to_string());
1406        }
1407        if !cursor.goto_next_sibling() {
1408            break;
1409        }
1410    }
1411    None
1412}
1413
1414/// Check if the import_statement has a `type` keyword (import type ...).
1415///
1416/// In tree-sitter-typescript, `import type { X } from 'y'` produces a `type`
1417/// node as a direct child of `import_statement`, between `import` and `import_clause`.
1418fn has_type_keyword(node: &Node) -> bool {
1419    let mut cursor = node.walk();
1420    if !cursor.goto_first_child() {
1421        return false;
1422    }
1423
1424    loop {
1425        let child = cursor.node();
1426        if child.kind() == "type" {
1427            return true;
1428        }
1429        if !cursor.goto_next_sibling() {
1430            break;
1431        }
1432    }
1433
1434    false
1435}
1436
1437/// Extract named imports, default import, and namespace import from an import_clause.
1438fn extract_import_clause(
1439    source: &str,
1440    node: &Node,
1441    names: &mut Vec<String>,
1442    default_import: &mut Option<String>,
1443    namespace_import: &mut Option<String>,
1444) {
1445    let mut cursor = node.walk();
1446    if !cursor.goto_first_child() {
1447        return;
1448    }
1449
1450    loop {
1451        let child = cursor.node();
1452        match child.kind() {
1453            "identifier" => {
1454                // This is a default import: `import Foo from 'bar'`
1455                let text = &source[child.byte_range()];
1456                if text != "type" {
1457                    *default_import = Some(text.to_string());
1458                }
1459            }
1460            "named_imports" => {
1461                // `{ name1, name2 }`
1462                extract_named_imports(source, &child, names);
1463            }
1464            "namespace_import" => {
1465                // `* as name`
1466                extract_namespace_import(source, &child, namespace_import);
1467            }
1468            _ => {}
1469        }
1470        if !cursor.goto_next_sibling() {
1471            break;
1472        }
1473    }
1474}
1475
1476/// Extract individual names from a named_imports node (`{ a, b, c }`).
1477///
1478/// Each name is stored verbatim including any alias and per-name `type`
1479/// modifier so the regenerator can round-trip them losslessly. Examples of
1480/// captured forms:
1481///
1482/// - `useState`               (plain)
1483/// - `stdin as input`         (renamed)
1484/// - `type Foo`               (per-specifier type-only)
1485/// - `type Foo as Bar`        (per-specifier type-only with rename)
1486///
1487/// **Why verbatim strings instead of a struct field per attribute:** dedup,
1488/// sort, dropping a single import, and the regenerator are all driven by
1489/// `Vec<String>` today. Encoding the alias inside the string preserves the
1490/// shape so the rest of the pipeline (organize, remove_import, move_symbol)
1491/// keeps working without a workspace-wide refactor. The cost is that callers
1492/// who want the canonical name (e.g. dedup) must compare on the leading
1493/// identifier only — see `extract_canonical_name` if you need that.
1494fn extract_named_imports(source: &str, node: &Node, names: &mut Vec<String>) {
1495    let mut cursor = node.walk();
1496    if !cursor.goto_first_child() {
1497        return;
1498    }
1499
1500    loop {
1501        let child = cursor.node();
1502        if child.kind() == "import_specifier" {
1503            // Capture the full text of the specifier so per-name `type` markers
1504            // and `as alias` clauses are preserved across organize/regenerate
1505            // round-trips. Falls back to the imported name if the specifier
1506            // text is empty for any reason.
1507            let raw = source[child.byte_range()].trim().to_string();
1508            if !raw.is_empty() {
1509                names.push(raw);
1510            } else if let Some(name_node) = child.child_by_field_name("name") {
1511                names.push(source[name_node.byte_range()].to_string());
1512            }
1513        }
1514        if !cursor.goto_next_sibling() {
1515            break;
1516        }
1517    }
1518}
1519
1520/// Extract the alias name from a namespace_import node (`* as name`).
1521fn extract_namespace_import(source: &str, node: &Node, namespace_import: &mut Option<String>) {
1522    let mut cursor = node.walk();
1523    if !cursor.goto_first_child() {
1524        return;
1525    }
1526
1527    loop {
1528        let child = cursor.node();
1529        if child.kind() == "identifier" {
1530            *namespace_import = Some(source[child.byte_range()].to_string());
1531            return;
1532        }
1533        if !cursor.goto_next_sibling() {
1534            break;
1535        }
1536    }
1537}
1538
1539/// Generate an import line for TS/JS/TSX.
1540fn generate_ts_import_line(
1541    module_path: &str,
1542    names: &[String],
1543    default_import: Option<&str>,
1544    namespace_import: Option<&str>,
1545    type_only: bool,
1546) -> String {
1547    let type_prefix = if type_only { "type " } else { "" };
1548
1549    // Side-effect import
1550    if names.is_empty() && default_import.is_none() && namespace_import.is_none() {
1551        return format!("import '{module_path}';");
1552    }
1553
1554    // Namespace import only
1555    if names.is_empty() && default_import.is_none() {
1556        if let Some(namespace) = namespace_import {
1557            return format!("import {type_prefix}* as {namespace} from '{module_path}';");
1558        }
1559    }
1560
1561    // Default + namespace import
1562    if names.is_empty() {
1563        if let (Some(def), Some(namespace)) = (default_import, namespace_import) {
1564            return format!("import {type_prefix}{def}, * as {namespace} from '{module_path}';");
1565        }
1566    }
1567
1568    // Default import only
1569    if names.is_empty() && namespace_import.is_none() {
1570        if let Some(def) = default_import {
1571            return format!("import {type_prefix}{def} from '{module_path}';");
1572        }
1573    }
1574
1575    // Named imports only
1576    if default_import.is_none() && namespace_import.is_none() {
1577        let mut sorted_names = names.to_vec();
1578        sort_named_specifiers(&mut sorted_names);
1579        let names_str = sorted_names.join(", ");
1580        return format!("import {type_prefix}{{ {names_str} }} from '{module_path}';");
1581    }
1582
1583    // Namespace + named imports
1584    if default_import.is_none() {
1585        if let Some(namespace) = namespace_import {
1586            let mut sorted_names = names.to_vec();
1587            sort_named_specifiers(&mut sorted_names);
1588            let names_str = sorted_names.join(", ");
1589            return format!(
1590                "import {type_prefix}{{ {names_str} }}, * as {namespace} from '{module_path}';"
1591            );
1592        }
1593    }
1594
1595    // Default + named + namespace imports
1596    if let (Some(def), Some(namespace)) = (default_import, namespace_import) {
1597        let mut sorted_names = names.to_vec();
1598        sort_named_specifiers(&mut sorted_names);
1599        let names_str = sorted_names.join(", ");
1600        return format!(
1601            "import {type_prefix}{def}, {{ {names_str} }}, * as {namespace} from '{module_path}';"
1602        );
1603    }
1604
1605    // Both default and named imports
1606    if let Some(def) = default_import {
1607        let mut sorted_names = names.to_vec();
1608        sort_named_specifiers(&mut sorted_names);
1609        let names_str = sorted_names.join(", ");
1610        return format!("import {type_prefix}{def}, {{ {names_str} }} from '{module_path}';");
1611    }
1612
1613    // Shouldn't reach here, but handle gracefully
1614    format!("import '{module_path}';")
1615}
1616
1617// ---------------------------------------------------------------------------
1618// Python implementation
1619// ---------------------------------------------------------------------------
1620
1621/// Python 3.x standard library module names (top-level modules).
1622/// Used for import group classification. Covers the commonly-used modules;
1623/// unknown modules are assumed third-party.
1624const PYTHON_STDLIB: &[&str] = &[
1625    "__future__",
1626    "_thread",
1627    "abc",
1628    "aifc",
1629    "argparse",
1630    "array",
1631    "ast",
1632    "asynchat",
1633    "asyncio",
1634    "asyncore",
1635    "atexit",
1636    "audioop",
1637    "base64",
1638    "bdb",
1639    "binascii",
1640    "bisect",
1641    "builtins",
1642    "bz2",
1643    "calendar",
1644    "cgi",
1645    "cgitb",
1646    "chunk",
1647    "cmath",
1648    "cmd",
1649    "code",
1650    "codecs",
1651    "codeop",
1652    "collections",
1653    "colorsys",
1654    "compileall",
1655    "concurrent",
1656    "configparser",
1657    "contextlib",
1658    "contextvars",
1659    "copy",
1660    "copyreg",
1661    "cProfile",
1662    "crypt",
1663    "csv",
1664    "ctypes",
1665    "curses",
1666    "dataclasses",
1667    "datetime",
1668    "dbm",
1669    "decimal",
1670    "difflib",
1671    "dis",
1672    "distutils",
1673    "doctest",
1674    "email",
1675    "encodings",
1676    "enum",
1677    "errno",
1678    "faulthandler",
1679    "fcntl",
1680    "filecmp",
1681    "fileinput",
1682    "fnmatch",
1683    "fractions",
1684    "ftplib",
1685    "functools",
1686    "gc",
1687    "getopt",
1688    "getpass",
1689    "gettext",
1690    "glob",
1691    "grp",
1692    "gzip",
1693    "hashlib",
1694    "heapq",
1695    "hmac",
1696    "html",
1697    "http",
1698    "idlelib",
1699    "imaplib",
1700    "imghdr",
1701    "importlib",
1702    "inspect",
1703    "io",
1704    "ipaddress",
1705    "itertools",
1706    "json",
1707    "keyword",
1708    "lib2to3",
1709    "linecache",
1710    "locale",
1711    "logging",
1712    "lzma",
1713    "mailbox",
1714    "mailcap",
1715    "marshal",
1716    "math",
1717    "mimetypes",
1718    "mmap",
1719    "modulefinder",
1720    "multiprocessing",
1721    "netrc",
1722    "numbers",
1723    "operator",
1724    "optparse",
1725    "os",
1726    "pathlib",
1727    "pdb",
1728    "pickle",
1729    "pickletools",
1730    "pipes",
1731    "pkgutil",
1732    "platform",
1733    "plistlib",
1734    "poplib",
1735    "posixpath",
1736    "pprint",
1737    "profile",
1738    "pstats",
1739    "pty",
1740    "pwd",
1741    "py_compile",
1742    "pyclbr",
1743    "pydoc",
1744    "queue",
1745    "quopri",
1746    "random",
1747    "re",
1748    "readline",
1749    "reprlib",
1750    "resource",
1751    "rlcompleter",
1752    "runpy",
1753    "sched",
1754    "secrets",
1755    "select",
1756    "selectors",
1757    "shelve",
1758    "shlex",
1759    "shutil",
1760    "signal",
1761    "site",
1762    "smtplib",
1763    "sndhdr",
1764    "socket",
1765    "socketserver",
1766    "sqlite3",
1767    "ssl",
1768    "stat",
1769    "statistics",
1770    "string",
1771    "stringprep",
1772    "struct",
1773    "subprocess",
1774    "symtable",
1775    "sys",
1776    "sysconfig",
1777    "syslog",
1778    "tabnanny",
1779    "tarfile",
1780    "tempfile",
1781    "termios",
1782    "textwrap",
1783    "threading",
1784    "time",
1785    "timeit",
1786    "tkinter",
1787    "token",
1788    "tokenize",
1789    "tomllib",
1790    "trace",
1791    "traceback",
1792    "tracemalloc",
1793    "tty",
1794    "turtle",
1795    "types",
1796    "typing",
1797    "unicodedata",
1798    "unittest",
1799    "urllib",
1800    "uuid",
1801    "venv",
1802    "warnings",
1803    "wave",
1804    "weakref",
1805    "webbrowser",
1806    "wsgiref",
1807    "xml",
1808    "xmlrpc",
1809    "zipapp",
1810    "zipfile",
1811    "zipimport",
1812    "zlib",
1813];
1814
1815/// Classify a Python import into a group.
1816pub fn classify_group_py(module_path: &str) -> ImportGroup {
1817    // Relative imports start with '.'
1818    if module_path.starts_with('.') {
1819        return ImportGroup::Internal;
1820    }
1821    // Check stdlib: use the top-level module name (before first '.')
1822    let top_module = module_path.split('.').next().unwrap_or(module_path);
1823    if PYTHON_STDLIB.contains(&top_module) {
1824        ImportGroup::Stdlib
1825    } else {
1826        ImportGroup::External
1827    }
1828}
1829
1830/// Parse imports from a Python file.
1831fn parse_py_imports(source: &str, tree: &Tree) -> ImportBlock {
1832    let root = tree.root_node();
1833    let mut imports = Vec::new();
1834
1835    let mut cursor = root.walk();
1836    if !cursor.goto_first_child() {
1837        return ImportBlock::empty();
1838    }
1839
1840    loop {
1841        let node = cursor.node();
1842        match node.kind() {
1843            "import_statement" => {
1844                if let Some(imp) = parse_py_import_statement(source, &node) {
1845                    imports.push(imp);
1846                }
1847            }
1848            "import_from_statement" => {
1849                if let Some(imp) = parse_py_import_from_statement(source, &node) {
1850                    imports.push(imp);
1851                }
1852            }
1853            _ => {}
1854        }
1855        if !cursor.goto_next_sibling() {
1856            break;
1857        }
1858    }
1859
1860    let byte_range = import_byte_range(&imports);
1861
1862    ImportBlock {
1863        imports,
1864        byte_range,
1865    }
1866}
1867
1868/// Parse `import X` or `import X.Y` Python statements.
1869fn parse_py_import_statement(source: &str, node: &Node) -> Option<ImportStatement> {
1870    let raw_text = source[node.byte_range()].to_string();
1871    let byte_range = node.byte_range();
1872
1873    // Find the dotted_name child (the module name)
1874    let mut module_path = String::new();
1875    let mut c = node.walk();
1876    if c.goto_first_child() {
1877        loop {
1878            if c.node().kind() == "dotted_name" {
1879                module_path = source[c.node().byte_range()].to_string();
1880                break;
1881            }
1882            if !c.goto_next_sibling() {
1883                break;
1884            }
1885        }
1886    }
1887    if module_path.is_empty() {
1888        return None;
1889    }
1890
1891    let group = classify_group_py(&module_path);
1892
1893    Some(ImportStatement {
1894        module_path,
1895        names: Vec::new(),
1896        default_import: None,
1897        namespace_import: None,
1898        kind: ImportKind::Value,
1899        group,
1900        byte_range,
1901        raw_text,
1902        form: ImportForm::Python {
1903            from_import: false,
1904            named: Vec::new(),
1905        },
1906    })
1907}
1908
1909/// Parse `from X import Y, Z` or `from . import Y` Python statements.
1910fn parse_py_import_from_statement(source: &str, node: &Node) -> Option<ImportStatement> {
1911    let raw_text = source[node.byte_range()].to_string();
1912    let byte_range = node.byte_range();
1913
1914    let mut module_path = String::new();
1915    let mut names = Vec::new();
1916
1917    let mut c = node.walk();
1918    if c.goto_first_child() {
1919        loop {
1920            let child = c.node();
1921            match child.kind() {
1922                "dotted_name" => {
1923                    // Could be the module name or an imported name
1924                    // The module name comes right after `from`, imported names come after `import`
1925                    // Use position: if we haven't set module_path yet and this comes
1926                    // before the `import` keyword, it's the module.
1927                    if module_path.is_empty()
1928                        && !has_seen_import_keyword(source, node, child.start_byte())
1929                    {
1930                        module_path = source[child.byte_range()].to_string();
1931                    } else {
1932                        // It's an imported name
1933                        names.push(source[child.byte_range()].to_string());
1934                    }
1935                }
1936                "relative_import" => {
1937                    // from . import X or from ..module import X
1938                    module_path = source[child.byte_range()].to_string();
1939                }
1940                _ => {}
1941            }
1942            if !c.goto_next_sibling() {
1943                break;
1944            }
1945        }
1946    }
1947
1948    // module_path must be non-empty for a valid import
1949    if module_path.is_empty() {
1950        return None;
1951    }
1952
1953    let group = classify_group_py(&module_path);
1954
1955    Some(ImportStatement {
1956        module_path,
1957        names: names.clone(),
1958        default_import: None,
1959        namespace_import: None,
1960        kind: ImportKind::Value,
1961        group,
1962        byte_range,
1963        raw_text,
1964        form: ImportForm::Python {
1965            from_import: true,
1966            named: names,
1967        },
1968    })
1969}
1970
1971/// Check if the `import` keyword appears before the given byte position in a from...import node.
1972fn has_seen_import_keyword(_source: &str, parent: &Node, before_byte: usize) -> bool {
1973    let mut c = parent.walk();
1974    if c.goto_first_child() {
1975        loop {
1976            let child = c.node();
1977            if child.kind() == "import" && child.start_byte() < before_byte {
1978                return true;
1979            }
1980            if child.start_byte() >= before_byte {
1981                return false;
1982            }
1983            if !c.goto_next_sibling() {
1984                break;
1985            }
1986        }
1987    }
1988    false
1989}
1990
1991/// Generate a Python import line.
1992fn generate_py_import_line(
1993    module_path: &str,
1994    names: &[String],
1995    _default_import: Option<&str>,
1996) -> String {
1997    if names.is_empty() {
1998        // `import module`
1999        format!("import {module_path}")
2000    } else {
2001        // `from module import name1, name2`
2002        let mut sorted = names.to_vec();
2003        sorted.sort();
2004        let names_str = sorted.join(", ");
2005        format!("from {module_path} import {names_str}")
2006    }
2007}
2008
2009// ---------------------------------------------------------------------------
2010// Rust implementation
2011// ---------------------------------------------------------------------------
2012
2013/// Classify a Rust use path into a group.
2014pub fn classify_group_rs(module_path: &str) -> ImportGroup {
2015    // Extract the first path segment (before ::)
2016    let first_seg = module_path.split("::").next().unwrap_or(module_path);
2017    match first_seg {
2018        "std" | "core" | "alloc" => ImportGroup::Stdlib,
2019        "crate" | "self" | "super" => ImportGroup::Internal,
2020        _ => ImportGroup::External,
2021    }
2022}
2023
2024/// Parse imports from a Rust file.
2025fn parse_rs_imports(source: &str, tree: &Tree) -> ImportBlock {
2026    let root = tree.root_node();
2027    let mut imports = Vec::new();
2028
2029    let mut cursor = root.walk();
2030    if !cursor.goto_first_child() {
2031        return ImportBlock::empty();
2032    }
2033
2034    loop {
2035        let node = cursor.node();
2036        if node.kind() == "use_declaration" {
2037            if let Some(imp) = parse_rs_use_declaration(source, &node) {
2038                imports.push(imp);
2039            }
2040        }
2041        if !cursor.goto_next_sibling() {
2042            break;
2043        }
2044    }
2045
2046    let byte_range = import_byte_range(&imports);
2047
2048    ImportBlock {
2049        imports,
2050        byte_range,
2051    }
2052}
2053
2054/// Parse a single `use` declaration from Rust.
2055fn parse_rs_use_declaration(source: &str, node: &Node) -> Option<ImportStatement> {
2056    let raw_text = source[node.byte_range()].to_string();
2057    let byte_range = node.byte_range();
2058
2059    // Check for `pub` visibility modifier
2060    let mut has_pub = false;
2061    let mut use_path = String::new();
2062    let mut names = Vec::new();
2063
2064    let mut c = node.walk();
2065    if c.goto_first_child() {
2066        loop {
2067            let child = c.node();
2068            match child.kind() {
2069                "visibility_modifier" => {
2070                    has_pub = true;
2071                }
2072                "scoped_identifier" | "identifier" | "use_as_clause" => {
2073                    // Full path like `std::collections::HashMap` or just `serde`
2074                    use_path = source[child.byte_range()].to_string();
2075                }
2076                "scoped_use_list" => {
2077                    // e.g. `serde::{Deserialize, Serialize}`
2078                    use_path = source[child.byte_range()].to_string();
2079                    // Also extract the individual names from the use_list
2080                    extract_rs_use_list_names(source, &child, &mut names);
2081                }
2082                _ => {}
2083            }
2084            if !c.goto_next_sibling() {
2085                break;
2086            }
2087        }
2088    }
2089
2090    if use_path.is_empty() {
2091        return None;
2092    }
2093
2094    let group = classify_group_rs(&use_path);
2095
2096    Some(ImportStatement {
2097        module_path: use_path,
2098        names: names.clone(),
2099        default_import: if has_pub {
2100            Some("pub".to_string())
2101        } else {
2102            None
2103        },
2104        namespace_import: None,
2105        kind: ImportKind::Value,
2106        group,
2107        byte_range,
2108        raw_text,
2109        form: ImportForm::RustUse {
2110            // Only bare `pub` is recognized by the current parser; richer
2111            // visibilities (`pub(crate)`, …) collapse to the same flag today and
2112            // will be parsed precisely when the Rust engine moves onto `form`.
2113            visibility: has_pub.then(|| "pub".to_string()),
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 (Phase 1: first new language on the ImportSyntax registry) ---
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}