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