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