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