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