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