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
572 | LangId::Pascal
573 | LangId::R
574 | LangId::ObjC => None,
575 }
576}
577
578pub fn parse_imports(source: &str, tree: &Tree, lang: LangId) -> ImportBlock {
584 match syntax_for(lang) {
585 Some(engine) => engine.parse(source, tree),
586 None => ImportBlock::empty(),
587 }
588}
589
590pub fn is_duplicate(
596 block: &ImportBlock,
597 module_path: &str,
598 names: &[String],
599 default_import: Option<&str>,
600 type_only: bool,
601) -> bool {
602 is_duplicate_with_namespace(block, module_path, names, default_import, None, type_only)
603}
604
605pub fn is_duplicate_with_namespace(
607 block: &ImportBlock,
608 module_path: &str,
609 names: &[String],
610 default_import: Option<&str>,
611 namespace_import: Option<&str>,
612 type_only: bool,
613) -> bool {
614 let target_kind = if type_only {
615 ImportKind::Type
616 } else {
617 ImportKind::Value
618 };
619
620 for imp in &block.imports {
621 if imp.module_path != module_path {
622 continue;
623 }
624
625 if names.is_empty()
631 && default_import.is_none()
632 && namespace_import.is_none()
633 && imp.names.is_empty()
634 && imp.default_import.is_none()
635 && imp.namespace_import.is_none()
636 {
637 return true;
638 }
639
640 if names.is_empty()
642 && default_import.is_none()
643 && namespace_import.is_none()
644 && imp.kind == ImportKind::SideEffect
645 {
646 return true;
647 }
648
649 if imp.kind != target_kind && imp.kind != ImportKind::SideEffect {
651 continue;
652 }
653
654 if let (Some(def), Some(namespace)) = (default_import, namespace_import) {
658 if imp.default_import.as_deref() == Some(def)
659 && imp.namespace_import.as_deref() == Some(namespace)
660 && names
661 .iter()
662 .all(|n| imp.names.iter().any(|stored| specifier_matches(stored, n)))
663 {
664 return true;
665 }
666 continue;
667 }
668
669 if names.is_empty()
673 && default_import.is_none()
674 && namespace_import.is_some()
675 && imp.namespace_import.as_deref() == namespace_import
676 {
677 return true;
678 }
679
680 if let Some(def) = default_import {
683 if namespace_import.is_none() && imp.default_import.as_deref() == Some(def) {
684 return true;
685 }
686 }
687
688 if !names.is_empty()
694 && names
695 .iter()
696 .all(|n| imp.names.iter().any(|stored| specifier_matches(stored, n)))
697 {
698 return true;
699 }
700 }
701
702 false
703}
704
705pub(crate) fn is_duplicate_import_request(
716 lang: LangId,
717 block: &ImportBlock,
718 req: &ImportRequest<'_>,
719) -> bool {
720 if !uses_form_aware_dedup(lang) {
721 return is_duplicate_with_namespace(
722 block,
723 req.module_path,
724 req.names,
725 req.default_import,
726 req.namespace,
727 req.type_only,
728 );
729 }
730
731 let target = request_dedup_key(lang, req);
732 block
733 .imports
734 .iter()
735 .map(|imp| statement_dedup_key(lang, imp))
736 .any(|key| key == target)
737}
738
739fn uses_form_aware_dedup(lang: LangId) -> bool {
740 matches!(
741 lang,
742 LangId::Solidity
743 | LangId::C
744 | LangId::Cpp
745 | LangId::Java
746 | LangId::CSharp
747 | LangId::Php
748 | LangId::Kotlin
749 | LangId::Scala
750 | LangId::Swift
751 | LangId::Ruby
752 | LangId::Lua
753 | LangId::Perl
754 )
755}
756
757#[derive(Debug, Clone, PartialEq, Eq)]
758struct ImportDedupKey {
759 module_path: String,
760 kind: ImportKind,
761 form: ImportForm,
762}
763
764fn statement_dedup_key(lang: LangId, imp: &ImportStatement) -> ImportDedupKey {
765 canonical_dedup_key(
766 lang,
767 ImportDedupKey {
768 module_path: imp.module_path.clone(),
769 kind: imp.kind,
770 form: imp.form.clone(),
771 },
772 )
773}
774
775fn request_dedup_key(lang: LangId, req: &ImportRequest<'_>) -> ImportDedupKey {
776 let key = match lang {
777 LangId::Solidity => {
778 let kind = if req.names.is_empty() && req.namespace.is_none() && req.alias.is_none() {
779 ImportKind::SideEffect
780 } else {
781 ImportKind::Value
782 };
783 ImportDedupKey {
784 module_path: req.module_path.to_string(),
785 kind,
786 form: ImportForm::Solidity {
787 named: req.names.to_vec(),
788 namespace: req.namespace.map(str::to_string),
789 alias: req.alias.map(str::to_string),
790 },
791 }
792 }
793 LangId::C | LangId::Cpp => structured_dedup_key(
794 req.module_path,
795 ImportKind::SideEffect,
796 &[],
797 None,
798 None,
799 &[],
800 Some(req.import_kind.or(req.default_import).unwrap_or("system")),
801 ),
802 LangId::Java => {
803 let (mut module_path, modifiers) = wildcard_suffix_request(
804 req.module_path,
805 req.modifiers,
806 req.default_import == Some("*"),
807 );
808 let mut names = req.names.to_vec();
809 normalize_java_static_member_key(&mut module_path, &modifiers, &mut names);
810 structured_dedup_key(
811 &module_path,
812 ImportKind::Value,
813 &names,
814 None,
815 None,
816 &modifiers,
817 None,
818 )
819 }
820 LangId::CSharp => structured_dedup_key(
821 req.module_path,
822 ImportKind::Value,
823 &[],
824 None,
825 req.alias,
826 req.modifiers,
827 None,
828 ),
829 LangId::Php => structured_dedup_key(
830 req.module_path,
831 ImportKind::Value,
832 &[],
833 None,
834 req.alias,
835 req.modifiers,
836 req.import_kind,
837 ),
838 LangId::Kotlin => {
839 let wildcard = req.default_import == Some("*") || req.module_path.ends_with(".*");
840 let (module_path, modifiers) =
841 wildcard_suffix_request(req.module_path, req.modifiers, wildcard);
842 let alias = req
843 .alias
844 .or(req.default_import.filter(|value| *value != "*"));
845 structured_dedup_key(
846 &module_path,
847 ImportKind::Value,
848 &[],
849 None,
850 alias,
851 &modifiers,
852 None,
853 )
854 }
855 LangId::Scala => scala_request_dedup_key(req),
856 LangId::Swift => structured_dedup_key(
857 req.module_path,
858 ImportKind::Value,
859 &[],
860 None,
861 None,
862 req.modifiers,
863 req.import_kind,
864 ),
865 LangId::Ruby => {
866 let mut modifiers = req.modifiers.to_vec();
867 if !modifiers
868 .iter()
869 .any(|modifier| modifier == "quote:single" || modifier == "quote:double")
870 {
871 modifiers.push("quote:single".to_string());
872 }
873 structured_dedup_key(
874 req.module_path,
875 ImportKind::SideEffect,
876 &[],
877 None,
878 None,
879 &modifiers,
880 Some(req.import_kind.unwrap_or("require")),
881 )
882 }
883 LangId::Lua => {
884 let alias = req.default_import.or(req.alias);
885 let kind = if alias.is_some() {
886 ImportKind::Value
887 } else {
888 ImportKind::SideEffect
889 };
890 structured_dedup_key(req.module_path, kind, &[], None, alias, req.modifiers, None)
891 }
892 LangId::Perl => structured_dedup_key(
893 req.module_path,
894 ImportKind::SideEffect,
895 &[],
896 None,
897 None,
898 req.modifiers,
899 Some(req.import_kind.unwrap_or("use")),
900 ),
901 _ => structured_dedup_key(
902 req.module_path,
903 if req.type_only {
904 ImportKind::Type
905 } else {
906 ImportKind::Value
907 },
908 req.names,
909 req.namespace,
910 req.alias,
911 req.modifiers,
912 req.import_kind,
913 ),
914 };
915
916 canonical_dedup_key(lang, key)
917}
918
919fn structured_dedup_key(
920 module_path: &str,
921 kind: ImportKind,
922 named: &[String],
923 namespace: Option<&str>,
924 alias: Option<&str>,
925 modifiers: &[String],
926 import_kind: Option<&str>,
927) -> ImportDedupKey {
928 ImportDedupKey {
929 module_path: module_path.to_string(),
930 kind,
931 form: ImportForm::Structured {
932 named: named.to_vec(),
933 namespace: namespace.map(str::to_string),
934 alias: alias.map(str::to_string),
935 modifiers: modifiers.to_vec(),
936 import_kind: import_kind.map(str::to_string),
937 },
938 }
939}
940
941fn wildcard_suffix_request(
942 module_path: &str,
943 modifiers: &[String],
944 wildcard: bool,
945) -> (String, Vec<String>) {
946 let stripped = module_path.strip_suffix(".*").unwrap_or(module_path);
947 let mut modifiers = modifiers.to_vec();
948 if (wildcard || stripped.len() != module_path.len())
949 && !modifiers.iter().any(|modifier| modifier == "wildcard")
950 {
951 modifiers.push("wildcard".to_string());
952 }
953 (stripped.to_string(), modifiers)
954}
955
956fn normalize_java_static_member_key(
957 module_path: &mut String,
958 modifiers: &[String],
959 names: &mut Vec<String>,
960) {
961 let is_static = modifiers.iter().any(|modifier| modifier == "static");
962 let is_wildcard = modifiers.iter().any(|modifier| modifier == "wildcard");
963 if !is_static || is_wildcard || !names.is_empty() {
964 return;
965 }
966
967 if let Some((prefix, member)) = module_path.rsplit_once('.') {
968 if !prefix.is_empty() && !member.is_empty() {
969 names.push(member.to_string());
970 *module_path = prefix.to_string();
971 }
972 }
973}
974
975fn scala_request_dedup_key(req: &ImportRequest<'_>) -> ImportDedupKey {
976 let mut module_path = req.module_path.to_string();
977 let mut names: Vec<String> = req
978 .names
979 .iter()
980 .map(|name| normalize_scala_selector_for_dedup(name))
981 .collect();
982 let mut modifiers = req.modifiers.to_vec();
983 let mut import_kind = req.import_kind.map(str::to_string);
984
985 if req.default_import == Some("given") || module_path.ends_with(".given") {
986 import_kind.get_or_insert_with(|| "given".to_string());
987 if let Some(stripped) = module_path.strip_suffix(".given") {
988 module_path = stripped.to_string();
989 }
990 }
991
992 if matches!(req.default_import, Some("*") | Some("_"))
993 || matches!(req.namespace, Some("*") | Some("_"))
994 || module_path.ends_with(".*")
995 || module_path.ends_with("._")
996 {
997 if !modifiers.iter().any(|modifier| modifier == "wildcard") {
998 modifiers.push("wildcard".to_string());
999 }
1000 module_path = module_path
1001 .strip_suffix(".*")
1002 .or_else(|| module_path.strip_suffix("._"))
1003 .unwrap_or(&module_path)
1004 .to_string();
1005 }
1006
1007 if names.is_empty() {
1008 if let Some(alias) = req.alias.filter(|alias| !alias.is_empty()) {
1009 if let Some((prefix, leaf)) = module_path.rsplit_once('.') {
1010 names.push(format!("{leaf} as {alias}"));
1011 module_path = prefix.to_string();
1012 }
1013 }
1014 }
1015
1016 structured_dedup_key(
1017 &module_path,
1018 ImportKind::Value,
1019 &names,
1020 None,
1021 None,
1022 &modifiers,
1023 import_kind.as_deref(),
1024 )
1025}
1026
1027fn normalize_scala_selector_for_dedup(name: &str) -> String {
1028 let trimmed = name.trim();
1029 if let Some((from, to)) = trimmed.split_once("=>") {
1030 format!("{} as {}", from.trim(), to.trim())
1031 } else {
1032 trimmed.to_string()
1033 }
1034}
1035
1036fn canonical_dedup_key(lang: LangId, mut key: ImportDedupKey) -> ImportDedupKey {
1037 match &mut key.form {
1038 ImportForm::Structured { named, .. } | ImportForm::Solidity { named, .. } => {
1039 sort_named_specifiers(named);
1040 }
1041 ImportForm::Es { named, .. } | ImportForm::Python { named, .. } => {
1042 sort_named_specifiers(named);
1043 }
1044 ImportForm::RustUse { named, .. } => {
1045 sort_named_specifiers(named);
1046 }
1047 ImportForm::Go { .. } => {}
1048 }
1049
1050 if matches!(lang, LangId::Java | LangId::Kotlin) {
1051 if let Some(stripped) = key.module_path.strip_suffix(".*") {
1052 key.module_path = stripped.to_string();
1053 }
1054 if matches!(lang, LangId::Java) {
1055 if let ImportForm::Structured {
1056 named, modifiers, ..
1057 } = &mut key.form
1058 {
1059 normalize_java_static_member_key(&mut key.module_path, modifiers, named);
1060 }
1061 }
1062 } else if matches!(lang, LangId::Scala) {
1063 key.module_path = key
1064 .module_path
1065 .strip_suffix(".given")
1066 .or_else(|| key.module_path.strip_suffix(".*"))
1067 .or_else(|| key.module_path.strip_suffix("._"))
1068 .unwrap_or(&key.module_path)
1069 .to_string();
1070 }
1071
1072 key
1073}
1074
1075fn sort_named_specifiers(names: &mut [String]) {
1076 names.sort_by(|a, b| {
1077 specifier_imported_name(a)
1078 .cmp(specifier_imported_name(b))
1079 .then_with(|| a.cmp(b))
1080 });
1081}
1082
1083pub fn find_insertion_point(
1094 source: &str,
1095 block: &ImportBlock,
1096 group: ImportGroup,
1097 module_path: &str,
1098 type_only: bool,
1099) -> (usize, bool, bool) {
1100 if block.imports.is_empty() {
1101 return (0, false, source.is_empty().then_some(false).unwrap_or(true));
1103 }
1104
1105 let target_kind = if type_only {
1106 ImportKind::Type
1107 } else {
1108 ImportKind::Value
1109 };
1110
1111 let group_imports: Vec<&ImportStatement> =
1113 block.imports.iter().filter(|i| i.group == group).collect();
1114
1115 if group_imports.is_empty() {
1116 let preceding_last = block.imports.iter().filter(|i| i.group < group).last();
1119
1120 if let Some(last) = preceding_last {
1121 let end = last.byte_range.end;
1122 let insert_at = skip_newline(source, end);
1123 return (insert_at, true, true);
1124 }
1125
1126 let following_first = block.imports.iter().find(|i| i.group > group);
1128
1129 if let Some(first) = following_first {
1130 return (first.byte_range.start, false, true);
1131 }
1132
1133 let first_byte = import_byte_range(&block.imports)
1135 .map(|range| range.start)
1136 .unwrap_or(0);
1137 return (first_byte, false, true);
1138 }
1139
1140 for imp in &group_imports {
1142 let cmp = module_path.cmp(&imp.module_path);
1143 match cmp {
1144 std::cmp::Ordering::Less => {
1145 return (imp.byte_range.start, false, false);
1147 }
1148 std::cmp::Ordering::Equal => {
1149 if target_kind == ImportKind::Type && imp.kind == ImportKind::Value {
1151 let end = imp.byte_range.end;
1153 let insert_at = skip_newline(source, end);
1154 return (insert_at, false, false);
1155 }
1156 return (imp.byte_range.start, false, false);
1158 }
1159 std::cmp::Ordering::Greater => continue,
1160 }
1161 }
1162
1163 let Some(last) = group_imports.last() else {
1165 return (
1166 import_byte_range(&block.imports)
1167 .map(|range| range.end)
1168 .unwrap_or(0),
1169 false,
1170 false,
1171 );
1172 };
1173 let end = last.byte_range.end;
1174 let insert_at = skip_newline(source, end);
1175 (insert_at, false, false)
1176}
1177
1178pub fn generate_import(lang: LangId, req: &ImportRequest) -> String {
1182 match syntax_for(lang) {
1183 Some(engine) => engine.generate_line(req),
1184 None => String::new(),
1185 }
1186}
1187
1188pub fn generate_import_line(
1191 lang: LangId,
1192 module_path: &str,
1193 names: &[String],
1194 default_import: Option<&str>,
1195 type_only: bool,
1196) -> String {
1197 generate_import(
1198 lang,
1199 &ImportRequest::legacy(module_path, names, default_import, None, type_only),
1200 )
1201}
1202
1203pub fn generate_import_line_with_namespace(
1206 lang: LangId,
1207 module_path: &str,
1208 names: &[String],
1209 default_import: Option<&str>,
1210 namespace_import: Option<&str>,
1211 type_only: bool,
1212) -> String {
1213 generate_import(
1214 lang,
1215 &ImportRequest::legacy(
1216 module_path,
1217 names,
1218 default_import,
1219 namespace_import,
1220 type_only,
1221 ),
1222 )
1223}
1224
1225pub fn is_supported(lang: LangId) -> bool {
1227 syntax_for(lang).is_some()
1228}
1229
1230pub fn classify_group_ts(module_path: &str) -> ImportGroup {
1232 if module_path.starts_with('.') {
1233 ImportGroup::Internal
1234 } else {
1235 ImportGroup::External
1236 }
1237}
1238
1239pub fn classify_group(lang: LangId, module_path: &str) -> ImportGroup {
1241 match syntax_for(lang) {
1242 Some(engine) => engine.classify_group(module_path),
1243 None => ImportGroup::External,
1246 }
1247}
1248
1249pub fn parse_file_imports(
1252 path: &std::path::Path,
1253 lang: LangId,
1254) -> Result<(String, Tree, ImportBlock), crate::error::AftError> {
1255 let source =
1256 std::fs::read_to_string(path).map_err(|e| crate::error::AftError::FileNotFound {
1257 path: format!("{}: {}", path.display(), e),
1258 })?;
1259
1260 let grammar = grammar_for(lang);
1261 let mut parser = Parser::new();
1262 parser
1263 .set_language(&grammar)
1264 .map_err(|e| crate::error::AftError::ParseError {
1265 message: format!("grammar init failed for {:?}: {}", lang, e),
1266 })?;
1267
1268 let tree = parser
1269 .parse(&source, None)
1270 .ok_or_else(|| crate::error::AftError::ParseError {
1271 message: format!("tree-sitter parse returned None for {}", path.display()),
1272 })?;
1273
1274 let block = parse_imports(&source, &tree, lang);
1275 Ok((source, tree, block))
1276}
1277
1278fn parse_ts_imports(source: &str, tree: &Tree) -> ImportBlock {
1286 let root = tree.root_node();
1287 let mut imports = Vec::new();
1288
1289 let mut cursor = root.walk();
1290 if !cursor.goto_first_child() {
1291 return ImportBlock::empty();
1292 }
1293
1294 loop {
1295 let node = cursor.node();
1296 if node.kind() == "import_statement" {
1297 if let Some(imp) = parse_single_ts_import(source, &node) {
1298 imports.push(imp);
1299 }
1300 }
1301 if !cursor.goto_next_sibling() {
1302 break;
1303 }
1304 }
1305
1306 let byte_range = import_byte_range(&imports);
1307
1308 ImportBlock {
1309 imports,
1310 byte_range,
1311 }
1312}
1313
1314fn parse_single_ts_import(source: &str, node: &Node) -> Option<ImportStatement> {
1316 let raw_text = source[node.byte_range()].to_string();
1317 let byte_range = node.byte_range();
1318
1319 let module_path = extract_module_path(source, node)?;
1321
1322 let is_type_only = has_type_keyword(node);
1324
1325 let mut names = Vec::new();
1327 let mut default_import = None;
1328 let mut namespace_import = None;
1329
1330 let mut child_cursor = node.walk();
1331 if child_cursor.goto_first_child() {
1332 loop {
1333 let child = child_cursor.node();
1334 match child.kind() {
1335 "import_clause" => {
1336 extract_import_clause(
1337 source,
1338 &child,
1339 &mut names,
1340 &mut default_import,
1341 &mut namespace_import,
1342 );
1343 }
1344 "identifier" => {
1346 let text = &source[child.byte_range()];
1347 if text != "import" && text != "from" && text != "type" {
1348 default_import = Some(text.to_string());
1349 }
1350 }
1351 _ => {}
1352 }
1353 if !child_cursor.goto_next_sibling() {
1354 break;
1355 }
1356 }
1357 }
1358
1359 let kind = if names.is_empty() && default_import.is_none() && namespace_import.is_none() {
1361 ImportKind::SideEffect
1362 } else if is_type_only {
1363 ImportKind::Type
1364 } else {
1365 ImportKind::Value
1366 };
1367
1368 let group = classify_group_ts(&module_path);
1369
1370 let form = ImportForm::Es {
1371 default_import: default_import.clone(),
1372 namespace_import: namespace_import.clone(),
1373 named: names.clone(),
1374 type_only: is_type_only,
1375 side_effect: matches!(kind, ImportKind::SideEffect),
1376 };
1377
1378 Some(ImportStatement {
1379 module_path,
1380 names,
1381 default_import,
1382 namespace_import,
1383 kind,
1384 group,
1385 byte_range,
1386 raw_text,
1387 form,
1388 })
1389}
1390
1391fn extract_module_path(source: &str, node: &Node) -> Option<String> {
1395 let mut cursor = node.walk();
1396 if !cursor.goto_first_child() {
1397 return None;
1398 }
1399
1400 loop {
1401 let child = cursor.node();
1402 if child.kind() == "string" {
1403 let text = &source[child.byte_range()];
1405 let stripped = text
1406 .trim_start_matches(|c| c == '\'' || c == '"')
1407 .trim_end_matches(|c| c == '\'' || c == '"');
1408 return Some(stripped.to_string());
1409 }
1410 if !cursor.goto_next_sibling() {
1411 break;
1412 }
1413 }
1414 None
1415}
1416
1417fn has_type_keyword(node: &Node) -> bool {
1422 let mut cursor = node.walk();
1423 if !cursor.goto_first_child() {
1424 return false;
1425 }
1426
1427 loop {
1428 let child = cursor.node();
1429 if child.kind() == "type" {
1430 return true;
1431 }
1432 if !cursor.goto_next_sibling() {
1433 break;
1434 }
1435 }
1436
1437 false
1438}
1439
1440fn extract_import_clause(
1442 source: &str,
1443 node: &Node,
1444 names: &mut Vec<String>,
1445 default_import: &mut Option<String>,
1446 namespace_import: &mut Option<String>,
1447) {
1448 let mut cursor = node.walk();
1449 if !cursor.goto_first_child() {
1450 return;
1451 }
1452
1453 loop {
1454 let child = cursor.node();
1455 match child.kind() {
1456 "identifier" => {
1457 let text = &source[child.byte_range()];
1459 if text != "type" {
1460 *default_import = Some(text.to_string());
1461 }
1462 }
1463 "named_imports" => {
1464 extract_named_imports(source, &child, names);
1466 }
1467 "namespace_import" => {
1468 extract_namespace_import(source, &child, namespace_import);
1470 }
1471 _ => {}
1472 }
1473 if !cursor.goto_next_sibling() {
1474 break;
1475 }
1476 }
1477}
1478
1479fn extract_named_imports(source: &str, node: &Node, names: &mut Vec<String>) {
1498 let mut cursor = node.walk();
1499 if !cursor.goto_first_child() {
1500 return;
1501 }
1502
1503 loop {
1504 let child = cursor.node();
1505 if child.kind() == "import_specifier" {
1506 let raw = source[child.byte_range()].trim().to_string();
1511 if !raw.is_empty() {
1512 names.push(raw);
1513 } else if let Some(name_node) = child.child_by_field_name("name") {
1514 names.push(source[name_node.byte_range()].to_string());
1515 }
1516 }
1517 if !cursor.goto_next_sibling() {
1518 break;
1519 }
1520 }
1521}
1522
1523fn extract_namespace_import(source: &str, node: &Node, namespace_import: &mut Option<String>) {
1525 let mut cursor = node.walk();
1526 if !cursor.goto_first_child() {
1527 return;
1528 }
1529
1530 loop {
1531 let child = cursor.node();
1532 if child.kind() == "identifier" {
1533 *namespace_import = Some(source[child.byte_range()].to_string());
1534 return;
1535 }
1536 if !cursor.goto_next_sibling() {
1537 break;
1538 }
1539 }
1540}
1541
1542fn generate_ts_import_line(
1544 module_path: &str,
1545 names: &[String],
1546 default_import: Option<&str>,
1547 namespace_import: Option<&str>,
1548 type_only: bool,
1549) -> String {
1550 let type_prefix = if type_only { "type " } else { "" };
1551
1552 if names.is_empty() && default_import.is_none() && namespace_import.is_none() {
1554 return format!("import '{module_path}';");
1555 }
1556
1557 if names.is_empty() && default_import.is_none() {
1559 if let Some(namespace) = namespace_import {
1560 return format!("import {type_prefix}* as {namespace} from '{module_path}';");
1561 }
1562 }
1563
1564 if names.is_empty() {
1566 if let (Some(def), Some(namespace)) = (default_import, namespace_import) {
1567 return format!("import {type_prefix}{def}, * as {namespace} from '{module_path}';");
1568 }
1569 }
1570
1571 if names.is_empty() && namespace_import.is_none() {
1573 if let Some(def) = default_import {
1574 return format!("import {type_prefix}{def} from '{module_path}';");
1575 }
1576 }
1577
1578 if default_import.is_none() && namespace_import.is_none() {
1580 let mut sorted_names = names.to_vec();
1581 sort_named_specifiers(&mut sorted_names);
1582 let names_str = sorted_names.join(", ");
1583 return format!("import {type_prefix}{{ {names_str} }} from '{module_path}';");
1584 }
1585
1586 if default_import.is_none() {
1588 if let Some(namespace) = namespace_import {
1589 let mut sorted_names = names.to_vec();
1590 sort_named_specifiers(&mut sorted_names);
1591 let names_str = sorted_names.join(", ");
1592 return format!(
1593 "import {type_prefix}{{ {names_str} }}, * as {namespace} from '{module_path}';"
1594 );
1595 }
1596 }
1597
1598 if let (Some(def), Some(namespace)) = (default_import, namespace_import) {
1600 let mut sorted_names = names.to_vec();
1601 sort_named_specifiers(&mut sorted_names);
1602 let names_str = sorted_names.join(", ");
1603 return format!(
1604 "import {type_prefix}{def}, {{ {names_str} }}, * as {namespace} from '{module_path}';"
1605 );
1606 }
1607
1608 if let Some(def) = default_import {
1610 let mut sorted_names = names.to_vec();
1611 sort_named_specifiers(&mut sorted_names);
1612 let names_str = sorted_names.join(", ");
1613 return format!("import {type_prefix}{def}, {{ {names_str} }} from '{module_path}';");
1614 }
1615
1616 format!("import '{module_path}';")
1618}
1619
1620const PYTHON_STDLIB: &[&str] = &[
1628 "__future__",
1629 "_thread",
1630 "abc",
1631 "aifc",
1632 "argparse",
1633 "array",
1634 "ast",
1635 "asynchat",
1636 "asyncio",
1637 "asyncore",
1638 "atexit",
1639 "audioop",
1640 "base64",
1641 "bdb",
1642 "binascii",
1643 "bisect",
1644 "builtins",
1645 "bz2",
1646 "calendar",
1647 "cgi",
1648 "cgitb",
1649 "chunk",
1650 "cmath",
1651 "cmd",
1652 "code",
1653 "codecs",
1654 "codeop",
1655 "collections",
1656 "colorsys",
1657 "compileall",
1658 "concurrent",
1659 "configparser",
1660 "contextlib",
1661 "contextvars",
1662 "copy",
1663 "copyreg",
1664 "cProfile",
1665 "crypt",
1666 "csv",
1667 "ctypes",
1668 "curses",
1669 "dataclasses",
1670 "datetime",
1671 "dbm",
1672 "decimal",
1673 "difflib",
1674 "dis",
1675 "distutils",
1676 "doctest",
1677 "email",
1678 "encodings",
1679 "enum",
1680 "errno",
1681 "faulthandler",
1682 "fcntl",
1683 "filecmp",
1684 "fileinput",
1685 "fnmatch",
1686 "fractions",
1687 "ftplib",
1688 "functools",
1689 "gc",
1690 "getopt",
1691 "getpass",
1692 "gettext",
1693 "glob",
1694 "grp",
1695 "gzip",
1696 "hashlib",
1697 "heapq",
1698 "hmac",
1699 "html",
1700 "http",
1701 "idlelib",
1702 "imaplib",
1703 "imghdr",
1704 "importlib",
1705 "inspect",
1706 "io",
1707 "ipaddress",
1708 "itertools",
1709 "json",
1710 "keyword",
1711 "lib2to3",
1712 "linecache",
1713 "locale",
1714 "logging",
1715 "lzma",
1716 "mailbox",
1717 "mailcap",
1718 "marshal",
1719 "math",
1720 "mimetypes",
1721 "mmap",
1722 "modulefinder",
1723 "multiprocessing",
1724 "netrc",
1725 "numbers",
1726 "operator",
1727 "optparse",
1728 "os",
1729 "pathlib",
1730 "pdb",
1731 "pickle",
1732 "pickletools",
1733 "pipes",
1734 "pkgutil",
1735 "platform",
1736 "plistlib",
1737 "poplib",
1738 "posixpath",
1739 "pprint",
1740 "profile",
1741 "pstats",
1742 "pty",
1743 "pwd",
1744 "py_compile",
1745 "pyclbr",
1746 "pydoc",
1747 "queue",
1748 "quopri",
1749 "random",
1750 "re",
1751 "readline",
1752 "reprlib",
1753 "resource",
1754 "rlcompleter",
1755 "runpy",
1756 "sched",
1757 "secrets",
1758 "select",
1759 "selectors",
1760 "shelve",
1761 "shlex",
1762 "shutil",
1763 "signal",
1764 "site",
1765 "smtplib",
1766 "sndhdr",
1767 "socket",
1768 "socketserver",
1769 "sqlite3",
1770 "ssl",
1771 "stat",
1772 "statistics",
1773 "string",
1774 "stringprep",
1775 "struct",
1776 "subprocess",
1777 "symtable",
1778 "sys",
1779 "sysconfig",
1780 "syslog",
1781 "tabnanny",
1782 "tarfile",
1783 "tempfile",
1784 "termios",
1785 "textwrap",
1786 "threading",
1787 "time",
1788 "timeit",
1789 "tkinter",
1790 "token",
1791 "tokenize",
1792 "tomllib",
1793 "trace",
1794 "traceback",
1795 "tracemalloc",
1796 "tty",
1797 "turtle",
1798 "types",
1799 "typing",
1800 "unicodedata",
1801 "unittest",
1802 "urllib",
1803 "uuid",
1804 "venv",
1805 "warnings",
1806 "wave",
1807 "weakref",
1808 "webbrowser",
1809 "wsgiref",
1810 "xml",
1811 "xmlrpc",
1812 "zipapp",
1813 "zipfile",
1814 "zipimport",
1815 "zlib",
1816];
1817
1818pub fn classify_group_py(module_path: &str) -> ImportGroup {
1820 if module_path.starts_with('.') {
1822 return ImportGroup::Internal;
1823 }
1824 let top_module = module_path.split('.').next().unwrap_or(module_path);
1826 if PYTHON_STDLIB.contains(&top_module) {
1827 ImportGroup::Stdlib
1828 } else {
1829 ImportGroup::External
1830 }
1831}
1832
1833fn parse_py_imports(source: &str, tree: &Tree) -> ImportBlock {
1835 let root = tree.root_node();
1836 let mut imports = Vec::new();
1837
1838 let mut cursor = root.walk();
1839 if !cursor.goto_first_child() {
1840 return ImportBlock::empty();
1841 }
1842
1843 loop {
1844 let node = cursor.node();
1845 match node.kind() {
1846 "import_statement" => {
1847 if let Some(imp) = parse_py_import_statement(source, &node) {
1848 imports.push(imp);
1849 }
1850 }
1851 "import_from_statement" => {
1852 if let Some(imp) = parse_py_import_from_statement(source, &node) {
1853 imports.push(imp);
1854 }
1855 }
1856 _ => {}
1857 }
1858 if !cursor.goto_next_sibling() {
1859 break;
1860 }
1861 }
1862
1863 let byte_range = import_byte_range(&imports);
1864
1865 ImportBlock {
1866 imports,
1867 byte_range,
1868 }
1869}
1870
1871fn parse_py_import_statement(source: &str, node: &Node) -> Option<ImportStatement> {
1873 let raw_text = source[node.byte_range()].to_string();
1874 let byte_range = node.byte_range();
1875
1876 let mut module_path = String::new();
1878 let mut c = node.walk();
1879 if c.goto_first_child() {
1880 loop {
1881 if c.node().kind() == "dotted_name" {
1882 module_path = source[c.node().byte_range()].to_string();
1883 break;
1884 }
1885 if !c.goto_next_sibling() {
1886 break;
1887 }
1888 }
1889 }
1890 if module_path.is_empty() {
1891 return None;
1892 }
1893
1894 let group = classify_group_py(&module_path);
1895
1896 Some(ImportStatement {
1897 module_path,
1898 names: Vec::new(),
1899 default_import: None,
1900 namespace_import: None,
1901 kind: ImportKind::Value,
1902 group,
1903 byte_range,
1904 raw_text,
1905 form: ImportForm::Python {
1906 from_import: false,
1907 named: Vec::new(),
1908 },
1909 })
1910}
1911
1912fn parse_py_import_from_statement(source: &str, node: &Node) -> Option<ImportStatement> {
1914 let raw_text = source[node.byte_range()].to_string();
1915 let byte_range = node.byte_range();
1916
1917 let mut module_path = String::new();
1918 let mut names = Vec::new();
1919
1920 let mut c = node.walk();
1921 if c.goto_first_child() {
1922 loop {
1923 let child = c.node();
1924 match child.kind() {
1925 "dotted_name" => {
1926 if module_path.is_empty()
1931 && !has_seen_import_keyword(source, node, child.start_byte())
1932 {
1933 module_path = source[child.byte_range()].to_string();
1934 } else {
1935 names.push(source[child.byte_range()].to_string());
1937 }
1938 }
1939 "relative_import" => {
1940 module_path = source[child.byte_range()].to_string();
1942 }
1943 _ => {}
1944 }
1945 if !c.goto_next_sibling() {
1946 break;
1947 }
1948 }
1949 }
1950
1951 if module_path.is_empty() {
1953 return None;
1954 }
1955
1956 let group = classify_group_py(&module_path);
1957
1958 Some(ImportStatement {
1959 module_path,
1960 names: names.clone(),
1961 default_import: None,
1962 namespace_import: None,
1963 kind: ImportKind::Value,
1964 group,
1965 byte_range,
1966 raw_text,
1967 form: ImportForm::Python {
1968 from_import: true,
1969 named: names,
1970 },
1971 })
1972}
1973
1974fn has_seen_import_keyword(_source: &str, parent: &Node, before_byte: usize) -> bool {
1976 let mut c = parent.walk();
1977 if c.goto_first_child() {
1978 loop {
1979 let child = c.node();
1980 if child.kind() == "import" && child.start_byte() < before_byte {
1981 return true;
1982 }
1983 if child.start_byte() >= before_byte {
1984 return false;
1985 }
1986 if !c.goto_next_sibling() {
1987 break;
1988 }
1989 }
1990 }
1991 false
1992}
1993
1994fn generate_py_import_line(
1996 module_path: &str,
1997 names: &[String],
1998 _default_import: Option<&str>,
1999) -> String {
2000 if names.is_empty() {
2001 format!("import {module_path}")
2003 } else {
2004 let mut sorted = names.to_vec();
2006 sorted.sort();
2007 let names_str = sorted.join(", ");
2008 format!("from {module_path} import {names_str}")
2009 }
2010}
2011
2012pub fn classify_group_rs(module_path: &str) -> ImportGroup {
2018 let first_seg = module_path.split("::").next().unwrap_or(module_path);
2020 match first_seg {
2021 "std" | "core" | "alloc" => ImportGroup::Stdlib,
2022 "crate" | "self" | "super" => ImportGroup::Internal,
2023 _ => ImportGroup::External,
2024 }
2025}
2026
2027fn parse_rs_imports(source: &str, tree: &Tree) -> ImportBlock {
2029 let root = tree.root_node();
2030 let mut imports = Vec::new();
2031
2032 let mut cursor = root.walk();
2033 if !cursor.goto_first_child() {
2034 return ImportBlock::empty();
2035 }
2036
2037 loop {
2038 let node = cursor.node();
2039 if node.kind() == "use_declaration" {
2040 if let Some(imp) = parse_rs_use_declaration(source, &node) {
2041 imports.push(imp);
2042 }
2043 }
2044 if !cursor.goto_next_sibling() {
2045 break;
2046 }
2047 }
2048
2049 let byte_range = import_byte_range(&imports);
2050
2051 ImportBlock {
2052 imports,
2053 byte_range,
2054 }
2055}
2056
2057fn parse_rs_use_declaration(source: &str, node: &Node) -> Option<ImportStatement> {
2059 let raw_text = source[node.byte_range()].to_string();
2060 let byte_range = node.byte_range();
2061
2062 let mut visibility: Option<String> = None;
2066 let mut use_path = String::new();
2067 let mut names = Vec::new();
2068
2069 let mut c = node.walk();
2070 if c.goto_first_child() {
2071 loop {
2072 let child = c.node();
2073 match child.kind() {
2074 "visibility_modifier" => {
2075 visibility = Some(source[child.byte_range()].to_string());
2076 }
2077 "scoped_identifier" | "identifier" | "use_as_clause" => {
2078 use_path = source[child.byte_range()].to_string();
2080 }
2081 "scoped_use_list" => {
2082 use_path = source[child.byte_range()].to_string();
2084 extract_rs_use_list_names(source, &child, &mut names);
2086 }
2087 _ => {}
2088 }
2089 if !c.goto_next_sibling() {
2090 break;
2091 }
2092 }
2093 }
2094
2095 if use_path.is_empty() {
2096 return None;
2097 }
2098
2099 let group = classify_group_rs(&use_path);
2100
2101 Some(ImportStatement {
2102 module_path: use_path,
2103 names: names.clone(),
2104 default_import: visibility.clone(),
2107 namespace_import: None,
2108 kind: ImportKind::Value,
2109 group,
2110 byte_range,
2111 raw_text,
2112 form: ImportForm::RustUse {
2113 visibility,
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}