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