1use crate::lang::Lang;
24use anyhow::Result;
25use tree_sitter::{Node, Parser};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
36pub enum RenameTarget {
37 Callable,
39 Signal,
42 Type,
44 Value,
46 #[default]
48 Unresolved,
49}
50
51impl RenameTarget {
52 pub fn from_indexed_kind(kind: &str) -> Self {
59 match kind {
60 "function" | "method" => Self::Callable,
61 "signal" => Self::Signal,
62 "struct" | "enum" | "enum_class" | "trait" | "class" | "data_class"
63 | "sealed_class" | "interface" | "type_alias" | "union" | "object"
64 | "companion_object" | "impl" => Self::Type,
65 "const" | "static" | "variable" => Self::Value,
66 _ => Self::Unresolved,
67 }
68 }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub struct IdentifierOccurrence {
74 pub start_byte: usize,
75 pub end_byte: usize,
76 pub expands_shorthand_key: bool,
81}
82
83pub fn identifier_node_kinds(lang: Lang) -> &'static [&'static str] {
89 match lang {
90 #[cfg(feature = "lang-rust")]
94 Lang::Rust => &[
95 "identifier",
96 "type_identifier",
97 "field_identifier",
98 "shorthand_field_identifier",
99 ],
100 #[cfg(feature = "lang-python")]
101 Lang::Python => &["identifier"],
102 #[cfg(feature = "lang-typescript")]
103 Lang::TypeScript | Lang::Tsx => &[
104 "identifier",
105 "type_identifier",
106 "property_identifier",
107 "shorthand_property_identifier",
108 "shorthand_property_identifier_pattern",
109 ],
110 #[cfg(feature = "lang-javascript")]
111 Lang::JavaScript | Lang::Jsx => &[
112 "identifier",
113 "property_identifier",
114 "shorthand_property_identifier",
115 "shorthand_property_identifier_pattern",
116 ],
117 #[cfg(feature = "lang-kotlin")]
118 Lang::Kotlin => &["identifier"],
119 #[cfg(feature = "lang-zig")]
120 Lang::Zig => &["identifier"],
121 #[cfg(feature = "lang-bash")]
126 Lang::Bash => &["word", "variable_name"],
127 #[cfg(feature = "lang-go")]
132 Lang::Go => &["identifier", "type_identifier", "field_identifier"],
133 #[cfg(feature = "lang-gdscript")]
136 Lang::GdScript => &["identifier", "name"],
137 #[cfg(feature = "lang-markdown")]
139 Lang::Markdown => &[],
140 }
141}
142
143fn occurrence_is_renamable(lang: Lang, node: Node) -> bool {
153 match lang {
154 #[cfg(feature = "lang-bash")]
155 Lang::Bash => {
156 if node.kind() != "word" {
157 return true;
160 }
161 node.parent().is_some_and(|parent| {
162 matches!(parent.kind(), "function_definition" | "command_name")
163 })
164 }
165 _ => {
166 let _ = node;
167 true
168 }
169 }
170}
171
172#[allow(unused_variables)]
179fn occurrence_matches_target(
180 lang: Lang,
181 node: Node,
182 source: &[u8],
183 target: RenameTarget,
184) -> bool {
185 if target == RenameTarget::Unresolved {
186 return true;
187 }
188 match lang {
189 #[cfg(feature = "lang-rust")]
190 Lang::Rust => rust_occurrence_matches_target(node, target),
191 #[cfg(feature = "lang-python")]
192 Lang::Python => python_occurrence_matches_target(node, source, target),
193 #[cfg(feature = "lang-gdscript")]
194 Lang::GdScript => gdscript_occurrence_matches_target(node, target),
195 #[cfg(feature = "lang-typescript")]
196 Lang::TypeScript | Lang::Tsx => js_like_occurrence_matches_target(node, target),
197 #[cfg(feature = "lang-javascript")]
198 Lang::JavaScript | Lang::Jsx => js_like_occurrence_matches_target(node, target),
199 #[cfg(feature = "lang-kotlin")]
200 Lang::Kotlin => kotlin_occurrence_matches_target(node, source, target),
201 #[cfg(feature = "lang-zig")]
202 Lang::Zig => zig_occurrence_matches_target(node, source, target),
203 #[cfg(feature = "lang-go")]
204 Lang::Go => go_occurrence_matches_target(node, source, target),
205 _ => {
206 let _ = node;
207 true
208 }
209 }
210}
211
212#[cfg(feature = "lang-go")]
221fn go_occurrence_matches_target(node: Node, source: &[u8], target: RenameTarget) -> bool {
222 let Some(parent) = node.parent() else {
223 return true;
224 };
225 match parent.kind() {
226 "field_declaration" => !parent
230 .children_by_field_name("name", &mut parent.walk())
231 .any(|name| name.id() == node.id()),
232 "selector_expression" => {
233 if parent
234 .child_by_field_name("field")
235 .is_none_or(|field| field.id() != node.id())
236 {
237 return true;
238 }
239 if go_receiver_is_imported_package(parent, source) {
240 return true;
241 }
242 target == RenameTarget::Callable
243 && parent.parent().is_some_and(|call| {
244 call.kind() == "call_expression"
245 && call
246 .child_by_field_name("function")
247 .is_some_and(|function| function.id() == parent.id())
248 })
249 }
250 _ => true,
251 }
252}
253
254#[cfg(feature = "lang-go")]
262fn go_receiver_is_imported_package(selector: Node, source: &[u8]) -> bool {
263 let Some(mut operand) = selector.child_by_field_name("operand") else {
264 return false;
265 };
266 while operand.kind() == "selector_expression" {
267 let Some(inner) = operand.child_by_field_name("operand") else {
268 return false;
269 };
270 operand = inner;
271 }
272 if operand.kind() != "identifier" && operand.kind() != "package_identifier" {
273 return false;
274 }
275 let Ok(name) = operand.utf8_text(source) else {
276 return false;
277 };
278 go_file_imports_package(selector, name, source)
279}
280
281#[cfg(feature = "lang-go")]
282fn go_file_imports_package(node: Node, name: &str, source: &[u8]) -> bool {
283 let mut root = node;
284 while let Some(parent) = root.parent() {
285 root = parent;
286 }
287 let mut found = false;
288 go_walk_import_specs(root, source, &mut |bound| {
289 if bound == name {
290 found = true;
291 }
292 });
293 found
294}
295
296#[cfg(feature = "lang-go")]
298fn go_walk_import_specs(node: Node, source: &[u8], visit: &mut impl FnMut(&str)) {
299 if node.kind() == "import_spec" {
300 if let Some(alias) = node.child_by_field_name("name")
301 && let Ok(text) = alias.utf8_text(source)
302 {
303 visit(text);
304 return;
305 }
306 if let Some(path) = node.child_by_field_name("path")
307 && let Ok(text) = path.utf8_text(source)
308 {
309 let trimmed = text.trim_matches('"');
310 if let Some(last) = trimmed.rsplit('/').next() {
311 visit(last);
312 }
313 }
314 return;
315 }
316 let mut cursor = node.walk();
317 for child in node.children(&mut cursor) {
318 go_walk_import_specs(child, source, visit);
319 }
320}
321
322#[cfg(feature = "lang-python")]
330fn python_occurrence_matches_target(node: Node, source: &[u8], target: RenameTarget) -> bool {
331 let Some(attribute) = node.parent().filter(|parent| parent.kind() == "attribute") else {
332 return true;
333 };
334 if attribute
335 .child_by_field_name("attribute")
336 .is_none_or(|name| name.id() != node.id())
337 {
338 return true;
339 }
340 if python_receiver_is_imported_module(attribute, source) {
341 return true;
342 }
343
344 target == RenameTarget::Callable
345 && attribute.parent().is_some_and(|call| {
346 call.kind() == "call"
347 && call
348 .child_by_field_name("function")
349 .is_some_and(|function| function.id() == attribute.id())
350 })
351}
352
353#[cfg(feature = "lang-python")]
364fn python_receiver_is_imported_module(attribute: Node, source: &[u8]) -> bool {
365 let Some(mut object) = attribute.child_by_field_name("object") else {
366 return false;
367 };
368 while object.kind() == "attribute" {
369 let Some(inner) = object.child_by_field_name("object") else {
370 return false;
371 };
372 object = inner;
373 }
374 if object.kind() != "identifier" {
375 return false;
376 }
377 let Ok(name) = object.utf8_text(source) else {
378 return false;
379 };
380 python_file_imports_module(attribute, name, source)
381}
382
383#[cfg(feature = "lang-python")]
385fn python_file_imports_module(node: Node, name: &str, source: &[u8]) -> bool {
386 let mut root = node;
387 while let Some(parent) = root.parent() {
388 root = parent;
389 }
390 let mut cursor = root.walk();
391 let mut descend = true;
392 loop {
393 if descend {
394 let current = cursor.node();
395 if current.kind() == "import_statement"
396 && python_import_binds(current, name, source)
397 {
398 return true;
399 }
400 if cursor.goto_first_child() {
401 continue;
402 }
403 }
404 if cursor.goto_next_sibling() {
405 descend = true;
406 continue;
407 }
408 if !cursor.goto_parent() {
409 return false;
410 }
411 descend = false;
412 }
413}
414
415#[cfg(feature = "lang-python")]
419fn python_import_binds(import: Node, name: &str, source: &[u8]) -> bool {
420 let mut cursor = import.walk();
421 import.named_children(&mut cursor).any(|clause| {
422 let bound = match clause.kind() {
423 "aliased_import" => clause.child_by_field_name("alias"),
424 "dotted_name" => clause.named_child(0),
425 _ => None,
426 };
427 bound.is_some_and(|bound| bound.utf8_text(source).is_ok_and(|text| text == name))
428 })
429}
430
431#[cfg(feature = "lang-kotlin")]
439fn kotlin_occurrence_matches_target(node: Node, source: &[u8], target: RenameTarget) -> bool {
440 let Some(navigation) = node
441 .parent()
442 .filter(|parent| parent.kind() == "navigation_expression")
443 else {
444 return true;
445 };
446 if node.prev_named_sibling().is_none() {
447 return true;
448 }
449 if kotlin_receiver_is_namespace(navigation, source) {
450 return true;
451 }
452
453 target == RenameTarget::Callable
454 && navigation.parent().is_some_and(|call| {
455 call.kind() == "call_expression"
456 && call
457 .named_child(0)
458 .is_some_and(|function| function.id() == navigation.id())
459 })
460}
461
462#[cfg(feature = "lang-kotlin")]
465fn kotlin_receiver_is_namespace(navigation: Node, source: &[u8]) -> bool {
466 let mut receiver = navigation;
467 while receiver.kind() == "navigation_expression" {
468 let Some(inner) = receiver.named_child(0) else {
469 return false;
470 };
471 receiver = inner;
472 }
473 if receiver.kind() != "identifier" {
474 return false;
475 }
476 let Ok(name) = receiver.utf8_text(source) else {
477 return false;
478 };
479 kotlin_file_declares_type(navigation, name, source)
480 || kotlin_file_imports_name(navigation, name, source)
481}
482
483#[cfg(feature = "lang-kotlin")]
486fn kotlin_file_declares_type(node: Node, name: &str, source: &[u8]) -> bool {
487 let mut root = node;
488 while let Some(parent) = root.parent() {
489 root = parent;
490 }
491 let mut cursor = root.walk();
492 let mut descend = true;
493 loop {
494 if descend {
495 let current = cursor.node();
496 if matches!(
497 current.kind(),
498 "class_declaration" | "object_declaration" | "interface_declaration"
499 ) && current
500 .child_by_field_name("name")
501 .and_then(|declared| declared.utf8_text(source).ok())
502 == Some(name)
503 {
504 return true;
505 }
506 if cursor.goto_first_child() {
507 continue;
508 }
509 }
510 if cursor.goto_next_sibling() {
511 descend = true;
512 continue;
513 }
514 if !cursor.goto_parent() {
515 return false;
516 }
517 descend = false;
518 }
519}
520
521#[cfg(feature = "lang-kotlin")]
527fn kotlin_file_imports_name(node: Node, name: &str, source: &[u8]) -> bool {
528 let mut root = node;
529 while let Some(parent) = root.parent() {
530 root = parent;
531 }
532 let mut cursor = root.walk();
533 let mut descend = true;
534 loop {
535 if descend {
536 let current = cursor.node();
537 if current.kind() == "import" && kotlin_import_binds(current, name, source) {
538 return true;
539 }
540 if cursor.goto_first_child() {
541 continue;
542 }
543 }
544 if cursor.goto_next_sibling() {
545 descend = true;
546 continue;
547 }
548 if !cursor.goto_parent() {
549 return false;
550 }
551 descend = false;
552 }
553}
554
555#[cfg(feature = "lang-kotlin")]
556fn kotlin_import_binds(import: Node, name: &str, source: &[u8]) -> bool {
557 let mut cursor = import.walk();
558 let children = import.named_children(&mut cursor).collect::<Vec<_>>();
559 if let Some(alias) = children
560 .get(1)
561 .filter(|child| child.kind() == "identifier")
562 {
563 return alias
564 .utf8_text(source)
565 .is_ok_and(|bound_name| bound_name == name);
566 }
567 children
568 .first()
569 .filter(|path| matches!(path.kind(), "identifier" | "qualified_identifier"))
570 .and_then(|path| path.utf8_text(source).ok())
571 .and_then(|path| path.rsplit('.').next())
572 .is_some_and(|bound_name| bound_name == name)
573}
574
575#[cfg(feature = "lang-zig")]
589fn zig_occurrence_matches_target(node: Node, source: &[u8], target: RenameTarget) -> bool {
590 let Some(parent) = node.parent() else {
591 return true;
592 };
593 match parent.kind() {
594 "container_field" => parent
598 .child_by_field_name("name")
599 .is_none_or(|name| name.id() != node.id()),
600 "field_expression" => {
601 if parent
602 .child_by_field_name("member")
603 .is_none_or(|member| member.id() != node.id())
604 {
605 return true;
606 }
607 if zig_receiver_is_namespace(parent, source) {
608 return true;
609 }
610 target == RenameTarget::Callable
611 && parent.parent().is_some_and(|call| {
612 call.kind() == "call_expression"
613 && call
614 .child_by_field_name("function")
615 .is_some_and(|function| function.id() == parent.id())
616 })
617 }
618 _ => true,
619 }
620}
621
622#[cfg(feature = "lang-zig")]
636fn zig_receiver_is_namespace(field_expression: Node, source: &[u8]) -> bool {
637 let Some(mut object) = field_expression.child_by_field_name("object") else {
638 return false;
639 };
640 while object.kind() == "field_expression" {
641 let Some(inner) = object.child_by_field_name("object") else {
642 return false;
643 };
644 object = inner;
645 }
646 match object.kind() {
647 "builtin_function" => zig_is_import_builtin(object, source),
648 "identifier" => object
649 .utf8_text(source)
650 .is_ok_and(|name| zig_file_binds_namespace(field_expression, name, source)),
651 _ => false,
652 }
653}
654
655#[cfg(feature = "lang-zig")]
657fn zig_is_import_builtin(builtin: Node, source: &[u8]) -> bool {
658 let mut cursor = builtin.walk();
659 builtin.named_children(&mut cursor).any(|child| {
660 child.kind() == "builtin_identifier"
661 && child.utf8_text(source).is_ok_and(|text| text == "@import")
662 })
663}
664
665#[cfg(feature = "lang-zig")]
673fn zig_file_binds_namespace(node: Node, name: &str, source: &[u8]) -> bool {
674 let mut root = node;
675 while let Some(parent) = root.parent() {
676 root = parent;
677 }
678 let mut cursor = root.walk();
679 let mut descend = true;
680 loop {
681 if descend {
682 let current = cursor.node();
683 if current.kind() == "variable_declaration"
684 && zig_declaration_binds_namespace(current, name, source)
685 {
686 return true;
687 }
688 if cursor.goto_first_child() {
689 continue;
690 }
691 }
692 if cursor.goto_next_sibling() {
693 descend = true;
694 continue;
695 }
696 if !cursor.goto_parent() {
697 return false;
698 }
699 descend = false;
700 }
701}
702
703#[cfg(feature = "lang-zig")]
705fn zig_declaration_binds_namespace(declaration: Node, name: &str, source: &[u8]) -> bool {
706 let mut cursor = declaration.walk();
707 let children: Vec<Node> = declaration.named_children(&mut cursor).collect();
708 let binds_name = children.iter().any(|child| {
709 child.kind() == "identifier" && child.utf8_text(source).is_ok_and(|text| text == name)
710 });
711 if !binds_name {
712 return false;
713 }
714 children.iter().any(|child| match child.kind() {
715 "builtin_function" => zig_is_import_builtin(*child, source),
716 "struct_declaration" | "enum_declaration" | "union_declaration"
719 | "opaque_declaration" => true,
720 _ => false,
721 })
722}
723
724#[cfg(any(feature = "lang-typescript", feature = "lang-javascript"))]
731fn js_like_occurrence_matches_target(node: Node, target: RenameTarget) -> bool {
732 match node.kind() {
733 "property_identifier" => false,
734 "type_identifier" => target == RenameTarget::Type,
735 _ => true,
736 }
737}
738
739#[allow(unused_variables)]
747fn occurrence_expands_shorthand_key(lang: Lang, node: Node, target: RenameTarget) -> bool {
748 if target == RenameTarget::Unresolved {
749 return false;
750 }
751 match lang {
752 #[cfg(feature = "lang-typescript")]
753 Lang::TypeScript | Lang::Tsx => js_like_shorthand_key(node),
754 #[cfg(feature = "lang-javascript")]
755 Lang::JavaScript | Lang::Jsx => js_like_shorthand_key(node),
756 _ => false,
757 }
758}
759
760#[cfg(any(feature = "lang-typescript", feature = "lang-javascript"))]
768fn js_like_shorthand_key(node: Node) -> bool {
769 node.kind() == "shorthand_property_identifier"
770 && node.parent().is_some_and(|parent| parent.kind() == "object")
771}
772
773#[cfg(feature = "lang-rust")]
778fn rust_occurrence_matches_target(node: Node, target: RenameTarget) -> bool {
779 let parent_kind = node.parent().map(|parent| parent.kind()).unwrap_or("");
780 match node.kind() {
781 "field_identifier" => {
782 target == RenameTarget::Callable && parent_kind == "field_expression" && {
787 node.parent()
788 .and_then(|field_expression| {
789 let call = field_expression.parent()?;
790 (call.kind() == "call_expression"
791 && call.child_by_field_name("function")?.id() == field_expression.id())
792 .then_some(())
793 })
794 .is_some()
795 }
796 }
797 "shorthand_field_identifier" => target == RenameTarget::Value,
798 "identifier" if parent_kind == "shorthand_field_initializer" => {
803 matches!(target, RenameTarget::Value)
804 }
805 "type_identifier" => target == RenameTarget::Type,
806 _ => true,
807 }
808}
809
810#[cfg(feature = "lang-gdscript")]
815fn gdscript_occurrence_matches_target(node: Node, target: RenameTarget) -> bool {
816 let parent_kind = node.parent().map(|parent| parent.kind()).unwrap_or("");
817 match node.kind() {
818 "name" => {
819 let declares: &[&str] = match target {
820 RenameTarget::Callable => &["function_definition"],
821 RenameTarget::Signal => &["signal_statement"],
822 RenameTarget::Type => &["class_definition", "class_name_statement", "enum_definition"],
823 RenameTarget::Value => &[
824 "variable_statement",
825 "const_statement",
826 "export_variable_statement",
827 "onready_variable_statement",
828 ],
829 RenameTarget::Unresolved => return true,
830 };
831 declares.contains(&parent_kind)
832 }
833 "identifier" if parent_kind == "parameters" => false,
836 _ => true,
837 }
838}
839
840pub fn identifier_occurrences(
846 lang: Lang,
847 source: &[u8],
848 name: &str,
849) -> Result<Vec<IdentifierOccurrence>> {
850 identifier_occurrences_for(lang, source, name, RenameTarget::Unresolved)
851}
852
853pub fn identifier_occurrences_for(
855 lang: Lang,
856 source: &[u8],
857 name: &str,
858 target: RenameTarget,
859) -> Result<Vec<IdentifierOccurrence>> {
860 let kinds = identifier_node_kinds(lang);
861 if kinds.is_empty() || name.is_empty() {
862 return Ok(Vec::new());
863 }
864
865 let ts_lang = lang.tree_sitter_language();
866 let mut parser = Parser::new();
867 parser.set_language(&ts_lang)?;
868 let tree = parser
869 .parse(source, None)
870 .ok_or_else(|| anyhow::anyhow!("parse failed"))?;
871
872 let mut occurrences = Vec::new();
873 let mut shadowing_declaration_line: Option<usize> = None;
876 let mut saw_ambiguous_reference = false;
878 let mut cursor = tree.walk();
879 let mut descend = true;
880 loop {
881 if descend {
882 let node = cursor.node();
883 if kinds.contains(&node.kind())
884 && node.utf8_text(source).is_ok_and(|it| it == name)
885 && occurrence_is_renamable(lang, node)
886 {
887 if occurrence_matches_target(lang, node, source, target) {
888 occurrences.push(IdentifierOccurrence {
889 start_byte: node.start_byte(),
890 end_byte: node.end_byte(),
891 expands_shorthand_key: occurrence_expands_shorthand_key(
892 lang, node, target,
893 ),
894 });
895 saw_ambiguous_reference |= occurrence_is_ambiguous_reference(lang, node, target);
896 } else if shadowing_declaration_line.is_none()
897 && occurrence_shadows_target(lang, node, target)
898 {
899 shadowing_declaration_line = Some(node.start_position().row + 1);
900 }
901 }
902 if cursor.goto_first_child() {
903 continue;
904 }
905 }
906 if cursor.goto_next_sibling() {
907 descend = true;
908 continue;
909 }
910 if !cursor.goto_parent() {
911 break;
912 }
913 descend = false;
914 }
915
916 occurrences.sort_by_key(|occurrence| (occurrence.start_byte, occurrence.end_byte));
920 occurrences.dedup();
921
922 if let Some(line) = shadowing_declaration_line
929 && saw_ambiguous_reference
930 {
931 anyhow::bail!(
932 "rename_symbol refuses {name:?}: a same-named declaration on line {line} shadows it, and a bare reference cannot say which one it belongs to"
933 );
934 }
935 Ok(occurrences)
936}
937
938fn occurrence_shadows_target(lang: Lang, node: Node, target: RenameTarget) -> bool {
945 match lang {
946 #[cfg(feature = "lang-gdscript")]
947 Lang::GdScript => {
948 if target != RenameTarget::Callable {
949 return false;
950 }
951 let parent_kind = node.parent().map(|parent| parent.kind()).unwrap_or("");
952 match node.kind() {
953 "name" => matches!(
954 parent_kind,
955 "variable_statement"
956 | "const_statement"
957 | "export_variable_statement"
958 | "onready_variable_statement"
959 ),
960 "identifier" => parent_kind == "parameters",
961 _ => false,
962 }
963 }
964 _ => {
965 let _ = (node, target);
966 false
967 }
968 }
969}
970
971fn occurrence_is_ambiguous_reference(lang: Lang, node: Node, target: RenameTarget) -> bool {
976 match lang {
977 #[cfg(feature = "lang-gdscript")]
978 Lang::GdScript => {
979 if target != RenameTarget::Callable || node.kind() != "identifier" {
980 return false;
981 }
982 let parent_kind = node.parent().map(|parent| parent.kind()).unwrap_or("");
983 !matches!(parent_kind, "call" | "attribute_call" | "base_call")
984 }
985 _ => {
986 let _ = (node, target);
987 false
988 }
989 }
990}
991
992pub fn replace_occurrences(
995 source: &str,
996 occurrences: &[IdentifierOccurrence],
997 replacement: &str,
998) -> (String, usize) {
999 let mut out = String::with_capacity(source.len());
1000 let mut last = 0usize;
1001 let mut replaced = 0usize;
1002 for occurrence in occurrences {
1003 if occurrence.start_byte < last {
1004 continue;
1006 }
1007 out.push_str(&source[last..occurrence.start_byte]);
1008 if occurrence.expands_shorthand_key {
1009 out.push_str(&source[occurrence.start_byte..occurrence.end_byte]);
1012 out.push_str(": ");
1013 }
1014 out.push_str(replacement);
1015 last = occurrence.end_byte;
1016 replaced += 1;
1017 }
1018 out.push_str(&source[last..]);
1019 (out, replaced)
1020}
1021
1022#[cfg(test)]
1023mod tests {
1024 use super::*;
1025
1026 #[cfg(feature = "lang-rust")]
1027 const RUST_SOURCE: &str = r#"/// doc widget_count
1028fn widget_count() -> usize { 3 }
1029
1030fn describe() -> String {
1031 // widget_count comment
1032 let label = "widget_count";
1033 format!("{label}: {}", widget_count())
1034}
1035"#;
1036
1037 #[cfg(feature = "lang-rust")]
1038 #[test]
1039 fn rust_skips_strings_and_comments_but_reaches_macro_arguments() {
1040 let found =
1041 identifier_occurrences(Lang::Rust, RUST_SOURCE.as_bytes(), "widget_count").unwrap();
1042 assert_eq!(
1045 found.len(),
1046 2,
1047 "expected the definition and the macro-argument call, got {found:?}"
1048 );
1049 for occurrence in &found {
1050 let before = &RUST_SOURCE[..occurrence.start_byte];
1051 assert!(
1052 !before.ends_with("/// doc ") && !before.ends_with("// "),
1053 "occurrence at {} is inside a comment",
1054 occurrence.start_byte
1055 );
1056 assert!(
1057 !before.ends_with('"'),
1058 "occurrence at {} is inside a string literal",
1059 occurrence.start_byte
1060 );
1061 }
1062 }
1063
1064 #[cfg(feature = "lang-rust")]
1065 #[test]
1066 fn replacing_rust_occurrences_leaves_prose_and_data_alone() {
1067 let found =
1068 identifier_occurrences(Lang::Rust, RUST_SOURCE.as_bytes(), "widget_count").unwrap();
1069 let (out, replaced) = replace_occurrences(RUST_SOURCE, &found, "gadget_count");
1070 assert_eq!(replaced, 2);
1071 assert!(out.contains("fn gadget_count()"), "definition not renamed");
1072 assert!(
1073 out.contains("gadget_count())"),
1074 "macro-argument call not renamed"
1075 );
1076 assert!(
1077 out.contains("/// doc widget_count"),
1078 "doc comment was renamed"
1079 );
1080 assert!(
1081 out.contains("// widget_count comment"),
1082 "line comment was renamed"
1083 );
1084 assert!(
1085 out.contains("\"widget_count\""),
1086 "string literal was renamed"
1087 );
1088 }
1089
1090 #[cfg(feature = "lang-python")]
1091 #[test]
1092 fn python_skips_strings_and_comments() {
1093 let source = "def widget_count():\n # widget_count comment\n return \"widget_count\"\n\nwidget_count()\n";
1094 let found = identifier_occurrences(Lang::Python, source.as_bytes(), "widget_count").unwrap();
1095 assert_eq!(found.len(), 2, "got {found:?}");
1096 let (out, replaced) = replace_occurrences(source, &found, "gadget_count");
1097 assert_eq!(replaced, 2);
1098 assert!(out.contains("def gadget_count()"));
1099 assert!(out.contains("gadget_count()\n"));
1100 assert!(out.contains("# widget_count comment"));
1101 assert!(out.contains("\"widget_count\""));
1102 }
1103
1104 #[cfg(feature = "lang-python")]
1105 #[test]
1106 fn python_callable_narrowing_keeps_method_calls_but_skips_attribute_reads() {
1107 let source = "def widget_count():\n return 1\n\nclass Panel:\n def widget_count(self):\n return 2\n\nread = panel.widget_count\ncalled = panel.widget_count()\ndirect = widget_count()\n";
1108 let found = identifier_occurrences_for(
1109 Lang::Python,
1110 source.as_bytes(),
1111 "widget_count",
1112 RenameTarget::Callable,
1113 )
1114 .unwrap();
1115 let (out, replaced) = replace_occurrences(source, &found, "gadget_count");
1116
1117 assert_eq!(replaced, 4, "got {found:?}\n{out}");
1118 assert!(out.contains("def gadget_count():"));
1119 assert!(out.contains("def gadget_count(self):"));
1120 assert!(out.contains("called = panel.gadget_count()"));
1121 assert!(out.contains("direct = gadget_count()"));
1122 assert!(out.contains("read = panel.widget_count\n"));
1123 }
1124
1125 #[cfg(feature = "lang-python")]
1129 #[test]
1130 fn python_narrowing_keeps_imported_module_attributes_including_bare_reads() {
1131 let source = "import mod\nimport pkg.deep as aliased\n\ndef widget_count():\n return 1\n\nread = panel.widget_count\nmodule_read = mod.widget_count\nmodule_call = mod.widget_count()\naliased_read = aliased.widget_count\n";
1132 let found = identifier_occurrences_for(
1133 Lang::Python,
1134 source.as_bytes(),
1135 "widget_count",
1136 RenameTarget::Callable,
1137 )
1138 .unwrap();
1139 let (out, replaced) = replace_occurrences(source, &found, "gadget_count");
1140
1141 assert_eq!(replaced, 4, "got {found:?}\n{out}");
1142 assert!(out.contains("def gadget_count():"), "{out}");
1143 assert!(
1144 out.contains("module_read = mod.gadget_count\n"),
1145 "an imported-module read was dropped:\n{out}"
1146 );
1147 assert!(out.contains("module_call = mod.gadget_count()"), "{out}");
1148 assert!(
1149 out.contains("aliased_read = aliased.gadget_count"),
1150 "an aliased-import read was dropped:\n{out}"
1151 );
1152 assert!(
1153 out.contains("read = panel.widget_count\n"),
1154 "an instance attribute read was renamed:\n{out}"
1155 );
1156 }
1157
1158 #[cfg(feature = "lang-kotlin")]
1159 #[test]
1160 fn kotlin_callable_narrowing_keeps_method_calls_but_skips_navigation_reads() {
1161 let source = "fun widgetCount(): Int = 1\n\nclass Panel {\n fun widgetCount(): Int = 2\n}\n\nval read = panel.widgetCount\nval called = panel.widgetCount()\nval direct = widgetCount()\n";
1162 let found = identifier_occurrences_for(
1163 Lang::Kotlin,
1164 source.as_bytes(),
1165 "widgetCount",
1166 RenameTarget::Callable,
1167 )
1168 .unwrap();
1169 let (out, replaced) = replace_occurrences(source, &found, "gadgetCount");
1170
1171 assert_eq!(replaced, 4, "got {found:?}\n{out}");
1172 assert!(out.contains("fun gadgetCount(): Int = 1"));
1173 assert!(out.contains("fun gadgetCount(): Int = 2"));
1174 assert!(out.contains("val called = panel.gadgetCount()"));
1175 assert!(out.contains("val direct = gadgetCount()"));
1176 assert!(out.contains("val read = panel.widgetCount\n"));
1177 }
1178
1179 #[cfg(feature = "lang-kotlin")]
1183 #[test]
1184 fn kotlin_narrowing_keeps_members_of_types_declared_in_the_file() {
1185 let source = "class Panel {\n companion object {\n fun widgetCount(): Int = 2\n }\n}\n\nobject Registry {\n fun widgetCount(): Int = 3\n}\n\nval fromClass = Panel.widgetCount\nval fromObject = Registry.widgetCount()\nval fromValue = panel.widgetCount\n";
1186 let found = identifier_occurrences_for(
1187 Lang::Kotlin,
1188 source.as_bytes(),
1189 "widgetCount",
1190 RenameTarget::Callable,
1191 )
1192 .unwrap();
1193 let (out, replaced) = replace_occurrences(source, &found, "gadgetCount");
1194
1195 assert_eq!(replaced, 4, "got {found:?}\n{out}");
1196 assert!(out.contains("fun gadgetCount(): Int = 2"), "{out}");
1197 assert!(out.contains("fun gadgetCount(): Int = 3"), "{out}");
1198 assert!(
1199 out.contains("val fromClass = Panel.gadgetCount\n"),
1200 "a companion member read was dropped:\n{out}"
1201 );
1202 assert!(
1203 out.contains("val fromObject = Registry.gadgetCount()"),
1204 "an object member call was dropped:\n{out}"
1205 );
1206 assert!(
1207 out.contains("val fromValue = panel.widgetCount\n"),
1208 "a value's member read was renamed:\n{out}"
1209 );
1210 }
1211
1212 #[cfg(feature = "lang-kotlin")]
1216 #[test]
1217 fn kotlin_narrowing_keeps_members_of_imported_names() {
1218 let source = "import widgets.Panel\n\
1219import widgets.Registry as ExternalRegistry\n\
1220\n\
1221val fromClass = Panel.widgetCount\n\
1222val fromAlias = ExternalRegistry.widgetCount()\n\
1223val fromValue = panel.widgetCount\n";
1224 let found = identifier_occurrences_for(
1225 Lang::Kotlin,
1226 source.as_bytes(),
1227 "widgetCount",
1228 RenameTarget::Callable,
1229 )
1230 .unwrap();
1231 let (out, replaced) = replace_occurrences(source, &found, "gadgetCount");
1232
1233 assert_eq!(replaced, 2, "got {found:?}\n{out}");
1234 assert!(
1235 out.contains("val fromClass = Panel.gadgetCount\n"),
1236 "{out}"
1237 );
1238 assert!(
1239 out.contains("val fromAlias = ExternalRegistry.gadgetCount()\n"),
1240 "{out}"
1241 );
1242 assert!(out.contains("val fromValue = panel.widgetCount\n"), "{out}");
1243 }
1244
1245 #[cfg(feature = "lang-typescript")]
1246 #[test]
1247 fn typescript_skips_strings_and_comments() {
1248 let source = "// widgetCount comment\nfunction widgetCount(): number { return 1; }\nconst label = \"widgetCount\";\nwidgetCount();\n";
1249 let found =
1250 identifier_occurrences(Lang::TypeScript, source.as_bytes(), "widgetCount").unwrap();
1251 assert_eq!(found.len(), 2, "got {found:?}");
1252 let (out, _) = replace_occurrences(source, &found, "gadgetCount");
1253 assert!(out.contains("function gadgetCount()"));
1254 assert!(out.contains("// widgetCount comment"));
1255 assert!(out.contains("\"widgetCount\""));
1256 }
1257
1258 #[cfg(feature = "lang-bash")]
1259 const BASH_SOURCE: &str = r#"widget_count() {
1260 echo widget_count
1261 local label="widget_count"
1262 # widget_count comment
1263 echo "$widget_count"
1264}
1265widget_count
1266"#;
1267
1268 #[cfg(feature = "lang-bash")]
1269 #[test]
1270 fn bash_renames_names_but_not_arguments_prose_or_data() {
1271 let found =
1272 identifier_occurrences(Lang::Bash, BASH_SOURCE.as_bytes(), "widget_count").unwrap();
1273 assert_eq!(found.len(), 3, "got {found:?}");
1276 let (out, replaced) = replace_occurrences(BASH_SOURCE, &found, "gadget_count");
1277 assert_eq!(replaced, 3);
1278 assert!(out.contains("gadget_count() {"), "definition not renamed");
1279 assert!(
1280 out.contains("echo \"$gadget_count\""),
1281 "expansion not renamed"
1282 );
1283 assert!(
1284 out.contains("}\ngadget_count\n"),
1285 "bare call not renamed:\n{out}"
1286 );
1287 assert!(
1288 out.contains("echo widget_count\n"),
1289 "an unquoted argument was renamed, which rewrites data:\n{out}"
1290 );
1291 assert!(out.contains("label=\"widget_count\""), "string was renamed");
1292 assert!(
1293 out.contains("# widget_count comment"),
1294 "comment was renamed"
1295 );
1296 }
1297
1298 #[cfg(feature = "lang-zig")]
1299 const ZIG_MEMBER_SOURCE: &str = "const m = @import(\"m.zig\");\n\npub fn widget_count() u32 { return 3; }\n\nconst Panel = struct {\n widget_count: u32 = 0,\n\n pub fn describe(self: Panel) u32 { return self.widget_count; }\n};\n\npub fn caller(p: Panel) u32 {\n return widget_count() + p.widget_count + m.widget_count() + m.widget_count + Panel.widget_count;\n}\n";
1300
1301 #[cfg(feature = "lang-zig")]
1306 #[test]
1307 fn zig_callable_narrowing_keeps_namespace_members_but_skips_field_reads() {
1308 let found = identifier_occurrences_for(
1309 Lang::Zig,
1310 ZIG_MEMBER_SOURCE.as_bytes(),
1311 "widget_count",
1312 RenameTarget::Callable,
1313 )
1314 .unwrap();
1315 let (out, replaced) = replace_occurrences(ZIG_MEMBER_SOURCE, &found, "gadget_count");
1316
1317 assert_eq!(replaced, 5, "got {found:?}\n{out}");
1318 assert!(out.contains("pub fn gadget_count() u32"), "{out}");
1319 assert!(out.contains("return gadget_count() +"), "{out}");
1320 assert!(out.contains("m.gadget_count()"), "import call dropped:\n{out}");
1321 assert!(
1322 out.contains("m.gadget_count +"),
1323 "import read dropped, which breaks every cross-file reference:\n{out}"
1324 );
1325 assert!(
1326 out.contains("Panel.gadget_count;"),
1327 "container-type member dropped:\n{out}"
1328 );
1329 assert!(
1330 out.contains(" widget_count: u32 = 0,"),
1331 "a struct field declaration was renamed:\n{out}"
1332 );
1333 assert!(
1334 out.contains("p.widget_count +"),
1335 "a field read off a value was renamed:\n{out}"
1336 );
1337 assert!(
1338 out.contains("return self.widget_count;"),
1339 "a field read off self was renamed:\n{out}"
1340 );
1341 }
1342
1343 #[cfg(feature = "lang-zig")]
1349 #[test]
1350 fn zig_value_narrowing_keeps_namespace_members_and_drops_struct_fields() {
1351 let found = identifier_occurrences_for(
1352 Lang::Zig,
1353 ZIG_MEMBER_SOURCE.as_bytes(),
1354 "widget_count",
1355 RenameTarget::Value,
1356 )
1357 .unwrap();
1358 let (out, _) = replace_occurrences(ZIG_MEMBER_SOURCE, &found, "gadget_count");
1359
1360 assert!(
1361 out.contains("m.gadget_count +"),
1362 "an import-qualified const read was dropped:\n{out}"
1363 );
1364 assert!(
1365 out.contains("Panel.gadget_count;"),
1366 "a container-type const read was dropped:\n{out}"
1367 );
1368 assert!(
1369 out.contains("p.widget_count +"),
1370 "a struct field read was renamed by a const rename:\n{out}"
1371 );
1372 assert!(
1373 out.contains(" widget_count: u32 = 0,"),
1374 "the field declaration is not an indexed symbol and must not move:\n{out}"
1375 );
1376 }
1377
1378 #[cfg(feature = "lang-zig")]
1379 #[test]
1380 fn zig_skips_strings_and_comments() {
1381 let source = "// widget_count comment\npub fn widget_count() u32 {\n const label = \"widget_count\";\n _ = label;\n return 3;\n}\npub fn caller() u32 { return widget_count(); }\n";
1382 let found = identifier_occurrences(Lang::Zig, source.as_bytes(), "widget_count").unwrap();
1383 assert_eq!(found.len(), 2, "got {found:?}");
1384 let (out, replaced) = replace_occurrences(source, &found, "gadget_count");
1385 assert_eq!(replaced, 2);
1386 assert!(out.contains("pub fn gadget_count()"), "definition not renamed");
1387 assert!(out.contains("return gadget_count();"), "call not renamed");
1388 assert!(
1389 out.contains("// widget_count comment"),
1390 "comment was renamed"
1391 );
1392 assert!(out.contains("\"widget_count\""), "string was renamed");
1393 }
1394
1395 #[cfg(feature = "lang-gdscript")]
1396 #[test]
1397 fn gdscript_renames_declaration_and_reference_but_not_prose() {
1398 let source = "# widget_count comment\nfunc widget_count():\n\tvar label = \"widget_count\"\n\treturn label\n\nfunc caller():\n\treturn widget_count()\n";
1399 let found =
1400 identifier_occurrences(Lang::GdScript, source.as_bytes(), "widget_count").unwrap();
1401 assert_eq!(found.len(), 2, "got {found:?}");
1404 let (out, replaced) = replace_occurrences(source, &found, "gadget_count");
1405 assert_eq!(replaced, 2);
1406 assert!(out.contains("func gadget_count():"), "definition not renamed");
1407 assert!(out.contains("return gadget_count()"), "call not renamed");
1408 assert!(
1409 out.contains("# widget_count comment"),
1410 "comment was renamed"
1411 );
1412 assert!(out.contains("\"widget_count\""), "string was renamed");
1413 }
1414
1415 #[cfg(feature = "lang-rust")]
1416 const RUST_FIELD_SOURCE: &str = r#"struct Meter { count: usize }
1417fn count() -> usize { 3 }
1418impl Meter {
1419 fn read(&self) -> usize { self.count }
1420 fn count(&self) -> usize { self.count }
1421}
1422fn use_it(m: &Meter) -> usize { m.count() + m.count + count() }
1423fn build() -> Meter { Meter { count: 1 } }
1424"#;
1425
1426 #[cfg(feature = "lang-rust")]
1427 #[test]
1428 fn renaming_a_rust_function_leaves_an_identically_named_field_alone() {
1429 let found =
1430 identifier_occurrences_for(Lang::Rust, RUST_FIELD_SOURCE.as_bytes(), "count", RenameTarget::Callable)
1431 .unwrap();
1432 let (out, _) = replace_occurrences(RUST_FIELD_SOURCE, &found, "tally");
1433 assert!(out.contains("fn tally() -> usize"), "free fn:\n{out}");
1435 assert!(out.contains("fn tally(&self)"), "inherent method:\n{out}");
1436 assert!(out.contains("m.tally()"), "method call:\n{out}");
1437 assert!(out.contains("+ tally()"), "free call:\n{out}");
1438 assert!(
1440 out.contains("struct Meter { count: usize }"),
1441 "field declaration was renamed:\n{out}"
1442 );
1443 assert!(
1444 out.contains("{ self.count }"),
1445 "field read was renamed:\n{out}"
1446 );
1447 assert!(
1448 out.contains("m.count +"),
1449 "field read was renamed:\n{out}"
1450 );
1451 assert!(
1452 out.contains("Meter { count: 1 }"),
1453 "struct literal field was renamed:\n{out}"
1454 );
1455 }
1456
1457 #[cfg(feature = "lang-rust")]
1458 #[test]
1459 fn an_unresolved_rust_target_keeps_the_pre_narrowing_behaviour() {
1460 let narrowed = identifier_occurrences_for(
1463 Lang::Rust,
1464 RUST_FIELD_SOURCE.as_bytes(),
1465 "count",
1466 RenameTarget::Callable,
1467 )
1468 .unwrap();
1469 let wide = identifier_occurrences(Lang::Rust, RUST_FIELD_SOURCE.as_bytes(), "count").unwrap();
1470 assert!(
1471 wide.len() > narrowed.len(),
1472 "narrowing dropped nothing: {} vs {}",
1473 wide.len(),
1474 narrowed.len()
1475 );
1476 }
1477
1478 #[cfg(feature = "lang-rust")]
1479 #[test]
1480 fn a_field_access_inside_a_macro_is_still_renamed() {
1481 let source = "struct Meter { count: usize }\nfn count() -> usize { 3 }\nfn f(m: &Meter) -> String { format!(\"{}\", m.count) }\n";
1488 let found =
1489 identifier_occurrences_for(Lang::Rust, source.as_bytes(), "count", RenameTarget::Callable)
1490 .unwrap();
1491 let (out, _) = replace_occurrences(source, &found, "tally");
1492 assert!(out.contains("m.tally)"), "expected the known over-rename:\n{out}");
1493 assert!(
1494 out.contains("struct Meter { count: usize }"),
1495 "the field declaration is outside the macro and must survive:\n{out}"
1496 );
1497 }
1498
1499 #[cfg(feature = "lang-gdscript")]
1500 #[test]
1501 fn renaming_a_gdscript_func_leaves_an_identically_named_var_declaration_alone() {
1502 let source = "func count():\n\tvar count = 1\n\treturn 2\n\nfunc caller():\n\treturn count()\n";
1505 let found =
1506 identifier_occurrences_for(Lang::GdScript, source.as_bytes(), "count", RenameTarget::Callable)
1507 .unwrap();
1508 let (out, _) = replace_occurrences(source, &found, "tally");
1509 assert!(out.contains("func tally():"), "declaration:\n{out}");
1510 assert!(out.contains("return tally()"), "call:\n{out}");
1511 assert!(
1512 out.contains("var count = 1"),
1513 "the local var declaration was renamed:\n{out}"
1514 );
1515 }
1516
1517 #[cfg(feature = "lang-gdscript")]
1518 #[test]
1519 fn a_gdscript_local_that_shadows_the_target_and_is_read_refuses() {
1520 let source = "func count():\n\tvar count = 1\n\treturn count\n\nfunc caller():\n\treturn count()\n";
1524 let err = identifier_occurrences_for(
1525 Lang::GdScript,
1526 source.as_bytes(),
1527 "count",
1528 RenameTarget::Callable,
1529 )
1530 .unwrap_err();
1531 let message = format!("{err:#}");
1532 assert!(message.contains("shadows it"), "{message}");
1533 assert!(message.contains("line 2"), "{message}");
1534 }
1535
1536 #[cfg(feature = "lang-gdscript")]
1537 #[test]
1538 fn a_gdscript_callee_is_never_ambiguous() {
1539 let source = "func count():\n\treturn 1\n\nfunc caller():\n\treturn count() + count()\n";
1542 let found = identifier_occurrences_for(
1543 Lang::GdScript,
1544 source.as_bytes(),
1545 "count",
1546 RenameTarget::Callable,
1547 )
1548 .unwrap();
1549 assert_eq!(found.len(), 3, "got {found:?}");
1550 }
1551
1552 #[cfg(feature = "lang-gdscript")]
1553 #[test]
1554 fn renaming_a_gdscript_var_leaves_the_function_declaration_alone() {
1555 let source = "var count = 1\nfunc count():\n\treturn count\n";
1557 let found =
1558 identifier_occurrences_for(Lang::GdScript, source.as_bytes(), "count", RenameTarget::Value)
1559 .unwrap();
1560 let (out, _) = replace_occurrences(source, &found, "tally");
1561 assert!(out.contains("var tally = 1"), "var declaration:\n{out}");
1562 assert!(
1563 out.contains("func count():"),
1564 "the function declaration was renamed:\n{out}"
1565 );
1566 }
1567
1568 #[cfg(feature = "lang-gdscript")]
1569 #[test]
1570 fn a_gdscript_parameter_is_a_binding_not_a_reference() {
1571 let shadowed = "func caller(count):\n\treturn count\n";
1574 let err = identifier_occurrences_for(
1575 Lang::GdScript,
1576 shadowed.as_bytes(),
1577 "count",
1578 RenameTarget::Callable,
1579 )
1580 .unwrap_err();
1581 assert!(format!("{err:#}").contains("shadows it"), "{err:#}");
1582
1583 let source = "func caller(count):\n\treturn 1\n";
1586 let found =
1587 identifier_occurrences_for(Lang::GdScript, source.as_bytes(), "count", RenameTarget::Callable)
1588 .unwrap();
1589 let (out, _) = replace_occurrences(source, &found, "tally");
1590 assert!(
1591 out.contains("func caller(count):"),
1592 "a parameter declaration was renamed:\n{out}"
1593 );
1594 }
1595
1596 #[cfg(feature = "lang-typescript")]
1597 const TS_PROPERTY_SOURCE: &str = r#"function beta(v: number) { return v; }
1598const keyed = { beta: 1 };
1599const shorthand = { beta };
1600class K { beta() { return 2; } }
1601const k = new K();
1602const read = k.beta() + keyed.beta + beta(3);
1603export { beta };
1604"#;
1605
1606 #[cfg(feature = "lang-typescript")]
1607 #[test]
1608 fn renaming_a_typescript_function_leaves_properties_alone() {
1609 let found = identifier_occurrences_for(
1610 Lang::TypeScript,
1611 TS_PROPERTY_SOURCE.as_bytes(),
1612 "beta",
1613 RenameTarget::Callable,
1614 )
1615 .unwrap();
1616 let (out, _) = replace_occurrences(TS_PROPERTY_SOURCE, &found, "gamma");
1617 assert!(out.contains("function gamma(v: number)"), "declaration:
1619{out}");
1620 assert!(out.contains("+ gamma(3)"), "call:
1621{out}");
1622 assert!(out.contains("export { gamma };"), "export:
1623{out}");
1624 assert!(out.contains("{ beta: 1 }"), "object key was renamed:
1626{out}");
1627 assert!(
1628 out.contains("class K { beta()"),
1629 "class method was renamed:
1630{out}"
1631 );
1632 assert!(out.contains("k.beta()"), "member call was renamed:
1633{out}");
1634 assert!(out.contains("keyed.beta"), "member read was renamed:
1635{out}");
1636 }
1637
1638 #[cfg(feature = "lang-typescript")]
1639 #[test]
1640 fn a_javascript_object_shorthand_is_expanded_rather_than_overwritten() {
1641 let found = identifier_occurrences_for(
1645 Lang::TypeScript,
1646 TS_PROPERTY_SOURCE.as_bytes(),
1647 "beta",
1648 RenameTarget::Callable,
1649 )
1650 .unwrap();
1651 let (out, _) = replace_occurrences(TS_PROPERTY_SOURCE, &found, "gamma");
1652 assert!(
1653 out.contains("const shorthand = { beta: gamma };"),
1654 "shorthand was not expanded:
1655{out}"
1656 );
1657 }
1658
1659 #[cfg(feature = "lang-typescript")]
1660 #[test]
1661 fn a_destructuring_pattern_is_renamed_in_place_not_expanded() {
1662 let source = "import * as mod from './mod';
1666const { beta } = mod;
1667beta();
1668";
1669 let found =
1670 identifier_occurrences_for(Lang::TypeScript, source.as_bytes(), "beta", RenameTarget::Callable)
1671 .unwrap();
1672 let (out, _) = replace_occurrences(source, &found, "gamma");
1673 assert!(out.contains("const { gamma } = mod;"), "{out}");
1674 assert!(!out.contains("beta: gamma"), "pattern was expanded:
1675{out}");
1676 }
1677
1678 #[cfg(feature = "lang-typescript")]
1679 #[test]
1680 fn a_typescript_type_rename_keeps_type_identifiers_and_drops_properties() {
1681 let source = "type Beta = number;
1682const o = { Beta: 1 };
1683const v: Beta = 1;
1684export type { Beta };
1685";
1686 let callable = identifier_occurrences_for(
1687 Lang::TypeScript,
1688 source.as_bytes(),
1689 "Beta",
1690 RenameTarget::Callable,
1691 )
1692 .unwrap();
1693 let typed =
1694 identifier_occurrences_for(Lang::TypeScript, source.as_bytes(), "Beta", RenameTarget::Type)
1695 .unwrap();
1696 assert!(
1697 typed.len() > callable.len(),
1698 "a type rename must reach type_identifier positions a callable rename does not: {typed:?} vs {callable:?}"
1699 );
1700 let (out, _) = replace_occurrences(source, &typed, "Gamma");
1701 assert!(out.contains("type Gamma = number;"), "{out}");
1702 assert!(out.contains("const v: Gamma = 1;"), "{out}");
1703 assert!(out.contains("{ Beta: 1 }"), "object key was renamed:
1704{out}");
1705 }
1706
1707 #[test]
1708 fn indexed_symbol_kinds_map_onto_what_a_grammar_can_check() {
1709 assert_eq!(RenameTarget::from_indexed_kind("function"), RenameTarget::Callable);
1710 assert_eq!(RenameTarget::from_indexed_kind("signal"), RenameTarget::Signal);
1711 assert_eq!(RenameTarget::from_indexed_kind("struct"), RenameTarget::Type);
1712 assert_eq!(RenameTarget::from_indexed_kind("class"), RenameTarget::Type);
1713 assert_eq!(RenameTarget::from_indexed_kind("variable"), RenameTarget::Value);
1714 assert_eq!(RenameTarget::from_indexed_kind("const"), RenameTarget::Value);
1715 assert_eq!(RenameTarget::from_indexed_kind("heading"), RenameTarget::Unresolved);
1717 assert_eq!(RenameTarget::from_indexed_kind(""), RenameTarget::Unresolved);
1718 assert_eq!(RenameTarget::default(), RenameTarget::Unresolved);
1719 }
1720
1721 #[test]
1722 fn a_name_that_only_appears_in_prose_has_no_occurrences() {
1723 #[cfg(feature = "lang-rust")]
1724 {
1725 let source = "// widget_count\nfn other() {}\n";
1726 let found =
1727 identifier_occurrences(Lang::Rust, source.as_bytes(), "widget_count").unwrap();
1728 assert!(found.is_empty(), "got {found:?}");
1729 }
1730 }
1731
1732 #[cfg(feature = "lang-markdown")]
1733 #[test]
1734 fn markdown_has_no_identifier_kinds() {
1735 assert!(identifier_node_kinds(Lang::Markdown).is_empty());
1736 assert!(
1737 identifier_occurrences(Lang::Markdown, b"# widget_count\n", "widget_count")
1738 .unwrap()
1739 .is_empty()
1740 );
1741 }
1742
1743 #[test]
1744 fn every_indexed_language_declares_its_identifier_kinds() {
1745 for lang in Lang::all() {
1748 let kinds = identifier_node_kinds(lang);
1749 if lang.name() == "markdown" {
1750 continue;
1751 }
1752 assert!(
1753 !kinds.is_empty(),
1754 "{} declares no identifier node kinds",
1755 lang.name()
1756 );
1757 let ts_lang = lang.tree_sitter_language();
1758 for kind in kinds {
1759 assert!(
1760 ts_lang.id_for_node_kind(kind, true) != 0,
1761 "{} declares node kind {kind:?}, which its grammar does not have",
1762 lang.name()
1763 );
1764 }
1765 }
1766 }
1767}