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 identity_modifiers: Vec<String> = req
1024 .modifiers
1025 .iter()
1026 .filter(|modifier| modifier.as_str() != "scala2")
1027 .cloned()
1028 .collect();
1029 let mut import_kind = req.import_kind.map(str::to_string);
1030
1031 if req.default_import == Some("given") || module_path.ends_with(".given") {
1032 import_kind.get_or_insert_with(|| "given".to_string());
1033 if let Some(stripped) = module_path.strip_suffix(".given") {
1034 module_path = stripped.to_string();
1035 }
1036 }
1037
1038 if matches!(req.default_import, Some("*") | Some("_"))
1039 || matches!(req.namespace, Some("*") | Some("_"))
1040 || module_path.ends_with(".*")
1041 || module_path.ends_with("._")
1042 {
1043 if !identity_modifiers
1044 .iter()
1045 .any(|modifier| modifier == "wildcard")
1046 {
1047 identity_modifiers.push("wildcard".to_string());
1048 }
1049 module_path = module_path
1050 .strip_suffix(".*")
1051 .or_else(|| module_path.strip_suffix("._"))
1052 .unwrap_or(&module_path)
1053 .to_string();
1054 }
1055
1056 if names.is_empty() {
1057 if let Some(alias) = req.alias.filter(|alias| !alias.is_empty()) {
1058 if let Some((prefix, leaf)) = module_path.rsplit_once('.') {
1059 names.push(format!("{leaf} as {alias}"));
1060 module_path = prefix.to_string();
1061 }
1062 }
1063 }
1064
1065 structured_dedup_key(
1066 &module_path,
1067 ImportKind::Value,
1068 &names,
1069 None,
1070 None,
1071 &identity_modifiers,
1072 import_kind.as_deref(),
1073 )
1074}
1075
1076fn normalize_scala_selector_for_dedup(name: &str) -> String {
1077 let trimmed = name.trim();
1078 if let Some((from, to)) = trimmed.split_once("=>") {
1079 format!("{} as {}", from.trim(), to.trim())
1080 } else {
1081 trimmed.to_string()
1082 }
1083}
1084
1085fn canonical_dedup_key(lang: LangId, mut key: ImportDedupKey) -> ImportDedupKey {
1086 match &mut key.form {
1087 ImportForm::Structured { named, .. } | ImportForm::Solidity { named, .. } => {
1088 sort_named_specifiers(named);
1089 }
1090 ImportForm::Es { named, .. } | ImportForm::Python { named, .. } => {
1091 sort_named_specifiers(named);
1092 }
1093 ImportForm::RustUse { named, .. } => {
1094 sort_named_specifiers(named);
1095 }
1096 ImportForm::Go { .. } | ImportForm::Php { .. } => {}
1097 }
1098
1099 if matches!(lang, LangId::Java | LangId::Kotlin) {
1100 if let Some(stripped) = key.module_path.strip_suffix(".*") {
1101 key.module_path = stripped.to_string();
1102 }
1103 if matches!(lang, LangId::Java) {
1104 if let ImportForm::Structured {
1105 named, modifiers, ..
1106 } = &mut key.form
1107 {
1108 normalize_java_static_member_key(&mut key.module_path, modifiers, named);
1109 }
1110 }
1111 } else if matches!(lang, LangId::Scala) {
1112 key.module_path = key
1113 .module_path
1114 .strip_suffix(".given")
1115 .or_else(|| key.module_path.strip_suffix(".*"))
1116 .or_else(|| key.module_path.strip_suffix("._"))
1117 .unwrap_or(&key.module_path)
1118 .to_string();
1119 }
1120
1121 key
1122}
1123
1124fn sort_named_specifiers(names: &mut [String]) {
1125 names.sort_by(|a, b| {
1126 specifier_imported_name(a)
1127 .cmp(specifier_imported_name(b))
1128 .then_with(|| a.cmp(b))
1129 });
1130}
1131
1132pub fn find_insertion_point(
1143 source: &str,
1144 block: &ImportBlock,
1145 group: ImportGroup,
1146 module_path: &str,
1147 type_only: bool,
1148) -> (usize, bool, bool) {
1149 if block.imports.is_empty() {
1150 return (0, false, source.is_empty().then_some(false).unwrap_or(true));
1152 }
1153
1154 let target_kind = if type_only {
1155 ImportKind::Type
1156 } else {
1157 ImportKind::Value
1158 };
1159
1160 let group_imports: Vec<&ImportStatement> =
1162 block.imports.iter().filter(|i| i.group == group).collect();
1163
1164 if group_imports.is_empty() {
1165 let preceding_last = block.imports.iter().filter(|i| i.group < group).last();
1168
1169 if let Some(last) = preceding_last {
1170 let end = last.byte_range.end;
1171 let insert_at = skip_newline(source, end);
1172 return (insert_at, true, true);
1173 }
1174
1175 let following_first = block.imports.iter().find(|i| i.group > group);
1177
1178 if let Some(first) = following_first {
1179 return (first.byte_range.start, false, true);
1180 }
1181
1182 let first_byte = import_byte_range(&block.imports)
1184 .map(|range| range.start)
1185 .unwrap_or(0);
1186 return (first_byte, false, true);
1187 }
1188
1189 for imp in &group_imports {
1191 let cmp = module_path.cmp(&imp.module_path);
1192 match cmp {
1193 std::cmp::Ordering::Less => {
1194 return (imp.byte_range.start, false, false);
1196 }
1197 std::cmp::Ordering::Equal => {
1198 if target_kind == ImportKind::Type && imp.kind == ImportKind::Value {
1200 let end = imp.byte_range.end;
1202 let insert_at = skip_newline(source, end);
1203 return (insert_at, false, false);
1204 }
1205 return (imp.byte_range.start, false, false);
1207 }
1208 std::cmp::Ordering::Greater => continue,
1209 }
1210 }
1211
1212 let Some(last) = group_imports.last() else {
1214 return (
1215 import_byte_range(&block.imports)
1216 .map(|range| range.end)
1217 .unwrap_or(0),
1218 false,
1219 false,
1220 );
1221 };
1222 let end = last.byte_range.end;
1223 let insert_at = skip_newline(source, end);
1224 (insert_at, false, false)
1225}
1226
1227pub fn generate_import(lang: LangId, req: &ImportRequest) -> String {
1231 match syntax_for(lang) {
1232 Some(engine) => engine.generate_line(req),
1233 None => String::new(),
1234 }
1235}
1236
1237pub fn generate_import_line(
1240 lang: LangId,
1241 module_path: &str,
1242 names: &[String],
1243 default_import: Option<&str>,
1244 type_only: bool,
1245) -> String {
1246 generate_import(
1247 lang,
1248 &ImportRequest::legacy(module_path, names, default_import, None, type_only),
1249 )
1250}
1251
1252pub fn generate_import_line_with_namespace(
1255 lang: LangId,
1256 module_path: &str,
1257 names: &[String],
1258 default_import: Option<&str>,
1259 namespace_import: Option<&str>,
1260 type_only: bool,
1261) -> String {
1262 generate_import(
1263 lang,
1264 &ImportRequest::legacy(
1265 module_path,
1266 names,
1267 default_import,
1268 namespace_import,
1269 type_only,
1270 ),
1271 )
1272}
1273
1274pub fn is_supported(lang: LangId) -> bool {
1276 syntax_for(lang).is_some()
1277}
1278
1279pub fn classify_group_ts(module_path: &str) -> ImportGroup {
1281 if module_path.starts_with('.') {
1282 ImportGroup::Internal
1283 } else {
1284 ImportGroup::External
1285 }
1286}
1287
1288pub fn classify_group(lang: LangId, module_path: &str) -> ImportGroup {
1290 match syntax_for(lang) {
1291 Some(engine) => engine.classify_group(module_path),
1292 None => ImportGroup::External,
1295 }
1296}
1297
1298pub fn parse_file_imports(
1301 path: &std::path::Path,
1302 lang: LangId,
1303) -> Result<(String, Tree, ImportBlock), crate::error::AftError> {
1304 let source =
1305 std::fs::read_to_string(path).map_err(|e| crate::error::AftError::FileNotFound {
1306 path: format!("{}: {}", path.display(), e),
1307 })?;
1308
1309 let grammar = grammar_for(lang);
1310 let mut parser = Parser::new();
1311 parser
1312 .set_language(&grammar)
1313 .map_err(|e| crate::error::AftError::ParseError {
1314 message: format!("grammar init failed for {:?}: {}", lang, e),
1315 })?;
1316
1317 let tree = parser
1318 .parse(&source, None)
1319 .ok_or_else(|| crate::error::AftError::ParseError {
1320 message: format!("tree-sitter parse returned None for {}", path.display()),
1321 })?;
1322
1323 let block = parse_imports(&source, &tree, lang);
1324 Ok((source, tree, block))
1325}
1326
1327fn parse_ts_imports(source: &str, tree: &Tree) -> ImportBlock {
1335 let root = tree.root_node();
1336 let mut imports = Vec::new();
1337
1338 let mut cursor = root.walk();
1339 if !cursor.goto_first_child() {
1340 return ImportBlock::empty();
1341 }
1342
1343 loop {
1344 let node = cursor.node();
1345 if node.kind() == "import_statement" {
1346 if let Some(imp) = parse_single_ts_import(source, &node) {
1347 imports.push(imp);
1348 }
1349 }
1350 if !cursor.goto_next_sibling() {
1351 break;
1352 }
1353 }
1354
1355 let byte_range = import_byte_range(&imports);
1356
1357 ImportBlock {
1358 imports,
1359 byte_range,
1360 }
1361}
1362
1363fn parse_single_ts_import(source: &str, node: &Node) -> Option<ImportStatement> {
1365 let raw_text = source[node.byte_range()].to_string();
1366 let byte_range = node.byte_range();
1367
1368 let module_path = extract_module_path(source, node)?;
1370
1371 let is_type_only = has_type_keyword(node);
1373
1374 let mut names = Vec::new();
1376 let mut default_import = None;
1377 let mut namespace_import = None;
1378
1379 let mut child_cursor = node.walk();
1380 if child_cursor.goto_first_child() {
1381 loop {
1382 let child = child_cursor.node();
1383 match child.kind() {
1384 "import_clause" => {
1385 extract_import_clause(
1386 source,
1387 &child,
1388 &mut names,
1389 &mut default_import,
1390 &mut namespace_import,
1391 );
1392 }
1393 "identifier" => {
1395 let text = &source[child.byte_range()];
1396 if text != "import" && text != "from" && text != "type" {
1397 default_import = Some(text.to_string());
1398 }
1399 }
1400 _ => {}
1401 }
1402 if !child_cursor.goto_next_sibling() {
1403 break;
1404 }
1405 }
1406 }
1407
1408 let kind = if names.is_empty() && default_import.is_none() && namespace_import.is_none() {
1410 ImportKind::SideEffect
1411 } else if is_type_only {
1412 ImportKind::Type
1413 } else {
1414 ImportKind::Value
1415 };
1416
1417 let group = classify_group_ts(&module_path);
1418
1419 let form = ImportForm::Es {
1420 default_import: default_import.clone(),
1421 namespace_import: namespace_import.clone(),
1422 named: names.clone(),
1423 type_only: is_type_only,
1424 side_effect: matches!(kind, ImportKind::SideEffect),
1425 };
1426
1427 Some(ImportStatement {
1428 module_path,
1429 names,
1430 default_import,
1431 namespace_import,
1432 kind,
1433 group,
1434 byte_range,
1435 raw_text,
1436 form,
1437 })
1438}
1439
1440fn extract_module_path(source: &str, node: &Node) -> Option<String> {
1444 let mut cursor = node.walk();
1445 if !cursor.goto_first_child() {
1446 return None;
1447 }
1448
1449 loop {
1450 let child = cursor.node();
1451 if child.kind() == "string" {
1452 let text = &source[child.byte_range()];
1454 let stripped = text
1455 .trim_start_matches(|c| c == '\'' || c == '"')
1456 .trim_end_matches(|c| c == '\'' || c == '"');
1457 return Some(stripped.to_string());
1458 }
1459 if !cursor.goto_next_sibling() {
1460 break;
1461 }
1462 }
1463 None
1464}
1465
1466fn has_type_keyword(node: &Node) -> bool {
1471 let mut cursor = node.walk();
1472 if !cursor.goto_first_child() {
1473 return false;
1474 }
1475
1476 loop {
1477 let child = cursor.node();
1478 if child.kind() == "type" {
1479 return true;
1480 }
1481 if !cursor.goto_next_sibling() {
1482 break;
1483 }
1484 }
1485
1486 false
1487}
1488
1489fn extract_import_clause(
1491 source: &str,
1492 node: &Node,
1493 names: &mut Vec<String>,
1494 default_import: &mut Option<String>,
1495 namespace_import: &mut Option<String>,
1496) {
1497 let mut cursor = node.walk();
1498 if !cursor.goto_first_child() {
1499 return;
1500 }
1501
1502 loop {
1503 let child = cursor.node();
1504 match child.kind() {
1505 "identifier" => {
1506 let text = &source[child.byte_range()];
1508 if text != "type" {
1509 *default_import = Some(text.to_string());
1510 }
1511 }
1512 "named_imports" => {
1513 extract_named_imports(source, &child, names);
1515 }
1516 "namespace_import" => {
1517 extract_namespace_import(source, &child, namespace_import);
1519 }
1520 _ => {}
1521 }
1522 if !cursor.goto_next_sibling() {
1523 break;
1524 }
1525 }
1526}
1527
1528fn extract_named_imports(source: &str, node: &Node, names: &mut Vec<String>) {
1547 let mut cursor = node.walk();
1548 if !cursor.goto_first_child() {
1549 return;
1550 }
1551
1552 loop {
1553 let child = cursor.node();
1554 if child.kind() == "import_specifier" {
1555 let raw = source[child.byte_range()].trim().to_string();
1560 if !raw.is_empty() {
1561 names.push(raw);
1562 } else if let Some(name_node) = child.child_by_field_name("name") {
1563 names.push(source[name_node.byte_range()].to_string());
1564 }
1565 }
1566 if !cursor.goto_next_sibling() {
1567 break;
1568 }
1569 }
1570}
1571
1572fn extract_namespace_import(source: &str, node: &Node, namespace_import: &mut Option<String>) {
1574 let mut cursor = node.walk();
1575 if !cursor.goto_first_child() {
1576 return;
1577 }
1578
1579 loop {
1580 let child = cursor.node();
1581 if child.kind() == "identifier" {
1582 *namespace_import = Some(source[child.byte_range()].to_string());
1583 return;
1584 }
1585 if !cursor.goto_next_sibling() {
1586 break;
1587 }
1588 }
1589}
1590
1591fn generate_ts_import_line(
1593 module_path: &str,
1594 names: &[String],
1595 default_import: Option<&str>,
1596 namespace_import: Option<&str>,
1597 type_only: bool,
1598) -> String {
1599 let type_prefix = if type_only { "type " } else { "" };
1600
1601 if names.is_empty() && default_import.is_none() && namespace_import.is_none() {
1603 return format!("import '{module_path}';");
1604 }
1605
1606 if names.is_empty() && default_import.is_none() {
1608 if let Some(namespace) = namespace_import {
1609 return format!("import {type_prefix}* as {namespace} from '{module_path}';");
1610 }
1611 }
1612
1613 if names.is_empty() {
1615 if let (Some(def), Some(namespace)) = (default_import, namespace_import) {
1616 return format!("import {type_prefix}{def}, * as {namespace} from '{module_path}';");
1617 }
1618 }
1619
1620 if names.is_empty() && namespace_import.is_none() {
1622 if let Some(def) = default_import {
1623 return format!("import {type_prefix}{def} from '{module_path}';");
1624 }
1625 }
1626
1627 if default_import.is_none() && namespace_import.is_none() {
1629 let mut sorted_names = names.to_vec();
1630 sort_named_specifiers(&mut sorted_names);
1631 let names_str = sorted_names.join(", ");
1632 return format!("import {type_prefix}{{ {names_str} }} from '{module_path}';");
1633 }
1634
1635 if default_import.is_none() {
1637 if let Some(namespace) = namespace_import {
1638 let mut sorted_names = names.to_vec();
1639 sort_named_specifiers(&mut sorted_names);
1640 let names_str = sorted_names.join(", ");
1641 return format!(
1642 "import {type_prefix}{{ {names_str} }}, * as {namespace} from '{module_path}';"
1643 );
1644 }
1645 }
1646
1647 if let (Some(def), Some(namespace)) = (default_import, namespace_import) {
1649 let mut sorted_names = names.to_vec();
1650 sort_named_specifiers(&mut sorted_names);
1651 let names_str = sorted_names.join(", ");
1652 return format!(
1653 "import {type_prefix}{def}, {{ {names_str} }}, * as {namespace} from '{module_path}';"
1654 );
1655 }
1656
1657 if let Some(def) = default_import {
1659 let mut sorted_names = names.to_vec();
1660 sort_named_specifiers(&mut sorted_names);
1661 let names_str = sorted_names.join(", ");
1662 return format!("import {type_prefix}{def}, {{ {names_str} }} from '{module_path}';");
1663 }
1664
1665 format!("import '{module_path}';")
1667}
1668
1669const PYTHON_STDLIB: &[&str] = &[
1677 "__future__",
1678 "_thread",
1679 "abc",
1680 "aifc",
1681 "argparse",
1682 "array",
1683 "ast",
1684 "asynchat",
1685 "asyncio",
1686 "asyncore",
1687 "atexit",
1688 "audioop",
1689 "base64",
1690 "bdb",
1691 "binascii",
1692 "bisect",
1693 "builtins",
1694 "bz2",
1695 "calendar",
1696 "cgi",
1697 "cgitb",
1698 "chunk",
1699 "cmath",
1700 "cmd",
1701 "code",
1702 "codecs",
1703 "codeop",
1704 "collections",
1705 "colorsys",
1706 "compileall",
1707 "concurrent",
1708 "configparser",
1709 "contextlib",
1710 "contextvars",
1711 "copy",
1712 "copyreg",
1713 "cProfile",
1714 "crypt",
1715 "csv",
1716 "ctypes",
1717 "curses",
1718 "dataclasses",
1719 "datetime",
1720 "dbm",
1721 "decimal",
1722 "difflib",
1723 "dis",
1724 "distutils",
1725 "doctest",
1726 "email",
1727 "encodings",
1728 "enum",
1729 "errno",
1730 "faulthandler",
1731 "fcntl",
1732 "filecmp",
1733 "fileinput",
1734 "fnmatch",
1735 "fractions",
1736 "ftplib",
1737 "functools",
1738 "gc",
1739 "getopt",
1740 "getpass",
1741 "gettext",
1742 "glob",
1743 "grp",
1744 "gzip",
1745 "hashlib",
1746 "heapq",
1747 "hmac",
1748 "html",
1749 "http",
1750 "idlelib",
1751 "imaplib",
1752 "imghdr",
1753 "importlib",
1754 "inspect",
1755 "io",
1756 "ipaddress",
1757 "itertools",
1758 "json",
1759 "keyword",
1760 "lib2to3",
1761 "linecache",
1762 "locale",
1763 "logging",
1764 "lzma",
1765 "mailbox",
1766 "mailcap",
1767 "marshal",
1768 "math",
1769 "mimetypes",
1770 "mmap",
1771 "modulefinder",
1772 "multiprocessing",
1773 "netrc",
1774 "numbers",
1775 "operator",
1776 "optparse",
1777 "os",
1778 "pathlib",
1779 "pdb",
1780 "pickle",
1781 "pickletools",
1782 "pipes",
1783 "pkgutil",
1784 "platform",
1785 "plistlib",
1786 "poplib",
1787 "posixpath",
1788 "pprint",
1789 "profile",
1790 "pstats",
1791 "pty",
1792 "pwd",
1793 "py_compile",
1794 "pyclbr",
1795 "pydoc",
1796 "queue",
1797 "quopri",
1798 "random",
1799 "re",
1800 "readline",
1801 "reprlib",
1802 "resource",
1803 "rlcompleter",
1804 "runpy",
1805 "sched",
1806 "secrets",
1807 "select",
1808 "selectors",
1809 "shelve",
1810 "shlex",
1811 "shutil",
1812 "signal",
1813 "site",
1814 "smtplib",
1815 "sndhdr",
1816 "socket",
1817 "socketserver",
1818 "sqlite3",
1819 "ssl",
1820 "stat",
1821 "statistics",
1822 "string",
1823 "stringprep",
1824 "struct",
1825 "subprocess",
1826 "symtable",
1827 "sys",
1828 "sysconfig",
1829 "syslog",
1830 "tabnanny",
1831 "tarfile",
1832 "tempfile",
1833 "termios",
1834 "textwrap",
1835 "threading",
1836 "time",
1837 "timeit",
1838 "tkinter",
1839 "token",
1840 "tokenize",
1841 "tomllib",
1842 "trace",
1843 "traceback",
1844 "tracemalloc",
1845 "tty",
1846 "turtle",
1847 "types",
1848 "typing",
1849 "unicodedata",
1850 "unittest",
1851 "urllib",
1852 "uuid",
1853 "venv",
1854 "warnings",
1855 "wave",
1856 "weakref",
1857 "webbrowser",
1858 "wsgiref",
1859 "xml",
1860 "xmlrpc",
1861 "zipapp",
1862 "zipfile",
1863 "zipimport",
1864 "zlib",
1865];
1866
1867pub fn classify_group_py(module_path: &str) -> ImportGroup {
1869 if module_path.starts_with('.') {
1871 return ImportGroup::Internal;
1872 }
1873 let top_module = module_path.split('.').next().unwrap_or(module_path);
1875 if PYTHON_STDLIB.contains(&top_module) {
1876 ImportGroup::Stdlib
1877 } else {
1878 ImportGroup::External
1879 }
1880}
1881
1882fn parse_py_imports(source: &str, tree: &Tree) -> ImportBlock {
1884 let root = tree.root_node();
1885 let mut imports = Vec::new();
1886
1887 let mut cursor = root.walk();
1888 if !cursor.goto_first_child() {
1889 return ImportBlock::empty();
1890 }
1891
1892 loop {
1893 let node = cursor.node();
1894 match node.kind() {
1895 "import_statement" => {
1896 if let Some(imp) = parse_py_import_statement(source, &node) {
1897 imports.push(imp);
1898 }
1899 }
1900 "import_from_statement" => {
1901 if let Some(imp) = parse_py_import_from_statement(source, &node) {
1902 imports.push(imp);
1903 }
1904 }
1905 _ => {}
1906 }
1907 if !cursor.goto_next_sibling() {
1908 break;
1909 }
1910 }
1911
1912 let byte_range = import_byte_range(&imports);
1913
1914 ImportBlock {
1915 imports,
1916 byte_range,
1917 }
1918}
1919
1920fn parse_py_import_statement(source: &str, node: &Node) -> Option<ImportStatement> {
1922 let raw_text = source[node.byte_range()].to_string();
1923 let byte_range = node.byte_range();
1924
1925 let mut specifiers = Vec::new();
1929 let mut c = node.walk();
1930 if c.goto_first_child() {
1931 loop {
1932 let child = c.node();
1933 if matches!(child.kind(), "dotted_name" | "aliased_import") {
1934 specifiers.push(source[child.byte_range()].trim().to_string());
1935 }
1936 if !c.goto_next_sibling() {
1937 break;
1938 }
1939 }
1940 }
1941 let module_path = specifiers
1942 .first()
1943 .map(|specifier| specifier_imported_name(specifier).to_string())?;
1944
1945 let group = classify_group_py(&module_path);
1946
1947 Some(ImportStatement {
1948 module_path,
1949 names: Vec::new(),
1950 default_import: None,
1951 namespace_import: None,
1952 kind: ImportKind::Value,
1953 group,
1954 byte_range,
1955 raw_text,
1956 form: ImportForm::Python {
1957 from_import: false,
1958 named: specifiers,
1959 },
1960 })
1961}
1962
1963fn parse_py_import_from_statement(source: &str, node: &Node) -> Option<ImportStatement> {
1965 let raw_text = source[node.byte_range()].to_string();
1966 let byte_range = node.byte_range();
1967
1968 let mut module_path = String::new();
1969 let mut names = Vec::new();
1970
1971 let mut c = node.walk();
1972 if c.goto_first_child() {
1973 loop {
1974 let child = c.node();
1975 match child.kind() {
1976 "dotted_name" => {
1977 if module_path.is_empty()
1982 && !has_seen_import_keyword(source, node, child.start_byte())
1983 {
1984 module_path = source[child.byte_range()].to_string();
1985 } else {
1986 names.push(source[child.byte_range()].to_string());
1988 }
1989 }
1990 "relative_import" => {
1991 module_path = source[child.byte_range()].to_string();
1993 }
1994 "aliased_import" => {
1995 names.push(source[child.byte_range()].trim().to_string());
1998 }
1999 _ => {}
2000 }
2001 if !c.goto_next_sibling() {
2002 break;
2003 }
2004 }
2005 }
2006
2007 if module_path.is_empty() {
2009 return None;
2010 }
2011
2012 let group = classify_group_py(&module_path);
2013
2014 Some(ImportStatement {
2015 module_path,
2016 names: names.clone(),
2017 default_import: None,
2018 namespace_import: None,
2019 kind: ImportKind::Value,
2020 group,
2021 byte_range,
2022 raw_text,
2023 form: ImportForm::Python {
2024 from_import: true,
2025 named: names,
2026 },
2027 })
2028}
2029
2030fn has_seen_import_keyword(_source: &str, parent: &Node, before_byte: usize) -> bool {
2032 let mut c = parent.walk();
2033 if c.goto_first_child() {
2034 loop {
2035 let child = c.node();
2036 if child.kind() == "import" && child.start_byte() < before_byte {
2037 return true;
2038 }
2039 if child.start_byte() >= before_byte {
2040 return false;
2041 }
2042 if !c.goto_next_sibling() {
2043 break;
2044 }
2045 }
2046 }
2047 false
2048}
2049
2050fn generate_py_import_line(
2052 module_path: &str,
2053 names: &[String],
2054 _default_import: Option<&str>,
2055) -> String {
2056 if names.is_empty() {
2057 format!("import {module_path}")
2059 } else {
2060 let mut sorted = names.to_vec();
2062 sorted.sort();
2063 let names_str = sorted.join(", ");
2064 format!("from {module_path} import {names_str}")
2065 }
2066}
2067
2068pub fn classify_group_rs(module_path: &str) -> ImportGroup {
2074 let first_seg = module_path.split("::").next().unwrap_or(module_path);
2076 match first_seg {
2077 "std" | "core" | "alloc" => ImportGroup::Stdlib,
2078 "crate" | "self" | "super" => ImportGroup::Internal,
2079 _ => ImportGroup::External,
2080 }
2081}
2082
2083fn parse_rs_imports(source: &str, tree: &Tree) -> ImportBlock {
2085 let root = tree.root_node();
2086 let mut imports = Vec::new();
2087
2088 let mut cursor = root.walk();
2089 if !cursor.goto_first_child() {
2090 return ImportBlock::empty();
2091 }
2092
2093 loop {
2094 let node = cursor.node();
2095 if node.kind() == "use_declaration" {
2096 if let Some(imp) = parse_rs_use_declaration(source, &node) {
2097 imports.push(imp);
2098 }
2099 }
2100 if !cursor.goto_next_sibling() {
2101 break;
2102 }
2103 }
2104
2105 let byte_range = import_byte_range(&imports);
2106
2107 ImportBlock {
2108 imports,
2109 byte_range,
2110 }
2111}
2112
2113fn parse_rs_use_declaration(source: &str, node: &Node) -> Option<ImportStatement> {
2115 let raw_text = source[node.byte_range()].to_string();
2116 let byte_range = node.byte_range();
2117
2118 let mut visibility: Option<String> = None;
2122 let mut use_path = String::new();
2123 let mut names = Vec::new();
2124
2125 let mut c = node.walk();
2126 if c.goto_first_child() {
2127 loop {
2128 let child = c.node();
2129 match child.kind() {
2130 "visibility_modifier" => {
2131 visibility = Some(source[child.byte_range()].to_string());
2132 }
2133 "scoped_identifier" | "identifier" | "use_as_clause" => {
2134 use_path = source[child.byte_range()].to_string();
2136 }
2137 "scoped_use_list" => {
2138 use_path = source[child.byte_range()].to_string();
2140 extract_rs_use_list_names(source, &child, &mut names);
2142 }
2143 _ => {}
2144 }
2145 if !c.goto_next_sibling() {
2146 break;
2147 }
2148 }
2149 }
2150
2151 if use_path.is_empty() {
2152 return None;
2153 }
2154
2155 let group = classify_group_rs(&use_path);
2156
2157 Some(ImportStatement {
2158 module_path: use_path,
2159 names: names.clone(),
2160 default_import: visibility.clone(),
2163 namespace_import: None,
2164 kind: ImportKind::Value,
2165 group,
2166 byte_range,
2167 raw_text,
2168 form: ImportForm::RustUse {
2169 visibility,
2170 named: names,
2171 },
2172 })
2173}
2174
2175fn extract_rs_use_list_names(source: &str, node: &Node, names: &mut Vec<String>) {
2177 let mut c = node.walk();
2178 if c.goto_first_child() {
2179 loop {
2180 let child = c.node();
2181 if child.kind() == "use_list" {
2182 let mut lc = child.walk();
2184 if lc.goto_first_child() {
2185 loop {
2186 let lchild = lc.node();
2187 if lchild.kind() == "identifier" || lchild.kind() == "scoped_identifier" {
2188 names.push(source[lchild.byte_range()].to_string());
2189 }
2190 if !lc.goto_next_sibling() {
2191 break;
2192 }
2193 }
2194 }
2195 }
2196 if !c.goto_next_sibling() {
2197 break;
2198 }
2199 }
2200 }
2201}
2202
2203fn generate_rs_import_line(module_path: &str, names: &[String], _type_only: bool) -> String {
2205 if names.is_empty() {
2206 format!("use {module_path};")
2207 } else {
2208 let mut sorted_names = names.to_vec();
2209 sort_named_specifiers(&mut sorted_names);
2210 format!("use {module_path}::{{{}}};", sorted_names.join(", "))
2211 }
2212}
2213
2214pub fn classify_group_go(module_path: &str) -> ImportGroup {
2220 if module_path.contains('.') {
2223 ImportGroup::External
2224 } else {
2225 ImportGroup::Stdlib
2226 }
2227}
2228
2229fn parse_go_imports(source: &str, tree: &Tree) -> ImportBlock {
2231 let root = tree.root_node();
2232 let mut imports = Vec::new();
2233
2234 let mut cursor = root.walk();
2235 if !cursor.goto_first_child() {
2236 return ImportBlock::empty();
2237 }
2238
2239 loop {
2240 let node = cursor.node();
2241 if node.kind() == "import_declaration" {
2242 parse_go_import_declaration(source, &node, &mut imports);
2243 }
2244 if !cursor.goto_next_sibling() {
2245 break;
2246 }
2247 }
2248
2249 let byte_range = import_byte_range(&imports);
2250
2251 ImportBlock {
2252 imports,
2253 byte_range,
2254 }
2255}
2256
2257fn parse_go_import_declaration(source: &str, node: &Node, imports: &mut Vec<ImportStatement>) {
2259 let mut c = node.walk();
2260 if c.goto_first_child() {
2261 loop {
2262 let child = c.node();
2263 match child.kind() {
2264 "import_spec" => {
2265 if let Some(imp) = parse_go_import_spec(source, &child) {
2266 imports.push(imp);
2267 }
2268 }
2269 "import_spec_list" => {
2270 let mut lc = child.walk();
2272 if lc.goto_first_child() {
2273 loop {
2274 if lc.node().kind() == "import_spec" {
2275 if let Some(imp) = parse_go_import_spec(source, &lc.node()) {
2276 imports.push(imp);
2277 }
2278 }
2279 if !lc.goto_next_sibling() {
2280 break;
2281 }
2282 }
2283 }
2284 }
2285 _ => {}
2286 }
2287 if !c.goto_next_sibling() {
2288 break;
2289 }
2290 }
2291 }
2292}
2293
2294fn parse_go_import_spec(source: &str, node: &Node) -> Option<ImportStatement> {
2296 let raw_text = source[node.byte_range()].to_string();
2297 let byte_range = node.byte_range();
2298
2299 let mut import_path = String::new();
2300 let mut alias = None;
2301
2302 let mut c = node.walk();
2303 if c.goto_first_child() {
2304 loop {
2305 let child = c.node();
2306 match child.kind() {
2307 "interpreted_string_literal" => {
2308 let text = source[child.byte_range()].to_string();
2310 import_path = text.trim_matches('"').to_string();
2311 }
2312 "identifier" | "blank_identifier" | "dot" => {
2313 alias = Some(source[child.byte_range()].to_string());
2315 }
2316 _ => {}
2317 }
2318 if !c.goto_next_sibling() {
2319 break;
2320 }
2321 }
2322 }
2323
2324 if import_path.is_empty() {
2325 return None;
2326 }
2327
2328 let group = classify_group_go(&import_path);
2329
2330 Some(ImportStatement {
2331 module_path: import_path,
2332 names: Vec::new(),
2333 default_import: alias.clone(),
2334 namespace_import: None,
2335 kind: ImportKind::Value,
2336 group,
2337 byte_range,
2338 raw_text,
2339 form: ImportForm::Go { alias },
2340 })
2341}
2342
2343pub fn generate_go_import_line_pub(
2345 module_path: &str,
2346 alias: Option<&str>,
2347 in_group: bool,
2348) -> String {
2349 generate_go_import_line(module_path, alias, in_group)
2350}
2351
2352fn generate_go_import_line(module_path: &str, alias: Option<&str>, in_group: bool) -> String {
2357 if in_group {
2358 match alias {
2360 Some(a) => format!("\t{a} \"{module_path}\""),
2361 None => format!("\t\"{module_path}\""),
2362 }
2363 } else {
2364 match alias {
2366 Some(a) => format!("import {a} \"{module_path}\""),
2367 None => format!("import \"{module_path}\""),
2368 }
2369 }
2370}
2371
2372pub fn go_has_grouped_import(_source: &str, tree: &Tree) -> Option<Range<usize>> {
2375 let root = tree.root_node();
2376 let mut cursor = root.walk();
2377 if !cursor.goto_first_child() {
2378 return None;
2379 }
2380
2381 loop {
2382 let node = cursor.node();
2383 if node.kind() == "import_declaration" && go_import_declaration_is_grouped(&node) {
2384 return Some(node.byte_range());
2385 }
2386 if !cursor.goto_next_sibling() {
2387 break;
2388 }
2389 }
2390 None
2391}
2392
2393pub fn go_import_declarations_range(_source: &str, tree: &Tree) -> Option<Range<usize>> {
2394 let root = tree.root_node();
2395 let mut cursor = root.walk();
2396 let mut range: Option<Range<usize>> = None;
2397 if !cursor.goto_first_child() {
2398 return None;
2399 }
2400
2401 loop {
2402 let node = cursor.node();
2403 if node.kind() == "import_declaration" {
2404 let node_range = node.byte_range();
2405 range = Some(match range {
2406 Some(existing) => {
2407 existing.start.min(node_range.start)..existing.end.max(node_range.end)
2408 }
2409 None => node_range,
2410 });
2411 }
2412 if !cursor.goto_next_sibling() {
2413 break;
2414 }
2415 }
2416
2417 range
2418}
2419
2420pub fn go_offset_is_in_grouped_import(_source: &str, tree: &Tree, offset: usize) -> bool {
2421 let root = tree.root_node();
2422 let mut cursor = root.walk();
2423 if !cursor.goto_first_child() {
2424 return false;
2425 }
2426
2427 loop {
2428 let node = cursor.node();
2429 if node.kind() == "import_declaration"
2430 && node.start_byte() < offset
2431 && offset < node.end_byte()
2432 && go_import_declaration_is_grouped(&node)
2433 {
2434 return true;
2435 }
2436 if !cursor.goto_next_sibling() {
2437 break;
2438 }
2439 }
2440
2441 false
2442}
2443
2444fn go_import_declaration_is_grouped(node: &Node) -> bool {
2445 let mut c = node.walk();
2446 if c.goto_first_child() {
2447 loop {
2448 if c.node().kind() == "import_spec_list" {
2449 return true;
2450 }
2451 if !c.goto_next_sibling() {
2452 break;
2453 }
2454 }
2455 }
2456 false
2457}
2458
2459pub fn classify_group_solidity(module_path: &str) -> ImportGroup {
2466 if module_path.starts_with('.') {
2467 ImportGroup::Internal
2468 } else {
2469 ImportGroup::External
2470 }
2471}
2472
2473fn parse_solidity_imports(source: &str, tree: &Tree) -> ImportBlock {
2474 let root = tree.root_node();
2475 let mut imports = Vec::new();
2476 let mut cursor = root.walk();
2477 if cursor.goto_first_child() {
2478 loop {
2479 let node = cursor.node();
2480 if node.kind() == "import_directive" {
2481 if let Some(imp) = parse_solidity_import_directive(source, &node) {
2482 imports.push(imp);
2483 }
2484 }
2485 if !cursor.goto_next_sibling() {
2486 break;
2487 }
2488 }
2489 }
2490 let byte_range = import_byte_range(&imports);
2491 ImportBlock {
2492 imports,
2493 byte_range,
2494 }
2495}
2496
2497fn strip_one_matching_solidity_quote_pair(value: &str) -> &str {
2498 if value.len() < 2 {
2499 return value;
2500 }
2501
2502 let bytes = value.as_bytes();
2503 let quote = bytes[0];
2504 if matches!(quote, b'\'' | b'"') && bytes.last() == Some("e) {
2505 &value[1..value.len() - 1]
2506 } else {
2507 value
2508 }
2509}
2510
2511fn parse_solidity_import_directive(source: &str, node: &Node) -> Option<ImportStatement> {
2516 let raw_text = source[node.byte_range()].to_string();
2517 let byte_range = node.byte_range();
2518
2519 let mut children: Vec<(String, String)> = Vec::new();
2520 let mut c = node.walk();
2521 if c.goto_first_child() {
2522 loop {
2523 let ch = c.node();
2524 children.push((ch.kind().to_string(), source[ch.byte_range()].to_string()));
2525 if !c.goto_next_sibling() {
2526 break;
2527 }
2528 }
2529 }
2530
2531 let module_path = children
2533 .iter()
2534 .find(|(k, _)| k == "string")
2535 .map(|(_, text)| strip_one_matching_solidity_quote_pair(text).to_string())?;
2536 if module_path.is_empty() {
2537 return None;
2538 }
2539
2540 let has_brace = children.iter().any(|(k, _)| k == "{");
2541 let has_star = children.iter().any(|(k, _)| k == "*");
2542
2543 let mut named: Vec<String> = Vec::new();
2544 let mut namespace: Option<String> = None;
2545 let mut alias: Option<String> = None;
2546
2547 if has_brace {
2548 named = parse_solidity_named_specifiers(&children);
2549 } else if has_star {
2550 namespace = solidity_identifier_after_as(&children);
2551 } else {
2552 alias = solidity_identifier_after_as(&children);
2555 }
2556
2557 let kind = if named.is_empty() && namespace.is_none() && alias.is_none() {
2558 ImportKind::SideEffect
2559 } else {
2560 ImportKind::Value
2561 };
2562 let group = classify_group_solidity(&module_path);
2563
2564 Some(ImportStatement {
2565 module_path,
2566 names: named.clone(),
2567 default_import: None,
2568 namespace_import: namespace.clone(),
2571 kind,
2572 group,
2573 byte_range,
2574 raw_text,
2575 form: ImportForm::Solidity {
2576 named,
2577 namespace,
2578 alias,
2579 },
2580 })
2581}
2582
2583fn solidity_identifier_after_as(children: &[(String, String)]) -> Option<String> {
2585 let as_pos = children.iter().position(|(k, _)| k == "as")?;
2586 children[as_pos + 1..]
2587 .iter()
2588 .find(|(k, _)| k == "identifier")
2589 .map(|(_, t)| t.clone())
2590}
2591
2592fn parse_solidity_named_specifiers(children: &[(String, String)]) -> Vec<String> {
2595 let mut names = Vec::new();
2596 let mut in_braces = false;
2597 let mut current: Option<String> = None;
2598 let mut expect_alias = false;
2599 for (k, t) in children {
2600 match k.as_str() {
2601 "{" => in_braces = true,
2602 "}" => {
2603 if let Some(n) = current.take() {
2604 names.push(n);
2605 }
2606 in_braces = false;
2607 }
2608 _ if !in_braces => {}
2609 "identifier" => {
2610 if expect_alias {
2611 if let Some(n) = current.take() {
2612 names.push(format!("{n} as {t}"));
2613 }
2614 expect_alias = false;
2615 } else {
2616 if let Some(n) = current.take() {
2617 names.push(n);
2618 }
2619 current = Some(t.clone());
2620 }
2621 }
2622 "as" => expect_alias = true,
2623 "," => {
2624 if let Some(n) = current.take() {
2625 names.push(n);
2626 }
2627 expect_alias = false;
2628 }
2629 _ => {}
2630 }
2631 }
2632 names
2633}
2634
2635fn generate_solidity_import_line(req: &ImportRequest) -> String {
2637 if !req.names.is_empty() {
2638 format!(
2639 "import {{ {} }} from \"{}\";",
2640 req.names.join(", "),
2641 req.module_path
2642 )
2643 } else if let Some(ns) = req.namespace {
2644 format!("import * as {} from \"{}\";", ns, req.module_path)
2645 } else if let Some(al) = req.alias {
2646 format!("import \"{}\" as {};", req.module_path, al)
2647 } else {
2648 format!("import \"{}\";", req.module_path)
2649 }
2650}
2651
2652fn skip_newline(source: &str, pos: usize) -> usize {
2654 if pos < source.len() {
2655 let bytes = source.as_bytes();
2656 if bytes[pos] == b'\n' {
2657 return pos + 1;
2658 }
2659 if bytes[pos] == b'\r' {
2660 if pos + 1 < source.len() && bytes[pos + 1] == b'\n' {
2661 return pos + 2;
2662 }
2663 return pos + 1;
2664 }
2665 }
2666 pos
2667}
2668
2669#[cfg(test)]
2674mod tests {
2675 use super::*;
2676
2677 #[test]
2686 fn form_es_mirrors_flat_fields() {
2687 let (_, block) = parse_ts(
2688 "import Default, { a, b as c } from \"ext\";\nimport type { T } from \"./t\";\nimport \"./side\";\nimport * as ns from \"nspkg\";\n",
2689 );
2690 match &block.imports[0].form {
2692 ImportForm::Es {
2693 default_import,
2694 namespace_import,
2695 named,
2696 type_only,
2697 side_effect,
2698 } => {
2699 assert_eq!(default_import.as_deref(), Some("Default"));
2700 assert_eq!(namespace_import, &None);
2701 assert_eq!(named, &block.imports[0].names);
2702 assert!(!type_only);
2703 assert!(!side_effect);
2704 }
2705 other => panic!("expected Es, got {other:?}"),
2706 }
2707 match &block.imports[1].form {
2709 ImportForm::Es {
2710 type_only, named, ..
2711 } => {
2712 assert!(type_only);
2713 assert_eq!(named, &block.imports[1].names);
2714 }
2715 other => panic!("expected Es type-only, got {other:?}"),
2716 }
2717 match &block.imports[2].form {
2719 ImportForm::Es { side_effect, .. } => assert!(side_effect),
2720 other => panic!("expected Es side-effect, got {other:?}"),
2721 }
2722 match &block.imports[3].form {
2724 ImportForm::Es {
2725 namespace_import, ..
2726 } => assert_eq!(namespace_import.as_deref(), Some("ns")),
2727 other => panic!("expected Es namespace, got {other:?}"),
2728 }
2729 }
2730
2731 #[test]
2732 fn form_python_mirrors_flat_fields() {
2733 let (_, block) = parse_py("import os\nfrom sys import argv, path\n");
2734 match &block.imports[0].form {
2735 ImportForm::Python { from_import, named } => {
2736 assert!(!from_import, "`import os` is not a from-import");
2737 assert_eq!(named, &["os"]);
2738 }
2739 other => panic!("expected Python import, got {other:?}"),
2740 }
2741 match &block.imports[1].form {
2742 ImportForm::Python { from_import, named } => {
2743 assert!(from_import, "`from sys import ...` is a from-import");
2744 assert_eq!(named, &block.imports[1].names);
2745 }
2746 other => panic!("expected Python from-import, got {other:?}"),
2747 }
2748 }
2749
2750 #[test]
2751 fn form_rust_de_overloads_pub_from_default_import() {
2752 let (_, block) = parse_rust("pub use crate::a::Exported;\nuse std::fmt::Debug;\n");
2753 match &block.imports[0].form {
2755 ImportForm::RustUse { visibility, named } => {
2756 assert_eq!(visibility.as_deref(), Some("pub"));
2757 assert_eq!(named, &block.imports[0].names);
2758 }
2759 other => panic!("expected RustUse, got {other:?}"),
2760 }
2761 assert_eq!(
2762 block.imports[0].default_import.as_deref(),
2763 Some("pub"),
2764 "flat field unchanged during additive migration"
2765 );
2766 match &block.imports[1].form {
2768 ImportForm::RustUse { visibility, .. } => assert_eq!(visibility, &None),
2769 other => panic!("expected RustUse, got {other:?}"),
2770 }
2771 assert_eq!(block.imports[1].default_import, None);
2772 }
2773
2774 #[test]
2775 fn form_go_de_overloads_alias_from_default_import() {
2776 let (_, block) =
2782 parse_go("package main\n\nimport (\n\t_ \"github.com/x/y\"\n\t\"fmt\"\n)\n");
2783 let blank = block
2784 .imports
2785 .iter()
2786 .find(|i| i.module_path == "github.com/x/y")
2787 .expect("blank import parsed");
2788 match &blank.form {
2789 ImportForm::Go { alias } => assert_eq!(alias.as_deref(), Some("_")),
2790 other => panic!("expected Go blank-aliased, got {other:?}"),
2791 }
2792 assert_eq!(
2793 blank.default_import.as_deref(),
2794 Some("_"),
2795 "form.alias mirrors the flat default_import field exactly"
2796 );
2797 let plain = block
2798 .imports
2799 .iter()
2800 .find(|i| i.module_path == "fmt")
2801 .expect("plain import parsed");
2802 match &plain.form {
2803 ImportForm::Go { alias } => assert_eq!(alias, &None),
2804 other => panic!("expected Go plain, got {other:?}"),
2805 }
2806 assert_eq!(plain.default_import, None);
2807 }
2808
2809 fn parse_ts(source: &str) -> (Tree, ImportBlock) {
2810 let grammar = grammar_for(LangId::TypeScript);
2811 let mut parser = Parser::new();
2812 parser.set_language(&grammar).unwrap();
2813 let tree = parser.parse(source, None).unwrap();
2814 let block = parse_imports(source, &tree, LangId::TypeScript);
2815 (tree, block)
2816 }
2817
2818 fn parse_js(source: &str) -> (Tree, ImportBlock) {
2819 let grammar = grammar_for(LangId::JavaScript);
2820 let mut parser = Parser::new();
2821 parser.set_language(&grammar).unwrap();
2822 let tree = parser.parse(source, None).unwrap();
2823 let block = parse_imports(source, &tree, LangId::JavaScript);
2824 (tree, block)
2825 }
2826
2827 fn parse_vue(source: &str) -> (Tree, ImportBlock) {
2828 let grammar = grammar_for(LangId::Vue);
2829 let mut parser = Parser::new();
2830 parser.set_language(&grammar).unwrap();
2831 let tree = parser.parse(source, None).unwrap();
2832 let block = parse_imports(source, &tree, LangId::Vue);
2833 (tree, block)
2834 }
2835
2836 #[test]
2841 fn vue_grammar_node_kinds_are_stable() {
2842 let src = "<template>\n <div />\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from 'vue'\n</script>\n";
2843 let grammar = grammar_for(LangId::Vue);
2844 let mut parser = Parser::new();
2845 parser.set_language(&grammar).unwrap();
2846 let tree = parser.parse(src, None).unwrap();
2847 let root = tree.root_node();
2848 let mut cursor = root.walk();
2849 let script = root
2850 .named_children(&mut cursor)
2851 .find(|n| n.kind() == "script_element")
2852 .expect("expected a script_element node");
2853 let mut inner = script.walk();
2854 assert!(
2855 script
2856 .named_children(&mut inner)
2857 .any(|n| n.kind() == "raw_text"),
2858 "expected script body exposed as raw_text"
2859 );
2860 }
2861
2862 #[test]
2863 fn vue_parses_script_imports_with_whole_file_offsets() {
2864 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";
2865 let (_tree, block) = parse_vue(src);
2866 assert_eq!(block.imports.len(), 2, "should find both script imports");
2867 for imp in &block.imports {
2870 assert_eq!(&src[imp.byte_range.clone()], imp.raw_text);
2871 assert!(
2872 imp.byte_range.start > src.find("<script").unwrap(),
2873 "import offset must fall inside the script block"
2874 );
2875 }
2876 assert_eq!(block.imports[0].module_path, "vue");
2877 assert_eq!(block.imports[1].module_path, "./Foo.vue");
2878 }
2879
2880 #[test]
2881 fn vue_without_script_block_has_no_imports() {
2882 let src = "<template>\n <div />\n</template>\n\n<style>.x{}</style>\n";
2883 let (_tree, block) = parse_vue(src);
2884 assert!(block.imports.is_empty());
2885 assert!(block.byte_range.is_none());
2886 }
2887
2888 #[test]
2891 fn parse_ts_named_imports() {
2892 let source = "import { useState, useEffect } from 'react';\n";
2893 let (_, block) = parse_ts(source);
2894 assert_eq!(block.imports.len(), 1);
2895 let imp = &block.imports[0];
2896 assert_eq!(imp.module_path, "react");
2897 assert!(imp.names.contains(&"useState".to_string()));
2898 assert!(imp.names.contains(&"useEffect".to_string()));
2899 assert_eq!(imp.kind, ImportKind::Value);
2900 assert_eq!(imp.group, ImportGroup::External);
2901 }
2902
2903 #[test]
2904 fn parse_ts_default_import() {
2905 let source = "import React from 'react';\n";
2906 let (_, block) = parse_ts(source);
2907 assert_eq!(block.imports.len(), 1);
2908 let imp = &block.imports[0];
2909 assert_eq!(imp.default_import.as_deref(), Some("React"));
2910 assert_eq!(imp.kind, ImportKind::Value);
2911 }
2912
2913 #[test]
2914 fn parse_ts_side_effect_import() {
2915 let source = "import './styles.css';\n";
2916 let (_, block) = parse_ts(source);
2917 assert_eq!(block.imports.len(), 1);
2918 assert_eq!(block.imports[0].kind, ImportKind::SideEffect);
2919 assert_eq!(block.imports[0].module_path, "./styles.css");
2920 }
2921
2922 #[test]
2923 fn parse_ts_relative_import() {
2924 let source = "import { helper } from './utils';\n";
2925 let (_, block) = parse_ts(source);
2926 assert_eq!(block.imports.len(), 1);
2927 assert_eq!(block.imports[0].group, ImportGroup::Internal);
2928 }
2929
2930 #[test]
2931 fn parse_ts_multiple_groups() {
2932 let source = "\
2933import React from 'react';
2934import { useState } from 'react';
2935import { helper } from './utils';
2936import { Config } from '../config';
2937";
2938 let (_, block) = parse_ts(source);
2939 assert_eq!(block.imports.len(), 4);
2940
2941 let external: Vec<_> = block
2942 .imports
2943 .iter()
2944 .filter(|i| i.group == ImportGroup::External)
2945 .collect();
2946 let relative: Vec<_> = block
2947 .imports
2948 .iter()
2949 .filter(|i| i.group == ImportGroup::Internal)
2950 .collect();
2951 assert_eq!(external.len(), 2);
2952 assert_eq!(relative.len(), 2);
2953 }
2954
2955 #[test]
2956 fn parse_ts_namespace_import() {
2957 let source = "import * as path from 'path';\n";
2958 let (_, block) = parse_ts(source);
2959 assert_eq!(block.imports.len(), 1);
2960 let imp = &block.imports[0];
2961 assert_eq!(imp.namespace_import.as_deref(), Some("path"));
2962 assert_eq!(imp.kind, ImportKind::Value);
2963 }
2964
2965 #[test]
2966 fn parse_js_imports() {
2967 let source = "import { readFile } from 'fs';\nimport { helper } from './helper';\n";
2968 let (_, block) = parse_js(source);
2969 assert_eq!(block.imports.len(), 2);
2970 assert_eq!(block.imports[0].group, ImportGroup::External);
2971 assert_eq!(block.imports[1].group, ImportGroup::Internal);
2972 }
2973
2974 #[test]
2977 fn classify_external() {
2978 assert_eq!(classify_group_ts("react"), ImportGroup::External);
2979 assert_eq!(classify_group_ts("@scope/pkg"), ImportGroup::External);
2980 assert_eq!(classify_group_ts("lodash/map"), ImportGroup::External);
2981 }
2982
2983 #[test]
2984 fn classify_relative() {
2985 assert_eq!(classify_group_ts("./utils"), ImportGroup::Internal);
2986 assert_eq!(classify_group_ts("../config"), ImportGroup::Internal);
2987 assert_eq!(classify_group_ts("./"), ImportGroup::Internal);
2988 }
2989
2990 #[test]
2993 fn dedup_detects_same_named_import() {
2994 let source = "import { useState } from 'react';\n";
2995 let (_, block) = parse_ts(source);
2996 assert!(is_duplicate(
2997 &block,
2998 "react",
2999 &["useState".to_string()],
3000 None,
3001 false
3002 ));
3003 }
3004
3005 #[test]
3006 fn dedup_misses_different_name() {
3007 let source = "import { useState } from 'react';\n";
3008 let (_, block) = parse_ts(source);
3009 assert!(!is_duplicate(
3010 &block,
3011 "react",
3012 &["useEffect".to_string()],
3013 None,
3014 false
3015 ));
3016 }
3017
3018 #[test]
3019 fn dedup_detects_default_import() {
3020 let source = "import React from 'react';\n";
3021 let (_, block) = parse_ts(source);
3022 assert!(is_duplicate(&block, "react", &[], Some("React"), false));
3023 }
3024
3025 #[test]
3026 fn dedup_side_effect() {
3027 let source = "import './styles.css';\n";
3028 let (_, block) = parse_ts(source);
3029 assert!(is_duplicate(&block, "./styles.css", &[], None, false));
3030 }
3031
3032 #[test]
3033 fn dedup_namespace_import_distinct_from_side_effect_import() {
3034 let side_effect_source = "import 'fs';\n";
3035 let (_, side_effect_block) = parse_ts(side_effect_source);
3036 assert!(!is_duplicate_with_namespace(
3037 &side_effect_block,
3038 "fs",
3039 &[],
3040 None,
3041 Some("fs"),
3042 false
3043 ));
3044
3045 let namespace_source = "import * as fs from 'fs';\n";
3046 let (_, namespace_block) = parse_ts(namespace_source);
3047 assert!(!is_duplicate(&namespace_block, "fs", &[], None, false));
3048 assert!(is_duplicate_with_namespace(
3049 &namespace_block,
3050 "fs",
3051 &[],
3052 None,
3053 Some("fs"),
3054 false
3055 ));
3056 assert!(!is_duplicate_with_namespace(
3057 &namespace_block,
3058 "fs",
3059 &[],
3060 None,
3061 Some("other"),
3062 false
3063 ));
3064 }
3065
3066 #[test]
3067 fn dedup_type_vs_value() {
3068 let source = "import { FC } from 'react';\n";
3069 let (_, block) = parse_ts(source);
3070 assert!(!is_duplicate(
3072 &block,
3073 "react",
3074 &["FC".to_string()],
3075 None,
3076 true
3077 ));
3078 }
3079
3080 #[test]
3083 fn generate_named_import() {
3084 let line = generate_import_line(
3085 LangId::TypeScript,
3086 "react",
3087 &["useState".to_string(), "useEffect".to_string()],
3088 None,
3089 false,
3090 );
3091 assert_eq!(line, "import { useEffect, useState } from 'react';");
3092 }
3093
3094 #[test]
3095 fn generate_named_import_sorts_by_imported_name() {
3096 let line = generate_import_line(
3097 LangId::TypeScript,
3098 "x",
3099 &[
3100 "useState".to_string(),
3101 "type Foo".to_string(),
3102 "stdin as input".to_string(),
3103 "type Bar".to_string(),
3104 ],
3105 None,
3106 false,
3107 );
3108 assert_eq!(
3109 line,
3110 "import { type Bar, type Foo, stdin as input, useState } from 'x';"
3111 );
3112 }
3113
3114 #[test]
3115 fn generate_default_import() {
3116 let line = generate_import_line(LangId::TypeScript, "react", &[], Some("React"), false);
3117 assert_eq!(line, "import React from 'react';");
3118 }
3119
3120 #[test]
3121 fn generate_type_import() {
3122 let line =
3123 generate_import_line(LangId::TypeScript, "react", &["FC".to_string()], None, true);
3124 assert_eq!(line, "import type { FC } from 'react';");
3125 }
3126
3127 #[test]
3128 fn generate_side_effect_import() {
3129 let line = generate_import_line(LangId::TypeScript, "./styles.css", &[], None, false);
3130 assert_eq!(line, "import './styles.css';");
3131 }
3132
3133 #[test]
3134 fn generate_default_and_named() {
3135 let line = generate_import_line(
3136 LangId::TypeScript,
3137 "react",
3138 &["useState".to_string()],
3139 Some("React"),
3140 false,
3141 );
3142 assert_eq!(line, "import React, { useState } from 'react';");
3143 }
3144
3145 #[test]
3146 fn parse_ts_type_import() {
3147 let source = "import type { FC } from 'react';\n";
3148 let (_, block) = parse_ts(source);
3149 assert_eq!(block.imports.len(), 1);
3150 let imp = &block.imports[0];
3151 assert_eq!(imp.kind, ImportKind::Type);
3152 assert!(imp.names.contains(&"FC".to_string()));
3153 assert_eq!(imp.group, ImportGroup::External);
3154 }
3155
3156 #[test]
3159 fn insertion_empty_file() {
3160 let source = "";
3161 let (_, block) = parse_ts(source);
3162 let (offset, _, _) =
3163 find_insertion_point(source, &block, ImportGroup::External, "react", false);
3164 assert_eq!(offset, 0);
3165 }
3166
3167 #[test]
3168 fn insertion_alphabetical_within_group() {
3169 let source = "\
3170import { a } from 'alpha';
3171import { c } from 'charlie';
3172";
3173 let (_, block) = parse_ts(source);
3174 let (offset, _, _) =
3175 find_insertion_point(source, &block, ImportGroup::External, "bravo", false);
3176 let before_charlie = source.find("import { c }").unwrap();
3178 assert_eq!(offset, before_charlie);
3179 }
3180
3181 fn parse_py(source: &str) -> (Tree, ImportBlock) {
3184 let grammar = grammar_for(LangId::Python);
3185 let mut parser = Parser::new();
3186 parser.set_language(&grammar).unwrap();
3187 let tree = parser.parse(source, None).unwrap();
3188 let block = parse_imports(source, &tree, LangId::Python);
3189 (tree, block)
3190 }
3191
3192 #[test]
3193 fn parse_py_import_statement() {
3194 let source = "import os\nimport sys\n";
3195 let (_, block) = parse_py(source);
3196 assert_eq!(block.imports.len(), 2);
3197 assert_eq!(block.imports[0].module_path, "os");
3198 assert_eq!(block.imports[1].module_path, "sys");
3199 assert_eq!(block.imports[0].group, ImportGroup::Stdlib);
3200 }
3201
3202 #[test]
3203 fn parse_py_import_statement_preserves_aliases_and_siblings() {
3204 let source = "import alpha, beta as local_beta\n";
3205 let (_, block) = parse_py(source);
3206 let imp = &block.imports[0];
3207 assert_eq!(imp.module_path, "alpha");
3208 assert!(imp.names.is_empty());
3209 match &imp.form {
3210 ImportForm::Python { from_import, named } => {
3211 assert!(!from_import);
3212 assert_eq!(named, &["alpha", "beta as local_beta"]);
3213 assert!(specifier_matches(&named[1], "beta"));
3214 assert!(specifier_matches(&named[1], "local_beta"));
3215 }
3216 other => panic!("expected Python import, got {other:?}"),
3217 }
3218 }
3219
3220 #[test]
3221 fn parse_py_from_import() {
3222 let source = "from collections import OrderedDict\nfrom typing import List, Optional\n";
3223 let (_, block) = parse_py(source);
3224 assert_eq!(block.imports.len(), 2);
3225 assert_eq!(block.imports[0].module_path, "collections");
3226 assert!(block.imports[0].names.contains(&"OrderedDict".to_string()));
3227 assert_eq!(block.imports[0].group, ImportGroup::Stdlib);
3228 assert_eq!(block.imports[1].module_path, "typing");
3229 assert!(block.imports[1].names.contains(&"List".to_string()));
3230 assert!(block.imports[1].names.contains(&"Optional".to_string()));
3231 }
3232
3233 #[test]
3234 fn parse_py_from_import_preserves_aliases() {
3235 let source = "from module import alpha, beta as local_beta\n";
3236 let (_, block) = parse_py(source);
3237 let imp = &block.imports[0];
3238 assert_eq!(imp.names, ["alpha", "beta as local_beta"]);
3239 assert!(specifier_matches(&imp.names[1], "beta"));
3240 assert!(specifier_matches(&imp.names[1], "local_beta"));
3241 assert_eq!(
3242 imp.form,
3243 ImportForm::Python {
3244 from_import: true,
3245 named: vec!["alpha".to_string(), "beta as local_beta".to_string()],
3246 }
3247 );
3248 }
3249
3250 #[test]
3251 fn parse_py_relative_import() {
3252 let source = "from . import utils\nfrom ..config import Settings\n";
3253 let (_, block) = parse_py(source);
3254 assert_eq!(block.imports.len(), 2);
3255 assert_eq!(block.imports[0].module_path, ".");
3256 assert!(block.imports[0].names.contains(&"utils".to_string()));
3257 assert_eq!(block.imports[0].group, ImportGroup::Internal);
3258 assert_eq!(block.imports[1].module_path, "..config");
3259 assert_eq!(block.imports[1].group, ImportGroup::Internal);
3260 }
3261
3262 #[test]
3263 fn classify_py_groups() {
3264 assert_eq!(classify_group_py("os"), ImportGroup::Stdlib);
3265 assert_eq!(classify_group_py("sys"), ImportGroup::Stdlib);
3266 assert_eq!(classify_group_py("json"), ImportGroup::Stdlib);
3267 assert_eq!(classify_group_py("collections"), ImportGroup::Stdlib);
3268 assert_eq!(classify_group_py("os.path"), ImportGroup::Stdlib);
3269 assert_eq!(classify_group_py("requests"), ImportGroup::External);
3270 assert_eq!(classify_group_py("flask"), ImportGroup::External);
3271 assert_eq!(classify_group_py("."), ImportGroup::Internal);
3272 assert_eq!(classify_group_py("..config"), ImportGroup::Internal);
3273 assert_eq!(classify_group_py(".utils"), ImportGroup::Internal);
3274 }
3275
3276 #[test]
3277 fn parse_py_three_groups() {
3278 let source = "import os\nimport sys\n\nimport requests\n\nfrom . import utils\n";
3279 let (_, block) = parse_py(source);
3280 let stdlib: Vec<_> = block
3281 .imports
3282 .iter()
3283 .filter(|i| i.group == ImportGroup::Stdlib)
3284 .collect();
3285 let external: Vec<_> = block
3286 .imports
3287 .iter()
3288 .filter(|i| i.group == ImportGroup::External)
3289 .collect();
3290 let internal: Vec<_> = block
3291 .imports
3292 .iter()
3293 .filter(|i| i.group == ImportGroup::Internal)
3294 .collect();
3295 assert_eq!(stdlib.len(), 2);
3296 assert_eq!(external.len(), 1);
3297 assert_eq!(internal.len(), 1);
3298 }
3299
3300 #[test]
3301 fn generate_py_import() {
3302 let line = generate_import_line(LangId::Python, "os", &[], None, false);
3303 assert_eq!(line, "import os");
3304 }
3305
3306 #[test]
3307 fn generate_py_from_import() {
3308 let line = generate_import_line(
3309 LangId::Python,
3310 "collections",
3311 &["OrderedDict".to_string()],
3312 None,
3313 false,
3314 );
3315 assert_eq!(line, "from collections import OrderedDict");
3316 }
3317
3318 #[test]
3319 fn generate_py_from_import_multiple() {
3320 let line = generate_import_line(
3321 LangId::Python,
3322 "typing",
3323 &["Optional".to_string(), "List".to_string()],
3324 None,
3325 false,
3326 );
3327 assert_eq!(line, "from typing import List, Optional");
3328 }
3329
3330 fn parse_rust(source: &str) -> (Tree, ImportBlock) {
3333 let grammar = grammar_for(LangId::Rust);
3334 let mut parser = Parser::new();
3335 parser.set_language(&grammar).unwrap();
3336 let tree = parser.parse(source, None).unwrap();
3337 let block = parse_imports(source, &tree, LangId::Rust);
3338 (tree, block)
3339 }
3340
3341 #[test]
3342 fn parse_rs_use_std() {
3343 let source = "use std::collections::HashMap;\nuse std::io::Read;\n";
3344 let (_, block) = parse_rust(source);
3345 assert_eq!(block.imports.len(), 2);
3346 assert_eq!(block.imports[0].module_path, "std::collections::HashMap");
3347 assert_eq!(block.imports[0].group, ImportGroup::Stdlib);
3348 assert_eq!(block.imports[1].group, ImportGroup::Stdlib);
3349 }
3350
3351 #[test]
3352 fn parse_rs_use_external() {
3353 let source = "use serde::{Deserialize, Serialize};\n";
3354 let (_, block) = parse_rust(source);
3355 assert_eq!(block.imports.len(), 1);
3356 assert_eq!(block.imports[0].group, ImportGroup::External);
3357 assert!(block.imports[0].names.contains(&"Deserialize".to_string()));
3358 assert!(block.imports[0].names.contains(&"Serialize".to_string()));
3359 }
3360
3361 #[test]
3362 fn parse_rs_use_crate() {
3363 let source = "use crate::config::Settings;\nuse super::parent::Thing;\n";
3364 let (_, block) = parse_rust(source);
3365 assert_eq!(block.imports.len(), 2);
3366 assert_eq!(block.imports[0].group, ImportGroup::Internal);
3367 assert_eq!(block.imports[1].group, ImportGroup::Internal);
3368 }
3369
3370 #[test]
3371 fn parse_rs_pub_use() {
3372 let source = "pub use super::parent::Thing;\n";
3373 let (_, block) = parse_rust(source);
3374 assert_eq!(block.imports.len(), 1);
3375 assert_eq!(block.imports[0].default_import.as_deref(), Some("pub"));
3377 }
3378
3379 #[test]
3380 fn classify_rs_groups() {
3381 assert_eq!(
3382 classify_group_rs("std::collections::HashMap"),
3383 ImportGroup::Stdlib
3384 );
3385 assert_eq!(classify_group_rs("core::mem"), ImportGroup::Stdlib);
3386 assert_eq!(classify_group_rs("alloc::vec"), ImportGroup::Stdlib);
3387 assert_eq!(
3388 classify_group_rs("serde::Deserialize"),
3389 ImportGroup::External
3390 );
3391 assert_eq!(classify_group_rs("tokio::runtime"), ImportGroup::External);
3392 assert_eq!(classify_group_rs("crate::config"), ImportGroup::Internal);
3393 assert_eq!(classify_group_rs("self::utils"), ImportGroup::Internal);
3394 assert_eq!(classify_group_rs("super::parent"), ImportGroup::Internal);
3395 }
3396
3397 #[test]
3398 fn generate_rs_use() {
3399 let line = generate_import_line(LangId::Rust, "std::fmt::Display", &[], None, false);
3400 assert_eq!(line, "use std::fmt::Display;");
3401 }
3402
3403 fn parse_go(source: &str) -> (Tree, ImportBlock) {
3406 let grammar = grammar_for(LangId::Go);
3407 let mut parser = Parser::new();
3408 parser.set_language(&grammar).unwrap();
3409 let tree = parser.parse(source, None).unwrap();
3410 let block = parse_imports(source, &tree, LangId::Go);
3411 (tree, block)
3412 }
3413
3414 #[test]
3415 fn parse_go_single_import() {
3416 let source = "package main\n\nimport \"fmt\"\n";
3417 let (_, block) = parse_go(source);
3418 assert_eq!(block.imports.len(), 1);
3419 assert_eq!(block.imports[0].module_path, "fmt");
3420 assert_eq!(block.imports[0].group, ImportGroup::Stdlib);
3421 }
3422
3423 #[test]
3424 fn parse_go_grouped_import() {
3425 let source =
3426 "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/pkg/errors\"\n)\n";
3427 let (_, block) = parse_go(source);
3428 assert_eq!(block.imports.len(), 3);
3429 assert_eq!(block.imports[0].module_path, "fmt");
3430 assert_eq!(block.imports[0].group, ImportGroup::Stdlib);
3431 assert_eq!(block.imports[1].module_path, "os");
3432 assert_eq!(block.imports[1].group, ImportGroup::Stdlib);
3433 assert_eq!(block.imports[2].module_path, "github.com/pkg/errors");
3434 assert_eq!(block.imports[2].group, ImportGroup::External);
3435 }
3436
3437 #[test]
3438 fn parse_go_mixed_imports() {
3439 let source = "package main\n\nimport \"fmt\"\n\nimport (\n\t\"os\"\n\t\"github.com/pkg/errors\"\n)\n";
3441 let (_, block) = parse_go(source);
3442 assert_eq!(block.imports.len(), 3);
3443 }
3444
3445 #[test]
3446 fn classify_go_groups() {
3447 assert_eq!(classify_group_go("fmt"), ImportGroup::Stdlib);
3448 assert_eq!(classify_group_go("os"), ImportGroup::Stdlib);
3449 assert_eq!(classify_group_go("net/http"), ImportGroup::Stdlib);
3450 assert_eq!(classify_group_go("encoding/json"), ImportGroup::Stdlib);
3451 assert_eq!(
3452 classify_group_go("github.com/pkg/errors"),
3453 ImportGroup::External
3454 );
3455 assert_eq!(
3456 classify_group_go("golang.org/x/tools"),
3457 ImportGroup::External
3458 );
3459 }
3460
3461 #[test]
3462 fn generate_go_standalone() {
3463 let line = generate_go_import_line("fmt", None, false);
3464 assert_eq!(line, "import \"fmt\"");
3465 }
3466
3467 #[test]
3468 fn generate_go_grouped_spec() {
3469 let line = generate_go_import_line("fmt", None, true);
3470 assert_eq!(line, "\t\"fmt\"");
3471 }
3472
3473 #[test]
3474 fn generate_go_with_alias() {
3475 let line = generate_go_import_line("github.com/pkg/errors", Some("errs"), false);
3476 assert_eq!(line, "import errs \"github.com/pkg/errors\"");
3477 }
3478
3479 fn parse_solidity(source: &str) -> (Tree, ImportBlock) {
3482 let grammar = grammar_for(LangId::Solidity);
3483 let mut parser = Parser::new();
3484 parser.set_language(&grammar).unwrap();
3485 let tree = parser.parse(source, None).unwrap();
3486 let block = parse_imports(source, &tree, LangId::Solidity);
3487 (tree, block)
3488 }
3489
3490 #[test]
3494 fn solidity_grammar_node_kinds_are_stable() {
3495 let grammar = grammar_for(LangId::Solidity);
3496 let mut parser = Parser::new();
3497 parser.set_language(&grammar).unwrap();
3498 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";
3499 let tree = parser.parse(src, None).unwrap();
3500 let mut kinds: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
3501 fn walk(node: tree_sitter::Node, kinds: &mut std::collections::BTreeSet<String>) {
3502 kinds.insert(node.kind().to_string());
3503 let mut c = node.walk();
3504 if c.goto_first_child() {
3505 loop {
3506 walk(c.node(), kinds);
3507 if !c.goto_next_sibling() {
3508 break;
3509 }
3510 }
3511 }
3512 }
3513 walk(tree.root_node(), &mut kinds);
3514 for required in [
3515 "import_directive",
3516 "string",
3517 "identifier",
3518 "as",
3519 "from",
3520 "*",
3521 "{",
3522 "}",
3523 ] {
3524 assert!(
3525 kinds.contains(required),
3526 "solidity grammar missing node kind {required:?}; present: {kinds:?}"
3527 );
3528 }
3529 }
3530
3531 #[test]
3532 fn solidity_string_path_strips_one_matching_quote_pair() {
3533 assert_eq!(
3534 strip_one_matching_solidity_quote_pair("\"./A.sol\""),
3535 "./A.sol"
3536 );
3537 assert_eq!(
3538 strip_one_matching_solidity_quote_pair("'./A.sol'"),
3539 "./A.sol"
3540 );
3541 assert_eq!(
3542 strip_one_matching_solidity_quote_pair("\"./a'b.sol\""),
3543 "./a'b.sol"
3544 );
3545 assert_eq!(
3546 strip_one_matching_solidity_quote_pair("\"\"./A.sol\"\""),
3547 "\"./A.sol\""
3548 );
3549 assert_eq!(
3550 strip_one_matching_solidity_quote_pair("'./A.sol\""),
3551 "'./A.sol\""
3552 );
3553 assert_eq!(
3554 strip_one_matching_solidity_quote_pair("\"./A.sol"),
3555 "\"./A.sol"
3556 );
3557 assert_eq!(strip_one_matching_solidity_quote_pair("\""), "\"");
3558 }
3559
3560 #[test]
3561 fn parse_solidity_all_four_forms() {
3562 let (_, block) = parse_solidity(
3563 "import \"./A.sol\";\nimport \"./B.sol\" as B;\nimport * as C from \"./C.sol\";\nimport { Foo, Bar as Baz } from \"./D.sol\";\nimport './E.sol';\nimport './F.sol' as F;\nimport * as G from './G.sol';\nimport { Quux, Corge as Grault } from './H.sol';\n",
3564 );
3565 assert_eq!(block.imports.len(), 8);
3566
3567 assert_eq!(block.imports[0].module_path, "./A.sol");
3569 assert_eq!(block.imports[0].kind, ImportKind::SideEffect);
3570 assert_eq!(
3571 block.imports[0].form,
3572 ImportForm::Solidity {
3573 named: vec![],
3574 namespace: None,
3575 alias: None
3576 }
3577 );
3578
3579 assert_eq!(
3581 block.imports[1].form,
3582 ImportForm::Solidity {
3583 named: vec![],
3584 namespace: None,
3585 alias: Some("B".to_string())
3586 }
3587 );
3588
3589 match &block.imports[2].form {
3591 ImportForm::Solidity { namespace, .. } => assert_eq!(namespace.as_deref(), Some("C")),
3592 other => panic!("expected Solidity namespace, got {other:?}"),
3593 }
3594 assert_eq!(block.imports[2].namespace_import.as_deref(), Some("C"));
3595
3596 match &block.imports[3].form {
3598 ImportForm::Solidity { named, .. } => {
3599 assert_eq!(named, &vec!["Foo".to_string(), "Bar as Baz".to_string()]);
3600 }
3601 other => panic!("expected Solidity named, got {other:?}"),
3602 }
3603 assert_eq!(
3604 block.imports[3].names,
3605 vec!["Foo".to_string(), "Bar as Baz".to_string()]
3606 );
3607
3608 assert_eq!(
3609 block.imports[4..]
3610 .iter()
3611 .map(|import| import.module_path.as_str())
3612 .collect::<Vec<_>>(),
3613 ["./E.sol", "./F.sol", "./G.sol", "./H.sol"]
3614 );
3615 assert_eq!(block.imports[4].kind, ImportKind::SideEffect);
3616 assert!(matches!(
3617 &block.imports[5].form,
3618 ImportForm::Solidity {
3619 alias: Some(alias),
3620 ..
3621 } if alias == "F"
3622 ));
3623 assert_eq!(block.imports[6].namespace_import.as_deref(), Some("G"));
3624 assert_eq!(
3625 block.imports[7].names,
3626 vec!["Quux".to_string(), "Corge as Grault".to_string()]
3627 );
3628 }
3629
3630 #[test]
3631 fn generate_solidity_all_forms() {
3632 assert_eq!(
3634 generate_import(
3635 LangId::Solidity,
3636 &ImportRequest::legacy("./A.sol", &[], None, None, false)
3637 ),
3638 "import \"./A.sol\";"
3639 );
3640 let names = vec!["Foo".to_string(), "Bar as Baz".to_string()];
3642 assert_eq!(
3643 generate_import(
3644 LangId::Solidity,
3645 &ImportRequest::legacy("./D.sol", &names, None, None, false)
3646 ),
3647 "import { Foo, Bar as Baz } from \"./D.sol\";"
3648 );
3649 assert_eq!(
3651 generate_import(
3652 LangId::Solidity,
3653 &ImportRequest::legacy("./C.sol", &[], None, Some("C"), false)
3654 ),
3655 "import * as C from \"./C.sol\";"
3656 );
3657 assert_eq!(
3659 generate_import(
3660 LangId::Solidity,
3661 &ImportRequest {
3662 module_path: "./B.sol",
3663 names: &[],
3664 default_import: None,
3665 namespace: None,
3666 alias: Some("B"),
3667 type_only: false,
3668 modifiers: &[],
3669 import_kind: None,
3670 }
3671 ),
3672 "import \"./B.sol\" as B;"
3673 );
3674 }
3675
3676 #[test]
3677 fn solidity_round_trips_through_parse_generate() {
3678 for src in [
3680 "import \"./A.sol\";",
3681 "import \"./B.sol\" as B;",
3682 "import * as C from \"./C.sol\";",
3683 "import { Foo, Bar as Baz } from \"./D.sol\";",
3684 ] {
3685 let (_, block) = parse_solidity(src);
3686 assert_eq!(block.imports.len(), 1, "parse {src:?}");
3687 let imp = &block.imports[0];
3688 let (namespace, alias) = match &imp.form {
3689 ImportForm::Solidity {
3690 namespace, alias, ..
3691 } => (namespace.as_deref(), alias.as_deref()),
3692 other => panic!("expected Solidity, got {other:?}"),
3693 };
3694 let regenerated = generate_import(
3695 LangId::Solidity,
3696 &ImportRequest {
3697 module_path: &imp.module_path,
3698 names: &imp.names,
3699 default_import: None,
3700 namespace,
3701 alias,
3702 type_only: false,
3703 modifiers: &[],
3704 import_kind: None,
3705 },
3706 );
3707 assert_eq!(regenerated, src, "round-trip mismatch for {src:?}");
3708 }
3709 }
3710
3711 #[test]
3712 fn classify_group_solidity_relative_vs_external() {
3713 assert_eq!(classify_group_solidity("./A.sol"), ImportGroup::Internal);
3714 assert_eq!(
3715 classify_group_solidity("../lib/B.sol"),
3716 ImportGroup::Internal
3717 );
3718 assert_eq!(
3719 classify_group_solidity("@openzeppelin/contracts/token/ERC20/ERC20.sol"),
3720 ImportGroup::External
3721 );
3722 }
3723}