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