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