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