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;
22pub(crate) mod 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};
28pub(crate) mod 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 attribute_clause: Option<String>,
119 attribute_type: Option<String>,
121 },
122 Python {
125 from_import: bool,
126 named: Vec<String>,
127 },
128 RustUse {
133 visibility: Option<String>,
134 named: Vec<String>,
135 },
136 Go { alias: Option<String> },
139 Solidity {
148 named: Vec<String>,
149 namespace: Option<String>,
150 alias: Option<String>,
151 },
152 Php { clauses: Vec<PhpImportClause> },
155 Structured {
161 named: Vec<String>,
162 namespace: Option<String>,
163 alias: Option<String>,
164 modifiers: Vec<String>,
165 import_kind: Option<String>,
166 },
167}
168
169#[derive(Debug, Clone)]
174pub struct ImportRequest<'a> {
175 pub module_path: &'a str,
176 pub names: &'a [String],
177 pub default_import: Option<&'a str>,
178 pub namespace: Option<&'a str>,
180 pub alias: Option<&'a str>,
182 pub type_only: bool,
183 pub modifiers: &'a [String],
186 pub import_kind: Option<&'a str>,
189}
190
191const NO_MODIFIERS: &[String] = &[];
193
194impl<'a> ImportRequest<'a> {
195 pub fn legacy(
199 module_path: &'a str,
200 names: &'a [String],
201 default_import: Option<&'a str>,
202 namespace: Option<&'a str>,
203 type_only: bool,
204 ) -> Self {
205 ImportRequest {
206 module_path,
207 names,
208 default_import,
209 namespace,
210 alias: None,
211 type_only,
212 modifiers: NO_MODIFIERS,
213 import_kind: None,
214 }
215 }
216}
217
218#[derive(Debug, Clone, PartialEq, Eq)]
220pub struct ImportStatement {
221 pub module_path: String,
223 pub names: Vec<String>,
225 pub default_import: Option<String>,
227 pub namespace_import: Option<String>,
229 pub kind: ImportKind,
231 pub group: ImportGroup,
233 pub byte_range: Range<usize>,
235 pub raw_text: String,
237 pub form: ImportForm,
241}
242
243#[derive(Debug, Clone, PartialEq, Eq)]
245pub struct ImportBlock {
246 pub imports: Vec<ImportStatement>,
248 pub byte_range: Option<Range<usize>>,
251}
252
253impl ImportBlock {
254 pub fn empty() -> Self {
255 ImportBlock {
256 imports: Vec::new(),
257 byte_range: None,
258 }
259 }
260}
261
262pub(crate) fn import_byte_range(imports: &[ImportStatement]) -> Option<Range<usize>> {
263 imports.first().zip(imports.last()).map(|(first, last)| {
264 let start = first.byte_range.start;
265 let end = last.byte_range.end;
266 start..end
267 })
268}
269
270pub fn specifier_local_name(spec: &str) -> &str {
286 let trimmed = spec.trim();
287 let after_type = trimmed
288 .strip_prefix("type ")
289 .unwrap_or(trimmed)
290 .trim_start();
291 if let Some(idx) = after_type.find(" as ") {
292 after_type[idx + 4..].trim()
293 } else {
294 after_type
295 }
296}
297
298pub fn specifier_imported_name(spec: &str) -> &str {
307 let trimmed = spec.trim();
308 let after_type = trimmed
309 .strip_prefix("type ")
310 .unwrap_or(trimmed)
311 .trim_start();
312 after_type
313 .find(" as ")
314 .map(|idx| after_type[..idx].trim())
315 .unwrap_or(after_type)
316}
317
318pub fn specifier_matches(spec: &str, target: &str) -> bool {
323 specifier_imported_name(spec) == target || specifier_local_name(spec) == target
324}
325
326pub trait ImportSyntax: Sync {
339 fn parse(&self, source: &str, tree: &Tree) -> ImportBlock;
341
342 fn generate_line(&self, req: &ImportRequest) -> String;
345
346 fn classify_group(&self, module_path: &str) -> ImportGroup;
348}
349
350struct EsSyntax;
352impl ImportSyntax for EsSyntax {
353 fn parse(&self, source: &str, tree: &Tree) -> ImportBlock {
354 parse_ts_imports(source, tree)
355 }
356 fn generate_line(&self, req: &ImportRequest) -> String {
357 generate_ts_import_line(
358 req.module_path,
359 req.names,
360 req.default_import,
361 req.namespace,
362 req.type_only,
363 )
364 }
365 fn classify_group(&self, module_path: &str) -> ImportGroup {
366 classify_group_ts(module_path)
367 }
368}
369
370struct PythonSyntax;
371impl ImportSyntax for PythonSyntax {
372 fn parse(&self, source: &str, tree: &Tree) -> ImportBlock {
373 parse_py_imports(source, tree)
374 }
375 fn generate_line(&self, req: &ImportRequest) -> String {
376 generate_py_import_line(req.module_path, req.names, req.default_import)
377 }
378 fn classify_group(&self, module_path: &str) -> ImportGroup {
379 classify_group_py(module_path)
380 }
381}
382
383struct RustSyntax;
384impl ImportSyntax for RustSyntax {
385 fn parse(&self, source: &str, tree: &Tree) -> ImportBlock {
386 parse_rs_imports(source, tree)
387 }
388 fn generate_line(&self, req: &ImportRequest) -> String {
389 generate_rs_import_line(req.module_path, req.names, req.type_only)
390 }
391 fn classify_group(&self, module_path: &str) -> ImportGroup {
392 classify_group_rs(module_path)
393 }
394}
395
396struct GoSyntax;
397impl ImportSyntax for GoSyntax {
398 fn parse(&self, source: &str, tree: &Tree) -> ImportBlock {
399 parse_go_imports(source, tree)
400 }
401 fn generate_line(&self, req: &ImportRequest) -> String {
402 generate_go_import_line(req.module_path, req.default_import, false)
403 }
404 fn classify_group(&self, module_path: &str) -> ImportGroup {
405 classify_group_go(module_path)
406 }
407}
408
409struct SoliditySyntax;
412impl ImportSyntax for SoliditySyntax {
413 fn parse(&self, source: &str, tree: &Tree) -> ImportBlock {
414 parse_solidity_imports(source, tree)
415 }
416 fn generate_line(&self, req: &ImportRequest) -> String {
417 generate_solidity_import_line(req)
418 }
419 fn classify_group(&self, module_path: &str) -> ImportGroup {
420 classify_group_solidity(module_path)
421 }
422}
423
424#[derive(Debug, Clone, Copy, PartialEq, Eq)]
425pub(crate) enum VueScriptRangeError {
426 MissingScript,
427 MultipleScripts,
428}
429
430impl VueScriptRangeError {
431 pub(crate) fn code(self) -> &'static str {
432 match self {
433 VueScriptRangeError::MissingScript => "missing_vue_script",
434 VueScriptRangeError::MultipleScripts => "ambiguous_vue_script",
435 }
436 }
437
438 pub(crate) fn message(self, command: &str) -> String {
439 match self {
440 VueScriptRangeError::MissingScript => format!(
441 "{command}: Vue import management requires exactly one <script> block; found none"
442 ),
443 VueScriptRangeError::MultipleScripts => format!(
444 "{command}: Vue import management requires exactly one <script> block; found multiple"
445 ),
446 }
447 }
448}
449
450pub(crate) fn vue_single_script_content_range(
458 tree: &Tree,
459) -> Result<(usize, usize), VueScriptRangeError> {
460 let root = tree.root_node();
461 let mut ranges = Vec::new();
462 let mut cursor = root.walk();
463 for child in root.named_children(&mut cursor) {
464 if child.kind() == "script_element" {
465 ranges.push(vue_script_element_content_range(&child));
466 }
467 }
468
469 match ranges.len() {
470 0 => Err(VueScriptRangeError::MissingScript),
471 1 => Ok(ranges[0]),
472 _ => Err(VueScriptRangeError::MultipleScripts),
473 }
474}
475
476pub(crate) fn vue_script_content_range(tree: &Tree) -> Option<(usize, usize)> {
479 vue_single_script_content_range(tree).ok()
480}
481
482fn vue_script_element_content_range(child: &Node) -> (usize, usize) {
483 let mut inner = child.walk();
484 for sub in child.named_children(&mut inner) {
485 if sub.kind() == "raw_text" {
486 return (sub.start_byte(), sub.end_byte());
487 }
488 }
489
490 let mut inner2 = child.walk();
492 for sub in child.named_children(&mut inner2) {
493 if sub.kind() == "start_tag" {
494 return (sub.end_byte(), sub.end_byte());
495 }
496 }
497
498 (child.end_byte(), child.end_byte())
499}
500
501fn parse_vue_imports(source: &str, tree: &Tree) -> ImportBlock {
506 let Ok((start, end)) = vue_single_script_content_range(tree) else {
507 return ImportBlock {
508 imports: Vec::new(),
509 byte_range: None,
510 };
511 };
512 let inner = &source[start..end];
513 let mut parser = Parser::new();
514 if parser
515 .set_language(&grammar_for(LangId::TypeScript))
516 .is_err()
517 {
518 return ImportBlock {
519 imports: Vec::new(),
520 byte_range: None,
521 };
522 }
523 let Some(inner_tree) = parser.parse(inner, None) else {
524 return ImportBlock {
525 imports: Vec::new(),
526 byte_range: None,
527 };
528 };
529 let mut block = parse_ts_imports(inner, &inner_tree);
530 for imp in &mut block.imports {
531 imp.byte_range = (imp.byte_range.start + start)..(imp.byte_range.end + start);
532 }
533 block.byte_range = block.byte_range.map(|r| (r.start + start)..(r.end + start));
534 block
535}
536
537struct VueSyntax;
543impl ImportSyntax for VueSyntax {
544 fn parse(&self, source: &str, tree: &Tree) -> ImportBlock {
545 parse_vue_imports(source, tree)
546 }
547 fn generate_line(&self, req: &ImportRequest) -> String {
548 generate_ts_import_line(
549 req.module_path,
550 req.names,
551 req.default_import,
552 req.namespace,
553 req.type_only,
554 )
555 }
556 fn classify_group(&self, module_path: &str) -> ImportGroup {
557 classify_group_ts(module_path)
558 }
559}
560
561static ES_SYNTAX: EsSyntax = EsSyntax;
562static PYTHON_SYNTAX: PythonSyntax = PythonSyntax;
563static RUST_SYNTAX: RustSyntax = RustSyntax;
564static GO_SYNTAX: GoSyntax = GoSyntax;
565static SOLIDITY_SYNTAX: SoliditySyntax = SoliditySyntax;
566static VUE_SYNTAX: VueSyntax = VueSyntax;
567
568pub fn syntax_for(lang: LangId) -> Option<&'static dyn ImportSyntax> {
570 match lang {
571 LangId::TypeScript | LangId::Tsx | LangId::JavaScript => Some(&ES_SYNTAX),
572 LangId::Python => Some(&PYTHON_SYNTAX),
573 LangId::Rust => Some(&RUST_SYNTAX),
574 LangId::Go => Some(&GO_SYNTAX),
575 LangId::Solidity => Some(&SOLIDITY_SYNTAX),
576 LangId::Vue => Some(&VUE_SYNTAX),
577 LangId::C => Some(&c::C_SYNTAX),
578 LangId::Cpp => Some(&c::C_SYNTAX),
579 LangId::Java => Some(&java::JAVA_SYNTAX),
580 LangId::Kotlin => Some(&kotlin::KOTLIN_SYNTAX),
581 LangId::Lua => Some(&lua::LUA_SYNTAX),
582 LangId::CSharp => Some(&csharp::CSHARP_SYNTAX),
583 LangId::Php => Some(&php::PHP_SYNTAX),
584 LangId::Perl => Some(&perl::PERL_SYNTAX),
585 LangId::Ruby => Some(&ruby::RUBY_SYNTAX),
586 LangId::Scala => Some(&scala::SCALA_SYNTAX),
587 LangId::Swift => Some(&swift::SWIFT_SYNTAX),
588 LangId::Zig
589 | LangId::Bash
590 | LangId::Scss
591 | LangId::Json
592 | LangId::Html
593 | LangId::Markdown
594 | LangId::Yaml
595 | LangId::Pascal
596 | LangId::R
597 | LangId::Groovy
598 | LangId::ObjC => None,
599 }
600}
601
602pub fn parse_imports(source: &str, tree: &Tree, lang: LangId) -> ImportBlock {
608 match syntax_for(lang) {
609 Some(engine) => engine.parse(source, tree),
610 None => ImportBlock::empty(),
611 }
612}
613
614pub fn is_duplicate(
620 block: &ImportBlock,
621 module_path: &str,
622 names: &[String],
623 default_import: Option<&str>,
624 type_only: bool,
625) -> bool {
626 is_duplicate_with_namespace(block, module_path, names, default_import, None, type_only)
627}
628
629pub fn is_duplicate_with_namespace(
631 block: &ImportBlock,
632 module_path: &str,
633 names: &[String],
634 default_import: Option<&str>,
635 namespace_import: Option<&str>,
636 type_only: bool,
637) -> bool {
638 let target_kind = if type_only {
639 ImportKind::Type
640 } else {
641 ImportKind::Value
642 };
643
644 for imp in &block.imports {
645 if imp.module_path != module_path {
646 continue;
647 }
648
649 if names.is_empty()
655 && default_import.is_none()
656 && namespace_import.is_none()
657 && imp.names.is_empty()
658 && imp.default_import.is_none()
659 && imp.namespace_import.is_none()
660 {
661 return true;
662 }
663
664 if names.is_empty()
666 && default_import.is_none()
667 && namespace_import.is_none()
668 && imp.kind == ImportKind::SideEffect
669 {
670 return true;
671 }
672
673 if imp.kind != target_kind && imp.kind != ImportKind::SideEffect {
675 continue;
676 }
677
678 if let (Some(def), Some(namespace)) = (default_import, namespace_import) {
682 if imp.default_import.as_deref() == Some(def)
683 && imp.namespace_import.as_deref() == Some(namespace)
684 && names
685 .iter()
686 .all(|n| imp.names.iter().any(|stored| specifier_matches(stored, n)))
687 {
688 return true;
689 }
690 continue;
691 }
692
693 if names.is_empty()
697 && default_import.is_none()
698 && namespace_import.is_some()
699 && imp.namespace_import.as_deref() == namespace_import
700 {
701 return true;
702 }
703
704 if let Some(def) = default_import {
707 if namespace_import.is_none() && imp.default_import.as_deref() == Some(def) {
708 return true;
709 }
710 }
711
712 if !names.is_empty()
718 && names
719 .iter()
720 .all(|n| imp.names.iter().any(|stored| specifier_matches(stored, n)))
721 {
722 return true;
723 }
724 }
725
726 false
727}
728
729pub(crate) fn is_duplicate_import_request(
740 lang: LangId,
741 block: &ImportBlock,
742 req: &ImportRequest<'_>,
743) -> bool {
744 if lang == LangId::Php
745 && block
746 .imports
747 .iter()
748 .any(|imp| php::php_import_satisfies_request(imp, req))
749 {
750 return true;
751 }
752
753 if lang == LangId::Python && req.names.is_empty() && req.default_import.is_none() {
754 return block.imports.iter().any(|imp| {
755 matches!(
756 &imp.form,
757 ImportForm::Python {
758 from_import: false,
759 named,
760 } if named.iter().any(|specifier| {
761 specifier_imported_name(specifier) == req.module_path
762 })
763 )
764 });
765 }
766
767 if !uses_form_aware_dedup(lang) {
768 return is_duplicate_with_namespace(
769 block,
770 req.module_path,
771 req.names,
772 req.default_import,
773 req.namespace,
774 req.type_only,
775 );
776 }
777
778 let target = request_dedup_key(lang, req);
779 block
780 .imports
781 .iter()
782 .map(|imp| statement_dedup_key(lang, imp))
783 .any(|key| key == target)
784}
785
786fn uses_form_aware_dedup(lang: LangId) -> bool {
787 matches!(
788 lang,
789 LangId::Solidity
790 | LangId::C
791 | LangId::Cpp
792 | LangId::Java
793 | LangId::CSharp
794 | LangId::Php
795 | LangId::Kotlin
796 | LangId::Scala
797 | LangId::Swift
798 | LangId::Ruby
799 | LangId::Lua
800 | LangId::Perl
801 )
802}
803
804#[derive(Debug, Clone, PartialEq, Eq)]
805struct ImportDedupKey {
806 module_path: String,
807 kind: ImportKind,
808 form: ImportForm,
809}
810
811fn statement_dedup_key(lang: LangId, imp: &ImportStatement) -> ImportDedupKey {
812 canonical_dedup_key(
813 lang,
814 ImportDedupKey {
815 module_path: imp.module_path.clone(),
816 kind: imp.kind,
817 form: imp.form.clone(),
818 },
819 )
820}
821
822fn request_dedup_key(lang: LangId, req: &ImportRequest<'_>) -> ImportDedupKey {
823 let key = match lang {
824 LangId::Solidity => {
825 let kind = if req.names.is_empty() && req.namespace.is_none() && req.alias.is_none() {
826 ImportKind::SideEffect
827 } else {
828 ImportKind::Value
829 };
830 ImportDedupKey {
831 module_path: req.module_path.to_string(),
832 kind,
833 form: ImportForm::Solidity {
834 named: req.names.to_vec(),
835 namespace: req.namespace.map(str::to_string),
836 alias: req.alias.map(str::to_string),
837 },
838 }
839 }
840 LangId::C | LangId::Cpp => structured_dedup_key(
841 req.module_path,
842 ImportKind::SideEffect,
843 &[],
844 None,
845 None,
846 &[],
847 Some(req.import_kind.or(req.default_import).unwrap_or("system")),
848 ),
849 LangId::Java => {
850 let (mut module_path, modifiers) = wildcard_suffix_request(
851 req.module_path,
852 req.modifiers,
853 req.default_import == Some("*"),
854 );
855 let mut names = req.names.to_vec();
856 normalize_java_static_member_key(&mut module_path, &modifiers, &mut names);
857 structured_dedup_key(
858 &module_path,
859 ImportKind::Value,
860 &names,
861 None,
862 None,
863 &modifiers,
864 None,
865 )
866 }
867 LangId::CSharp => structured_dedup_key(
868 req.module_path,
869 ImportKind::Value,
870 &[],
871 None,
872 req.alias,
873 req.modifiers,
874 None,
875 ),
876 LangId::Php => structured_dedup_key(
877 req.module_path,
878 ImportKind::Value,
879 &[],
880 None,
881 req.alias,
882 req.modifiers,
883 req.import_kind,
884 ),
885 LangId::Kotlin => {
886 let wildcard = req.default_import == Some("*") || req.module_path.ends_with(".*");
887 let (module_path, modifiers) =
888 wildcard_suffix_request(req.module_path, req.modifiers, wildcard);
889 let alias = req
890 .alias
891 .or(req.default_import.filter(|value| *value != "*"));
892 structured_dedup_key(
893 &module_path,
894 ImportKind::Value,
895 &[],
896 None,
897 alias,
898 &modifiers,
899 None,
900 )
901 }
902 LangId::Scala => scala_request_dedup_key(req),
903 LangId::Swift => structured_dedup_key(
904 req.module_path,
905 ImportKind::Value,
906 &[],
907 None,
908 None,
909 req.modifiers,
910 req.import_kind,
911 ),
912 LangId::Ruby => {
913 let mut modifiers = req.modifiers.to_vec();
914 if !modifiers
915 .iter()
916 .any(|modifier| modifier == "quote:single" || modifier == "quote:double")
917 {
918 modifiers.push("quote:single".to_string());
919 }
920 structured_dedup_key(
921 req.module_path,
922 ImportKind::SideEffect,
923 &[],
924 None,
925 None,
926 &modifiers,
927 Some(req.import_kind.unwrap_or("require")),
928 )
929 }
930 LangId::Lua => {
931 let alias = req.default_import.or(req.alias);
932 let kind = if alias.is_some() {
933 ImportKind::Value
934 } else {
935 ImportKind::SideEffect
936 };
937 structured_dedup_key(req.module_path, kind, &[], None, alias, req.modifiers, None)
938 }
939 LangId::Perl => structured_dedup_key(
940 req.module_path,
941 ImportKind::SideEffect,
942 &[],
943 None,
944 None,
945 req.modifiers,
946 Some(req.import_kind.unwrap_or("use")),
947 ),
948 _ => structured_dedup_key(
949 req.module_path,
950 if req.type_only {
951 ImportKind::Type
952 } else {
953 ImportKind::Value
954 },
955 req.names,
956 req.namespace,
957 req.alias,
958 req.modifiers,
959 req.import_kind,
960 ),
961 };
962
963 canonical_dedup_key(lang, key)
964}
965
966fn structured_dedup_key(
967 module_path: &str,
968 kind: ImportKind,
969 named: &[String],
970 namespace: Option<&str>,
971 alias: Option<&str>,
972 modifiers: &[String],
973 import_kind: Option<&str>,
974) -> ImportDedupKey {
975 ImportDedupKey {
976 module_path: module_path.to_string(),
977 kind,
978 form: ImportForm::Structured {
979 named: named.to_vec(),
980 namespace: namespace.map(str::to_string),
981 alias: alias.map(str::to_string),
982 modifiers: modifiers.to_vec(),
983 import_kind: import_kind.map(str::to_string),
984 },
985 }
986}
987
988fn wildcard_suffix_request(
989 module_path: &str,
990 modifiers: &[String],
991 wildcard: bool,
992) -> (String, Vec<String>) {
993 let stripped = module_path.strip_suffix(".*").unwrap_or(module_path);
994 let mut modifiers = modifiers.to_vec();
995 if (wildcard || stripped.len() != module_path.len())
996 && !modifiers.iter().any(|modifier| modifier == "wildcard")
997 {
998 modifiers.push("wildcard".to_string());
999 }
1000 (stripped.to_string(), modifiers)
1001}
1002
1003fn normalize_java_static_member_key(
1004 module_path: &mut String,
1005 modifiers: &[String],
1006 names: &mut Vec<String>,
1007) {
1008 let is_static = modifiers.iter().any(|modifier| modifier == "static");
1009 let is_wildcard = modifiers.iter().any(|modifier| modifier == "wildcard");
1010 if !is_static || is_wildcard || !names.is_empty() {
1011 return;
1012 }
1013
1014 if let Some((prefix, member)) = module_path.rsplit_once('.') {
1015 if !prefix.is_empty() && !member.is_empty() {
1016 names.push(member.to_string());
1017 *module_path = prefix.to_string();
1018 }
1019 }
1020}
1021
1022fn scala_request_dedup_key(req: &ImportRequest<'_>) -> ImportDedupKey {
1023 let mut module_path = req.module_path.to_string();
1024 let mut names: Vec<String> = req
1025 .names
1026 .iter()
1027 .map(|name| normalize_scala_selector_for_dedup(name))
1028 .collect();
1029 let mut identity_modifiers: Vec<String> = req
1030 .modifiers
1031 .iter()
1032 .filter(|modifier| modifier.as_str() != "scala2")
1033 .cloned()
1034 .collect();
1035 let mut import_kind = req.import_kind.map(str::to_string);
1036
1037 if req.default_import == Some("given") || module_path.ends_with(".given") {
1038 import_kind.get_or_insert_with(|| "given".to_string());
1039 if let Some(stripped) = module_path.strip_suffix(".given") {
1040 module_path = stripped.to_string();
1041 }
1042 }
1043
1044 if matches!(req.default_import, Some("*") | Some("_"))
1045 || matches!(req.namespace, Some("*") | Some("_"))
1046 || module_path.ends_with(".*")
1047 || module_path.ends_with("._")
1048 {
1049 if !identity_modifiers
1050 .iter()
1051 .any(|modifier| modifier == "wildcard")
1052 {
1053 identity_modifiers.push("wildcard".to_string());
1054 }
1055 module_path = module_path
1056 .strip_suffix(".*")
1057 .or_else(|| module_path.strip_suffix("._"))
1058 .unwrap_or(&module_path)
1059 .to_string();
1060 }
1061
1062 if names.is_empty() {
1063 if let Some(alias) = req.alias.filter(|alias| !alias.is_empty()) {
1064 if let Some((prefix, leaf)) = module_path.rsplit_once('.') {
1065 names.push(format!("{leaf} as {alias}"));
1066 module_path = prefix.to_string();
1067 }
1068 }
1069 }
1070
1071 structured_dedup_key(
1072 &module_path,
1073 ImportKind::Value,
1074 &names,
1075 None,
1076 None,
1077 &identity_modifiers,
1078 import_kind.as_deref(),
1079 )
1080}
1081
1082fn normalize_scala_selector_for_dedup(name: &str) -> String {
1083 let trimmed = name.trim();
1084 if let Some((from, to)) = trimmed.split_once("=>") {
1085 format!("{} as {}", from.trim(), to.trim())
1086 } else {
1087 trimmed.to_string()
1088 }
1089}
1090
1091fn canonical_dedup_key(lang: LangId, mut key: ImportDedupKey) -> ImportDedupKey {
1092 match &mut key.form {
1093 ImportForm::Structured { named, .. } | ImportForm::Solidity { named, .. } => {
1094 sort_named_specifiers(named);
1095 }
1096 ImportForm::Es { named, .. } | ImportForm::Python { named, .. } => {
1097 sort_named_specifiers(named);
1098 }
1099 ImportForm::RustUse { named, .. } => {
1100 sort_named_specifiers(named);
1101 }
1102 ImportForm::Go { .. } | ImportForm::Php { .. } => {}
1103 }
1104
1105 if matches!(lang, LangId::Java | LangId::Kotlin) {
1106 if let Some(stripped) = key.module_path.strip_suffix(".*") {
1107 key.module_path = stripped.to_string();
1108 }
1109 if matches!(lang, LangId::Java) {
1110 if let ImportForm::Structured {
1111 named, modifiers, ..
1112 } = &mut key.form
1113 {
1114 normalize_java_static_member_key(&mut key.module_path, modifiers, named);
1115 }
1116 }
1117 } else if matches!(lang, LangId::Scala) {
1118 key.module_path = key
1119 .module_path
1120 .strip_suffix(".given")
1121 .or_else(|| key.module_path.strip_suffix(".*"))
1122 .or_else(|| key.module_path.strip_suffix("._"))
1123 .unwrap_or(&key.module_path)
1124 .to_string();
1125 }
1126
1127 key
1128}
1129
1130fn sort_named_specifiers(names: &mut [String]) {
1131 names.sort_by(|a, b| {
1132 specifier_imported_name(a)
1133 .cmp(specifier_imported_name(b))
1134 .then_with(|| a.cmp(b))
1135 });
1136}
1137
1138pub fn find_insertion_point(
1149 source: &str,
1150 block: &ImportBlock,
1151 group: ImportGroup,
1152 module_path: &str,
1153 type_only: bool,
1154) -> (usize, bool, bool) {
1155 if block.imports.is_empty() {
1156 return (0, false, source.is_empty().then_some(false).unwrap_or(true));
1158 }
1159
1160 let target_kind = if type_only {
1161 ImportKind::Type
1162 } else {
1163 ImportKind::Value
1164 };
1165
1166 let group_imports: Vec<&ImportStatement> =
1168 block.imports.iter().filter(|i| i.group == group).collect();
1169
1170 if group_imports.is_empty() {
1171 let preceding_last = block.imports.iter().filter(|i| i.group < group).last();
1174
1175 if let Some(last) = preceding_last {
1176 let end = last.byte_range.end;
1177 let insert_at = skip_newline(source, end);
1178 return (insert_at, true, true);
1179 }
1180
1181 let following_first = block.imports.iter().find(|i| i.group > group);
1183
1184 if let Some(first) = following_first {
1185 return (first.byte_range.start, false, true);
1186 }
1187
1188 let first_byte = import_byte_range(&block.imports)
1190 .map(|range| range.start)
1191 .unwrap_or(0);
1192 return (first_byte, false, true);
1193 }
1194
1195 for imp in &group_imports {
1197 let cmp = module_path.cmp(&imp.module_path);
1198 match cmp {
1199 std::cmp::Ordering::Less => {
1200 return (imp.byte_range.start, false, false);
1202 }
1203 std::cmp::Ordering::Equal => {
1204 if target_kind == ImportKind::Type && imp.kind == ImportKind::Value {
1206 let end = imp.byte_range.end;
1208 let insert_at = skip_newline(source, end);
1209 return (insert_at, false, false);
1210 }
1211 return (imp.byte_range.start, false, false);
1213 }
1214 std::cmp::Ordering::Greater => continue,
1215 }
1216 }
1217
1218 let Some(last) = group_imports.last() else {
1220 return (
1221 import_byte_range(&block.imports)
1222 .map(|range| range.end)
1223 .unwrap_or(0),
1224 false,
1225 false,
1226 );
1227 };
1228 let end = last.byte_range.end;
1229 let insert_at = skip_newline(source, end);
1230 (insert_at, false, false)
1231}
1232
1233pub fn generate_import(lang: LangId, req: &ImportRequest) -> String {
1237 match syntax_for(lang) {
1238 Some(engine) => engine.generate_line(req),
1239 None => String::new(),
1240 }
1241}
1242
1243pub fn generate_import_line(
1246 lang: LangId,
1247 module_path: &str,
1248 names: &[String],
1249 default_import: Option<&str>,
1250 type_only: bool,
1251) -> String {
1252 generate_import(
1253 lang,
1254 &ImportRequest::legacy(module_path, names, default_import, None, type_only),
1255 )
1256}
1257
1258pub fn generate_import_line_with_namespace(
1261 lang: LangId,
1262 module_path: &str,
1263 names: &[String],
1264 default_import: Option<&str>,
1265 namespace_import: Option<&str>,
1266 type_only: bool,
1267) -> String {
1268 generate_import_line_with_namespace_and_attribute_clause(
1269 lang,
1270 module_path,
1271 names,
1272 default_import,
1273 namespace_import,
1274 type_only,
1275 None,
1276 )
1277}
1278
1279pub(crate) fn generate_import_line_with_namespace_and_attribute_clause(
1282 lang: LangId,
1283 module_path: &str,
1284 names: &[String],
1285 default_import: Option<&str>,
1286 namespace_import: Option<&str>,
1287 type_only: bool,
1288 attribute_clause: Option<&str>,
1289) -> String {
1290 if matches!(
1291 lang,
1292 LangId::TypeScript | LangId::Tsx | LangId::JavaScript | LangId::Vue
1293 ) {
1294 return generate_ts_import_line_with_attribute_clause(
1295 module_path,
1296 names,
1297 default_import,
1298 namespace_import,
1299 type_only,
1300 attribute_clause,
1301 );
1302 }
1303
1304 generate_import(
1305 lang,
1306 &ImportRequest::legacy(
1307 module_path,
1308 names,
1309 default_import,
1310 namespace_import,
1311 type_only,
1312 ),
1313 )
1314}
1315
1316pub fn is_supported(lang: LangId) -> bool {
1318 syntax_for(lang).is_some()
1319}
1320
1321pub fn classify_group_ts(module_path: &str) -> ImportGroup {
1323 if module_path.starts_with('.') {
1324 ImportGroup::Internal
1325 } else {
1326 ImportGroup::External
1327 }
1328}
1329
1330pub fn classify_group(lang: LangId, module_path: &str) -> ImportGroup {
1332 match syntax_for(lang) {
1333 Some(engine) => engine.classify_group(module_path),
1334 None => ImportGroup::External,
1337 }
1338}
1339
1340pub fn parse_file_imports(
1343 path: &std::path::Path,
1344 lang: LangId,
1345) -> Result<(String, Tree, ImportBlock), crate::error::AftError> {
1346 let source =
1347 std::fs::read_to_string(path).map_err(|e| crate::error::AftError::FileNotFound {
1348 path: format!("{}: {}", path.display(), e),
1349 })?;
1350
1351 let grammar = grammar_for(lang);
1352 let mut parser = Parser::new();
1353 parser
1354 .set_language(&grammar)
1355 .map_err(|e| crate::error::AftError::ParseError {
1356 message: format!("grammar init failed for {:?}: {}", lang, e),
1357 })?;
1358
1359 let tree = parser
1360 .parse(&source, None)
1361 .ok_or_else(|| crate::error::AftError::ParseError {
1362 message: format!("tree-sitter parse returned None for {}", path.display()),
1363 })?;
1364
1365 let block = parse_imports(&source, &tree, lang);
1366 Ok((source, tree, block))
1367}
1368
1369fn parse_ts_imports(source: &str, tree: &Tree) -> ImportBlock {
1377 let root = tree.root_node();
1378 let mut imports = Vec::new();
1379
1380 let mut cursor = root.walk();
1381 if !cursor.goto_first_child() {
1382 return ImportBlock::empty();
1383 }
1384
1385 loop {
1386 let node = cursor.node();
1387 if node.kind() == "import_statement" {
1388 if let Some(imp) = parse_single_ts_import(source, &node) {
1389 imports.push(imp);
1390 }
1391 }
1392 if !cursor.goto_next_sibling() {
1393 break;
1394 }
1395 }
1396
1397 let byte_range = import_byte_range(&imports);
1398
1399 ImportBlock {
1400 imports,
1401 byte_range,
1402 }
1403}
1404
1405fn parse_single_ts_import(source: &str, node: &Node) -> Option<ImportStatement> {
1407 let raw_text = source[node.byte_range()].to_string();
1408 let byte_range = node.byte_range();
1409
1410 let module_path = extract_module_path(source, node)?;
1412
1413 let is_type_only = has_type_keyword(node);
1415
1416 let mut names = Vec::new();
1418 let mut default_import = None;
1419 let mut namespace_import = None;
1420
1421 let mut child_cursor = node.walk();
1422 if child_cursor.goto_first_child() {
1423 loop {
1424 let child = child_cursor.node();
1425 match child.kind() {
1426 "import_clause" => {
1427 extract_import_clause(
1428 source,
1429 &child,
1430 &mut names,
1431 &mut default_import,
1432 &mut namespace_import,
1433 );
1434 }
1435 "identifier" => {
1437 let text = &source[child.byte_range()];
1438 if text != "import" && text != "from" && text != "type" {
1439 default_import = Some(text.to_string());
1440 }
1441 }
1442 _ => {}
1443 }
1444 if !child_cursor.goto_next_sibling() {
1445 break;
1446 }
1447 }
1448 }
1449
1450 let kind = if names.is_empty() && default_import.is_none() && namespace_import.is_none() {
1452 ImportKind::SideEffect
1453 } else if is_type_only {
1454 ImportKind::Type
1455 } else {
1456 ImportKind::Value
1457 };
1458
1459 let group = classify_group_ts(&module_path);
1460
1461 let attribute_clause = extract_es_import_attribute_clause(source, node);
1462 let attribute_type = attribute_clause
1463 .as_deref()
1464 .and_then(parse_es_import_attribute_type);
1465 let form = ImportForm::Es {
1466 default_import: default_import.clone(),
1467 namespace_import: namespace_import.clone(),
1468 named: names.clone(),
1469 type_only: is_type_only,
1470 side_effect: matches!(kind, ImportKind::SideEffect),
1471 attribute_clause,
1472 attribute_type,
1473 };
1474
1475 Some(ImportStatement {
1476 module_path,
1477 names,
1478 default_import,
1479 namespace_import,
1480 kind,
1481 group,
1482 byte_range,
1483 raw_text,
1484 form,
1485 })
1486}
1487
1488fn extract_es_import_attribute_clause(source: &str, node: &Node) -> Option<String> {
1491 let mut cursor = node.walk();
1492 if !cursor.goto_first_child() {
1493 return None;
1494 }
1495
1496 let module_end = loop {
1497 let child = cursor.node();
1498 if child.kind() == "string" {
1499 break child.end_byte();
1500 }
1501 if !cursor.goto_next_sibling() {
1502 return None;
1503 }
1504 };
1505
1506 let tail = source[module_end..node.end_byte()].trim();
1507 let clause = tail.strip_suffix(';').unwrap_or(tail).trim();
1508 (clause.starts_with("with") || clause.starts_with("assert")).then(|| clause.to_string())
1509}
1510
1511pub(crate) fn es_import_attribute_clause(imp: &ImportStatement) -> Option<&str> {
1513 match &imp.form {
1514 ImportForm::Es {
1515 attribute_clause, ..
1516 } => attribute_clause.as_deref(),
1517 _ => None,
1518 }
1519}
1520
1521pub(crate) fn es_import_attribute_type(imp: &ImportStatement) -> Option<&str> {
1523 match &imp.form {
1524 ImportForm::Es { attribute_type, .. } => attribute_type.as_deref(),
1525 _ => None,
1526 }
1527}
1528
1529fn parse_es_import_attribute_type(clause: &str) -> Option<String> {
1533 let body = ["with", "assert"].into_iter().find_map(|keyword| {
1534 let rest = clause.strip_prefix(keyword)?;
1535 rest.chars()
1536 .next()
1537 .is_some_and(|ch| ch.is_whitespace() || ch == '{' || ch == '/')
1538 .then_some(rest)
1539 })?;
1540 let synthetic = format!("import 'module' with{body};");
1541 let mut parser = Parser::new();
1542 parser.set_language(&grammar_for(LangId::TypeScript)).ok()?;
1543 let tree = parser.parse(&synthetic, None)?;
1544 (!tree.root_node().has_error())
1545 .then(|| find_type_attribute(&synthetic, tree.root_node()))
1546 .flatten()
1547}
1548
1549fn find_type_attribute(source: &str, node: Node<'_>) -> Option<String> {
1550 if node.kind() == "pair" {
1551 let key = node
1552 .child_by_field_name("key")
1553 .or_else(|| node.named_child(0))?;
1554 let value = node
1555 .child_by_field_name("value")
1556 .or_else(|| node.named_child(1))?;
1557 if decode_js_attribute_atom(source.get(key.byte_range())?)?.as_str() == "type" {
1558 return decode_js_attribute_atom(source.get(value.byte_range())?);
1559 }
1560 }
1561
1562 let mut cursor = node.walk();
1563 let found = node
1564 .children(&mut cursor)
1565 .find_map(|child| find_type_attribute(source, child));
1566 found
1567}
1568
1569fn decode_js_attribute_atom(text: &str) -> Option<String> {
1570 let text = text.trim();
1571 let quote = text.as_bytes().first().copied();
1572 if !matches!(quote, Some(b'\'') | Some(b'"')) {
1573 return Some(text.to_string());
1574 }
1575 if text.as_bytes().last().copied() != quote || text.len() < 2 {
1576 return None;
1577 }
1578
1579 let mut chars = text[1..text.len() - 1].chars();
1580 let mut decoded = String::new();
1581 while let Some(ch) = chars.next() {
1582 if ch != '\\' {
1583 decoded.push(ch);
1584 continue;
1585 }
1586
1587 match chars.next()? {
1588 '\n' => {}
1589 '\r' => {
1590 if chars.clone().next() == Some('\n') {
1591 chars.next();
1592 }
1593 }
1594 'b' => decoded.push('\u{0008}'),
1595 'f' => decoded.push('\u{000c}'),
1596 'n' => decoded.push('\n'),
1597 'r' => decoded.push('\r'),
1598 't' => decoded.push('\t'),
1599 'v' => decoded.push('\u{000b}'),
1600 '0' => decoded.push('\0'),
1601 'x' => decoded.push(decode_hex_escape(&mut chars, 2)?),
1602 'u' => {
1603 let digits = if chars.clone().next() == Some('{') {
1604 chars.next();
1605 let mut digits = String::new();
1606 loop {
1607 let ch = chars.next()?;
1608 if ch == '}' {
1609 break;
1610 }
1611 digits.push(ch);
1612 }
1613 digits
1614 } else {
1615 chars.by_ref().take(4).collect()
1616 };
1617 decoded.push(char::from_u32(u32::from_str_radix(&digits, 16).ok()?)?);
1618 }
1619 escaped => decoded.push(escaped),
1620 }
1621 }
1622 Some(decoded)
1623}
1624
1625fn decode_hex_escape(chars: &mut impl Iterator<Item = char>, count: usize) -> Option<char> {
1626 let digits: String = chars.take(count).collect();
1627 (digits.len() == count)
1628 .then(|| u32::from_str_radix(&digits, 16).ok())
1629 .flatten()
1630 .and_then(char::from_u32)
1631}
1632
1633fn extract_module_path(source: &str, node: &Node) -> Option<String> {
1637 let mut cursor = node.walk();
1638 if !cursor.goto_first_child() {
1639 return None;
1640 }
1641
1642 loop {
1643 let child = cursor.node();
1644 if child.kind() == "string" {
1645 let text = &source[child.byte_range()];
1647 let stripped = text
1648 .trim_start_matches(|c| c == '\'' || c == '"')
1649 .trim_end_matches(|c| c == '\'' || c == '"');
1650 return Some(stripped.to_string());
1651 }
1652 if !cursor.goto_next_sibling() {
1653 break;
1654 }
1655 }
1656 None
1657}
1658
1659fn has_type_keyword(node: &Node) -> bool {
1664 let mut cursor = node.walk();
1665 if !cursor.goto_first_child() {
1666 return false;
1667 }
1668
1669 loop {
1670 let child = cursor.node();
1671 if child.kind() == "type" {
1672 return true;
1673 }
1674 if !cursor.goto_next_sibling() {
1675 break;
1676 }
1677 }
1678
1679 false
1680}
1681
1682fn extract_import_clause(
1684 source: &str,
1685 node: &Node,
1686 names: &mut Vec<String>,
1687 default_import: &mut Option<String>,
1688 namespace_import: &mut Option<String>,
1689) {
1690 let mut cursor = node.walk();
1691 if !cursor.goto_first_child() {
1692 return;
1693 }
1694
1695 loop {
1696 let child = cursor.node();
1697 match child.kind() {
1698 "identifier" => {
1699 let text = &source[child.byte_range()];
1701 if text != "type" {
1702 *default_import = Some(text.to_string());
1703 }
1704 }
1705 "named_imports" => {
1706 extract_named_imports(source, &child, names);
1708 }
1709 "namespace_import" => {
1710 extract_namespace_import(source, &child, namespace_import);
1712 }
1713 _ => {}
1714 }
1715 if !cursor.goto_next_sibling() {
1716 break;
1717 }
1718 }
1719}
1720
1721fn extract_named_imports(source: &str, node: &Node, names: &mut Vec<String>) {
1740 let mut cursor = node.walk();
1741 if !cursor.goto_first_child() {
1742 return;
1743 }
1744
1745 loop {
1746 let child = cursor.node();
1747 if child.kind() == "import_specifier" {
1748 let raw = source[child.byte_range()].trim().to_string();
1753 if !raw.is_empty() {
1754 names.push(raw);
1755 } else if let Some(name_node) = child.child_by_field_name("name") {
1756 names.push(source[name_node.byte_range()].to_string());
1757 }
1758 }
1759 if !cursor.goto_next_sibling() {
1760 break;
1761 }
1762 }
1763}
1764
1765fn extract_namespace_import(source: &str, node: &Node, namespace_import: &mut Option<String>) {
1767 let mut cursor = node.walk();
1768 if !cursor.goto_first_child() {
1769 return;
1770 }
1771
1772 loop {
1773 let child = cursor.node();
1774 if child.kind() == "identifier" {
1775 *namespace_import = Some(source[child.byte_range()].to_string());
1776 return;
1777 }
1778 if !cursor.goto_next_sibling() {
1779 break;
1780 }
1781 }
1782}
1783
1784fn generate_ts_import_line(
1786 module_path: &str,
1787 names: &[String],
1788 default_import: Option<&str>,
1789 namespace_import: Option<&str>,
1790 type_only: bool,
1791) -> String {
1792 generate_ts_import_line_with_attribute_clause(
1793 module_path,
1794 names,
1795 default_import,
1796 namespace_import,
1797 type_only,
1798 None,
1799 )
1800}
1801
1802fn generate_ts_import_line_with_attribute_clause(
1803 module_path: &str,
1804 names: &[String],
1805 default_import: Option<&str>,
1806 namespace_import: Option<&str>,
1807 type_only: bool,
1808 attribute_clause: Option<&str>,
1809) -> String {
1810 let line = generate_ts_import_line_base(
1811 module_path,
1812 names,
1813 default_import,
1814 namespace_import,
1815 type_only,
1816 );
1817 let Some(attribute_clause) = attribute_clause else {
1818 return line;
1819 };
1820
1821 let line = line.strip_suffix(';').unwrap_or(&line);
1822 format!("{line} {};", attribute_clause.trim())
1823}
1824
1825fn generate_ts_import_line_base(
1826 module_path: &str,
1827 names: &[String],
1828 default_import: Option<&str>,
1829 namespace_import: Option<&str>,
1830 type_only: bool,
1831) -> String {
1832 let type_prefix = if type_only { "type " } else { "" };
1833
1834 if names.is_empty() && default_import.is_none() && namespace_import.is_none() {
1836 return format!("import '{module_path}';");
1837 }
1838
1839 if names.is_empty() && default_import.is_none() {
1841 if let Some(namespace) = namespace_import {
1842 return format!("import {type_prefix}* as {namespace} from '{module_path}';");
1843 }
1844 }
1845
1846 if names.is_empty() {
1848 if let (Some(def), Some(namespace)) = (default_import, namespace_import) {
1849 return format!("import {type_prefix}{def}, * as {namespace} from '{module_path}';");
1850 }
1851 }
1852
1853 if names.is_empty() && namespace_import.is_none() {
1855 if let Some(def) = default_import {
1856 return format!("import {type_prefix}{def} from '{module_path}';");
1857 }
1858 }
1859
1860 if default_import.is_none() && namespace_import.is_none() {
1862 let mut sorted_names = names.to_vec();
1863 sort_named_specifiers(&mut sorted_names);
1864 let names_str = sorted_names.join(", ");
1865 return format!("import {type_prefix}{{ {names_str} }} from '{module_path}';");
1866 }
1867
1868 if default_import.is_none() {
1870 if let Some(namespace) = namespace_import {
1871 let mut sorted_names = names.to_vec();
1872 sort_named_specifiers(&mut sorted_names);
1873 let names_str = sorted_names.join(", ");
1874 return format!(
1875 "import {type_prefix}{{ {names_str} }}, * as {namespace} from '{module_path}';"
1876 );
1877 }
1878 }
1879
1880 if let (Some(def), Some(namespace)) = (default_import, namespace_import) {
1882 let mut sorted_names = names.to_vec();
1883 sort_named_specifiers(&mut sorted_names);
1884 let names_str = sorted_names.join(", ");
1885 return format!(
1886 "import {type_prefix}{def}, {{ {names_str} }}, * as {namespace} from '{module_path}';"
1887 );
1888 }
1889
1890 if let Some(def) = default_import {
1892 let mut sorted_names = names.to_vec();
1893 sort_named_specifiers(&mut sorted_names);
1894 let names_str = sorted_names.join(", ");
1895 return format!("import {type_prefix}{def}, {{ {names_str} }} from '{module_path}';");
1896 }
1897
1898 format!("import '{module_path}';")
1900}
1901
1902const PYTHON_STDLIB: &[&str] = &[
1910 "__future__",
1911 "_thread",
1912 "abc",
1913 "aifc",
1914 "argparse",
1915 "array",
1916 "ast",
1917 "asynchat",
1918 "asyncio",
1919 "asyncore",
1920 "atexit",
1921 "audioop",
1922 "base64",
1923 "bdb",
1924 "binascii",
1925 "bisect",
1926 "builtins",
1927 "bz2",
1928 "calendar",
1929 "cgi",
1930 "cgitb",
1931 "chunk",
1932 "cmath",
1933 "cmd",
1934 "code",
1935 "codecs",
1936 "codeop",
1937 "collections",
1938 "colorsys",
1939 "compileall",
1940 "concurrent",
1941 "configparser",
1942 "contextlib",
1943 "contextvars",
1944 "copy",
1945 "copyreg",
1946 "cProfile",
1947 "crypt",
1948 "csv",
1949 "ctypes",
1950 "curses",
1951 "dataclasses",
1952 "datetime",
1953 "dbm",
1954 "decimal",
1955 "difflib",
1956 "dis",
1957 "distutils",
1958 "doctest",
1959 "email",
1960 "encodings",
1961 "enum",
1962 "errno",
1963 "faulthandler",
1964 "fcntl",
1965 "filecmp",
1966 "fileinput",
1967 "fnmatch",
1968 "fractions",
1969 "ftplib",
1970 "functools",
1971 "gc",
1972 "getopt",
1973 "getpass",
1974 "gettext",
1975 "glob",
1976 "grp",
1977 "gzip",
1978 "hashlib",
1979 "heapq",
1980 "hmac",
1981 "html",
1982 "http",
1983 "idlelib",
1984 "imaplib",
1985 "imghdr",
1986 "importlib",
1987 "inspect",
1988 "io",
1989 "ipaddress",
1990 "itertools",
1991 "json",
1992 "keyword",
1993 "lib2to3",
1994 "linecache",
1995 "locale",
1996 "logging",
1997 "lzma",
1998 "mailbox",
1999 "mailcap",
2000 "marshal",
2001 "math",
2002 "mimetypes",
2003 "mmap",
2004 "modulefinder",
2005 "multiprocessing",
2006 "netrc",
2007 "numbers",
2008 "operator",
2009 "optparse",
2010 "os",
2011 "pathlib",
2012 "pdb",
2013 "pickle",
2014 "pickletools",
2015 "pipes",
2016 "pkgutil",
2017 "platform",
2018 "plistlib",
2019 "poplib",
2020 "posixpath",
2021 "pprint",
2022 "profile",
2023 "pstats",
2024 "pty",
2025 "pwd",
2026 "py_compile",
2027 "pyclbr",
2028 "pydoc",
2029 "queue",
2030 "quopri",
2031 "random",
2032 "re",
2033 "readline",
2034 "reprlib",
2035 "resource",
2036 "rlcompleter",
2037 "runpy",
2038 "sched",
2039 "secrets",
2040 "select",
2041 "selectors",
2042 "shelve",
2043 "shlex",
2044 "shutil",
2045 "signal",
2046 "site",
2047 "smtplib",
2048 "sndhdr",
2049 "socket",
2050 "socketserver",
2051 "sqlite3",
2052 "ssl",
2053 "stat",
2054 "statistics",
2055 "string",
2056 "stringprep",
2057 "struct",
2058 "subprocess",
2059 "symtable",
2060 "sys",
2061 "sysconfig",
2062 "syslog",
2063 "tabnanny",
2064 "tarfile",
2065 "tempfile",
2066 "termios",
2067 "textwrap",
2068 "threading",
2069 "time",
2070 "timeit",
2071 "tkinter",
2072 "token",
2073 "tokenize",
2074 "tomllib",
2075 "trace",
2076 "traceback",
2077 "tracemalloc",
2078 "tty",
2079 "turtle",
2080 "types",
2081 "typing",
2082 "unicodedata",
2083 "unittest",
2084 "urllib",
2085 "uuid",
2086 "venv",
2087 "warnings",
2088 "wave",
2089 "weakref",
2090 "webbrowser",
2091 "wsgiref",
2092 "xml",
2093 "xmlrpc",
2094 "zipapp",
2095 "zipfile",
2096 "zipimport",
2097 "zlib",
2098];
2099
2100pub fn classify_group_py(module_path: &str) -> ImportGroup {
2102 if module_path.starts_with('.') {
2104 return ImportGroup::Internal;
2105 }
2106 let top_module = module_path.split('.').next().unwrap_or(module_path);
2108 if PYTHON_STDLIB.contains(&top_module) {
2109 ImportGroup::Stdlib
2110 } else {
2111 ImportGroup::External
2112 }
2113}
2114
2115fn parse_py_imports(source: &str, tree: &Tree) -> ImportBlock {
2117 let root = tree.root_node();
2118 let mut imports = Vec::new();
2119
2120 let mut cursor = root.walk();
2121 if !cursor.goto_first_child() {
2122 return ImportBlock::empty();
2123 }
2124
2125 loop {
2126 let node = cursor.node();
2127 match node.kind() {
2128 "import_statement" => {
2129 if let Some(imp) = parse_py_import_statement(source, &node) {
2130 imports.push(imp);
2131 }
2132 }
2133 "import_from_statement" => {
2134 if let Some(imp) = parse_py_import_from_statement(source, &node) {
2135 imports.push(imp);
2136 }
2137 }
2138 _ => {}
2139 }
2140 if !cursor.goto_next_sibling() {
2141 break;
2142 }
2143 }
2144
2145 let byte_range = import_byte_range(&imports);
2146
2147 ImportBlock {
2148 imports,
2149 byte_range,
2150 }
2151}
2152
2153fn parse_py_import_statement(source: &str, node: &Node) -> Option<ImportStatement> {
2155 let raw_text = source[node.byte_range()].to_string();
2156 let byte_range = node.byte_range();
2157
2158 let mut specifiers = Vec::new();
2162 let mut c = node.walk();
2163 if c.goto_first_child() {
2164 loop {
2165 let child = c.node();
2166 if matches!(child.kind(), "dotted_name" | "aliased_import") {
2167 specifiers.push(source[child.byte_range()].trim().to_string());
2168 }
2169 if !c.goto_next_sibling() {
2170 break;
2171 }
2172 }
2173 }
2174 let module_path = specifiers
2175 .first()
2176 .map(|specifier| specifier_imported_name(specifier).to_string())?;
2177
2178 let group = classify_group_py(&module_path);
2179
2180 Some(ImportStatement {
2181 module_path,
2182 names: Vec::new(),
2183 default_import: None,
2184 namespace_import: None,
2185 kind: ImportKind::Value,
2186 group,
2187 byte_range,
2188 raw_text,
2189 form: ImportForm::Python {
2190 from_import: false,
2191 named: specifiers,
2192 },
2193 })
2194}
2195
2196fn parse_py_import_from_statement(source: &str, node: &Node) -> Option<ImportStatement> {
2198 let raw_text = source[node.byte_range()].to_string();
2199 let byte_range = node.byte_range();
2200
2201 let mut module_path = String::new();
2202 let mut names = Vec::new();
2203
2204 let mut c = node.walk();
2205 if c.goto_first_child() {
2206 loop {
2207 let child = c.node();
2208 match child.kind() {
2209 "dotted_name" => {
2210 if module_path.is_empty()
2215 && !has_seen_import_keyword(source, node, child.start_byte())
2216 {
2217 module_path = source[child.byte_range()].to_string();
2218 } else {
2219 names.push(source[child.byte_range()].to_string());
2221 }
2222 }
2223 "relative_import" => {
2224 module_path = source[child.byte_range()].to_string();
2226 }
2227 "aliased_import" => {
2228 names.push(source[child.byte_range()].trim().to_string());
2231 }
2232 _ => {}
2233 }
2234 if !c.goto_next_sibling() {
2235 break;
2236 }
2237 }
2238 }
2239
2240 if module_path.is_empty() {
2242 return None;
2243 }
2244
2245 let group = classify_group_py(&module_path);
2246
2247 Some(ImportStatement {
2248 module_path,
2249 names: names.clone(),
2250 default_import: None,
2251 namespace_import: None,
2252 kind: ImportKind::Value,
2253 group,
2254 byte_range,
2255 raw_text,
2256 form: ImportForm::Python {
2257 from_import: true,
2258 named: names,
2259 },
2260 })
2261}
2262
2263fn has_seen_import_keyword(_source: &str, parent: &Node, before_byte: usize) -> bool {
2265 let mut c = parent.walk();
2266 if c.goto_first_child() {
2267 loop {
2268 let child = c.node();
2269 if child.kind() == "import" && child.start_byte() < before_byte {
2270 return true;
2271 }
2272 if child.start_byte() >= before_byte {
2273 return false;
2274 }
2275 if !c.goto_next_sibling() {
2276 break;
2277 }
2278 }
2279 }
2280 false
2281}
2282
2283fn generate_py_import_line(
2285 module_path: &str,
2286 names: &[String],
2287 _default_import: Option<&str>,
2288) -> String {
2289 if names.is_empty() {
2290 format!("import {module_path}")
2292 } else {
2293 let mut sorted = names.to_vec();
2295 sorted.sort();
2296 let names_str = sorted.join(", ");
2297 format!("from {module_path} import {names_str}")
2298 }
2299}
2300
2301pub fn classify_group_rs(module_path: &str) -> ImportGroup {
2307 let first_seg = module_path.split("::").next().unwrap_or(module_path);
2309 match first_seg {
2310 "std" | "core" | "alloc" => ImportGroup::Stdlib,
2311 "crate" | "self" | "super" => ImportGroup::Internal,
2312 _ => ImportGroup::External,
2313 }
2314}
2315
2316fn parse_rs_imports(source: &str, tree: &Tree) -> ImportBlock {
2318 let root = tree.root_node();
2319 let mut imports = Vec::new();
2320
2321 let mut cursor = root.walk();
2322 if !cursor.goto_first_child() {
2323 return ImportBlock::empty();
2324 }
2325
2326 loop {
2327 let node = cursor.node();
2328 if node.kind() == "use_declaration" {
2329 if let Some(imp) = parse_rs_use_declaration(source, &node) {
2330 imports.push(imp);
2331 }
2332 }
2333 if !cursor.goto_next_sibling() {
2334 break;
2335 }
2336 }
2337
2338 let byte_range = import_byte_range(&imports);
2339
2340 ImportBlock {
2341 imports,
2342 byte_range,
2343 }
2344}
2345
2346fn parse_rs_use_declaration(source: &str, node: &Node) -> Option<ImportStatement> {
2348 let raw_text = source[node.byte_range()].to_string();
2349 let byte_range = node.byte_range();
2350
2351 let mut visibility: Option<String> = None;
2355 let mut use_path = String::new();
2356 let mut names = Vec::new();
2357
2358 let mut c = node.walk();
2359 if c.goto_first_child() {
2360 loop {
2361 let child = c.node();
2362 match child.kind() {
2363 "visibility_modifier" => {
2364 visibility = Some(source[child.byte_range()].to_string());
2365 }
2366 "scoped_identifier" | "identifier" | "use_as_clause" => {
2367 use_path = source[child.byte_range()].to_string();
2369 }
2370 "scoped_use_list" => {
2371 use_path = source[child.byte_range()].to_string();
2373 extract_rs_use_list_names(source, &child, &mut names);
2375 }
2376 _ => {}
2377 }
2378 if !c.goto_next_sibling() {
2379 break;
2380 }
2381 }
2382 }
2383
2384 if use_path.is_empty() {
2385 return None;
2386 }
2387
2388 let group = classify_group_rs(&use_path);
2389
2390 Some(ImportStatement {
2391 module_path: use_path,
2392 names: names.clone(),
2393 default_import: visibility.clone(),
2396 namespace_import: None,
2397 kind: ImportKind::Value,
2398 group,
2399 byte_range,
2400 raw_text,
2401 form: ImportForm::RustUse {
2402 visibility,
2403 named: names,
2404 },
2405 })
2406}
2407
2408fn extract_rs_use_list_names(source: &str, node: &Node, names: &mut Vec<String>) {
2410 let mut c = node.walk();
2411 if c.goto_first_child() {
2412 loop {
2413 let child = c.node();
2414 if child.kind() == "use_list" {
2415 let mut lc = child.walk();
2417 if lc.goto_first_child() {
2418 loop {
2419 let lchild = lc.node();
2420 if lchild.kind() == "identifier" || lchild.kind() == "scoped_identifier" {
2421 names.push(source[lchild.byte_range()].to_string());
2422 }
2423 if !lc.goto_next_sibling() {
2424 break;
2425 }
2426 }
2427 }
2428 }
2429 if !c.goto_next_sibling() {
2430 break;
2431 }
2432 }
2433 }
2434}
2435
2436fn generate_rs_import_line(module_path: &str, names: &[String], _type_only: bool) -> String {
2438 if names.is_empty() {
2439 format!("use {module_path};")
2440 } else {
2441 let mut sorted_names = names.to_vec();
2442 sort_named_specifiers(&mut sorted_names);
2443 format!("use {module_path}::{{{}}};", sorted_names.join(", "))
2444 }
2445}
2446
2447pub fn classify_group_go(module_path: &str) -> ImportGroup {
2453 if module_path.contains('.') {
2456 ImportGroup::External
2457 } else {
2458 ImportGroup::Stdlib
2459 }
2460}
2461
2462fn parse_go_imports(source: &str, tree: &Tree) -> ImportBlock {
2464 let root = tree.root_node();
2465 let mut imports = Vec::new();
2466
2467 let mut cursor = root.walk();
2468 if !cursor.goto_first_child() {
2469 return ImportBlock::empty();
2470 }
2471
2472 loop {
2473 let node = cursor.node();
2474 if node.kind() == "import_declaration" {
2475 parse_go_import_declaration(source, &node, &mut imports);
2476 }
2477 if !cursor.goto_next_sibling() {
2478 break;
2479 }
2480 }
2481
2482 let byte_range = import_byte_range(&imports);
2483
2484 ImportBlock {
2485 imports,
2486 byte_range,
2487 }
2488}
2489
2490fn parse_go_import_declaration(source: &str, node: &Node, imports: &mut Vec<ImportStatement>) {
2492 let mut c = node.walk();
2493 if c.goto_first_child() {
2494 loop {
2495 let child = c.node();
2496 match child.kind() {
2497 "import_spec" => {
2498 if let Some(imp) = parse_go_import_spec(source, &child) {
2499 imports.push(imp);
2500 }
2501 }
2502 "import_spec_list" => {
2503 let mut lc = child.walk();
2505 if lc.goto_first_child() {
2506 loop {
2507 if lc.node().kind() == "import_spec" {
2508 if let Some(imp) = parse_go_import_spec(source, &lc.node()) {
2509 imports.push(imp);
2510 }
2511 }
2512 if !lc.goto_next_sibling() {
2513 break;
2514 }
2515 }
2516 }
2517 }
2518 _ => {}
2519 }
2520 if !c.goto_next_sibling() {
2521 break;
2522 }
2523 }
2524 }
2525}
2526
2527fn parse_go_import_spec(source: &str, node: &Node) -> Option<ImportStatement> {
2529 let raw_text = source[node.byte_range()].to_string();
2530 let byte_range = node.byte_range();
2531
2532 let mut import_path = String::new();
2533 let mut alias = None;
2534
2535 let mut c = node.walk();
2536 if c.goto_first_child() {
2537 loop {
2538 let child = c.node();
2539 match child.kind() {
2540 "interpreted_string_literal" => {
2541 let text = source[child.byte_range()].to_string();
2543 import_path = text.trim_matches('"').to_string();
2544 }
2545 "identifier" | "blank_identifier" | "dot" => {
2546 alias = Some(source[child.byte_range()].to_string());
2548 }
2549 _ => {}
2550 }
2551 if !c.goto_next_sibling() {
2552 break;
2553 }
2554 }
2555 }
2556
2557 if import_path.is_empty() {
2558 return None;
2559 }
2560
2561 let group = classify_group_go(&import_path);
2562
2563 Some(ImportStatement {
2564 module_path: import_path,
2565 names: Vec::new(),
2566 default_import: alias.clone(),
2567 namespace_import: None,
2568 kind: ImportKind::Value,
2569 group,
2570 byte_range,
2571 raw_text,
2572 form: ImportForm::Go { alias },
2573 })
2574}
2575
2576pub fn generate_go_import_line_pub(
2578 module_path: &str,
2579 alias: Option<&str>,
2580 in_group: bool,
2581) -> String {
2582 generate_go_import_line(module_path, alias, in_group)
2583}
2584
2585fn generate_go_import_line(module_path: &str, alias: Option<&str>, in_group: bool) -> String {
2590 if in_group {
2591 match alias {
2593 Some(a) => format!("\t{a} \"{module_path}\""),
2594 None => format!("\t\"{module_path}\""),
2595 }
2596 } else {
2597 match alias {
2599 Some(a) => format!("import {a} \"{module_path}\""),
2600 None => format!("import \"{module_path}\""),
2601 }
2602 }
2603}
2604
2605pub fn go_has_grouped_import(_source: &str, tree: &Tree) -> Option<Range<usize>> {
2608 let root = tree.root_node();
2609 let mut cursor = root.walk();
2610 if !cursor.goto_first_child() {
2611 return None;
2612 }
2613
2614 loop {
2615 let node = cursor.node();
2616 if node.kind() == "import_declaration" && go_import_declaration_is_grouped(&node) {
2617 return Some(node.byte_range());
2618 }
2619 if !cursor.goto_next_sibling() {
2620 break;
2621 }
2622 }
2623 None
2624}
2625
2626pub fn go_import_declarations_range(_source: &str, tree: &Tree) -> Option<Range<usize>> {
2627 let root = tree.root_node();
2628 let mut cursor = root.walk();
2629 let mut range: Option<Range<usize>> = None;
2630 if !cursor.goto_first_child() {
2631 return None;
2632 }
2633
2634 loop {
2635 let node = cursor.node();
2636 if node.kind() == "import_declaration" {
2637 let node_range = node.byte_range();
2638 range = Some(match range {
2639 Some(existing) => {
2640 existing.start.min(node_range.start)..existing.end.max(node_range.end)
2641 }
2642 None => node_range,
2643 });
2644 }
2645 if !cursor.goto_next_sibling() {
2646 break;
2647 }
2648 }
2649
2650 range
2651}
2652
2653pub fn go_offset_is_in_grouped_import(_source: &str, tree: &Tree, offset: usize) -> bool {
2654 let root = tree.root_node();
2655 let mut cursor = root.walk();
2656 if !cursor.goto_first_child() {
2657 return false;
2658 }
2659
2660 loop {
2661 let node = cursor.node();
2662 if node.kind() == "import_declaration"
2663 && node.start_byte() < offset
2664 && offset < node.end_byte()
2665 && go_import_declaration_is_grouped(&node)
2666 {
2667 return true;
2668 }
2669 if !cursor.goto_next_sibling() {
2670 break;
2671 }
2672 }
2673
2674 false
2675}
2676
2677fn go_import_declaration_is_grouped(node: &Node) -> bool {
2678 let mut c = node.walk();
2679 if c.goto_first_child() {
2680 loop {
2681 if c.node().kind() == "import_spec_list" {
2682 return true;
2683 }
2684 if !c.goto_next_sibling() {
2685 break;
2686 }
2687 }
2688 }
2689 false
2690}
2691
2692pub fn classify_group_solidity(module_path: &str) -> ImportGroup {
2699 if module_path.starts_with('.') {
2700 ImportGroup::Internal
2701 } else {
2702 ImportGroup::External
2703 }
2704}
2705
2706fn parse_solidity_imports(source: &str, tree: &Tree) -> ImportBlock {
2707 let root = tree.root_node();
2708 let mut imports = Vec::new();
2709 let mut cursor = root.walk();
2710 if cursor.goto_first_child() {
2711 loop {
2712 let node = cursor.node();
2713 if node.kind() == "import_directive" {
2714 if let Some(imp) = parse_solidity_import_directive(source, &node) {
2715 imports.push(imp);
2716 }
2717 }
2718 if !cursor.goto_next_sibling() {
2719 break;
2720 }
2721 }
2722 }
2723 let byte_range = import_byte_range(&imports);
2724 ImportBlock {
2725 imports,
2726 byte_range,
2727 }
2728}
2729
2730fn strip_one_matching_solidity_quote_pair(value: &str) -> &str {
2731 if value.len() < 2 {
2732 return value;
2733 }
2734
2735 let bytes = value.as_bytes();
2736 let quote = bytes[0];
2737 if matches!(quote, b'\'' | b'"') && bytes.last() == Some("e) {
2738 &value[1..value.len() - 1]
2739 } else {
2740 value
2741 }
2742}
2743
2744fn parse_solidity_import_directive(source: &str, node: &Node) -> Option<ImportStatement> {
2749 let raw_text = source[node.byte_range()].to_string();
2750 let byte_range = node.byte_range();
2751
2752 let mut children: Vec<(String, String)> = Vec::new();
2753 let mut c = node.walk();
2754 if c.goto_first_child() {
2755 loop {
2756 let ch = c.node();
2757 children.push((ch.kind().to_string(), source[ch.byte_range()].to_string()));
2758 if !c.goto_next_sibling() {
2759 break;
2760 }
2761 }
2762 }
2763
2764 let module_path = children
2766 .iter()
2767 .find(|(k, _)| k == "string")
2768 .map(|(_, text)| strip_one_matching_solidity_quote_pair(text).to_string())?;
2769 if module_path.is_empty() {
2770 return None;
2771 }
2772
2773 let has_brace = children.iter().any(|(k, _)| k == "{");
2774 let has_star = children.iter().any(|(k, _)| k == "*");
2775
2776 let mut named: Vec<String> = Vec::new();
2777 let mut namespace: Option<String> = None;
2778 let mut alias: Option<String> = None;
2779
2780 if has_brace {
2781 named = parse_solidity_named_specifiers(&children);
2782 } else if has_star {
2783 namespace = solidity_identifier_after_as(&children);
2784 } else {
2785 alias = solidity_identifier_after_as(&children);
2788 }
2789
2790 let kind = if named.is_empty() && namespace.is_none() && alias.is_none() {
2791 ImportKind::SideEffect
2792 } else {
2793 ImportKind::Value
2794 };
2795 let group = classify_group_solidity(&module_path);
2796
2797 Some(ImportStatement {
2798 module_path,
2799 names: named.clone(),
2800 default_import: None,
2801 namespace_import: namespace.clone(),
2804 kind,
2805 group,
2806 byte_range,
2807 raw_text,
2808 form: ImportForm::Solidity {
2809 named,
2810 namespace,
2811 alias,
2812 },
2813 })
2814}
2815
2816fn solidity_identifier_after_as(children: &[(String, String)]) -> Option<String> {
2818 let as_pos = children.iter().position(|(k, _)| k == "as")?;
2819 children[as_pos + 1..]
2820 .iter()
2821 .find(|(k, _)| k == "identifier")
2822 .map(|(_, t)| t.clone())
2823}
2824
2825fn parse_solidity_named_specifiers(children: &[(String, String)]) -> Vec<String> {
2828 let mut names = Vec::new();
2829 let mut in_braces = false;
2830 let mut current: Option<String> = None;
2831 let mut expect_alias = false;
2832 for (k, t) in children {
2833 match k.as_str() {
2834 "{" => in_braces = true,
2835 "}" => {
2836 if let Some(n) = current.take() {
2837 names.push(n);
2838 }
2839 in_braces = false;
2840 }
2841 _ if !in_braces => {}
2842 "identifier" => {
2843 if expect_alias {
2844 if let Some(n) = current.take() {
2845 names.push(format!("{n} as {t}"));
2846 }
2847 expect_alias = false;
2848 } else {
2849 if let Some(n) = current.take() {
2850 names.push(n);
2851 }
2852 current = Some(t.clone());
2853 }
2854 }
2855 "as" => expect_alias = true,
2856 "," => {
2857 if let Some(n) = current.take() {
2858 names.push(n);
2859 }
2860 expect_alias = false;
2861 }
2862 _ => {}
2863 }
2864 }
2865 names
2866}
2867
2868fn generate_solidity_import_line(req: &ImportRequest) -> String {
2870 if !req.names.is_empty() {
2871 format!(
2872 "import {{ {} }} from \"{}\";",
2873 req.names.join(", "),
2874 req.module_path
2875 )
2876 } else if let Some(ns) = req.namespace {
2877 format!("import * as {} from \"{}\";", ns, req.module_path)
2878 } else if let Some(al) = req.alias {
2879 format!("import \"{}\" as {};", req.module_path, al)
2880 } else {
2881 format!("import \"{}\";", req.module_path)
2882 }
2883}
2884
2885fn skip_newline(source: &str, pos: usize) -> usize {
2887 if pos < source.len() {
2888 let bytes = source.as_bytes();
2889 if bytes[pos] == b'\n' {
2890 return pos + 1;
2891 }
2892 if bytes[pos] == b'\r' {
2893 if pos + 1 < source.len() && bytes[pos + 1] == b'\n' {
2894 return pos + 2;
2895 }
2896 return pos + 1;
2897 }
2898 }
2899 pos
2900}
2901
2902#[cfg(test)]
2907mod tests {
2908 use super::*;
2909
2910 #[test]
2919 fn form_es_mirrors_flat_fields() {
2920 let (_, block) = parse_ts(
2921 "import Default, { a, b as c } from \"ext\";\nimport type { T } from \"./t\";\nimport \"./side\";\nimport * as ns from \"nspkg\";\n",
2922 );
2923 match &block.imports[0].form {
2925 ImportForm::Es {
2926 default_import,
2927 namespace_import,
2928 named,
2929 type_only,
2930 side_effect,
2931 attribute_clause,
2932 attribute_type,
2933 } => {
2934 assert_eq!(default_import.as_deref(), Some("Default"));
2935 assert_eq!(namespace_import, &None);
2936 assert_eq!(named, &block.imports[0].names);
2937 assert!(!type_only);
2938 assert!(!side_effect);
2939 assert_eq!(attribute_clause, &None);
2940 assert_eq!(attribute_type, &None);
2941 }
2942 other => panic!("expected Es, got {other:?}"),
2943 }
2944 match &block.imports[1].form {
2946 ImportForm::Es {
2947 type_only, named, ..
2948 } => {
2949 assert!(type_only);
2950 assert_eq!(named, &block.imports[1].names);
2951 }
2952 other => panic!("expected Es type-only, got {other:?}"),
2953 }
2954 match &block.imports[2].form {
2956 ImportForm::Es { side_effect, .. } => assert!(side_effect),
2957 other => panic!("expected Es side-effect, got {other:?}"),
2958 }
2959 match &block.imports[3].form {
2961 ImportForm::Es {
2962 namespace_import, ..
2963 } => assert_eq!(namespace_import.as_deref(), Some("ns")),
2964 other => panic!("expected Es namespace, got {other:?}"),
2965 }
2966 }
2967
2968 #[test]
2969 fn form_python_mirrors_flat_fields() {
2970 let (_, block) = parse_py("import os\nfrom sys import argv, path\n");
2971 match &block.imports[0].form {
2972 ImportForm::Python { from_import, named } => {
2973 assert!(!from_import, "`import os` is not a from-import");
2974 assert_eq!(named, &["os"]);
2975 }
2976 other => panic!("expected Python import, got {other:?}"),
2977 }
2978 match &block.imports[1].form {
2979 ImportForm::Python { from_import, named } => {
2980 assert!(from_import, "`from sys import ...` is a from-import");
2981 assert_eq!(named, &block.imports[1].names);
2982 }
2983 other => panic!("expected Python from-import, got {other:?}"),
2984 }
2985 }
2986
2987 #[test]
2988 fn form_rust_de_overloads_pub_from_default_import() {
2989 let (_, block) = parse_rust("pub use crate::a::Exported;\nuse std::fmt::Debug;\n");
2990 match &block.imports[0].form {
2992 ImportForm::RustUse { visibility, named } => {
2993 assert_eq!(visibility.as_deref(), Some("pub"));
2994 assert_eq!(named, &block.imports[0].names);
2995 }
2996 other => panic!("expected RustUse, got {other:?}"),
2997 }
2998 assert_eq!(
2999 block.imports[0].default_import.as_deref(),
3000 Some("pub"),
3001 "flat field unchanged during additive migration"
3002 );
3003 match &block.imports[1].form {
3005 ImportForm::RustUse { visibility, .. } => assert_eq!(visibility, &None),
3006 other => panic!("expected RustUse, got {other:?}"),
3007 }
3008 assert_eq!(block.imports[1].default_import, None);
3009 }
3010
3011 #[test]
3012 fn form_go_de_overloads_alias_from_default_import() {
3013 let (_, block) =
3019 parse_go("package main\n\nimport (\n\t_ \"github.com/x/y\"\n\t\"fmt\"\n)\n");
3020 let blank = block
3021 .imports
3022 .iter()
3023 .find(|i| i.module_path == "github.com/x/y")
3024 .expect("blank import parsed");
3025 match &blank.form {
3026 ImportForm::Go { alias } => assert_eq!(alias.as_deref(), Some("_")),
3027 other => panic!("expected Go blank-aliased, got {other:?}"),
3028 }
3029 assert_eq!(
3030 blank.default_import.as_deref(),
3031 Some("_"),
3032 "form.alias mirrors the flat default_import field exactly"
3033 );
3034 let plain = block
3035 .imports
3036 .iter()
3037 .find(|i| i.module_path == "fmt")
3038 .expect("plain import parsed");
3039 match &plain.form {
3040 ImportForm::Go { alias } => assert_eq!(alias, &None),
3041 other => panic!("expected Go plain, got {other:?}"),
3042 }
3043 assert_eq!(plain.default_import, None);
3044 }
3045
3046 fn parse_ts(source: &str) -> (Tree, ImportBlock) {
3047 let grammar = grammar_for(LangId::TypeScript);
3048 let mut parser = Parser::new();
3049 parser.set_language(&grammar).unwrap();
3050 let tree = parser.parse(source, None).unwrap();
3051 let block = parse_imports(source, &tree, LangId::TypeScript);
3052 (tree, block)
3053 }
3054
3055 fn parse_js(source: &str) -> (Tree, ImportBlock) {
3056 let grammar = grammar_for(LangId::JavaScript);
3057 let mut parser = Parser::new();
3058 parser.set_language(&grammar).unwrap();
3059 let tree = parser.parse(source, None).unwrap();
3060 let block = parse_imports(source, &tree, LangId::JavaScript);
3061 (tree, block)
3062 }
3063
3064 fn parse_vue(source: &str) -> (Tree, ImportBlock) {
3065 let grammar = grammar_for(LangId::Vue);
3066 let mut parser = Parser::new();
3067 parser.set_language(&grammar).unwrap();
3068 let tree = parser.parse(source, None).unwrap();
3069 let block = parse_imports(source, &tree, LangId::Vue);
3070 (tree, block)
3071 }
3072
3073 #[test]
3078 fn vue_grammar_node_kinds_are_stable() {
3079 let src = "<template>\n <div />\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from 'vue'\n</script>\n";
3080 let grammar = grammar_for(LangId::Vue);
3081 let mut parser = Parser::new();
3082 parser.set_language(&grammar).unwrap();
3083 let tree = parser.parse(src, None).unwrap();
3084 let root = tree.root_node();
3085 let mut cursor = root.walk();
3086 let script = root
3087 .named_children(&mut cursor)
3088 .find(|n| n.kind() == "script_element")
3089 .expect("expected a script_element node");
3090 let mut inner = script.walk();
3091 assert!(
3092 script
3093 .named_children(&mut inner)
3094 .any(|n| n.kind() == "raw_text"),
3095 "expected script body exposed as raw_text"
3096 );
3097 }
3098
3099 #[test]
3100 fn vue_parses_script_imports_with_whole_file_offsets() {
3101 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";
3102 let (_tree, block) = parse_vue(src);
3103 assert_eq!(block.imports.len(), 2, "should find both script imports");
3104 for imp in &block.imports {
3107 assert_eq!(&src[imp.byte_range.clone()], imp.raw_text);
3108 assert!(
3109 imp.byte_range.start > src.find("<script").unwrap(),
3110 "import offset must fall inside the script block"
3111 );
3112 }
3113 assert_eq!(block.imports[0].module_path, "vue");
3114 assert_eq!(block.imports[1].module_path, "./Foo.vue");
3115 }
3116
3117 #[test]
3118 fn vue_without_script_block_has_no_imports() {
3119 let src = "<template>\n <div />\n</template>\n\n<style>.x{}</style>\n";
3120 let (_tree, block) = parse_vue(src);
3121 assert!(block.imports.is_empty());
3122 assert!(block.byte_range.is_none());
3123 }
3124
3125 #[test]
3128 fn parse_ts_named_imports() {
3129 let source = "import { useState, useEffect } from 'react';\n";
3130 let (_, block) = parse_ts(source);
3131 assert_eq!(block.imports.len(), 1);
3132 let imp = &block.imports[0];
3133 assert_eq!(imp.module_path, "react");
3134 assert!(imp.names.contains(&"useState".to_string()));
3135 assert!(imp.names.contains(&"useEffect".to_string()));
3136 assert_eq!(imp.kind, ImportKind::Value);
3137 assert_eq!(imp.group, ImportGroup::External);
3138 }
3139
3140 #[test]
3141 fn parse_ts_default_import() {
3142 let source = "import React from 'react';\n";
3143 let (_, block) = parse_ts(source);
3144 assert_eq!(block.imports.len(), 1);
3145 let imp = &block.imports[0];
3146 assert_eq!(imp.default_import.as_deref(), Some("React"));
3147 assert_eq!(imp.kind, ImportKind::Value);
3148 }
3149
3150 #[test]
3151 fn parse_ts_side_effect_import() {
3152 let source = "import './styles.css';\n";
3153 let (_, block) = parse_ts(source);
3154 assert_eq!(block.imports.len(), 1);
3155 assert_eq!(block.imports[0].kind, ImportKind::SideEffect);
3156 assert_eq!(block.imports[0].module_path, "./styles.css");
3157 }
3158
3159 #[test]
3160 fn parse_ts_relative_import() {
3161 let source = "import { helper } from './utils';\n";
3162 let (_, block) = parse_ts(source);
3163 assert_eq!(block.imports.len(), 1);
3164 assert_eq!(block.imports[0].group, ImportGroup::Internal);
3165 }
3166
3167 #[test]
3168 fn parse_ts_multiple_groups() {
3169 let source = "\
3170import React from 'react';
3171import { useState } from 'react';
3172import { helper } from './utils';
3173import { Config } from '../config';
3174";
3175 let (_, block) = parse_ts(source);
3176 assert_eq!(block.imports.len(), 4);
3177
3178 let external: Vec<_> = block
3179 .imports
3180 .iter()
3181 .filter(|i| i.group == ImportGroup::External)
3182 .collect();
3183 let relative: Vec<_> = block
3184 .imports
3185 .iter()
3186 .filter(|i| i.group == ImportGroup::Internal)
3187 .collect();
3188 assert_eq!(external.len(), 2);
3189 assert_eq!(relative.len(), 2);
3190 }
3191
3192 #[test]
3193 fn parse_ts_namespace_import() {
3194 let source = "import * as path from 'path';\n";
3195 let (_, block) = parse_ts(source);
3196 assert_eq!(block.imports.len(), 1);
3197 let imp = &block.imports[0];
3198 assert_eq!(imp.namespace_import.as_deref(), Some("path"));
3199 assert_eq!(imp.kind, ImportKind::Value);
3200 }
3201
3202 #[test]
3203 fn parse_js_imports() {
3204 let source = "import { readFile } from 'fs';\nimport { helper } from './helper';\n";
3205 let (_, block) = parse_js(source);
3206 assert_eq!(block.imports.len(), 2);
3207 assert_eq!(block.imports[0].group, ImportGroup::External);
3208 assert_eq!(block.imports[1].group, ImportGroup::Internal);
3209 }
3210
3211 #[test]
3214 fn classify_external() {
3215 assert_eq!(classify_group_ts("react"), ImportGroup::External);
3216 assert_eq!(classify_group_ts("@scope/pkg"), ImportGroup::External);
3217 assert_eq!(classify_group_ts("lodash/map"), ImportGroup::External);
3218 }
3219
3220 #[test]
3221 fn classify_relative() {
3222 assert_eq!(classify_group_ts("./utils"), ImportGroup::Internal);
3223 assert_eq!(classify_group_ts("../config"), ImportGroup::Internal);
3224 assert_eq!(classify_group_ts("./"), ImportGroup::Internal);
3225 }
3226
3227 #[test]
3230 fn dedup_detects_same_named_import() {
3231 let source = "import { useState } from 'react';\n";
3232 let (_, block) = parse_ts(source);
3233 assert!(is_duplicate(
3234 &block,
3235 "react",
3236 &["useState".to_string()],
3237 None,
3238 false
3239 ));
3240 }
3241
3242 #[test]
3243 fn dedup_misses_different_name() {
3244 let source = "import { useState } from 'react';\n";
3245 let (_, block) = parse_ts(source);
3246 assert!(!is_duplicate(
3247 &block,
3248 "react",
3249 &["useEffect".to_string()],
3250 None,
3251 false
3252 ));
3253 }
3254
3255 #[test]
3256 fn dedup_detects_default_import() {
3257 let source = "import React from 'react';\n";
3258 let (_, block) = parse_ts(source);
3259 assert!(is_duplicate(&block, "react", &[], Some("React"), false));
3260 }
3261
3262 #[test]
3263 fn dedup_side_effect() {
3264 let source = "import './styles.css';\n";
3265 let (_, block) = parse_ts(source);
3266 assert!(is_duplicate(&block, "./styles.css", &[], None, false));
3267 }
3268
3269 #[test]
3270 fn dedup_namespace_import_distinct_from_side_effect_import() {
3271 let side_effect_source = "import 'fs';\n";
3272 let (_, side_effect_block) = parse_ts(side_effect_source);
3273 assert!(!is_duplicate_with_namespace(
3274 &side_effect_block,
3275 "fs",
3276 &[],
3277 None,
3278 Some("fs"),
3279 false
3280 ));
3281
3282 let namespace_source = "import * as fs from 'fs';\n";
3283 let (_, namespace_block) = parse_ts(namespace_source);
3284 assert!(!is_duplicate(&namespace_block, "fs", &[], None, false));
3285 assert!(is_duplicate_with_namespace(
3286 &namespace_block,
3287 "fs",
3288 &[],
3289 None,
3290 Some("fs"),
3291 false
3292 ));
3293 assert!(!is_duplicate_with_namespace(
3294 &namespace_block,
3295 "fs",
3296 &[],
3297 None,
3298 Some("other"),
3299 false
3300 ));
3301 }
3302
3303 #[test]
3304 fn dedup_type_vs_value() {
3305 let source = "import { FC } from 'react';\n";
3306 let (_, block) = parse_ts(source);
3307 assert!(!is_duplicate(
3309 &block,
3310 "react",
3311 &["FC".to_string()],
3312 None,
3313 true
3314 ));
3315 }
3316
3317 #[test]
3320 fn generate_named_import() {
3321 let line = generate_import_line(
3322 LangId::TypeScript,
3323 "react",
3324 &["useState".to_string(), "useEffect".to_string()],
3325 None,
3326 false,
3327 );
3328 assert_eq!(line, "import { useEffect, useState } from 'react';");
3329 }
3330
3331 #[test]
3332 fn generate_named_import_sorts_by_imported_name() {
3333 let line = generate_import_line(
3334 LangId::TypeScript,
3335 "x",
3336 &[
3337 "useState".to_string(),
3338 "type Foo".to_string(),
3339 "stdin as input".to_string(),
3340 "type Bar".to_string(),
3341 ],
3342 None,
3343 false,
3344 );
3345 assert_eq!(
3346 line,
3347 "import { type Bar, type Foo, stdin as input, useState } from 'x';"
3348 );
3349 }
3350
3351 #[test]
3352 fn generate_default_import() {
3353 let line = generate_import_line(LangId::TypeScript, "react", &[], Some("React"), false);
3354 assert_eq!(line, "import React from 'react';");
3355 }
3356
3357 #[test]
3358 fn generate_type_import() {
3359 let line =
3360 generate_import_line(LangId::TypeScript, "react", &["FC".to_string()], None, true);
3361 assert_eq!(line, "import type { FC } from 'react';");
3362 }
3363
3364 #[test]
3365 fn generate_side_effect_import() {
3366 let line = generate_import_line(LangId::TypeScript, "./styles.css", &[], None, false);
3367 assert_eq!(line, "import './styles.css';");
3368 }
3369
3370 #[test]
3371 fn generate_default_and_named() {
3372 let line = generate_import_line(
3373 LangId::TypeScript,
3374 "react",
3375 &["useState".to_string()],
3376 Some("React"),
3377 false,
3378 );
3379 assert_eq!(line, "import React, { useState } from 'react';");
3380 }
3381
3382 #[test]
3383 fn parse_es_import_attributes_preserve_standard_and_legacy_spellings() {
3384 let source = r#"import data from './data.json' with { type: 'json' };
3385import legacy from './legacy.json' assert { type: 'json' };
3386import escaped from './escaped.json' with { "t\u0079pe": "j\u0073on" };
3387"#;
3388 let (_, block) = parse_js(source);
3389
3390 assert_eq!(
3391 es_import_attribute_clause(&block.imports[0]),
3392 Some("with { type: 'json' }")
3393 );
3394 assert_eq!(
3395 es_import_attribute_clause(&block.imports[1]),
3396 Some("assert { type: 'json' }")
3397 );
3398 assert_eq!(es_import_attribute_type(&block.imports[0]), Some("json"));
3399 assert_eq!(es_import_attribute_type(&block.imports[1]), Some("json"));
3400 assert_eq!(es_import_attribute_type(&block.imports[2]), Some("json"));
3401 }
3402
3403 #[test]
3404 fn parse_ts_type_import() {
3405 let source = "import type { FC } from 'react';\n";
3406 let (_, block) = parse_ts(source);
3407 assert_eq!(block.imports.len(), 1);
3408 let imp = &block.imports[0];
3409 assert_eq!(imp.kind, ImportKind::Type);
3410 assert!(imp.names.contains(&"FC".to_string()));
3411 assert_eq!(imp.group, ImportGroup::External);
3412 }
3413
3414 #[test]
3417 fn insertion_empty_file() {
3418 let source = "";
3419 let (_, block) = parse_ts(source);
3420 let (offset, _, _) =
3421 find_insertion_point(source, &block, ImportGroup::External, "react", false);
3422 assert_eq!(offset, 0);
3423 }
3424
3425 #[test]
3426 fn insertion_alphabetical_within_group() {
3427 let source = "\
3428import { a } from 'alpha';
3429import { c } from 'charlie';
3430";
3431 let (_, block) = parse_ts(source);
3432 let (offset, _, _) =
3433 find_insertion_point(source, &block, ImportGroup::External, "bravo", false);
3434 let before_charlie = source.find("import { c }").unwrap();
3436 assert_eq!(offset, before_charlie);
3437 }
3438
3439 fn parse_py(source: &str) -> (Tree, ImportBlock) {
3442 let grammar = grammar_for(LangId::Python);
3443 let mut parser = Parser::new();
3444 parser.set_language(&grammar).unwrap();
3445 let tree = parser.parse(source, None).unwrap();
3446 let block = parse_imports(source, &tree, LangId::Python);
3447 (tree, block)
3448 }
3449
3450 #[test]
3451 fn parse_py_import_statement() {
3452 let source = "import os\nimport sys\n";
3453 let (_, block) = parse_py(source);
3454 assert_eq!(block.imports.len(), 2);
3455 assert_eq!(block.imports[0].module_path, "os");
3456 assert_eq!(block.imports[1].module_path, "sys");
3457 assert_eq!(block.imports[0].group, ImportGroup::Stdlib);
3458 }
3459
3460 #[test]
3461 fn parse_py_import_statement_preserves_aliases_and_siblings() {
3462 let source = "import alpha, beta as local_beta\n";
3463 let (_, block) = parse_py(source);
3464 let imp = &block.imports[0];
3465 assert_eq!(imp.module_path, "alpha");
3466 assert!(imp.names.is_empty());
3467 match &imp.form {
3468 ImportForm::Python { from_import, named } => {
3469 assert!(!from_import);
3470 assert_eq!(named, &["alpha", "beta as local_beta"]);
3471 assert!(specifier_matches(&named[1], "beta"));
3472 assert!(specifier_matches(&named[1], "local_beta"));
3473 }
3474 other => panic!("expected Python import, got {other:?}"),
3475 }
3476 }
3477
3478 #[test]
3479 fn parse_py_from_import() {
3480 let source = "from collections import OrderedDict\nfrom typing import List, Optional\n";
3481 let (_, block) = parse_py(source);
3482 assert_eq!(block.imports.len(), 2);
3483 assert_eq!(block.imports[0].module_path, "collections");
3484 assert!(block.imports[0].names.contains(&"OrderedDict".to_string()));
3485 assert_eq!(block.imports[0].group, ImportGroup::Stdlib);
3486 assert_eq!(block.imports[1].module_path, "typing");
3487 assert!(block.imports[1].names.contains(&"List".to_string()));
3488 assert!(block.imports[1].names.contains(&"Optional".to_string()));
3489 }
3490
3491 #[test]
3492 fn parse_py_from_import_preserves_aliases() {
3493 let source = "from module import alpha, beta as local_beta\n";
3494 let (_, block) = parse_py(source);
3495 let imp = &block.imports[0];
3496 assert_eq!(imp.names, ["alpha", "beta as local_beta"]);
3497 assert!(specifier_matches(&imp.names[1], "beta"));
3498 assert!(specifier_matches(&imp.names[1], "local_beta"));
3499 assert_eq!(
3500 imp.form,
3501 ImportForm::Python {
3502 from_import: true,
3503 named: vec!["alpha".to_string(), "beta as local_beta".to_string()],
3504 }
3505 );
3506 }
3507
3508 #[test]
3509 fn parse_py_relative_import() {
3510 let source = "from . import utils\nfrom ..config import Settings\n";
3511 let (_, block) = parse_py(source);
3512 assert_eq!(block.imports.len(), 2);
3513 assert_eq!(block.imports[0].module_path, ".");
3514 assert!(block.imports[0].names.contains(&"utils".to_string()));
3515 assert_eq!(block.imports[0].group, ImportGroup::Internal);
3516 assert_eq!(block.imports[1].module_path, "..config");
3517 assert_eq!(block.imports[1].group, ImportGroup::Internal);
3518 }
3519
3520 #[test]
3521 fn classify_py_groups() {
3522 assert_eq!(classify_group_py("os"), ImportGroup::Stdlib);
3523 assert_eq!(classify_group_py("sys"), ImportGroup::Stdlib);
3524 assert_eq!(classify_group_py("json"), ImportGroup::Stdlib);
3525 assert_eq!(classify_group_py("collections"), ImportGroup::Stdlib);
3526 assert_eq!(classify_group_py("os.path"), ImportGroup::Stdlib);
3527 assert_eq!(classify_group_py("requests"), ImportGroup::External);
3528 assert_eq!(classify_group_py("flask"), ImportGroup::External);
3529 assert_eq!(classify_group_py("."), ImportGroup::Internal);
3530 assert_eq!(classify_group_py("..config"), ImportGroup::Internal);
3531 assert_eq!(classify_group_py(".utils"), ImportGroup::Internal);
3532 }
3533
3534 #[test]
3535 fn parse_py_three_groups() {
3536 let source = "import os\nimport sys\n\nimport requests\n\nfrom . import utils\n";
3537 let (_, block) = parse_py(source);
3538 let stdlib: Vec<_> = block
3539 .imports
3540 .iter()
3541 .filter(|i| i.group == ImportGroup::Stdlib)
3542 .collect();
3543 let external: Vec<_> = block
3544 .imports
3545 .iter()
3546 .filter(|i| i.group == ImportGroup::External)
3547 .collect();
3548 let internal: Vec<_> = block
3549 .imports
3550 .iter()
3551 .filter(|i| i.group == ImportGroup::Internal)
3552 .collect();
3553 assert_eq!(stdlib.len(), 2);
3554 assert_eq!(external.len(), 1);
3555 assert_eq!(internal.len(), 1);
3556 }
3557
3558 #[test]
3559 fn generate_py_import() {
3560 let line = generate_import_line(LangId::Python, "os", &[], None, false);
3561 assert_eq!(line, "import os");
3562 }
3563
3564 #[test]
3565 fn generate_py_from_import() {
3566 let line = generate_import_line(
3567 LangId::Python,
3568 "collections",
3569 &["OrderedDict".to_string()],
3570 None,
3571 false,
3572 );
3573 assert_eq!(line, "from collections import OrderedDict");
3574 }
3575
3576 #[test]
3577 fn generate_py_from_import_multiple() {
3578 let line = generate_import_line(
3579 LangId::Python,
3580 "typing",
3581 &["Optional".to_string(), "List".to_string()],
3582 None,
3583 false,
3584 );
3585 assert_eq!(line, "from typing import List, Optional");
3586 }
3587
3588 fn parse_rust(source: &str) -> (Tree, ImportBlock) {
3591 let grammar = grammar_for(LangId::Rust);
3592 let mut parser = Parser::new();
3593 parser.set_language(&grammar).unwrap();
3594 let tree = parser.parse(source, None).unwrap();
3595 let block = parse_imports(source, &tree, LangId::Rust);
3596 (tree, block)
3597 }
3598
3599 #[test]
3600 fn parse_rs_use_std() {
3601 let source = "use std::collections::HashMap;\nuse std::io::Read;\n";
3602 let (_, block) = parse_rust(source);
3603 assert_eq!(block.imports.len(), 2);
3604 assert_eq!(block.imports[0].module_path, "std::collections::HashMap");
3605 assert_eq!(block.imports[0].group, ImportGroup::Stdlib);
3606 assert_eq!(block.imports[1].group, ImportGroup::Stdlib);
3607 }
3608
3609 #[test]
3610 fn parse_rs_use_external() {
3611 let source = "use serde::{Deserialize, Serialize};\n";
3612 let (_, block) = parse_rust(source);
3613 assert_eq!(block.imports.len(), 1);
3614 assert_eq!(block.imports[0].group, ImportGroup::External);
3615 assert!(block.imports[0].names.contains(&"Deserialize".to_string()));
3616 assert!(block.imports[0].names.contains(&"Serialize".to_string()));
3617 }
3618
3619 #[test]
3620 fn parse_rs_use_crate() {
3621 let source = "use crate::config::Settings;\nuse super::parent::Thing;\n";
3622 let (_, block) = parse_rust(source);
3623 assert_eq!(block.imports.len(), 2);
3624 assert_eq!(block.imports[0].group, ImportGroup::Internal);
3625 assert_eq!(block.imports[1].group, ImportGroup::Internal);
3626 }
3627
3628 #[test]
3629 fn parse_rs_pub_use() {
3630 let source = "pub use super::parent::Thing;\n";
3631 let (_, block) = parse_rust(source);
3632 assert_eq!(block.imports.len(), 1);
3633 assert_eq!(block.imports[0].default_import.as_deref(), Some("pub"));
3635 }
3636
3637 #[test]
3638 fn classify_rs_groups() {
3639 assert_eq!(
3640 classify_group_rs("std::collections::HashMap"),
3641 ImportGroup::Stdlib
3642 );
3643 assert_eq!(classify_group_rs("core::mem"), ImportGroup::Stdlib);
3644 assert_eq!(classify_group_rs("alloc::vec"), ImportGroup::Stdlib);
3645 assert_eq!(
3646 classify_group_rs("serde::Deserialize"),
3647 ImportGroup::External
3648 );
3649 assert_eq!(classify_group_rs("tokio::runtime"), ImportGroup::External);
3650 assert_eq!(classify_group_rs("crate::config"), ImportGroup::Internal);
3651 assert_eq!(classify_group_rs("self::utils"), ImportGroup::Internal);
3652 assert_eq!(classify_group_rs("super::parent"), ImportGroup::Internal);
3653 }
3654
3655 #[test]
3656 fn generate_rs_use() {
3657 let line = generate_import_line(LangId::Rust, "std::fmt::Display", &[], None, false);
3658 assert_eq!(line, "use std::fmt::Display;");
3659 }
3660
3661 fn parse_go(source: &str) -> (Tree, ImportBlock) {
3664 let grammar = grammar_for(LangId::Go);
3665 let mut parser = Parser::new();
3666 parser.set_language(&grammar).unwrap();
3667 let tree = parser.parse(source, None).unwrap();
3668 let block = parse_imports(source, &tree, LangId::Go);
3669 (tree, block)
3670 }
3671
3672 #[test]
3673 fn parse_go_single_import() {
3674 let source = "package main\n\nimport \"fmt\"\n";
3675 let (_, block) = parse_go(source);
3676 assert_eq!(block.imports.len(), 1);
3677 assert_eq!(block.imports[0].module_path, "fmt");
3678 assert_eq!(block.imports[0].group, ImportGroup::Stdlib);
3679 }
3680
3681 #[test]
3682 fn parse_go_grouped_import() {
3683 let source =
3684 "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/pkg/errors\"\n)\n";
3685 let (_, block) = parse_go(source);
3686 assert_eq!(block.imports.len(), 3);
3687 assert_eq!(block.imports[0].module_path, "fmt");
3688 assert_eq!(block.imports[0].group, ImportGroup::Stdlib);
3689 assert_eq!(block.imports[1].module_path, "os");
3690 assert_eq!(block.imports[1].group, ImportGroup::Stdlib);
3691 assert_eq!(block.imports[2].module_path, "github.com/pkg/errors");
3692 assert_eq!(block.imports[2].group, ImportGroup::External);
3693 }
3694
3695 #[test]
3696 fn parse_go_mixed_imports() {
3697 let source = "package main\n\nimport \"fmt\"\n\nimport (\n\t\"os\"\n\t\"github.com/pkg/errors\"\n)\n";
3699 let (_, block) = parse_go(source);
3700 assert_eq!(block.imports.len(), 3);
3701 }
3702
3703 #[test]
3704 fn classify_go_groups() {
3705 assert_eq!(classify_group_go("fmt"), ImportGroup::Stdlib);
3706 assert_eq!(classify_group_go("os"), ImportGroup::Stdlib);
3707 assert_eq!(classify_group_go("net/http"), ImportGroup::Stdlib);
3708 assert_eq!(classify_group_go("encoding/json"), ImportGroup::Stdlib);
3709 assert_eq!(
3710 classify_group_go("github.com/pkg/errors"),
3711 ImportGroup::External
3712 );
3713 assert_eq!(
3714 classify_group_go("golang.org/x/tools"),
3715 ImportGroup::External
3716 );
3717 }
3718
3719 #[test]
3720 fn generate_go_standalone() {
3721 let line = generate_go_import_line("fmt", None, false);
3722 assert_eq!(line, "import \"fmt\"");
3723 }
3724
3725 #[test]
3726 fn generate_go_grouped_spec() {
3727 let line = generate_go_import_line("fmt", None, true);
3728 assert_eq!(line, "\t\"fmt\"");
3729 }
3730
3731 #[test]
3732 fn generate_go_with_alias() {
3733 let line = generate_go_import_line("github.com/pkg/errors", Some("errs"), false);
3734 assert_eq!(line, "import errs \"github.com/pkg/errors\"");
3735 }
3736
3737 fn parse_solidity(source: &str) -> (Tree, ImportBlock) {
3740 let grammar = grammar_for(LangId::Solidity);
3741 let mut parser = Parser::new();
3742 parser.set_language(&grammar).unwrap();
3743 let tree = parser.parse(source, None).unwrap();
3744 let block = parse_imports(source, &tree, LangId::Solidity);
3745 (tree, block)
3746 }
3747
3748 #[test]
3752 fn solidity_grammar_node_kinds_are_stable() {
3753 let grammar = grammar_for(LangId::Solidity);
3754 let mut parser = Parser::new();
3755 parser.set_language(&grammar).unwrap();
3756 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";
3757 let tree = parser.parse(src, None).unwrap();
3758 let mut kinds: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
3759 fn walk(node: tree_sitter::Node, kinds: &mut std::collections::BTreeSet<String>) {
3760 kinds.insert(node.kind().to_string());
3761 let mut c = node.walk();
3762 if c.goto_first_child() {
3763 loop {
3764 walk(c.node(), kinds);
3765 if !c.goto_next_sibling() {
3766 break;
3767 }
3768 }
3769 }
3770 }
3771 walk(tree.root_node(), &mut kinds);
3772 for required in [
3773 "import_directive",
3774 "string",
3775 "identifier",
3776 "as",
3777 "from",
3778 "*",
3779 "{",
3780 "}",
3781 ] {
3782 assert!(
3783 kinds.contains(required),
3784 "solidity grammar missing node kind {required:?}; present: {kinds:?}"
3785 );
3786 }
3787 }
3788
3789 #[test]
3790 fn solidity_string_path_strips_one_matching_quote_pair() {
3791 assert_eq!(
3792 strip_one_matching_solidity_quote_pair("\"./A.sol\""),
3793 "./A.sol"
3794 );
3795 assert_eq!(
3796 strip_one_matching_solidity_quote_pair("'./A.sol'"),
3797 "./A.sol"
3798 );
3799 assert_eq!(
3800 strip_one_matching_solidity_quote_pair("\"./a'b.sol\""),
3801 "./a'b.sol"
3802 );
3803 assert_eq!(
3804 strip_one_matching_solidity_quote_pair("\"\"./A.sol\"\""),
3805 "\"./A.sol\""
3806 );
3807 assert_eq!(
3808 strip_one_matching_solidity_quote_pair("'./A.sol\""),
3809 "'./A.sol\""
3810 );
3811 assert_eq!(
3812 strip_one_matching_solidity_quote_pair("\"./A.sol"),
3813 "\"./A.sol"
3814 );
3815 assert_eq!(strip_one_matching_solidity_quote_pair("\""), "\"");
3816 }
3817
3818 #[test]
3819 fn parse_solidity_all_four_forms() {
3820 let (_, block) = parse_solidity(
3821 "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",
3822 );
3823 assert_eq!(block.imports.len(), 8);
3824
3825 assert_eq!(block.imports[0].module_path, "./A.sol");
3827 assert_eq!(block.imports[0].kind, ImportKind::SideEffect);
3828 assert_eq!(
3829 block.imports[0].form,
3830 ImportForm::Solidity {
3831 named: vec![],
3832 namespace: None,
3833 alias: None
3834 }
3835 );
3836
3837 assert_eq!(
3839 block.imports[1].form,
3840 ImportForm::Solidity {
3841 named: vec![],
3842 namespace: None,
3843 alias: Some("B".to_string())
3844 }
3845 );
3846
3847 match &block.imports[2].form {
3849 ImportForm::Solidity { namespace, .. } => assert_eq!(namespace.as_deref(), Some("C")),
3850 other => panic!("expected Solidity namespace, got {other:?}"),
3851 }
3852 assert_eq!(block.imports[2].namespace_import.as_deref(), Some("C"));
3853
3854 match &block.imports[3].form {
3856 ImportForm::Solidity { named, .. } => {
3857 assert_eq!(named, &vec!["Foo".to_string(), "Bar as Baz".to_string()]);
3858 }
3859 other => panic!("expected Solidity named, got {other:?}"),
3860 }
3861 assert_eq!(
3862 block.imports[3].names,
3863 vec!["Foo".to_string(), "Bar as Baz".to_string()]
3864 );
3865
3866 assert_eq!(
3867 block.imports[4..]
3868 .iter()
3869 .map(|import| import.module_path.as_str())
3870 .collect::<Vec<_>>(),
3871 ["./E.sol", "./F.sol", "./G.sol", "./H.sol"]
3872 );
3873 assert_eq!(block.imports[4].kind, ImportKind::SideEffect);
3874 assert!(matches!(
3875 &block.imports[5].form,
3876 ImportForm::Solidity {
3877 alias: Some(alias),
3878 ..
3879 } if alias == "F"
3880 ));
3881 assert_eq!(block.imports[6].namespace_import.as_deref(), Some("G"));
3882 assert_eq!(
3883 block.imports[7].names,
3884 vec!["Quux".to_string(), "Corge as Grault".to_string()]
3885 );
3886 }
3887
3888 #[test]
3889 fn generate_solidity_all_forms() {
3890 assert_eq!(
3892 generate_import(
3893 LangId::Solidity,
3894 &ImportRequest::legacy("./A.sol", &[], None, None, false)
3895 ),
3896 "import \"./A.sol\";"
3897 );
3898 let names = vec!["Foo".to_string(), "Bar as Baz".to_string()];
3900 assert_eq!(
3901 generate_import(
3902 LangId::Solidity,
3903 &ImportRequest::legacy("./D.sol", &names, None, None, false)
3904 ),
3905 "import { Foo, Bar as Baz } from \"./D.sol\";"
3906 );
3907 assert_eq!(
3909 generate_import(
3910 LangId::Solidity,
3911 &ImportRequest::legacy("./C.sol", &[], None, Some("C"), false)
3912 ),
3913 "import * as C from \"./C.sol\";"
3914 );
3915 assert_eq!(
3917 generate_import(
3918 LangId::Solidity,
3919 &ImportRequest {
3920 module_path: "./B.sol",
3921 names: &[],
3922 default_import: None,
3923 namespace: None,
3924 alias: Some("B"),
3925 type_only: false,
3926 modifiers: &[],
3927 import_kind: None,
3928 }
3929 ),
3930 "import \"./B.sol\" as B;"
3931 );
3932 }
3933
3934 #[test]
3935 fn solidity_round_trips_through_parse_generate() {
3936 for src in [
3938 "import \"./A.sol\";",
3939 "import \"./B.sol\" as B;",
3940 "import * as C from \"./C.sol\";",
3941 "import { Foo, Bar as Baz } from \"./D.sol\";",
3942 ] {
3943 let (_, block) = parse_solidity(src);
3944 assert_eq!(block.imports.len(), 1, "parse {src:?}");
3945 let imp = &block.imports[0];
3946 let (namespace, alias) = match &imp.form {
3947 ImportForm::Solidity {
3948 namespace, alias, ..
3949 } => (namespace.as_deref(), alias.as_deref()),
3950 other => panic!("expected Solidity, got {other:?}"),
3951 };
3952 let regenerated = generate_import(
3953 LangId::Solidity,
3954 &ImportRequest {
3955 module_path: &imp.module_path,
3956 names: &imp.names,
3957 default_import: None,
3958 namespace,
3959 alias,
3960 type_only: false,
3961 modifiers: &[],
3962 import_kind: None,
3963 },
3964 );
3965 assert_eq!(regenerated, src, "round-trip mismatch for {src:?}");
3966 }
3967 }
3968
3969 #[test]
3970 fn classify_group_solidity_relative_vs_external() {
3971 assert_eq!(classify_group_solidity("./A.sol"), ImportGroup::Internal);
3972 assert_eq!(
3973 classify_group_solidity("../lib/B.sol"),
3974 ImportGroup::Internal
3975 );
3976 assert_eq!(
3977 classify_group_solidity("@openzeppelin/contracts/token/ERC20/ERC20.sol"),
3978 ImportGroup::External
3979 );
3980 }
3981}