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::{
25 php_grouped_use_matches_module, php_grouped_use_shares_prefix, php_import_matches_module,
26 rewrite_php_import_without_module,
27};
28mod ruby;
29mod scala;
30pub(crate) use scala::scala_block_uses_scala2_dialect;
31mod swift;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum ImportKind {
40 Value,
42 Type,
44 SideEffect,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
59pub enum ImportGroup {
60 Stdlib,
63 External,
65 Internal,
67}
68
69impl ImportGroup {
70 pub fn label(&self) -> &'static str {
72 match self {
73 ImportGroup::Stdlib => "stdlib",
74 ImportGroup::External => "external",
75 ImportGroup::Internal => "internal",
76 }
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct PhpImportClause {
83 pub module_path: String,
85 pub alias: Option<String>,
87 pub import_kind: Option<String>,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
103pub enum ImportForm {
104 Es {
108 default_import: Option<String>,
109 namespace_import: Option<String>,
110 named: Vec<String>,
111 type_only: bool,
113 side_effect: bool,
115 },
116 Python {
119 from_import: bool,
120 named: Vec<String>,
121 },
122 RustUse {
127 visibility: Option<String>,
128 named: Vec<String>,
129 },
130 Go { alias: Option<String> },
133 Solidity {
142 named: Vec<String>,
143 namespace: Option<String>,
144 alias: Option<String>,
145 },
146 Php { clauses: Vec<PhpImportClause> },
149 Structured {
155 named: Vec<String>,
156 namespace: Option<String>,
157 alias: Option<String>,
158 modifiers: Vec<String>,
159 import_kind: Option<String>,
160 },
161}
162
163#[derive(Debug, Clone)]
168pub struct ImportRequest<'a> {
169 pub module_path: &'a str,
170 pub names: &'a [String],
171 pub default_import: Option<&'a str>,
172 pub namespace: Option<&'a str>,
174 pub alias: Option<&'a str>,
176 pub type_only: bool,
177 pub modifiers: &'a [String],
180 pub import_kind: Option<&'a str>,
183}
184
185const NO_MODIFIERS: &[String] = &[];
187
188impl<'a> ImportRequest<'a> {
189 pub fn legacy(
193 module_path: &'a str,
194 names: &'a [String],
195 default_import: Option<&'a str>,
196 namespace: Option<&'a str>,
197 type_only: bool,
198 ) -> Self {
199 ImportRequest {
200 module_path,
201 names,
202 default_import,
203 namespace,
204 alias: None,
205 type_only,
206 modifiers: NO_MODIFIERS,
207 import_kind: None,
208 }
209 }
210}
211
212#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct ImportStatement {
215 pub module_path: String,
217 pub names: Vec<String>,
219 pub default_import: Option<String>,
221 pub namespace_import: Option<String>,
223 pub kind: ImportKind,
225 pub group: ImportGroup,
227 pub byte_range: Range<usize>,
229 pub raw_text: String,
231 pub form: ImportForm,
235}
236
237#[derive(Debug, Clone, PartialEq, Eq)]
239pub struct ImportBlock {
240 pub imports: Vec<ImportStatement>,
242 pub byte_range: Option<Range<usize>>,
245}
246
247impl ImportBlock {
248 pub fn empty() -> Self {
249 ImportBlock {
250 imports: Vec::new(),
251 byte_range: None,
252 }
253 }
254}
255
256pub(crate) fn import_byte_range(imports: &[ImportStatement]) -> Option<Range<usize>> {
257 imports.first().zip(imports.last()).map(|(first, last)| {
258 let start = first.byte_range.start;
259 let end = last.byte_range.end;
260 start..end
261 })
262}
263
264pub fn specifier_local_name(spec: &str) -> &str {
280 let trimmed = spec.trim();
281 let after_type = trimmed
282 .strip_prefix("type ")
283 .unwrap_or(trimmed)
284 .trim_start();
285 if let Some(idx) = after_type.find(" as ") {
286 after_type[idx + 4..].trim()
287 } else {
288 after_type
289 }
290}
291
292pub fn specifier_imported_name(spec: &str) -> &str {
301 let trimmed = spec.trim();
302 let after_type = trimmed
303 .strip_prefix("type ")
304 .unwrap_or(trimmed)
305 .trim_start();
306 after_type
307 .find(" as ")
308 .map(|idx| after_type[..idx].trim())
309 .unwrap_or(after_type)
310}
311
312pub fn specifier_matches(spec: &str, target: &str) -> bool {
317 specifier_imported_name(spec) == target || specifier_local_name(spec) == target
318}
319
320pub trait ImportSyntax: Sync {
333 fn parse(&self, source: &str, tree: &Tree) -> ImportBlock;
335
336 fn generate_line(&self, req: &ImportRequest) -> String;
339
340 fn classify_group(&self, module_path: &str) -> ImportGroup;
342}
343
344struct EsSyntax;
346impl ImportSyntax for EsSyntax {
347 fn parse(&self, source: &str, tree: &Tree) -> ImportBlock {
348 parse_ts_imports(source, tree)
349 }
350 fn generate_line(&self, req: &ImportRequest) -> String {
351 generate_ts_import_line(
352 req.module_path,
353 req.names,
354 req.default_import,
355 req.namespace,
356 req.type_only,
357 )
358 }
359 fn classify_group(&self, module_path: &str) -> ImportGroup {
360 classify_group_ts(module_path)
361 }
362}
363
364struct PythonSyntax;
365impl ImportSyntax for PythonSyntax {
366 fn parse(&self, source: &str, tree: &Tree) -> ImportBlock {
367 parse_py_imports(source, tree)
368 }
369 fn generate_line(&self, req: &ImportRequest) -> String {
370 generate_py_import_line(req.module_path, req.names, req.default_import)
371 }
372 fn classify_group(&self, module_path: &str) -> ImportGroup {
373 classify_group_py(module_path)
374 }
375}
376
377struct RustSyntax;
378impl ImportSyntax for RustSyntax {
379 fn parse(&self, source: &str, tree: &Tree) -> ImportBlock {
380 parse_rs_imports(source, tree)
381 }
382 fn generate_line(&self, req: &ImportRequest) -> String {
383 generate_rs_import_line(req.module_path, req.names, req.type_only)
384 }
385 fn classify_group(&self, module_path: &str) -> ImportGroup {
386 classify_group_rs(module_path)
387 }
388}
389
390struct GoSyntax;
391impl ImportSyntax for GoSyntax {
392 fn parse(&self, source: &str, tree: &Tree) -> ImportBlock {
393 parse_go_imports(source, tree)
394 }
395 fn generate_line(&self, req: &ImportRequest) -> String {
396 generate_go_import_line(req.module_path, req.default_import, false)
397 }
398 fn classify_group(&self, module_path: &str) -> ImportGroup {
399 classify_group_go(module_path)
400 }
401}
402
403struct SoliditySyntax;
406impl ImportSyntax for SoliditySyntax {
407 fn parse(&self, source: &str, tree: &Tree) -> ImportBlock {
408 parse_solidity_imports(source, tree)
409 }
410 fn generate_line(&self, req: &ImportRequest) -> String {
411 generate_solidity_import_line(req)
412 }
413 fn classify_group(&self, module_path: &str) -> ImportGroup {
414 classify_group_solidity(module_path)
415 }
416}
417
418#[derive(Debug, Clone, Copy, PartialEq, Eq)]
419pub(crate) enum VueScriptRangeError {
420 MissingScript,
421 MultipleScripts,
422}
423
424impl VueScriptRangeError {
425 pub(crate) fn code(self) -> &'static str {
426 match self {
427 VueScriptRangeError::MissingScript => "missing_vue_script",
428 VueScriptRangeError::MultipleScripts => "ambiguous_vue_script",
429 }
430 }
431
432 pub(crate) fn message(self, command: &str) -> String {
433 match self {
434 VueScriptRangeError::MissingScript => format!(
435 "{command}: Vue import management requires exactly one <script> block; found none"
436 ),
437 VueScriptRangeError::MultipleScripts => format!(
438 "{command}: Vue import management requires exactly one <script> block; found multiple"
439 ),
440 }
441 }
442}
443
444pub(crate) fn vue_single_script_content_range(
452 tree: &Tree,
453) -> Result<(usize, usize), VueScriptRangeError> {
454 let root = tree.root_node();
455 let mut ranges = Vec::new();
456 let mut cursor = root.walk();
457 for child in root.named_children(&mut cursor) {
458 if child.kind() == "script_element" {
459 ranges.push(vue_script_element_content_range(&child));
460 }
461 }
462
463 match ranges.len() {
464 0 => Err(VueScriptRangeError::MissingScript),
465 1 => Ok(ranges[0]),
466 _ => Err(VueScriptRangeError::MultipleScripts),
467 }
468}
469
470pub(crate) fn vue_script_content_range(tree: &Tree) -> Option<(usize, usize)> {
473 vue_single_script_content_range(tree).ok()
474}
475
476fn vue_script_element_content_range(child: &Node) -> (usize, usize) {
477 let mut inner = child.walk();
478 for sub in child.named_children(&mut inner) {
479 if sub.kind() == "raw_text" {
480 return (sub.start_byte(), sub.end_byte());
481 }
482 }
483
484 let mut inner2 = child.walk();
486 for sub in child.named_children(&mut inner2) {
487 if sub.kind() == "start_tag" {
488 return (sub.end_byte(), sub.end_byte());
489 }
490 }
491
492 (child.end_byte(), child.end_byte())
493}
494
495fn parse_vue_imports(source: &str, tree: &Tree) -> ImportBlock {
500 let Ok((start, end)) = vue_single_script_content_range(tree) else {
501 return ImportBlock {
502 imports: Vec::new(),
503 byte_range: None,
504 };
505 };
506 let inner = &source[start..end];
507 let mut parser = Parser::new();
508 if parser
509 .set_language(&grammar_for(LangId::TypeScript))
510 .is_err()
511 {
512 return ImportBlock {
513 imports: Vec::new(),
514 byte_range: None,
515 };
516 }
517 let Some(inner_tree) = parser.parse(inner, None) else {
518 return ImportBlock {
519 imports: Vec::new(),
520 byte_range: None,
521 };
522 };
523 let mut block = parse_ts_imports(inner, &inner_tree);
524 for imp in &mut block.imports {
525 imp.byte_range = (imp.byte_range.start + start)..(imp.byte_range.end + start);
526 }
527 block.byte_range = block.byte_range.map(|r| (r.start + start)..(r.end + start));
528 block
529}
530
531struct VueSyntax;
537impl ImportSyntax for VueSyntax {
538 fn parse(&self, source: &str, tree: &Tree) -> ImportBlock {
539 parse_vue_imports(source, tree)
540 }
541 fn generate_line(&self, req: &ImportRequest) -> String {
542 generate_ts_import_line(
543 req.module_path,
544 req.names,
545 req.default_import,
546 req.namespace,
547 req.type_only,
548 )
549 }
550 fn classify_group(&self, module_path: &str) -> ImportGroup {
551 classify_group_ts(module_path)
552 }
553}
554
555static ES_SYNTAX: EsSyntax = EsSyntax;
556static PYTHON_SYNTAX: PythonSyntax = PythonSyntax;
557static RUST_SYNTAX: RustSyntax = RustSyntax;
558static GO_SYNTAX: GoSyntax = GoSyntax;
559static SOLIDITY_SYNTAX: SoliditySyntax = SoliditySyntax;
560static VUE_SYNTAX: VueSyntax = VueSyntax;
561
562pub fn syntax_for(lang: LangId) -> Option<&'static dyn ImportSyntax> {
564 match lang {
565 LangId::TypeScript | LangId::Tsx | LangId::JavaScript => Some(&ES_SYNTAX),
566 LangId::Python => Some(&PYTHON_SYNTAX),
567 LangId::Rust => Some(&RUST_SYNTAX),
568 LangId::Go => Some(&GO_SYNTAX),
569 LangId::Solidity => Some(&SOLIDITY_SYNTAX),
570 LangId::Vue => Some(&VUE_SYNTAX),
571 LangId::C => Some(&c::C_SYNTAX),
572 LangId::Cpp => Some(&c::C_SYNTAX),
573 LangId::Java => Some(&java::JAVA_SYNTAX),
574 LangId::Kotlin => Some(&kotlin::KOTLIN_SYNTAX),
575 LangId::Lua => Some(&lua::LUA_SYNTAX),
576 LangId::CSharp => Some(&csharp::CSHARP_SYNTAX),
577 LangId::Php => Some(&php::PHP_SYNTAX),
578 LangId::Perl => Some(&perl::PERL_SYNTAX),
579 LangId::Ruby => Some(&ruby::RUBY_SYNTAX),
580 LangId::Scala => Some(&scala::SCALA_SYNTAX),
581 LangId::Swift => Some(&swift::SWIFT_SYNTAX),
582 LangId::Zig
583 | LangId::Bash
584 | LangId::Scss
585 | LangId::Json
586 | LangId::Html
587 | LangId::Markdown
588 | LangId::Yaml
589 | LangId::Pascal
590 | LangId::R
591 | LangId::Groovy
592 | LangId::ObjC => None,
593 }
594}
595
596pub fn parse_imports(source: &str, tree: &Tree, lang: LangId) -> ImportBlock {
602 match syntax_for(lang) {
603 Some(engine) => engine.parse(source, tree),
604 None => ImportBlock::empty(),
605 }
606}
607
608pub fn is_duplicate(
614 block: &ImportBlock,
615 module_path: &str,
616 names: &[String],
617 default_import: Option<&str>,
618 type_only: bool,
619) -> bool {
620 is_duplicate_with_namespace(block, module_path, names, default_import, None, type_only)
621}
622
623pub fn is_duplicate_with_namespace(
625 block: &ImportBlock,
626 module_path: &str,
627 names: &[String],
628 default_import: Option<&str>,
629 namespace_import: Option<&str>,
630 type_only: bool,
631) -> bool {
632 let target_kind = if type_only {
633 ImportKind::Type
634 } else {
635 ImportKind::Value
636 };
637
638 for imp in &block.imports {
639 if imp.module_path != module_path {
640 continue;
641 }
642
643 if names.is_empty()
649 && default_import.is_none()
650 && namespace_import.is_none()
651 && imp.names.is_empty()
652 && imp.default_import.is_none()
653 && imp.namespace_import.is_none()
654 {
655 return true;
656 }
657
658 if names.is_empty()
660 && default_import.is_none()
661 && namespace_import.is_none()
662 && imp.kind == ImportKind::SideEffect
663 {
664 return true;
665 }
666
667 if imp.kind != target_kind && imp.kind != ImportKind::SideEffect {
669 continue;
670 }
671
672 if let (Some(def), Some(namespace)) = (default_import, namespace_import) {
676 if imp.default_import.as_deref() == Some(def)
677 && imp.namespace_import.as_deref() == Some(namespace)
678 && names
679 .iter()
680 .all(|n| imp.names.iter().any(|stored| specifier_matches(stored, n)))
681 {
682 return true;
683 }
684 continue;
685 }
686
687 if names.is_empty()
691 && default_import.is_none()
692 && namespace_import.is_some()
693 && imp.namespace_import.as_deref() == namespace_import
694 {
695 return true;
696 }
697
698 if let Some(def) = default_import {
701 if namespace_import.is_none() && imp.default_import.as_deref() == Some(def) {
702 return true;
703 }
704 }
705
706 if !names.is_empty()
712 && names
713 .iter()
714 .all(|n| imp.names.iter().any(|stored| specifier_matches(stored, n)))
715 {
716 return true;
717 }
718 }
719
720 false
721}
722
723pub(crate) fn is_duplicate_import_request(
734 lang: LangId,
735 block: &ImportBlock,
736 req: &ImportRequest<'_>,
737) -> bool {
738 if lang == LangId::Php
739 && block
740 .imports
741 .iter()
742 .any(|imp| php::php_import_satisfies_request(imp, req))
743 {
744 return true;
745 }
746
747 if lang == LangId::Python && req.names.is_empty() && req.default_import.is_none() {
748 return block.imports.iter().any(|imp| {
749 matches!(
750 &imp.form,
751 ImportForm::Python {
752 from_import: false,
753 named,
754 } if named.iter().any(|specifier| {
755 specifier_imported_name(specifier) == req.module_path
756 })
757 )
758 });
759 }
760
761 if !uses_form_aware_dedup(lang) {
762 return is_duplicate_with_namespace(
763 block,
764 req.module_path,
765 req.names,
766 req.default_import,
767 req.namespace,
768 req.type_only,
769 );
770 }
771
772 let target = request_dedup_key(lang, req);
773 block
774 .imports
775 .iter()
776 .map(|imp| statement_dedup_key(lang, imp))
777 .any(|key| key == target)
778}
779
780fn uses_form_aware_dedup(lang: LangId) -> bool {
781 matches!(
782 lang,
783 LangId::Solidity
784 | LangId::C
785 | LangId::Cpp
786 | LangId::Java
787 | LangId::CSharp
788 | LangId::Php
789 | LangId::Kotlin
790 | LangId::Scala
791 | LangId::Swift
792 | LangId::Ruby
793 | LangId::Lua
794 | LangId::Perl
795 )
796}
797
798#[derive(Debug, Clone, PartialEq, Eq)]
799struct ImportDedupKey {
800 module_path: String,
801 kind: ImportKind,
802 form: ImportForm,
803}
804
805fn statement_dedup_key(lang: LangId, imp: &ImportStatement) -> ImportDedupKey {
806 canonical_dedup_key(
807 lang,
808 ImportDedupKey {
809 module_path: imp.module_path.clone(),
810 kind: imp.kind,
811 form: imp.form.clone(),
812 },
813 )
814}
815
816fn request_dedup_key(lang: LangId, req: &ImportRequest<'_>) -> ImportDedupKey {
817 let key = match lang {
818 LangId::Solidity => {
819 let kind = if req.names.is_empty() && req.namespace.is_none() && req.alias.is_none() {
820 ImportKind::SideEffect
821 } else {
822 ImportKind::Value
823 };
824 ImportDedupKey {
825 module_path: req.module_path.to_string(),
826 kind,
827 form: ImportForm::Solidity {
828 named: req.names.to_vec(),
829 namespace: req.namespace.map(str::to_string),
830 alias: req.alias.map(str::to_string),
831 },
832 }
833 }
834 LangId::C | LangId::Cpp => structured_dedup_key(
835 req.module_path,
836 ImportKind::SideEffect,
837 &[],
838 None,
839 None,
840 &[],
841 Some(req.import_kind.or(req.default_import).unwrap_or("system")),
842 ),
843 LangId::Java => {
844 let (mut module_path, modifiers) = wildcard_suffix_request(
845 req.module_path,
846 req.modifiers,
847 req.default_import == Some("*"),
848 );
849 let mut names = req.names.to_vec();
850 normalize_java_static_member_key(&mut module_path, &modifiers, &mut names);
851 structured_dedup_key(
852 &module_path,
853 ImportKind::Value,
854 &names,
855 None,
856 None,
857 &modifiers,
858 None,
859 )
860 }
861 LangId::CSharp => structured_dedup_key(
862 req.module_path,
863 ImportKind::Value,
864 &[],
865 None,
866 req.alias,
867 req.modifiers,
868 None,
869 ),
870 LangId::Php => structured_dedup_key(
871 req.module_path,
872 ImportKind::Value,
873 &[],
874 None,
875 req.alias,
876 req.modifiers,
877 req.import_kind,
878 ),
879 LangId::Kotlin => {
880 let wildcard = req.default_import == Some("*") || req.module_path.ends_with(".*");
881 let (module_path, modifiers) =
882 wildcard_suffix_request(req.module_path, req.modifiers, wildcard);
883 let alias = req
884 .alias
885 .or(req.default_import.filter(|value| *value != "*"));
886 structured_dedup_key(
887 &module_path,
888 ImportKind::Value,
889 &[],
890 None,
891 alias,
892 &modifiers,
893 None,
894 )
895 }
896 LangId::Scala => scala_request_dedup_key(req),
897 LangId::Swift => structured_dedup_key(
898 req.module_path,
899 ImportKind::Value,
900 &[],
901 None,
902 None,
903 req.modifiers,
904 req.import_kind,
905 ),
906 LangId::Ruby => {
907 let mut modifiers = req.modifiers.to_vec();
908 if !modifiers
909 .iter()
910 .any(|modifier| modifier == "quote:single" || modifier == "quote:double")
911 {
912 modifiers.push("quote:single".to_string());
913 }
914 structured_dedup_key(
915 req.module_path,
916 ImportKind::SideEffect,
917 &[],
918 None,
919 None,
920 &modifiers,
921 Some(req.import_kind.unwrap_or("require")),
922 )
923 }
924 LangId::Lua => {
925 let alias = req.default_import.or(req.alias);
926 let kind = if alias.is_some() {
927 ImportKind::Value
928 } else {
929 ImportKind::SideEffect
930 };
931 structured_dedup_key(req.module_path, kind, &[], None, alias, req.modifiers, None)
932 }
933 LangId::Perl => structured_dedup_key(
934 req.module_path,
935 ImportKind::SideEffect,
936 &[],
937 None,
938 None,
939 req.modifiers,
940 Some(req.import_kind.unwrap_or("use")),
941 ),
942 _ => structured_dedup_key(
943 req.module_path,
944 if req.type_only {
945 ImportKind::Type
946 } else {
947 ImportKind::Value
948 },
949 req.names,
950 req.namespace,
951 req.alias,
952 req.modifiers,
953 req.import_kind,
954 ),
955 };
956
957 canonical_dedup_key(lang, key)
958}
959
960fn structured_dedup_key(
961 module_path: &str,
962 kind: ImportKind,
963 named: &[String],
964 namespace: Option<&str>,
965 alias: Option<&str>,
966 modifiers: &[String],
967 import_kind: Option<&str>,
968) -> ImportDedupKey {
969 ImportDedupKey {
970 module_path: module_path.to_string(),
971 kind,
972 form: ImportForm::Structured {
973 named: named.to_vec(),
974 namespace: namespace.map(str::to_string),
975 alias: alias.map(str::to_string),
976 modifiers: modifiers.to_vec(),
977 import_kind: import_kind.map(str::to_string),
978 },
979 }
980}
981
982fn wildcard_suffix_request(
983 module_path: &str,
984 modifiers: &[String],
985 wildcard: bool,
986) -> (String, Vec<String>) {
987 let stripped = module_path.strip_suffix(".*").unwrap_or(module_path);
988 let mut modifiers = modifiers.to_vec();
989 if (wildcard || stripped.len() != module_path.len())
990 && !modifiers.iter().any(|modifier| modifier == "wildcard")
991 {
992 modifiers.push("wildcard".to_string());
993 }
994 (stripped.to_string(), modifiers)
995}
996
997fn normalize_java_static_member_key(
998 module_path: &mut String,
999 modifiers: &[String],
1000 names: &mut Vec<String>,
1001) {
1002 let is_static = modifiers.iter().any(|modifier| modifier == "static");
1003 let is_wildcard = modifiers.iter().any(|modifier| modifier == "wildcard");
1004 if !is_static || is_wildcard || !names.is_empty() {
1005 return;
1006 }
1007
1008 if let Some((prefix, member)) = module_path.rsplit_once('.') {
1009 if !prefix.is_empty() && !member.is_empty() {
1010 names.push(member.to_string());
1011 *module_path = prefix.to_string();
1012 }
1013 }
1014}
1015
1016fn scala_request_dedup_key(req: &ImportRequest<'_>) -> ImportDedupKey {
1017 let mut module_path = req.module_path.to_string();
1018 let mut names: Vec<String> = req
1019 .names
1020 .iter()
1021 .map(|name| normalize_scala_selector_for_dedup(name))
1022 .collect();
1023 let mut modifiers = req.modifiers.to_vec();
1024 let mut import_kind = req.import_kind.map(str::to_string);
1025
1026 if req.default_import == Some("given") || module_path.ends_with(".given") {
1027 import_kind.get_or_insert_with(|| "given".to_string());
1028 if let Some(stripped) = module_path.strip_suffix(".given") {
1029 module_path = stripped.to_string();
1030 }
1031 }
1032
1033 if matches!(req.default_import, Some("*") | Some("_"))
1034 || matches!(req.namespace, Some("*") | Some("_"))
1035 || module_path.ends_with(".*")
1036 || module_path.ends_with("._")
1037 {
1038 if !modifiers.iter().any(|modifier| modifier == "wildcard") {
1039 modifiers.push("wildcard".to_string());
1040 }
1041 module_path = module_path
1042 .strip_suffix(".*")
1043 .or_else(|| module_path.strip_suffix("._"))
1044 .unwrap_or(&module_path)
1045 .to_string();
1046 }
1047
1048 if names.is_empty() {
1049 if let Some(alias) = req.alias.filter(|alias| !alias.is_empty()) {
1050 if let Some((prefix, leaf)) = module_path.rsplit_once('.') {
1051 names.push(format!("{leaf} as {alias}"));
1052 module_path = prefix.to_string();
1053 }
1054 }
1055 }
1056
1057 structured_dedup_key(
1058 &module_path,
1059 ImportKind::Value,
1060 &names,
1061 None,
1062 None,
1063 &modifiers,
1064 import_kind.as_deref(),
1065 )
1066}
1067
1068fn normalize_scala_selector_for_dedup(name: &str) -> String {
1069 let trimmed = name.trim();
1070 if let Some((from, to)) = trimmed.split_once("=>") {
1071 format!("{} as {}", from.trim(), to.trim())
1072 } else {
1073 trimmed.to_string()
1074 }
1075}
1076
1077fn canonical_dedup_key(lang: LangId, mut key: ImportDedupKey) -> ImportDedupKey {
1078 match &mut key.form {
1079 ImportForm::Structured { named, .. } | ImportForm::Solidity { named, .. } => {
1080 sort_named_specifiers(named);
1081 }
1082 ImportForm::Es { named, .. } | ImportForm::Python { named, .. } => {
1083 sort_named_specifiers(named);
1084 }
1085 ImportForm::RustUse { named, .. } => {
1086 sort_named_specifiers(named);
1087 }
1088 ImportForm::Go { .. } | ImportForm::Php { .. } => {}
1089 }
1090
1091 if matches!(lang, LangId::Java | LangId::Kotlin) {
1092 if let Some(stripped) = key.module_path.strip_suffix(".*") {
1093 key.module_path = stripped.to_string();
1094 }
1095 if matches!(lang, LangId::Java) {
1096 if let ImportForm::Structured {
1097 named, modifiers, ..
1098 } = &mut key.form
1099 {
1100 normalize_java_static_member_key(&mut key.module_path, modifiers, named);
1101 }
1102 }
1103 } else if matches!(lang, LangId::Scala) {
1104 key.module_path = key
1105 .module_path
1106 .strip_suffix(".given")
1107 .or_else(|| key.module_path.strip_suffix(".*"))
1108 .or_else(|| key.module_path.strip_suffix("._"))
1109 .unwrap_or(&key.module_path)
1110 .to_string();
1111 }
1112
1113 key
1114}
1115
1116fn sort_named_specifiers(names: &mut [String]) {
1117 names.sort_by(|a, b| {
1118 specifier_imported_name(a)
1119 .cmp(specifier_imported_name(b))
1120 .then_with(|| a.cmp(b))
1121 });
1122}
1123
1124pub fn find_insertion_point(
1135 source: &str,
1136 block: &ImportBlock,
1137 group: ImportGroup,
1138 module_path: &str,
1139 type_only: bool,
1140) -> (usize, bool, bool) {
1141 if block.imports.is_empty() {
1142 return (0, false, source.is_empty().then_some(false).unwrap_or(true));
1144 }
1145
1146 let target_kind = if type_only {
1147 ImportKind::Type
1148 } else {
1149 ImportKind::Value
1150 };
1151
1152 let group_imports: Vec<&ImportStatement> =
1154 block.imports.iter().filter(|i| i.group == group).collect();
1155
1156 if group_imports.is_empty() {
1157 let preceding_last = block.imports.iter().filter(|i| i.group < group).last();
1160
1161 if let Some(last) = preceding_last {
1162 let end = last.byte_range.end;
1163 let insert_at = skip_newline(source, end);
1164 return (insert_at, true, true);
1165 }
1166
1167 let following_first = block.imports.iter().find(|i| i.group > group);
1169
1170 if let Some(first) = following_first {
1171 return (first.byte_range.start, false, true);
1172 }
1173
1174 let first_byte = import_byte_range(&block.imports)
1176 .map(|range| range.start)
1177 .unwrap_or(0);
1178 return (first_byte, false, true);
1179 }
1180
1181 for imp in &group_imports {
1183 let cmp = module_path.cmp(&imp.module_path);
1184 match cmp {
1185 std::cmp::Ordering::Less => {
1186 return (imp.byte_range.start, false, false);
1188 }
1189 std::cmp::Ordering::Equal => {
1190 if target_kind == ImportKind::Type && imp.kind == ImportKind::Value {
1192 let end = imp.byte_range.end;
1194 let insert_at = skip_newline(source, end);
1195 return (insert_at, false, false);
1196 }
1197 return (imp.byte_range.start, false, false);
1199 }
1200 std::cmp::Ordering::Greater => continue,
1201 }
1202 }
1203
1204 let Some(last) = group_imports.last() else {
1206 return (
1207 import_byte_range(&block.imports)
1208 .map(|range| range.end)
1209 .unwrap_or(0),
1210 false,
1211 false,
1212 );
1213 };
1214 let end = last.byte_range.end;
1215 let insert_at = skip_newline(source, end);
1216 (insert_at, false, false)
1217}
1218
1219pub fn generate_import(lang: LangId, req: &ImportRequest) -> String {
1223 match syntax_for(lang) {
1224 Some(engine) => engine.generate_line(req),
1225 None => String::new(),
1226 }
1227}
1228
1229pub fn generate_import_line(
1232 lang: LangId,
1233 module_path: &str,
1234 names: &[String],
1235 default_import: Option<&str>,
1236 type_only: bool,
1237) -> String {
1238 generate_import(
1239 lang,
1240 &ImportRequest::legacy(module_path, names, default_import, None, type_only),
1241 )
1242}
1243
1244pub fn generate_import_line_with_namespace(
1247 lang: LangId,
1248 module_path: &str,
1249 names: &[String],
1250 default_import: Option<&str>,
1251 namespace_import: Option<&str>,
1252 type_only: bool,
1253) -> String {
1254 generate_import(
1255 lang,
1256 &ImportRequest::legacy(
1257 module_path,
1258 names,
1259 default_import,
1260 namespace_import,
1261 type_only,
1262 ),
1263 )
1264}
1265
1266pub fn is_supported(lang: LangId) -> bool {
1268 syntax_for(lang).is_some()
1269}
1270
1271pub fn classify_group_ts(module_path: &str) -> ImportGroup {
1273 if module_path.starts_with('.') {
1274 ImportGroup::Internal
1275 } else {
1276 ImportGroup::External
1277 }
1278}
1279
1280pub fn classify_group(lang: LangId, module_path: &str) -> ImportGroup {
1282 match syntax_for(lang) {
1283 Some(engine) => engine.classify_group(module_path),
1284 None => ImportGroup::External,
1287 }
1288}
1289
1290pub fn parse_file_imports(
1293 path: &std::path::Path,
1294 lang: LangId,
1295) -> Result<(String, Tree, ImportBlock), crate::error::AftError> {
1296 let source =
1297 std::fs::read_to_string(path).map_err(|e| crate::error::AftError::FileNotFound {
1298 path: format!("{}: {}", path.display(), e),
1299 })?;
1300
1301 let grammar = grammar_for(lang);
1302 let mut parser = Parser::new();
1303 parser
1304 .set_language(&grammar)
1305 .map_err(|e| crate::error::AftError::ParseError {
1306 message: format!("grammar init failed for {:?}: {}", lang, e),
1307 })?;
1308
1309 let tree = parser
1310 .parse(&source, None)
1311 .ok_or_else(|| crate::error::AftError::ParseError {
1312 message: format!("tree-sitter parse returned None for {}", path.display()),
1313 })?;
1314
1315 let block = parse_imports(&source, &tree, lang);
1316 Ok((source, tree, block))
1317}
1318
1319fn parse_ts_imports(source: &str, tree: &Tree) -> ImportBlock {
1327 let root = tree.root_node();
1328 let mut imports = Vec::new();
1329
1330 let mut cursor = root.walk();
1331 if !cursor.goto_first_child() {
1332 return ImportBlock::empty();
1333 }
1334
1335 loop {
1336 let node = cursor.node();
1337 if node.kind() == "import_statement" {
1338 if let Some(imp) = parse_single_ts_import(source, &node) {
1339 imports.push(imp);
1340 }
1341 }
1342 if !cursor.goto_next_sibling() {
1343 break;
1344 }
1345 }
1346
1347 let byte_range = import_byte_range(&imports);
1348
1349 ImportBlock {
1350 imports,
1351 byte_range,
1352 }
1353}
1354
1355fn parse_single_ts_import(source: &str, node: &Node) -> Option<ImportStatement> {
1357 let raw_text = source[node.byte_range()].to_string();
1358 let byte_range = node.byte_range();
1359
1360 let module_path = extract_module_path(source, node)?;
1362
1363 let is_type_only = has_type_keyword(node);
1365
1366 let mut names = Vec::new();
1368 let mut default_import = None;
1369 let mut namespace_import = None;
1370
1371 let mut child_cursor = node.walk();
1372 if child_cursor.goto_first_child() {
1373 loop {
1374 let child = child_cursor.node();
1375 match child.kind() {
1376 "import_clause" => {
1377 extract_import_clause(
1378 source,
1379 &child,
1380 &mut names,
1381 &mut default_import,
1382 &mut namespace_import,
1383 );
1384 }
1385 "identifier" => {
1387 let text = &source[child.byte_range()];
1388 if text != "import" && text != "from" && text != "type" {
1389 default_import = Some(text.to_string());
1390 }
1391 }
1392 _ => {}
1393 }
1394 if !child_cursor.goto_next_sibling() {
1395 break;
1396 }
1397 }
1398 }
1399
1400 let kind = if names.is_empty() && default_import.is_none() && namespace_import.is_none() {
1402 ImportKind::SideEffect
1403 } else if is_type_only {
1404 ImportKind::Type
1405 } else {
1406 ImportKind::Value
1407 };
1408
1409 let group = classify_group_ts(&module_path);
1410
1411 let form = ImportForm::Es {
1412 default_import: default_import.clone(),
1413 namespace_import: namespace_import.clone(),
1414 named: names.clone(),
1415 type_only: is_type_only,
1416 side_effect: matches!(kind, ImportKind::SideEffect),
1417 };
1418
1419 Some(ImportStatement {
1420 module_path,
1421 names,
1422 default_import,
1423 namespace_import,
1424 kind,
1425 group,
1426 byte_range,
1427 raw_text,
1428 form,
1429 })
1430}
1431
1432fn extract_module_path(source: &str, node: &Node) -> Option<String> {
1436 let mut cursor = node.walk();
1437 if !cursor.goto_first_child() {
1438 return None;
1439 }
1440
1441 loop {
1442 let child = cursor.node();
1443 if child.kind() == "string" {
1444 let text = &source[child.byte_range()];
1446 let stripped = text
1447 .trim_start_matches(|c| c == '\'' || c == '"')
1448 .trim_end_matches(|c| c == '\'' || c == '"');
1449 return Some(stripped.to_string());
1450 }
1451 if !cursor.goto_next_sibling() {
1452 break;
1453 }
1454 }
1455 None
1456}
1457
1458fn has_type_keyword(node: &Node) -> bool {
1463 let mut cursor = node.walk();
1464 if !cursor.goto_first_child() {
1465 return false;
1466 }
1467
1468 loop {
1469 let child = cursor.node();
1470 if child.kind() == "type" {
1471 return true;
1472 }
1473 if !cursor.goto_next_sibling() {
1474 break;
1475 }
1476 }
1477
1478 false
1479}
1480
1481fn extract_import_clause(
1483 source: &str,
1484 node: &Node,
1485 names: &mut Vec<String>,
1486 default_import: &mut Option<String>,
1487 namespace_import: &mut Option<String>,
1488) {
1489 let mut cursor = node.walk();
1490 if !cursor.goto_first_child() {
1491 return;
1492 }
1493
1494 loop {
1495 let child = cursor.node();
1496 match child.kind() {
1497 "identifier" => {
1498 let text = &source[child.byte_range()];
1500 if text != "type" {
1501 *default_import = Some(text.to_string());
1502 }
1503 }
1504 "named_imports" => {
1505 extract_named_imports(source, &child, names);
1507 }
1508 "namespace_import" => {
1509 extract_namespace_import(source, &child, namespace_import);
1511 }
1512 _ => {}
1513 }
1514 if !cursor.goto_next_sibling() {
1515 break;
1516 }
1517 }
1518}
1519
1520fn extract_named_imports(source: &str, node: &Node, names: &mut Vec<String>) {
1539 let mut cursor = node.walk();
1540 if !cursor.goto_first_child() {
1541 return;
1542 }
1543
1544 loop {
1545 let child = cursor.node();
1546 if child.kind() == "import_specifier" {
1547 let raw = source[child.byte_range()].trim().to_string();
1552 if !raw.is_empty() {
1553 names.push(raw);
1554 } else if let Some(name_node) = child.child_by_field_name("name") {
1555 names.push(source[name_node.byte_range()].to_string());
1556 }
1557 }
1558 if !cursor.goto_next_sibling() {
1559 break;
1560 }
1561 }
1562}
1563
1564fn extract_namespace_import(source: &str, node: &Node, namespace_import: &mut Option<String>) {
1566 let mut cursor = node.walk();
1567 if !cursor.goto_first_child() {
1568 return;
1569 }
1570
1571 loop {
1572 let child = cursor.node();
1573 if child.kind() == "identifier" {
1574 *namespace_import = Some(source[child.byte_range()].to_string());
1575 return;
1576 }
1577 if !cursor.goto_next_sibling() {
1578 break;
1579 }
1580 }
1581}
1582
1583fn generate_ts_import_line(
1585 module_path: &str,
1586 names: &[String],
1587 default_import: Option<&str>,
1588 namespace_import: Option<&str>,
1589 type_only: bool,
1590) -> String {
1591 let type_prefix = if type_only { "type " } else { "" };
1592
1593 if names.is_empty() && default_import.is_none() && namespace_import.is_none() {
1595 return format!("import '{module_path}';");
1596 }
1597
1598 if names.is_empty() && default_import.is_none() {
1600 if let Some(namespace) = namespace_import {
1601 return format!("import {type_prefix}* as {namespace} from '{module_path}';");
1602 }
1603 }
1604
1605 if names.is_empty() {
1607 if let (Some(def), Some(namespace)) = (default_import, namespace_import) {
1608 return format!("import {type_prefix}{def}, * as {namespace} from '{module_path}';");
1609 }
1610 }
1611
1612 if names.is_empty() && namespace_import.is_none() {
1614 if let Some(def) = default_import {
1615 return format!("import {type_prefix}{def} from '{module_path}';");
1616 }
1617 }
1618
1619 if default_import.is_none() && namespace_import.is_none() {
1621 let mut sorted_names = names.to_vec();
1622 sort_named_specifiers(&mut sorted_names);
1623 let names_str = sorted_names.join(", ");
1624 return format!("import {type_prefix}{{ {names_str} }} from '{module_path}';");
1625 }
1626
1627 if default_import.is_none() {
1629 if let Some(namespace) = namespace_import {
1630 let mut sorted_names = names.to_vec();
1631 sort_named_specifiers(&mut sorted_names);
1632 let names_str = sorted_names.join(", ");
1633 return format!(
1634 "import {type_prefix}{{ {names_str} }}, * as {namespace} from '{module_path}';"
1635 );
1636 }
1637 }
1638
1639 if let (Some(def), Some(namespace)) = (default_import, namespace_import) {
1641 let mut sorted_names = names.to_vec();
1642 sort_named_specifiers(&mut sorted_names);
1643 let names_str = sorted_names.join(", ");
1644 return format!(
1645 "import {type_prefix}{def}, {{ {names_str} }}, * as {namespace} from '{module_path}';"
1646 );
1647 }
1648
1649 if let Some(def) = default_import {
1651 let mut sorted_names = names.to_vec();
1652 sort_named_specifiers(&mut sorted_names);
1653 let names_str = sorted_names.join(", ");
1654 return format!("import {type_prefix}{def}, {{ {names_str} }} from '{module_path}';");
1655 }
1656
1657 format!("import '{module_path}';")
1659}
1660
1661const PYTHON_STDLIB: &[&str] = &[
1669 "__future__",
1670 "_thread",
1671 "abc",
1672 "aifc",
1673 "argparse",
1674 "array",
1675 "ast",
1676 "asynchat",
1677 "asyncio",
1678 "asyncore",
1679 "atexit",
1680 "audioop",
1681 "base64",
1682 "bdb",
1683 "binascii",
1684 "bisect",
1685 "builtins",
1686 "bz2",
1687 "calendar",
1688 "cgi",
1689 "cgitb",
1690 "chunk",
1691 "cmath",
1692 "cmd",
1693 "code",
1694 "codecs",
1695 "codeop",
1696 "collections",
1697 "colorsys",
1698 "compileall",
1699 "concurrent",
1700 "configparser",
1701 "contextlib",
1702 "contextvars",
1703 "copy",
1704 "copyreg",
1705 "cProfile",
1706 "crypt",
1707 "csv",
1708 "ctypes",
1709 "curses",
1710 "dataclasses",
1711 "datetime",
1712 "dbm",
1713 "decimal",
1714 "difflib",
1715 "dis",
1716 "distutils",
1717 "doctest",
1718 "email",
1719 "encodings",
1720 "enum",
1721 "errno",
1722 "faulthandler",
1723 "fcntl",
1724 "filecmp",
1725 "fileinput",
1726 "fnmatch",
1727 "fractions",
1728 "ftplib",
1729 "functools",
1730 "gc",
1731 "getopt",
1732 "getpass",
1733 "gettext",
1734 "glob",
1735 "grp",
1736 "gzip",
1737 "hashlib",
1738 "heapq",
1739 "hmac",
1740 "html",
1741 "http",
1742 "idlelib",
1743 "imaplib",
1744 "imghdr",
1745 "importlib",
1746 "inspect",
1747 "io",
1748 "ipaddress",
1749 "itertools",
1750 "json",
1751 "keyword",
1752 "lib2to3",
1753 "linecache",
1754 "locale",
1755 "logging",
1756 "lzma",
1757 "mailbox",
1758 "mailcap",
1759 "marshal",
1760 "math",
1761 "mimetypes",
1762 "mmap",
1763 "modulefinder",
1764 "multiprocessing",
1765 "netrc",
1766 "numbers",
1767 "operator",
1768 "optparse",
1769 "os",
1770 "pathlib",
1771 "pdb",
1772 "pickle",
1773 "pickletools",
1774 "pipes",
1775 "pkgutil",
1776 "platform",
1777 "plistlib",
1778 "poplib",
1779 "posixpath",
1780 "pprint",
1781 "profile",
1782 "pstats",
1783 "pty",
1784 "pwd",
1785 "py_compile",
1786 "pyclbr",
1787 "pydoc",
1788 "queue",
1789 "quopri",
1790 "random",
1791 "re",
1792 "readline",
1793 "reprlib",
1794 "resource",
1795 "rlcompleter",
1796 "runpy",
1797 "sched",
1798 "secrets",
1799 "select",
1800 "selectors",
1801 "shelve",
1802 "shlex",
1803 "shutil",
1804 "signal",
1805 "site",
1806 "smtplib",
1807 "sndhdr",
1808 "socket",
1809 "socketserver",
1810 "sqlite3",
1811 "ssl",
1812 "stat",
1813 "statistics",
1814 "string",
1815 "stringprep",
1816 "struct",
1817 "subprocess",
1818 "symtable",
1819 "sys",
1820 "sysconfig",
1821 "syslog",
1822 "tabnanny",
1823 "tarfile",
1824 "tempfile",
1825 "termios",
1826 "textwrap",
1827 "threading",
1828 "time",
1829 "timeit",
1830 "tkinter",
1831 "token",
1832 "tokenize",
1833 "tomllib",
1834 "trace",
1835 "traceback",
1836 "tracemalloc",
1837 "tty",
1838 "turtle",
1839 "types",
1840 "typing",
1841 "unicodedata",
1842 "unittest",
1843 "urllib",
1844 "uuid",
1845 "venv",
1846 "warnings",
1847 "wave",
1848 "weakref",
1849 "webbrowser",
1850 "wsgiref",
1851 "xml",
1852 "xmlrpc",
1853 "zipapp",
1854 "zipfile",
1855 "zipimport",
1856 "zlib",
1857];
1858
1859pub fn classify_group_py(module_path: &str) -> ImportGroup {
1861 if module_path.starts_with('.') {
1863 return ImportGroup::Internal;
1864 }
1865 let top_module = module_path.split('.').next().unwrap_or(module_path);
1867 if PYTHON_STDLIB.contains(&top_module) {
1868 ImportGroup::Stdlib
1869 } else {
1870 ImportGroup::External
1871 }
1872}
1873
1874fn parse_py_imports(source: &str, tree: &Tree) -> ImportBlock {
1876 let root = tree.root_node();
1877 let mut imports = Vec::new();
1878
1879 let mut cursor = root.walk();
1880 if !cursor.goto_first_child() {
1881 return ImportBlock::empty();
1882 }
1883
1884 loop {
1885 let node = cursor.node();
1886 match node.kind() {
1887 "import_statement" => {
1888 if let Some(imp) = parse_py_import_statement(source, &node) {
1889 imports.push(imp);
1890 }
1891 }
1892 "import_from_statement" => {
1893 if let Some(imp) = parse_py_import_from_statement(source, &node) {
1894 imports.push(imp);
1895 }
1896 }
1897 _ => {}
1898 }
1899 if !cursor.goto_next_sibling() {
1900 break;
1901 }
1902 }
1903
1904 let byte_range = import_byte_range(&imports);
1905
1906 ImportBlock {
1907 imports,
1908 byte_range,
1909 }
1910}
1911
1912fn parse_py_import_statement(source: &str, node: &Node) -> Option<ImportStatement> {
1914 let raw_text = source[node.byte_range()].to_string();
1915 let byte_range = node.byte_range();
1916
1917 let mut specifiers = Vec::new();
1921 let mut c = node.walk();
1922 if c.goto_first_child() {
1923 loop {
1924 let child = c.node();
1925 if matches!(child.kind(), "dotted_name" | "aliased_import") {
1926 specifiers.push(source[child.byte_range()].trim().to_string());
1927 }
1928 if !c.goto_next_sibling() {
1929 break;
1930 }
1931 }
1932 }
1933 let module_path = specifiers
1934 .first()
1935 .map(|specifier| specifier_imported_name(specifier).to_string())?;
1936
1937 let group = classify_group_py(&module_path);
1938
1939 Some(ImportStatement {
1940 module_path,
1941 names: Vec::new(),
1942 default_import: None,
1943 namespace_import: None,
1944 kind: ImportKind::Value,
1945 group,
1946 byte_range,
1947 raw_text,
1948 form: ImportForm::Python {
1949 from_import: false,
1950 named: specifiers,
1951 },
1952 })
1953}
1954
1955fn parse_py_import_from_statement(source: &str, node: &Node) -> Option<ImportStatement> {
1957 let raw_text = source[node.byte_range()].to_string();
1958 let byte_range = node.byte_range();
1959
1960 let mut module_path = String::new();
1961 let mut names = Vec::new();
1962
1963 let mut c = node.walk();
1964 if c.goto_first_child() {
1965 loop {
1966 let child = c.node();
1967 match child.kind() {
1968 "dotted_name" => {
1969 if module_path.is_empty()
1974 && !has_seen_import_keyword(source, node, child.start_byte())
1975 {
1976 module_path = source[child.byte_range()].to_string();
1977 } else {
1978 names.push(source[child.byte_range()].to_string());
1980 }
1981 }
1982 "relative_import" => {
1983 module_path = source[child.byte_range()].to_string();
1985 }
1986 "aliased_import" => {
1987 names.push(source[child.byte_range()].trim().to_string());
1990 }
1991 _ => {}
1992 }
1993 if !c.goto_next_sibling() {
1994 break;
1995 }
1996 }
1997 }
1998
1999 if module_path.is_empty() {
2001 return None;
2002 }
2003
2004 let group = classify_group_py(&module_path);
2005
2006 Some(ImportStatement {
2007 module_path,
2008 names: names.clone(),
2009 default_import: None,
2010 namespace_import: None,
2011 kind: ImportKind::Value,
2012 group,
2013 byte_range,
2014 raw_text,
2015 form: ImportForm::Python {
2016 from_import: true,
2017 named: names,
2018 },
2019 })
2020}
2021
2022fn has_seen_import_keyword(_source: &str, parent: &Node, before_byte: usize) -> bool {
2024 let mut c = parent.walk();
2025 if c.goto_first_child() {
2026 loop {
2027 let child = c.node();
2028 if child.kind() == "import" && child.start_byte() < before_byte {
2029 return true;
2030 }
2031 if child.start_byte() >= before_byte {
2032 return false;
2033 }
2034 if !c.goto_next_sibling() {
2035 break;
2036 }
2037 }
2038 }
2039 false
2040}
2041
2042fn generate_py_import_line(
2044 module_path: &str,
2045 names: &[String],
2046 _default_import: Option<&str>,
2047) -> String {
2048 if names.is_empty() {
2049 format!("import {module_path}")
2051 } else {
2052 let mut sorted = names.to_vec();
2054 sorted.sort();
2055 let names_str = sorted.join(", ");
2056 format!("from {module_path} import {names_str}")
2057 }
2058}
2059
2060pub fn classify_group_rs(module_path: &str) -> ImportGroup {
2066 let first_seg = module_path.split("::").next().unwrap_or(module_path);
2068 match first_seg {
2069 "std" | "core" | "alloc" => ImportGroup::Stdlib,
2070 "crate" | "self" | "super" => ImportGroup::Internal,
2071 _ => ImportGroup::External,
2072 }
2073}
2074
2075fn parse_rs_imports(source: &str, tree: &Tree) -> ImportBlock {
2077 let root = tree.root_node();
2078 let mut imports = Vec::new();
2079
2080 let mut cursor = root.walk();
2081 if !cursor.goto_first_child() {
2082 return ImportBlock::empty();
2083 }
2084
2085 loop {
2086 let node = cursor.node();
2087 if node.kind() == "use_declaration" {
2088 if let Some(imp) = parse_rs_use_declaration(source, &node) {
2089 imports.push(imp);
2090 }
2091 }
2092 if !cursor.goto_next_sibling() {
2093 break;
2094 }
2095 }
2096
2097 let byte_range = import_byte_range(&imports);
2098
2099 ImportBlock {
2100 imports,
2101 byte_range,
2102 }
2103}
2104
2105fn parse_rs_use_declaration(source: &str, node: &Node) -> Option<ImportStatement> {
2107 let raw_text = source[node.byte_range()].to_string();
2108 let byte_range = node.byte_range();
2109
2110 let mut visibility: Option<String> = None;
2114 let mut use_path = String::new();
2115 let mut names = Vec::new();
2116
2117 let mut c = node.walk();
2118 if c.goto_first_child() {
2119 loop {
2120 let child = c.node();
2121 match child.kind() {
2122 "visibility_modifier" => {
2123 visibility = Some(source[child.byte_range()].to_string());
2124 }
2125 "scoped_identifier" | "identifier" | "use_as_clause" => {
2126 use_path = source[child.byte_range()].to_string();
2128 }
2129 "scoped_use_list" => {
2130 use_path = source[child.byte_range()].to_string();
2132 extract_rs_use_list_names(source, &child, &mut names);
2134 }
2135 _ => {}
2136 }
2137 if !c.goto_next_sibling() {
2138 break;
2139 }
2140 }
2141 }
2142
2143 if use_path.is_empty() {
2144 return None;
2145 }
2146
2147 let group = classify_group_rs(&use_path);
2148
2149 Some(ImportStatement {
2150 module_path: use_path,
2151 names: names.clone(),
2152 default_import: visibility.clone(),
2155 namespace_import: None,
2156 kind: ImportKind::Value,
2157 group,
2158 byte_range,
2159 raw_text,
2160 form: ImportForm::RustUse {
2161 visibility,
2162 named: names,
2163 },
2164 })
2165}
2166
2167fn extract_rs_use_list_names(source: &str, node: &Node, names: &mut Vec<String>) {
2169 let mut c = node.walk();
2170 if c.goto_first_child() {
2171 loop {
2172 let child = c.node();
2173 if child.kind() == "use_list" {
2174 let mut lc = child.walk();
2176 if lc.goto_first_child() {
2177 loop {
2178 let lchild = lc.node();
2179 if lchild.kind() == "identifier" || lchild.kind() == "scoped_identifier" {
2180 names.push(source[lchild.byte_range()].to_string());
2181 }
2182 if !lc.goto_next_sibling() {
2183 break;
2184 }
2185 }
2186 }
2187 }
2188 if !c.goto_next_sibling() {
2189 break;
2190 }
2191 }
2192 }
2193}
2194
2195fn generate_rs_import_line(module_path: &str, names: &[String], _type_only: bool) -> String {
2197 if names.is_empty() {
2198 format!("use {module_path};")
2199 } else {
2200 let mut sorted_names = names.to_vec();
2201 sort_named_specifiers(&mut sorted_names);
2202 format!("use {module_path}::{{{}}};", sorted_names.join(", "))
2203 }
2204}
2205
2206pub fn classify_group_go(module_path: &str) -> ImportGroup {
2212 if module_path.contains('.') {
2215 ImportGroup::External
2216 } else {
2217 ImportGroup::Stdlib
2218 }
2219}
2220
2221fn parse_go_imports(source: &str, tree: &Tree) -> ImportBlock {
2223 let root = tree.root_node();
2224 let mut imports = Vec::new();
2225
2226 let mut cursor = root.walk();
2227 if !cursor.goto_first_child() {
2228 return ImportBlock::empty();
2229 }
2230
2231 loop {
2232 let node = cursor.node();
2233 if node.kind() == "import_declaration" {
2234 parse_go_import_declaration(source, &node, &mut imports);
2235 }
2236 if !cursor.goto_next_sibling() {
2237 break;
2238 }
2239 }
2240
2241 let byte_range = import_byte_range(&imports);
2242
2243 ImportBlock {
2244 imports,
2245 byte_range,
2246 }
2247}
2248
2249fn parse_go_import_declaration(source: &str, node: &Node, imports: &mut Vec<ImportStatement>) {
2251 let mut c = node.walk();
2252 if c.goto_first_child() {
2253 loop {
2254 let child = c.node();
2255 match child.kind() {
2256 "import_spec" => {
2257 if let Some(imp) = parse_go_import_spec(source, &child) {
2258 imports.push(imp);
2259 }
2260 }
2261 "import_spec_list" => {
2262 let mut lc = child.walk();
2264 if lc.goto_first_child() {
2265 loop {
2266 if lc.node().kind() == "import_spec" {
2267 if let Some(imp) = parse_go_import_spec(source, &lc.node()) {
2268 imports.push(imp);
2269 }
2270 }
2271 if !lc.goto_next_sibling() {
2272 break;
2273 }
2274 }
2275 }
2276 }
2277 _ => {}
2278 }
2279 if !c.goto_next_sibling() {
2280 break;
2281 }
2282 }
2283 }
2284}
2285
2286fn parse_go_import_spec(source: &str, node: &Node) -> Option<ImportStatement> {
2288 let raw_text = source[node.byte_range()].to_string();
2289 let byte_range = node.byte_range();
2290
2291 let mut import_path = String::new();
2292 let mut alias = None;
2293
2294 let mut c = node.walk();
2295 if c.goto_first_child() {
2296 loop {
2297 let child = c.node();
2298 match child.kind() {
2299 "interpreted_string_literal" => {
2300 let text = source[child.byte_range()].to_string();
2302 import_path = text.trim_matches('"').to_string();
2303 }
2304 "identifier" | "blank_identifier" | "dot" => {
2305 alias = Some(source[child.byte_range()].to_string());
2307 }
2308 _ => {}
2309 }
2310 if !c.goto_next_sibling() {
2311 break;
2312 }
2313 }
2314 }
2315
2316 if import_path.is_empty() {
2317 return None;
2318 }
2319
2320 let group = classify_group_go(&import_path);
2321
2322 Some(ImportStatement {
2323 module_path: import_path,
2324 names: Vec::new(),
2325 default_import: alias.clone(),
2326 namespace_import: None,
2327 kind: ImportKind::Value,
2328 group,
2329 byte_range,
2330 raw_text,
2331 form: ImportForm::Go { alias },
2332 })
2333}
2334
2335pub fn generate_go_import_line_pub(
2337 module_path: &str,
2338 alias: Option<&str>,
2339 in_group: bool,
2340) -> String {
2341 generate_go_import_line(module_path, alias, in_group)
2342}
2343
2344fn generate_go_import_line(module_path: &str, alias: Option<&str>, in_group: bool) -> String {
2349 if in_group {
2350 match alias {
2352 Some(a) => format!("\t{a} \"{module_path}\""),
2353 None => format!("\t\"{module_path}\""),
2354 }
2355 } else {
2356 match alias {
2358 Some(a) => format!("import {a} \"{module_path}\""),
2359 None => format!("import \"{module_path}\""),
2360 }
2361 }
2362}
2363
2364pub fn go_has_grouped_import(_source: &str, tree: &Tree) -> Option<Range<usize>> {
2367 let root = tree.root_node();
2368 let mut cursor = root.walk();
2369 if !cursor.goto_first_child() {
2370 return None;
2371 }
2372
2373 loop {
2374 let node = cursor.node();
2375 if node.kind() == "import_declaration" && go_import_declaration_is_grouped(&node) {
2376 return Some(node.byte_range());
2377 }
2378 if !cursor.goto_next_sibling() {
2379 break;
2380 }
2381 }
2382 None
2383}
2384
2385pub fn go_import_declarations_range(_source: &str, tree: &Tree) -> Option<Range<usize>> {
2386 let root = tree.root_node();
2387 let mut cursor = root.walk();
2388 let mut range: Option<Range<usize>> = None;
2389 if !cursor.goto_first_child() {
2390 return None;
2391 }
2392
2393 loop {
2394 let node = cursor.node();
2395 if node.kind() == "import_declaration" {
2396 let node_range = node.byte_range();
2397 range = Some(match range {
2398 Some(existing) => {
2399 existing.start.min(node_range.start)..existing.end.max(node_range.end)
2400 }
2401 None => node_range,
2402 });
2403 }
2404 if !cursor.goto_next_sibling() {
2405 break;
2406 }
2407 }
2408
2409 range
2410}
2411
2412pub fn go_offset_is_in_grouped_import(_source: &str, tree: &Tree, offset: usize) -> bool {
2413 let root = tree.root_node();
2414 let mut cursor = root.walk();
2415 if !cursor.goto_first_child() {
2416 return false;
2417 }
2418
2419 loop {
2420 let node = cursor.node();
2421 if node.kind() == "import_declaration"
2422 && node.start_byte() < offset
2423 && offset < node.end_byte()
2424 && go_import_declaration_is_grouped(&node)
2425 {
2426 return true;
2427 }
2428 if !cursor.goto_next_sibling() {
2429 break;
2430 }
2431 }
2432
2433 false
2434}
2435
2436fn go_import_declaration_is_grouped(node: &Node) -> bool {
2437 let mut c = node.walk();
2438 if c.goto_first_child() {
2439 loop {
2440 if c.node().kind() == "import_spec_list" {
2441 return true;
2442 }
2443 if !c.goto_next_sibling() {
2444 break;
2445 }
2446 }
2447 }
2448 false
2449}
2450
2451pub fn classify_group_solidity(module_path: &str) -> ImportGroup {
2458 if module_path.starts_with('.') {
2459 ImportGroup::Internal
2460 } else {
2461 ImportGroup::External
2462 }
2463}
2464
2465fn parse_solidity_imports(source: &str, tree: &Tree) -> ImportBlock {
2466 let root = tree.root_node();
2467 let mut imports = Vec::new();
2468 let mut cursor = root.walk();
2469 if cursor.goto_first_child() {
2470 loop {
2471 let node = cursor.node();
2472 if node.kind() == "import_directive" {
2473 if let Some(imp) = parse_solidity_import_directive(source, &node) {
2474 imports.push(imp);
2475 }
2476 }
2477 if !cursor.goto_next_sibling() {
2478 break;
2479 }
2480 }
2481 }
2482 let byte_range = import_byte_range(&imports);
2483 ImportBlock {
2484 imports,
2485 byte_range,
2486 }
2487}
2488
2489fn parse_solidity_import_directive(source: &str, node: &Node) -> Option<ImportStatement> {
2494 let raw_text = source[node.byte_range()].to_string();
2495 let byte_range = node.byte_range();
2496
2497 let mut children: Vec<(String, String)> = Vec::new();
2498 let mut c = node.walk();
2499 if c.goto_first_child() {
2500 loop {
2501 let ch = c.node();
2502 children.push((ch.kind().to_string(), source[ch.byte_range()].to_string()));
2503 if !c.goto_next_sibling() {
2504 break;
2505 }
2506 }
2507 }
2508
2509 let module_path = children
2511 .iter()
2512 .find(|(k, _)| k == "string")
2513 .map(|(_, t)| t.trim_matches('"').to_string())?;
2514 if module_path.is_empty() {
2515 return None;
2516 }
2517
2518 let has_brace = children.iter().any(|(k, _)| k == "{");
2519 let has_star = children.iter().any(|(k, _)| k == "*");
2520
2521 let mut named: Vec<String> = Vec::new();
2522 let mut namespace: Option<String> = None;
2523 let mut alias: Option<String> = None;
2524
2525 if has_brace {
2526 named = parse_solidity_named_specifiers(&children);
2527 } else if has_star {
2528 namespace = solidity_identifier_after_as(&children);
2529 } else {
2530 alias = solidity_identifier_after_as(&children);
2533 }
2534
2535 let kind = if named.is_empty() && namespace.is_none() && alias.is_none() {
2536 ImportKind::SideEffect
2537 } else {
2538 ImportKind::Value
2539 };
2540 let group = classify_group_solidity(&module_path);
2541
2542 Some(ImportStatement {
2543 module_path,
2544 names: named.clone(),
2545 default_import: None,
2546 namespace_import: namespace.clone(),
2549 kind,
2550 group,
2551 byte_range,
2552 raw_text,
2553 form: ImportForm::Solidity {
2554 named,
2555 namespace,
2556 alias,
2557 },
2558 })
2559}
2560
2561fn solidity_identifier_after_as(children: &[(String, String)]) -> Option<String> {
2563 let as_pos = children.iter().position(|(k, _)| k == "as")?;
2564 children[as_pos + 1..]
2565 .iter()
2566 .find(|(k, _)| k == "identifier")
2567 .map(|(_, t)| t.clone())
2568}
2569
2570fn parse_solidity_named_specifiers(children: &[(String, String)]) -> Vec<String> {
2573 let mut names = Vec::new();
2574 let mut in_braces = false;
2575 let mut current: Option<String> = None;
2576 let mut expect_alias = false;
2577 for (k, t) in children {
2578 match k.as_str() {
2579 "{" => in_braces = true,
2580 "}" => {
2581 if let Some(n) = current.take() {
2582 names.push(n);
2583 }
2584 in_braces = false;
2585 }
2586 _ if !in_braces => {}
2587 "identifier" => {
2588 if expect_alias {
2589 if let Some(n) = current.take() {
2590 names.push(format!("{n} as {t}"));
2591 }
2592 expect_alias = false;
2593 } else {
2594 if let Some(n) = current.take() {
2595 names.push(n);
2596 }
2597 current = Some(t.clone());
2598 }
2599 }
2600 "as" => expect_alias = true,
2601 "," => {
2602 if let Some(n) = current.take() {
2603 names.push(n);
2604 }
2605 expect_alias = false;
2606 }
2607 _ => {}
2608 }
2609 }
2610 names
2611}
2612
2613fn generate_solidity_import_line(req: &ImportRequest) -> String {
2615 if !req.names.is_empty() {
2616 format!(
2617 "import {{ {} }} from \"{}\";",
2618 req.names.join(", "),
2619 req.module_path
2620 )
2621 } else if let Some(ns) = req.namespace {
2622 format!("import * as {} from \"{}\";", ns, req.module_path)
2623 } else if let Some(al) = req.alias {
2624 format!("import \"{}\" as {};", req.module_path, al)
2625 } else {
2626 format!("import \"{}\";", req.module_path)
2627 }
2628}
2629
2630fn skip_newline(source: &str, pos: usize) -> usize {
2632 if pos < source.len() {
2633 let bytes = source.as_bytes();
2634 if bytes[pos] == b'\n' {
2635 return pos + 1;
2636 }
2637 if bytes[pos] == b'\r' {
2638 if pos + 1 < source.len() && bytes[pos + 1] == b'\n' {
2639 return pos + 2;
2640 }
2641 return pos + 1;
2642 }
2643 }
2644 pos
2645}
2646
2647#[cfg(test)]
2652mod tests {
2653 use super::*;
2654
2655 #[test]
2664 fn form_es_mirrors_flat_fields() {
2665 let (_, block) = parse_ts(
2666 "import Default, { a, b as c } from \"ext\";\nimport type { T } from \"./t\";\nimport \"./side\";\nimport * as ns from \"nspkg\";\n",
2667 );
2668 match &block.imports[0].form {
2670 ImportForm::Es {
2671 default_import,
2672 namespace_import,
2673 named,
2674 type_only,
2675 side_effect,
2676 } => {
2677 assert_eq!(default_import.as_deref(), Some("Default"));
2678 assert_eq!(namespace_import, &None);
2679 assert_eq!(named, &block.imports[0].names);
2680 assert!(!type_only);
2681 assert!(!side_effect);
2682 }
2683 other => panic!("expected Es, got {other:?}"),
2684 }
2685 match &block.imports[1].form {
2687 ImportForm::Es {
2688 type_only, named, ..
2689 } => {
2690 assert!(type_only);
2691 assert_eq!(named, &block.imports[1].names);
2692 }
2693 other => panic!("expected Es type-only, got {other:?}"),
2694 }
2695 match &block.imports[2].form {
2697 ImportForm::Es { side_effect, .. } => assert!(side_effect),
2698 other => panic!("expected Es side-effect, got {other:?}"),
2699 }
2700 match &block.imports[3].form {
2702 ImportForm::Es {
2703 namespace_import, ..
2704 } => assert_eq!(namespace_import.as_deref(), Some("ns")),
2705 other => panic!("expected Es namespace, got {other:?}"),
2706 }
2707 }
2708
2709 #[test]
2710 fn form_python_mirrors_flat_fields() {
2711 let (_, block) = parse_py("import os\nfrom sys import argv, path\n");
2712 match &block.imports[0].form {
2713 ImportForm::Python { from_import, named } => {
2714 assert!(!from_import, "`import os` is not a from-import");
2715 assert_eq!(named, &["os"]);
2716 }
2717 other => panic!("expected Python import, got {other:?}"),
2718 }
2719 match &block.imports[1].form {
2720 ImportForm::Python { from_import, named } => {
2721 assert!(from_import, "`from sys import ...` is a from-import");
2722 assert_eq!(named, &block.imports[1].names);
2723 }
2724 other => panic!("expected Python from-import, got {other:?}"),
2725 }
2726 }
2727
2728 #[test]
2729 fn form_rust_de_overloads_pub_from_default_import() {
2730 let (_, block) = parse_rust("pub use crate::a::Exported;\nuse std::fmt::Debug;\n");
2731 match &block.imports[0].form {
2733 ImportForm::RustUse { visibility, named } => {
2734 assert_eq!(visibility.as_deref(), Some("pub"));
2735 assert_eq!(named, &block.imports[0].names);
2736 }
2737 other => panic!("expected RustUse, got {other:?}"),
2738 }
2739 assert_eq!(
2740 block.imports[0].default_import.as_deref(),
2741 Some("pub"),
2742 "flat field unchanged during additive migration"
2743 );
2744 match &block.imports[1].form {
2746 ImportForm::RustUse { visibility, .. } => assert_eq!(visibility, &None),
2747 other => panic!("expected RustUse, got {other:?}"),
2748 }
2749 assert_eq!(block.imports[1].default_import, None);
2750 }
2751
2752 #[test]
2753 fn form_go_de_overloads_alias_from_default_import() {
2754 let (_, block) =
2760 parse_go("package main\n\nimport (\n\t_ \"github.com/x/y\"\n\t\"fmt\"\n)\n");
2761 let blank = block
2762 .imports
2763 .iter()
2764 .find(|i| i.module_path == "github.com/x/y")
2765 .expect("blank import parsed");
2766 match &blank.form {
2767 ImportForm::Go { alias } => assert_eq!(alias.as_deref(), Some("_")),
2768 other => panic!("expected Go blank-aliased, got {other:?}"),
2769 }
2770 assert_eq!(
2771 blank.default_import.as_deref(),
2772 Some("_"),
2773 "form.alias mirrors the flat default_import field exactly"
2774 );
2775 let plain = block
2776 .imports
2777 .iter()
2778 .find(|i| i.module_path == "fmt")
2779 .expect("plain import parsed");
2780 match &plain.form {
2781 ImportForm::Go { alias } => assert_eq!(alias, &None),
2782 other => panic!("expected Go plain, got {other:?}"),
2783 }
2784 assert_eq!(plain.default_import, None);
2785 }
2786
2787 fn parse_ts(source: &str) -> (Tree, ImportBlock) {
2788 let grammar = grammar_for(LangId::TypeScript);
2789 let mut parser = Parser::new();
2790 parser.set_language(&grammar).unwrap();
2791 let tree = parser.parse(source, None).unwrap();
2792 let block = parse_imports(source, &tree, LangId::TypeScript);
2793 (tree, block)
2794 }
2795
2796 fn parse_js(source: &str) -> (Tree, ImportBlock) {
2797 let grammar = grammar_for(LangId::JavaScript);
2798 let mut parser = Parser::new();
2799 parser.set_language(&grammar).unwrap();
2800 let tree = parser.parse(source, None).unwrap();
2801 let block = parse_imports(source, &tree, LangId::JavaScript);
2802 (tree, block)
2803 }
2804
2805 fn parse_vue(source: &str) -> (Tree, ImportBlock) {
2806 let grammar = grammar_for(LangId::Vue);
2807 let mut parser = Parser::new();
2808 parser.set_language(&grammar).unwrap();
2809 let tree = parser.parse(source, None).unwrap();
2810 let block = parse_imports(source, &tree, LangId::Vue);
2811 (tree, block)
2812 }
2813
2814 #[test]
2819 fn vue_grammar_node_kinds_are_stable() {
2820 let src = "<template>\n <div />\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from 'vue'\n</script>\n";
2821 let grammar = grammar_for(LangId::Vue);
2822 let mut parser = Parser::new();
2823 parser.set_language(&grammar).unwrap();
2824 let tree = parser.parse(src, None).unwrap();
2825 let root = tree.root_node();
2826 let mut cursor = root.walk();
2827 let script = root
2828 .named_children(&mut cursor)
2829 .find(|n| n.kind() == "script_element")
2830 .expect("expected a script_element node");
2831 let mut inner = script.walk();
2832 assert!(
2833 script
2834 .named_children(&mut inner)
2835 .any(|n| n.kind() == "raw_text"),
2836 "expected script body exposed as raw_text"
2837 );
2838 }
2839
2840 #[test]
2841 fn vue_parses_script_imports_with_whole_file_offsets() {
2842 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";
2843 let (_tree, block) = parse_vue(src);
2844 assert_eq!(block.imports.len(), 2, "should find both script imports");
2845 for imp in &block.imports {
2848 assert_eq!(&src[imp.byte_range.clone()], imp.raw_text);
2849 assert!(
2850 imp.byte_range.start > src.find("<script").unwrap(),
2851 "import offset must fall inside the script block"
2852 );
2853 }
2854 assert_eq!(block.imports[0].module_path, "vue");
2855 assert_eq!(block.imports[1].module_path, "./Foo.vue");
2856 }
2857
2858 #[test]
2859 fn vue_without_script_block_has_no_imports() {
2860 let src = "<template>\n <div />\n</template>\n\n<style>.x{}</style>\n";
2861 let (_tree, block) = parse_vue(src);
2862 assert!(block.imports.is_empty());
2863 assert!(block.byte_range.is_none());
2864 }
2865
2866 #[test]
2869 fn parse_ts_named_imports() {
2870 let source = "import { useState, useEffect } from 'react';\n";
2871 let (_, block) = parse_ts(source);
2872 assert_eq!(block.imports.len(), 1);
2873 let imp = &block.imports[0];
2874 assert_eq!(imp.module_path, "react");
2875 assert!(imp.names.contains(&"useState".to_string()));
2876 assert!(imp.names.contains(&"useEffect".to_string()));
2877 assert_eq!(imp.kind, ImportKind::Value);
2878 assert_eq!(imp.group, ImportGroup::External);
2879 }
2880
2881 #[test]
2882 fn parse_ts_default_import() {
2883 let source = "import React from 'react';\n";
2884 let (_, block) = parse_ts(source);
2885 assert_eq!(block.imports.len(), 1);
2886 let imp = &block.imports[0];
2887 assert_eq!(imp.default_import.as_deref(), Some("React"));
2888 assert_eq!(imp.kind, ImportKind::Value);
2889 }
2890
2891 #[test]
2892 fn parse_ts_side_effect_import() {
2893 let source = "import './styles.css';\n";
2894 let (_, block) = parse_ts(source);
2895 assert_eq!(block.imports.len(), 1);
2896 assert_eq!(block.imports[0].kind, ImportKind::SideEffect);
2897 assert_eq!(block.imports[0].module_path, "./styles.css");
2898 }
2899
2900 #[test]
2901 fn parse_ts_relative_import() {
2902 let source = "import { helper } from './utils';\n";
2903 let (_, block) = parse_ts(source);
2904 assert_eq!(block.imports.len(), 1);
2905 assert_eq!(block.imports[0].group, ImportGroup::Internal);
2906 }
2907
2908 #[test]
2909 fn parse_ts_multiple_groups() {
2910 let source = "\
2911import React from 'react';
2912import { useState } from 'react';
2913import { helper } from './utils';
2914import { Config } from '../config';
2915";
2916 let (_, block) = parse_ts(source);
2917 assert_eq!(block.imports.len(), 4);
2918
2919 let external: Vec<_> = block
2920 .imports
2921 .iter()
2922 .filter(|i| i.group == ImportGroup::External)
2923 .collect();
2924 let relative: Vec<_> = block
2925 .imports
2926 .iter()
2927 .filter(|i| i.group == ImportGroup::Internal)
2928 .collect();
2929 assert_eq!(external.len(), 2);
2930 assert_eq!(relative.len(), 2);
2931 }
2932
2933 #[test]
2934 fn parse_ts_namespace_import() {
2935 let source = "import * as path from 'path';\n";
2936 let (_, block) = parse_ts(source);
2937 assert_eq!(block.imports.len(), 1);
2938 let imp = &block.imports[0];
2939 assert_eq!(imp.namespace_import.as_deref(), Some("path"));
2940 assert_eq!(imp.kind, ImportKind::Value);
2941 }
2942
2943 #[test]
2944 fn parse_js_imports() {
2945 let source = "import { readFile } from 'fs';\nimport { helper } from './helper';\n";
2946 let (_, block) = parse_js(source);
2947 assert_eq!(block.imports.len(), 2);
2948 assert_eq!(block.imports[0].group, ImportGroup::External);
2949 assert_eq!(block.imports[1].group, ImportGroup::Internal);
2950 }
2951
2952 #[test]
2955 fn classify_external() {
2956 assert_eq!(classify_group_ts("react"), ImportGroup::External);
2957 assert_eq!(classify_group_ts("@scope/pkg"), ImportGroup::External);
2958 assert_eq!(classify_group_ts("lodash/map"), ImportGroup::External);
2959 }
2960
2961 #[test]
2962 fn classify_relative() {
2963 assert_eq!(classify_group_ts("./utils"), ImportGroup::Internal);
2964 assert_eq!(classify_group_ts("../config"), ImportGroup::Internal);
2965 assert_eq!(classify_group_ts("./"), ImportGroup::Internal);
2966 }
2967
2968 #[test]
2971 fn dedup_detects_same_named_import() {
2972 let source = "import { useState } from 'react';\n";
2973 let (_, block) = parse_ts(source);
2974 assert!(is_duplicate(
2975 &block,
2976 "react",
2977 &["useState".to_string()],
2978 None,
2979 false
2980 ));
2981 }
2982
2983 #[test]
2984 fn dedup_misses_different_name() {
2985 let source = "import { useState } from 'react';\n";
2986 let (_, block) = parse_ts(source);
2987 assert!(!is_duplicate(
2988 &block,
2989 "react",
2990 &["useEffect".to_string()],
2991 None,
2992 false
2993 ));
2994 }
2995
2996 #[test]
2997 fn dedup_detects_default_import() {
2998 let source = "import React from 'react';\n";
2999 let (_, block) = parse_ts(source);
3000 assert!(is_duplicate(&block, "react", &[], Some("React"), false));
3001 }
3002
3003 #[test]
3004 fn dedup_side_effect() {
3005 let source = "import './styles.css';\n";
3006 let (_, block) = parse_ts(source);
3007 assert!(is_duplicate(&block, "./styles.css", &[], None, false));
3008 }
3009
3010 #[test]
3011 fn dedup_namespace_import_distinct_from_side_effect_import() {
3012 let side_effect_source = "import 'fs';\n";
3013 let (_, side_effect_block) = parse_ts(side_effect_source);
3014 assert!(!is_duplicate_with_namespace(
3015 &side_effect_block,
3016 "fs",
3017 &[],
3018 None,
3019 Some("fs"),
3020 false
3021 ));
3022
3023 let namespace_source = "import * as fs from 'fs';\n";
3024 let (_, namespace_block) = parse_ts(namespace_source);
3025 assert!(!is_duplicate(&namespace_block, "fs", &[], None, false));
3026 assert!(is_duplicate_with_namespace(
3027 &namespace_block,
3028 "fs",
3029 &[],
3030 None,
3031 Some("fs"),
3032 false
3033 ));
3034 assert!(!is_duplicate_with_namespace(
3035 &namespace_block,
3036 "fs",
3037 &[],
3038 None,
3039 Some("other"),
3040 false
3041 ));
3042 }
3043
3044 #[test]
3045 fn dedup_type_vs_value() {
3046 let source = "import { FC } from 'react';\n";
3047 let (_, block) = parse_ts(source);
3048 assert!(!is_duplicate(
3050 &block,
3051 "react",
3052 &["FC".to_string()],
3053 None,
3054 true
3055 ));
3056 }
3057
3058 #[test]
3061 fn generate_named_import() {
3062 let line = generate_import_line(
3063 LangId::TypeScript,
3064 "react",
3065 &["useState".to_string(), "useEffect".to_string()],
3066 None,
3067 false,
3068 );
3069 assert_eq!(line, "import { useEffect, useState } from 'react';");
3070 }
3071
3072 #[test]
3073 fn generate_named_import_sorts_by_imported_name() {
3074 let line = generate_import_line(
3075 LangId::TypeScript,
3076 "x",
3077 &[
3078 "useState".to_string(),
3079 "type Foo".to_string(),
3080 "stdin as input".to_string(),
3081 "type Bar".to_string(),
3082 ],
3083 None,
3084 false,
3085 );
3086 assert_eq!(
3087 line,
3088 "import { type Bar, type Foo, stdin as input, useState } from 'x';"
3089 );
3090 }
3091
3092 #[test]
3093 fn generate_default_import() {
3094 let line = generate_import_line(LangId::TypeScript, "react", &[], Some("React"), false);
3095 assert_eq!(line, "import React from 'react';");
3096 }
3097
3098 #[test]
3099 fn generate_type_import() {
3100 let line =
3101 generate_import_line(LangId::TypeScript, "react", &["FC".to_string()], None, true);
3102 assert_eq!(line, "import type { FC } from 'react';");
3103 }
3104
3105 #[test]
3106 fn generate_side_effect_import() {
3107 let line = generate_import_line(LangId::TypeScript, "./styles.css", &[], None, false);
3108 assert_eq!(line, "import './styles.css';");
3109 }
3110
3111 #[test]
3112 fn generate_default_and_named() {
3113 let line = generate_import_line(
3114 LangId::TypeScript,
3115 "react",
3116 &["useState".to_string()],
3117 Some("React"),
3118 false,
3119 );
3120 assert_eq!(line, "import React, { useState } from 'react';");
3121 }
3122
3123 #[test]
3124 fn parse_ts_type_import() {
3125 let source = "import type { FC } from 'react';\n";
3126 let (_, block) = parse_ts(source);
3127 assert_eq!(block.imports.len(), 1);
3128 let imp = &block.imports[0];
3129 assert_eq!(imp.kind, ImportKind::Type);
3130 assert!(imp.names.contains(&"FC".to_string()));
3131 assert_eq!(imp.group, ImportGroup::External);
3132 }
3133
3134 #[test]
3137 fn insertion_empty_file() {
3138 let source = "";
3139 let (_, block) = parse_ts(source);
3140 let (offset, _, _) =
3141 find_insertion_point(source, &block, ImportGroup::External, "react", false);
3142 assert_eq!(offset, 0);
3143 }
3144
3145 #[test]
3146 fn insertion_alphabetical_within_group() {
3147 let source = "\
3148import { a } from 'alpha';
3149import { c } from 'charlie';
3150";
3151 let (_, block) = parse_ts(source);
3152 let (offset, _, _) =
3153 find_insertion_point(source, &block, ImportGroup::External, "bravo", false);
3154 let before_charlie = source.find("import { c }").unwrap();
3156 assert_eq!(offset, before_charlie);
3157 }
3158
3159 fn parse_py(source: &str) -> (Tree, ImportBlock) {
3162 let grammar = grammar_for(LangId::Python);
3163 let mut parser = Parser::new();
3164 parser.set_language(&grammar).unwrap();
3165 let tree = parser.parse(source, None).unwrap();
3166 let block = parse_imports(source, &tree, LangId::Python);
3167 (tree, block)
3168 }
3169
3170 #[test]
3171 fn parse_py_import_statement() {
3172 let source = "import os\nimport sys\n";
3173 let (_, block) = parse_py(source);
3174 assert_eq!(block.imports.len(), 2);
3175 assert_eq!(block.imports[0].module_path, "os");
3176 assert_eq!(block.imports[1].module_path, "sys");
3177 assert_eq!(block.imports[0].group, ImportGroup::Stdlib);
3178 }
3179
3180 #[test]
3181 fn parse_py_import_statement_preserves_aliases_and_siblings() {
3182 let source = "import alpha, beta as local_beta\n";
3183 let (_, block) = parse_py(source);
3184 let imp = &block.imports[0];
3185 assert_eq!(imp.module_path, "alpha");
3186 assert!(imp.names.is_empty());
3187 match &imp.form {
3188 ImportForm::Python { from_import, named } => {
3189 assert!(!from_import);
3190 assert_eq!(named, &["alpha", "beta as local_beta"]);
3191 assert!(specifier_matches(&named[1], "beta"));
3192 assert!(specifier_matches(&named[1], "local_beta"));
3193 }
3194 other => panic!("expected Python import, got {other:?}"),
3195 }
3196 }
3197
3198 #[test]
3199 fn parse_py_from_import() {
3200 let source = "from collections import OrderedDict\nfrom typing import List, Optional\n";
3201 let (_, block) = parse_py(source);
3202 assert_eq!(block.imports.len(), 2);
3203 assert_eq!(block.imports[0].module_path, "collections");
3204 assert!(block.imports[0].names.contains(&"OrderedDict".to_string()));
3205 assert_eq!(block.imports[0].group, ImportGroup::Stdlib);
3206 assert_eq!(block.imports[1].module_path, "typing");
3207 assert!(block.imports[1].names.contains(&"List".to_string()));
3208 assert!(block.imports[1].names.contains(&"Optional".to_string()));
3209 }
3210
3211 #[test]
3212 fn parse_py_from_import_preserves_aliases() {
3213 let source = "from module import alpha, beta as local_beta\n";
3214 let (_, block) = parse_py(source);
3215 let imp = &block.imports[0];
3216 assert_eq!(imp.names, ["alpha", "beta as local_beta"]);
3217 assert!(specifier_matches(&imp.names[1], "beta"));
3218 assert!(specifier_matches(&imp.names[1], "local_beta"));
3219 assert_eq!(
3220 imp.form,
3221 ImportForm::Python {
3222 from_import: true,
3223 named: vec!["alpha".to_string(), "beta as local_beta".to_string()],
3224 }
3225 );
3226 }
3227
3228 #[test]
3229 fn parse_py_relative_import() {
3230 let source = "from . import utils\nfrom ..config import Settings\n";
3231 let (_, block) = parse_py(source);
3232 assert_eq!(block.imports.len(), 2);
3233 assert_eq!(block.imports[0].module_path, ".");
3234 assert!(block.imports[0].names.contains(&"utils".to_string()));
3235 assert_eq!(block.imports[0].group, ImportGroup::Internal);
3236 assert_eq!(block.imports[1].module_path, "..config");
3237 assert_eq!(block.imports[1].group, ImportGroup::Internal);
3238 }
3239
3240 #[test]
3241 fn classify_py_groups() {
3242 assert_eq!(classify_group_py("os"), ImportGroup::Stdlib);
3243 assert_eq!(classify_group_py("sys"), ImportGroup::Stdlib);
3244 assert_eq!(classify_group_py("json"), ImportGroup::Stdlib);
3245 assert_eq!(classify_group_py("collections"), ImportGroup::Stdlib);
3246 assert_eq!(classify_group_py("os.path"), ImportGroup::Stdlib);
3247 assert_eq!(classify_group_py("requests"), ImportGroup::External);
3248 assert_eq!(classify_group_py("flask"), ImportGroup::External);
3249 assert_eq!(classify_group_py("."), ImportGroup::Internal);
3250 assert_eq!(classify_group_py("..config"), ImportGroup::Internal);
3251 assert_eq!(classify_group_py(".utils"), ImportGroup::Internal);
3252 }
3253
3254 #[test]
3255 fn parse_py_three_groups() {
3256 let source = "import os\nimport sys\n\nimport requests\n\nfrom . import utils\n";
3257 let (_, block) = parse_py(source);
3258 let stdlib: Vec<_> = block
3259 .imports
3260 .iter()
3261 .filter(|i| i.group == ImportGroup::Stdlib)
3262 .collect();
3263 let external: Vec<_> = block
3264 .imports
3265 .iter()
3266 .filter(|i| i.group == ImportGroup::External)
3267 .collect();
3268 let internal: Vec<_> = block
3269 .imports
3270 .iter()
3271 .filter(|i| i.group == ImportGroup::Internal)
3272 .collect();
3273 assert_eq!(stdlib.len(), 2);
3274 assert_eq!(external.len(), 1);
3275 assert_eq!(internal.len(), 1);
3276 }
3277
3278 #[test]
3279 fn generate_py_import() {
3280 let line = generate_import_line(LangId::Python, "os", &[], None, false);
3281 assert_eq!(line, "import os");
3282 }
3283
3284 #[test]
3285 fn generate_py_from_import() {
3286 let line = generate_import_line(
3287 LangId::Python,
3288 "collections",
3289 &["OrderedDict".to_string()],
3290 None,
3291 false,
3292 );
3293 assert_eq!(line, "from collections import OrderedDict");
3294 }
3295
3296 #[test]
3297 fn generate_py_from_import_multiple() {
3298 let line = generate_import_line(
3299 LangId::Python,
3300 "typing",
3301 &["Optional".to_string(), "List".to_string()],
3302 None,
3303 false,
3304 );
3305 assert_eq!(line, "from typing import List, Optional");
3306 }
3307
3308 fn parse_rust(source: &str) -> (Tree, ImportBlock) {
3311 let grammar = grammar_for(LangId::Rust);
3312 let mut parser = Parser::new();
3313 parser.set_language(&grammar).unwrap();
3314 let tree = parser.parse(source, None).unwrap();
3315 let block = parse_imports(source, &tree, LangId::Rust);
3316 (tree, block)
3317 }
3318
3319 #[test]
3320 fn parse_rs_use_std() {
3321 let source = "use std::collections::HashMap;\nuse std::io::Read;\n";
3322 let (_, block) = parse_rust(source);
3323 assert_eq!(block.imports.len(), 2);
3324 assert_eq!(block.imports[0].module_path, "std::collections::HashMap");
3325 assert_eq!(block.imports[0].group, ImportGroup::Stdlib);
3326 assert_eq!(block.imports[1].group, ImportGroup::Stdlib);
3327 }
3328
3329 #[test]
3330 fn parse_rs_use_external() {
3331 let source = "use serde::{Deserialize, Serialize};\n";
3332 let (_, block) = parse_rust(source);
3333 assert_eq!(block.imports.len(), 1);
3334 assert_eq!(block.imports[0].group, ImportGroup::External);
3335 assert!(block.imports[0].names.contains(&"Deserialize".to_string()));
3336 assert!(block.imports[0].names.contains(&"Serialize".to_string()));
3337 }
3338
3339 #[test]
3340 fn parse_rs_use_crate() {
3341 let source = "use crate::config::Settings;\nuse super::parent::Thing;\n";
3342 let (_, block) = parse_rust(source);
3343 assert_eq!(block.imports.len(), 2);
3344 assert_eq!(block.imports[0].group, ImportGroup::Internal);
3345 assert_eq!(block.imports[1].group, ImportGroup::Internal);
3346 }
3347
3348 #[test]
3349 fn parse_rs_pub_use() {
3350 let source = "pub use super::parent::Thing;\n";
3351 let (_, block) = parse_rust(source);
3352 assert_eq!(block.imports.len(), 1);
3353 assert_eq!(block.imports[0].default_import.as_deref(), Some("pub"));
3355 }
3356
3357 #[test]
3358 fn classify_rs_groups() {
3359 assert_eq!(
3360 classify_group_rs("std::collections::HashMap"),
3361 ImportGroup::Stdlib
3362 );
3363 assert_eq!(classify_group_rs("core::mem"), ImportGroup::Stdlib);
3364 assert_eq!(classify_group_rs("alloc::vec"), ImportGroup::Stdlib);
3365 assert_eq!(
3366 classify_group_rs("serde::Deserialize"),
3367 ImportGroup::External
3368 );
3369 assert_eq!(classify_group_rs("tokio::runtime"), ImportGroup::External);
3370 assert_eq!(classify_group_rs("crate::config"), ImportGroup::Internal);
3371 assert_eq!(classify_group_rs("self::utils"), ImportGroup::Internal);
3372 assert_eq!(classify_group_rs("super::parent"), ImportGroup::Internal);
3373 }
3374
3375 #[test]
3376 fn generate_rs_use() {
3377 let line = generate_import_line(LangId::Rust, "std::fmt::Display", &[], None, false);
3378 assert_eq!(line, "use std::fmt::Display;");
3379 }
3380
3381 fn parse_go(source: &str) -> (Tree, ImportBlock) {
3384 let grammar = grammar_for(LangId::Go);
3385 let mut parser = Parser::new();
3386 parser.set_language(&grammar).unwrap();
3387 let tree = parser.parse(source, None).unwrap();
3388 let block = parse_imports(source, &tree, LangId::Go);
3389 (tree, block)
3390 }
3391
3392 #[test]
3393 fn parse_go_single_import() {
3394 let source = "package main\n\nimport \"fmt\"\n";
3395 let (_, block) = parse_go(source);
3396 assert_eq!(block.imports.len(), 1);
3397 assert_eq!(block.imports[0].module_path, "fmt");
3398 assert_eq!(block.imports[0].group, ImportGroup::Stdlib);
3399 }
3400
3401 #[test]
3402 fn parse_go_grouped_import() {
3403 let source =
3404 "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/pkg/errors\"\n)\n";
3405 let (_, block) = parse_go(source);
3406 assert_eq!(block.imports.len(), 3);
3407 assert_eq!(block.imports[0].module_path, "fmt");
3408 assert_eq!(block.imports[0].group, ImportGroup::Stdlib);
3409 assert_eq!(block.imports[1].module_path, "os");
3410 assert_eq!(block.imports[1].group, ImportGroup::Stdlib);
3411 assert_eq!(block.imports[2].module_path, "github.com/pkg/errors");
3412 assert_eq!(block.imports[2].group, ImportGroup::External);
3413 }
3414
3415 #[test]
3416 fn parse_go_mixed_imports() {
3417 let source = "package main\n\nimport \"fmt\"\n\nimport (\n\t\"os\"\n\t\"github.com/pkg/errors\"\n)\n";
3419 let (_, block) = parse_go(source);
3420 assert_eq!(block.imports.len(), 3);
3421 }
3422
3423 #[test]
3424 fn classify_go_groups() {
3425 assert_eq!(classify_group_go("fmt"), ImportGroup::Stdlib);
3426 assert_eq!(classify_group_go("os"), ImportGroup::Stdlib);
3427 assert_eq!(classify_group_go("net/http"), ImportGroup::Stdlib);
3428 assert_eq!(classify_group_go("encoding/json"), ImportGroup::Stdlib);
3429 assert_eq!(
3430 classify_group_go("github.com/pkg/errors"),
3431 ImportGroup::External
3432 );
3433 assert_eq!(
3434 classify_group_go("golang.org/x/tools"),
3435 ImportGroup::External
3436 );
3437 }
3438
3439 #[test]
3440 fn generate_go_standalone() {
3441 let line = generate_go_import_line("fmt", None, false);
3442 assert_eq!(line, "import \"fmt\"");
3443 }
3444
3445 #[test]
3446 fn generate_go_grouped_spec() {
3447 let line = generate_go_import_line("fmt", None, true);
3448 assert_eq!(line, "\t\"fmt\"");
3449 }
3450
3451 #[test]
3452 fn generate_go_with_alias() {
3453 let line = generate_go_import_line("github.com/pkg/errors", Some("errs"), false);
3454 assert_eq!(line, "import errs \"github.com/pkg/errors\"");
3455 }
3456
3457 fn parse_solidity(source: &str) -> (Tree, ImportBlock) {
3460 let grammar = grammar_for(LangId::Solidity);
3461 let mut parser = Parser::new();
3462 parser.set_language(&grammar).unwrap();
3463 let tree = parser.parse(source, None).unwrap();
3464 let block = parse_imports(source, &tree, LangId::Solidity);
3465 (tree, block)
3466 }
3467
3468 #[test]
3472 fn solidity_grammar_node_kinds_are_stable() {
3473 let grammar = grammar_for(LangId::Solidity);
3474 let mut parser = Parser::new();
3475 parser.set_language(&grammar).unwrap();
3476 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";
3477 let tree = parser.parse(src, None).unwrap();
3478 let mut kinds: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
3479 fn walk(node: tree_sitter::Node, kinds: &mut std::collections::BTreeSet<String>) {
3480 kinds.insert(node.kind().to_string());
3481 let mut c = node.walk();
3482 if c.goto_first_child() {
3483 loop {
3484 walk(c.node(), kinds);
3485 if !c.goto_next_sibling() {
3486 break;
3487 }
3488 }
3489 }
3490 }
3491 walk(tree.root_node(), &mut kinds);
3492 for required in [
3493 "import_directive",
3494 "string",
3495 "identifier",
3496 "as",
3497 "from",
3498 "*",
3499 "{",
3500 "}",
3501 ] {
3502 assert!(
3503 kinds.contains(required),
3504 "solidity grammar missing node kind {required:?}; present: {kinds:?}"
3505 );
3506 }
3507 }
3508
3509 #[test]
3510 fn parse_solidity_all_four_forms() {
3511 let (_, block) = parse_solidity(
3512 "import \"./A.sol\";\nimport \"./B.sol\" as B;\nimport * as C from \"./C.sol\";\nimport { Foo, Bar as Baz } from \"./D.sol\";\n",
3513 );
3514 assert_eq!(block.imports.len(), 4);
3515
3516 assert_eq!(block.imports[0].module_path, "./A.sol");
3518 assert_eq!(block.imports[0].kind, ImportKind::SideEffect);
3519 assert_eq!(
3520 block.imports[0].form,
3521 ImportForm::Solidity {
3522 named: vec![],
3523 namespace: None,
3524 alias: None
3525 }
3526 );
3527
3528 assert_eq!(
3530 block.imports[1].form,
3531 ImportForm::Solidity {
3532 named: vec![],
3533 namespace: None,
3534 alias: Some("B".to_string())
3535 }
3536 );
3537
3538 match &block.imports[2].form {
3540 ImportForm::Solidity { namespace, .. } => assert_eq!(namespace.as_deref(), Some("C")),
3541 other => panic!("expected Solidity namespace, got {other:?}"),
3542 }
3543 assert_eq!(block.imports[2].namespace_import.as_deref(), Some("C"));
3544
3545 match &block.imports[3].form {
3547 ImportForm::Solidity { named, .. } => {
3548 assert_eq!(named, &vec!["Foo".to_string(), "Bar as Baz".to_string()]);
3549 }
3550 other => panic!("expected Solidity named, got {other:?}"),
3551 }
3552 assert_eq!(
3553 block.imports[3].names,
3554 vec!["Foo".to_string(), "Bar as Baz".to_string()]
3555 );
3556 }
3557
3558 #[test]
3559 fn generate_solidity_all_forms() {
3560 assert_eq!(
3562 generate_import(
3563 LangId::Solidity,
3564 &ImportRequest::legacy("./A.sol", &[], None, None, false)
3565 ),
3566 "import \"./A.sol\";"
3567 );
3568 let names = vec!["Foo".to_string(), "Bar as Baz".to_string()];
3570 assert_eq!(
3571 generate_import(
3572 LangId::Solidity,
3573 &ImportRequest::legacy("./D.sol", &names, None, None, false)
3574 ),
3575 "import { Foo, Bar as Baz } from \"./D.sol\";"
3576 );
3577 assert_eq!(
3579 generate_import(
3580 LangId::Solidity,
3581 &ImportRequest::legacy("./C.sol", &[], None, Some("C"), false)
3582 ),
3583 "import * as C from \"./C.sol\";"
3584 );
3585 assert_eq!(
3587 generate_import(
3588 LangId::Solidity,
3589 &ImportRequest {
3590 module_path: "./B.sol",
3591 names: &[],
3592 default_import: None,
3593 namespace: None,
3594 alias: Some("B"),
3595 type_only: false,
3596 modifiers: &[],
3597 import_kind: None,
3598 }
3599 ),
3600 "import \"./B.sol\" as B;"
3601 );
3602 }
3603
3604 #[test]
3605 fn solidity_round_trips_through_parse_generate() {
3606 for src in [
3608 "import \"./A.sol\";",
3609 "import \"./B.sol\" as B;",
3610 "import * as C from \"./C.sol\";",
3611 "import { Foo, Bar as Baz } from \"./D.sol\";",
3612 ] {
3613 let (_, block) = parse_solidity(src);
3614 assert_eq!(block.imports.len(), 1, "parse {src:?}");
3615 let imp = &block.imports[0];
3616 let (namespace, alias) = match &imp.form {
3617 ImportForm::Solidity {
3618 namespace, alias, ..
3619 } => (namespace.as_deref(), alias.as_deref()),
3620 other => panic!("expected Solidity, got {other:?}"),
3621 };
3622 let regenerated = generate_import(
3623 LangId::Solidity,
3624 &ImportRequest {
3625 module_path: &imp.module_path,
3626 names: &imp.names,
3627 default_import: None,
3628 namespace,
3629 alias,
3630 type_only: false,
3631 modifiers: &[],
3632 import_kind: None,
3633 },
3634 );
3635 assert_eq!(regenerated, src, "round-trip mismatch for {src:?}");
3636 }
3637 }
3638
3639 #[test]
3640 fn classify_group_solidity_relative_vs_external() {
3641 assert_eq!(classify_group_solidity("./A.sol"), ImportGroup::Internal);
3642 assert_eq!(
3643 classify_group_solidity("../lib/B.sol"),
3644 ImportGroup::Internal
3645 );
3646 assert_eq!(
3647 classify_group_solidity("@openzeppelin/contracts/token/ERC20/ERC20.sol"),
3648 ImportGroup::External
3649 );
3650 }
3651}