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-gdscript")]
130 Lang::GdScript => &["identifier", "name"],
131 #[cfg(feature = "lang-markdown")]
133 Lang::Markdown => &[],
134 }
135}
136
137fn occurrence_is_renamable(lang: Lang, node: Node) -> bool {
147 match lang {
148 #[cfg(feature = "lang-bash")]
149 Lang::Bash => {
150 if node.kind() != "word" {
151 return true;
154 }
155 node.parent().is_some_and(|parent| {
156 matches!(parent.kind(), "function_definition" | "command_name")
157 })
158 }
159 _ => {
160 let _ = node;
161 true
162 }
163 }
164}
165
166#[allow(unused_variables)]
173fn occurrence_matches_target(
174 lang: Lang,
175 node: Node,
176 source: &[u8],
177 target: RenameTarget,
178) -> bool {
179 if target == RenameTarget::Unresolved {
180 return true;
181 }
182 match lang {
183 #[cfg(feature = "lang-rust")]
184 Lang::Rust => rust_occurrence_matches_target(node, target),
185 #[cfg(feature = "lang-python")]
186 Lang::Python => python_occurrence_matches_target(node, source, target),
187 #[cfg(feature = "lang-gdscript")]
188 Lang::GdScript => gdscript_occurrence_matches_target(node, target),
189 #[cfg(feature = "lang-typescript")]
190 Lang::TypeScript | Lang::Tsx => js_like_occurrence_matches_target(node, target),
191 #[cfg(feature = "lang-javascript")]
192 Lang::JavaScript | Lang::Jsx => js_like_occurrence_matches_target(node, target),
193 #[cfg(feature = "lang-kotlin")]
194 Lang::Kotlin => kotlin_occurrence_matches_target(node, source, target),
195 #[cfg(feature = "lang-zig")]
196 Lang::Zig => zig_occurrence_matches_target(node, source, target),
197 _ => {
198 let _ = node;
199 true
200 }
201 }
202}
203
204#[cfg(feature = "lang-python")]
212fn python_occurrence_matches_target(node: Node, source: &[u8], target: RenameTarget) -> bool {
213 let Some(attribute) = node.parent().filter(|parent| parent.kind() == "attribute") else {
214 return true;
215 };
216 if attribute
217 .child_by_field_name("attribute")
218 .is_none_or(|name| name.id() != node.id())
219 {
220 return true;
221 }
222 if python_receiver_is_imported_module(attribute, source) {
223 return true;
224 }
225
226 target == RenameTarget::Callable
227 && attribute.parent().is_some_and(|call| {
228 call.kind() == "call"
229 && call
230 .child_by_field_name("function")
231 .is_some_and(|function| function.id() == attribute.id())
232 })
233}
234
235#[cfg(feature = "lang-python")]
246fn python_receiver_is_imported_module(attribute: Node, source: &[u8]) -> bool {
247 let Some(mut object) = attribute.child_by_field_name("object") else {
248 return false;
249 };
250 while object.kind() == "attribute" {
251 let Some(inner) = object.child_by_field_name("object") else {
252 return false;
253 };
254 object = inner;
255 }
256 if object.kind() != "identifier" {
257 return false;
258 }
259 let Ok(name) = object.utf8_text(source) else {
260 return false;
261 };
262 python_file_imports_module(attribute, name, source)
263}
264
265#[cfg(feature = "lang-python")]
267fn python_file_imports_module(node: Node, name: &str, source: &[u8]) -> bool {
268 let mut root = node;
269 while let Some(parent) = root.parent() {
270 root = parent;
271 }
272 let mut cursor = root.walk();
273 let mut descend = true;
274 loop {
275 if descend {
276 let current = cursor.node();
277 if current.kind() == "import_statement"
278 && python_import_binds(current, name, source)
279 {
280 return true;
281 }
282 if cursor.goto_first_child() {
283 continue;
284 }
285 }
286 if cursor.goto_next_sibling() {
287 descend = true;
288 continue;
289 }
290 if !cursor.goto_parent() {
291 return false;
292 }
293 descend = false;
294 }
295}
296
297#[cfg(feature = "lang-python")]
301fn python_import_binds(import: Node, name: &str, source: &[u8]) -> bool {
302 let mut cursor = import.walk();
303 import.named_children(&mut cursor).any(|clause| {
304 let bound = match clause.kind() {
305 "aliased_import" => clause.child_by_field_name("alias"),
306 "dotted_name" => clause.named_child(0),
307 _ => None,
308 };
309 bound.is_some_and(|bound| bound.utf8_text(source).is_ok_and(|text| text == name))
310 })
311}
312
313#[cfg(feature = "lang-kotlin")]
321fn kotlin_occurrence_matches_target(node: Node, source: &[u8], target: RenameTarget) -> bool {
322 let Some(navigation) = node
323 .parent()
324 .filter(|parent| parent.kind() == "navigation_expression")
325 else {
326 return true;
327 };
328 if node.prev_named_sibling().is_none() {
329 return true;
330 }
331 if kotlin_receiver_is_namespace(navigation, source) {
332 return true;
333 }
334
335 target == RenameTarget::Callable
336 && navigation.parent().is_some_and(|call| {
337 call.kind() == "call_expression"
338 && call
339 .named_child(0)
340 .is_some_and(|function| function.id() == navigation.id())
341 })
342}
343
344#[cfg(feature = "lang-kotlin")]
347fn kotlin_receiver_is_namespace(navigation: Node, source: &[u8]) -> bool {
348 let mut receiver = navigation;
349 while receiver.kind() == "navigation_expression" {
350 let Some(inner) = receiver.named_child(0) else {
351 return false;
352 };
353 receiver = inner;
354 }
355 if receiver.kind() != "identifier" {
356 return false;
357 }
358 let Ok(name) = receiver.utf8_text(source) else {
359 return false;
360 };
361 kotlin_file_declares_type(navigation, name, source)
362 || kotlin_file_imports_name(navigation, name, source)
363}
364
365#[cfg(feature = "lang-kotlin")]
368fn kotlin_file_declares_type(node: Node, name: &str, source: &[u8]) -> bool {
369 let mut root = node;
370 while let Some(parent) = root.parent() {
371 root = parent;
372 }
373 let mut cursor = root.walk();
374 let mut descend = true;
375 loop {
376 if descend {
377 let current = cursor.node();
378 if matches!(
379 current.kind(),
380 "class_declaration" | "object_declaration" | "interface_declaration"
381 ) && current
382 .child_by_field_name("name")
383 .and_then(|declared| declared.utf8_text(source).ok())
384 == Some(name)
385 {
386 return true;
387 }
388 if cursor.goto_first_child() {
389 continue;
390 }
391 }
392 if cursor.goto_next_sibling() {
393 descend = true;
394 continue;
395 }
396 if !cursor.goto_parent() {
397 return false;
398 }
399 descend = false;
400 }
401}
402
403#[cfg(feature = "lang-kotlin")]
409fn kotlin_file_imports_name(node: Node, name: &str, source: &[u8]) -> bool {
410 let mut root = node;
411 while let Some(parent) = root.parent() {
412 root = parent;
413 }
414 let mut cursor = root.walk();
415 let mut descend = true;
416 loop {
417 if descend {
418 let current = cursor.node();
419 if current.kind() == "import" && kotlin_import_binds(current, name, source) {
420 return true;
421 }
422 if cursor.goto_first_child() {
423 continue;
424 }
425 }
426 if cursor.goto_next_sibling() {
427 descend = true;
428 continue;
429 }
430 if !cursor.goto_parent() {
431 return false;
432 }
433 descend = false;
434 }
435}
436
437#[cfg(feature = "lang-kotlin")]
438fn kotlin_import_binds(import: Node, name: &str, source: &[u8]) -> bool {
439 let mut cursor = import.walk();
440 let children = import.named_children(&mut cursor).collect::<Vec<_>>();
441 if let Some(alias) = children
442 .get(1)
443 .filter(|child| child.kind() == "identifier")
444 {
445 return alias
446 .utf8_text(source)
447 .is_ok_and(|bound_name| bound_name == name);
448 }
449 children
450 .first()
451 .filter(|path| matches!(path.kind(), "identifier" | "qualified_identifier"))
452 .and_then(|path| path.utf8_text(source).ok())
453 .and_then(|path| path.rsplit('.').next())
454 .is_some_and(|bound_name| bound_name == name)
455}
456
457#[cfg(feature = "lang-zig")]
471fn zig_occurrence_matches_target(node: Node, source: &[u8], target: RenameTarget) -> bool {
472 let Some(parent) = node.parent() else {
473 return true;
474 };
475 match parent.kind() {
476 "container_field" => parent
480 .child_by_field_name("name")
481 .is_none_or(|name| name.id() != node.id()),
482 "field_expression" => {
483 if parent
484 .child_by_field_name("member")
485 .is_none_or(|member| member.id() != node.id())
486 {
487 return true;
488 }
489 if zig_receiver_is_namespace(parent, source) {
490 return true;
491 }
492 target == RenameTarget::Callable
493 && parent.parent().is_some_and(|call| {
494 call.kind() == "call_expression"
495 && call
496 .child_by_field_name("function")
497 .is_some_and(|function| function.id() == parent.id())
498 })
499 }
500 _ => true,
501 }
502}
503
504#[cfg(feature = "lang-zig")]
518fn zig_receiver_is_namespace(field_expression: Node, source: &[u8]) -> bool {
519 let Some(mut object) = field_expression.child_by_field_name("object") else {
520 return false;
521 };
522 while object.kind() == "field_expression" {
523 let Some(inner) = object.child_by_field_name("object") else {
524 return false;
525 };
526 object = inner;
527 }
528 match object.kind() {
529 "builtin_function" => zig_is_import_builtin(object, source),
530 "identifier" => object
531 .utf8_text(source)
532 .is_ok_and(|name| zig_file_binds_namespace(field_expression, name, source)),
533 _ => false,
534 }
535}
536
537#[cfg(feature = "lang-zig")]
539fn zig_is_import_builtin(builtin: Node, source: &[u8]) -> bool {
540 let mut cursor = builtin.walk();
541 builtin.named_children(&mut cursor).any(|child| {
542 child.kind() == "builtin_identifier"
543 && child.utf8_text(source).is_ok_and(|text| text == "@import")
544 })
545}
546
547#[cfg(feature = "lang-zig")]
555fn zig_file_binds_namespace(node: Node, name: &str, source: &[u8]) -> bool {
556 let mut root = node;
557 while let Some(parent) = root.parent() {
558 root = parent;
559 }
560 let mut cursor = root.walk();
561 let mut descend = true;
562 loop {
563 if descend {
564 let current = cursor.node();
565 if current.kind() == "variable_declaration"
566 && zig_declaration_binds_namespace(current, name, source)
567 {
568 return true;
569 }
570 if cursor.goto_first_child() {
571 continue;
572 }
573 }
574 if cursor.goto_next_sibling() {
575 descend = true;
576 continue;
577 }
578 if !cursor.goto_parent() {
579 return false;
580 }
581 descend = false;
582 }
583}
584
585#[cfg(feature = "lang-zig")]
587fn zig_declaration_binds_namespace(declaration: Node, name: &str, source: &[u8]) -> bool {
588 let mut cursor = declaration.walk();
589 let children: Vec<Node> = declaration.named_children(&mut cursor).collect();
590 let binds_name = children.iter().any(|child| {
591 child.kind() == "identifier" && child.utf8_text(source).is_ok_and(|text| text == name)
592 });
593 if !binds_name {
594 return false;
595 }
596 children.iter().any(|child| match child.kind() {
597 "builtin_function" => zig_is_import_builtin(*child, source),
598 "struct_declaration" | "enum_declaration" | "union_declaration"
601 | "opaque_declaration" => true,
602 _ => false,
603 })
604}
605
606#[cfg(any(feature = "lang-typescript", feature = "lang-javascript"))]
613fn js_like_occurrence_matches_target(node: Node, target: RenameTarget) -> bool {
614 match node.kind() {
615 "property_identifier" => false,
616 "type_identifier" => target == RenameTarget::Type,
617 _ => true,
618 }
619}
620
621#[allow(unused_variables)]
629fn occurrence_expands_shorthand_key(lang: Lang, node: Node, target: RenameTarget) -> bool {
630 if target == RenameTarget::Unresolved {
631 return false;
632 }
633 match lang {
634 #[cfg(feature = "lang-typescript")]
635 Lang::TypeScript | Lang::Tsx => js_like_shorthand_key(node),
636 #[cfg(feature = "lang-javascript")]
637 Lang::JavaScript | Lang::Jsx => js_like_shorthand_key(node),
638 _ => false,
639 }
640}
641
642#[cfg(any(feature = "lang-typescript", feature = "lang-javascript"))]
650fn js_like_shorthand_key(node: Node) -> bool {
651 node.kind() == "shorthand_property_identifier"
652 && node.parent().is_some_and(|parent| parent.kind() == "object")
653}
654
655#[cfg(feature = "lang-rust")]
660fn rust_occurrence_matches_target(node: Node, target: RenameTarget) -> bool {
661 let parent_kind = node.parent().map(|parent| parent.kind()).unwrap_or("");
662 match node.kind() {
663 "field_identifier" => {
664 target == RenameTarget::Callable && parent_kind == "field_expression" && {
669 node.parent()
670 .and_then(|field_expression| {
671 let call = field_expression.parent()?;
672 (call.kind() == "call_expression"
673 && call.child_by_field_name("function")?.id() == field_expression.id())
674 .then_some(())
675 })
676 .is_some()
677 }
678 }
679 "shorthand_field_identifier" => target == RenameTarget::Value,
680 "identifier" if parent_kind == "shorthand_field_initializer" => {
685 matches!(target, RenameTarget::Value)
686 }
687 "type_identifier" => target == RenameTarget::Type,
688 _ => true,
689 }
690}
691
692#[cfg(feature = "lang-gdscript")]
697fn gdscript_occurrence_matches_target(node: Node, target: RenameTarget) -> bool {
698 let parent_kind = node.parent().map(|parent| parent.kind()).unwrap_or("");
699 match node.kind() {
700 "name" => {
701 let declares: &[&str] = match target {
702 RenameTarget::Callable => &["function_definition"],
703 RenameTarget::Signal => &["signal_statement"],
704 RenameTarget::Type => &["class_definition", "class_name_statement", "enum_definition"],
705 RenameTarget::Value => &[
706 "variable_statement",
707 "const_statement",
708 "export_variable_statement",
709 "onready_variable_statement",
710 ],
711 RenameTarget::Unresolved => return true,
712 };
713 declares.contains(&parent_kind)
714 }
715 "identifier" if parent_kind == "parameters" => false,
718 _ => true,
719 }
720}
721
722pub fn identifier_occurrences(
728 lang: Lang,
729 source: &[u8],
730 name: &str,
731) -> Result<Vec<IdentifierOccurrence>> {
732 identifier_occurrences_for(lang, source, name, RenameTarget::Unresolved)
733}
734
735pub fn identifier_occurrences_for(
737 lang: Lang,
738 source: &[u8],
739 name: &str,
740 target: RenameTarget,
741) -> Result<Vec<IdentifierOccurrence>> {
742 let kinds = identifier_node_kinds(lang);
743 if kinds.is_empty() || name.is_empty() {
744 return Ok(Vec::new());
745 }
746
747 let ts_lang = lang.tree_sitter_language();
748 let mut parser = Parser::new();
749 parser.set_language(&ts_lang)?;
750 let tree = parser
751 .parse(source, None)
752 .ok_or_else(|| anyhow::anyhow!("parse failed"))?;
753
754 let mut occurrences = Vec::new();
755 let mut shadowing_declaration_line: Option<usize> = None;
758 let mut saw_ambiguous_reference = false;
760 let mut cursor = tree.walk();
761 let mut descend = true;
762 loop {
763 if descend {
764 let node = cursor.node();
765 if kinds.contains(&node.kind())
766 && node.utf8_text(source).is_ok_and(|it| it == name)
767 && occurrence_is_renamable(lang, node)
768 {
769 if occurrence_matches_target(lang, node, source, target) {
770 occurrences.push(IdentifierOccurrence {
771 start_byte: node.start_byte(),
772 end_byte: node.end_byte(),
773 expands_shorthand_key: occurrence_expands_shorthand_key(
774 lang, node, target,
775 ),
776 });
777 saw_ambiguous_reference |= occurrence_is_ambiguous_reference(lang, node, target);
778 } else if shadowing_declaration_line.is_none()
779 && occurrence_shadows_target(lang, node, target)
780 {
781 shadowing_declaration_line = Some(node.start_position().row + 1);
782 }
783 }
784 if cursor.goto_first_child() {
785 continue;
786 }
787 }
788 if cursor.goto_next_sibling() {
789 descend = true;
790 continue;
791 }
792 if !cursor.goto_parent() {
793 break;
794 }
795 descend = false;
796 }
797
798 occurrences.sort_by_key(|occurrence| (occurrence.start_byte, occurrence.end_byte));
802 occurrences.dedup();
803
804 if let Some(line) = shadowing_declaration_line
811 && saw_ambiguous_reference
812 {
813 anyhow::bail!(
814 "rename_symbol refuses {name:?}: a same-named declaration on line {line} shadows it, and a bare reference cannot say which one it belongs to"
815 );
816 }
817 Ok(occurrences)
818}
819
820fn occurrence_shadows_target(lang: Lang, node: Node, target: RenameTarget) -> bool {
827 match lang {
828 #[cfg(feature = "lang-gdscript")]
829 Lang::GdScript => {
830 if target != RenameTarget::Callable {
831 return false;
832 }
833 let parent_kind = node.parent().map(|parent| parent.kind()).unwrap_or("");
834 match node.kind() {
835 "name" => matches!(
836 parent_kind,
837 "variable_statement"
838 | "const_statement"
839 | "export_variable_statement"
840 | "onready_variable_statement"
841 ),
842 "identifier" => parent_kind == "parameters",
843 _ => false,
844 }
845 }
846 _ => {
847 let _ = (node, target);
848 false
849 }
850 }
851}
852
853fn occurrence_is_ambiguous_reference(lang: Lang, node: Node, target: RenameTarget) -> bool {
858 match lang {
859 #[cfg(feature = "lang-gdscript")]
860 Lang::GdScript => {
861 if target != RenameTarget::Callable || node.kind() != "identifier" {
862 return false;
863 }
864 let parent_kind = node.parent().map(|parent| parent.kind()).unwrap_or("");
865 !matches!(parent_kind, "call" | "attribute_call" | "base_call")
866 }
867 _ => {
868 let _ = (node, target);
869 false
870 }
871 }
872}
873
874pub fn replace_occurrences(
877 source: &str,
878 occurrences: &[IdentifierOccurrence],
879 replacement: &str,
880) -> (String, usize) {
881 let mut out = String::with_capacity(source.len());
882 let mut last = 0usize;
883 let mut replaced = 0usize;
884 for occurrence in occurrences {
885 if occurrence.start_byte < last {
886 continue;
888 }
889 out.push_str(&source[last..occurrence.start_byte]);
890 if occurrence.expands_shorthand_key {
891 out.push_str(&source[occurrence.start_byte..occurrence.end_byte]);
894 out.push_str(": ");
895 }
896 out.push_str(replacement);
897 last = occurrence.end_byte;
898 replaced += 1;
899 }
900 out.push_str(&source[last..]);
901 (out, replaced)
902}
903
904#[cfg(test)]
905mod tests {
906 use super::*;
907
908 #[cfg(feature = "lang-rust")]
909 const RUST_SOURCE: &str = r#"/// doc widget_count
910fn widget_count() -> usize { 3 }
911
912fn describe() -> String {
913 // widget_count comment
914 let label = "widget_count";
915 format!("{label}: {}", widget_count())
916}
917"#;
918
919 #[cfg(feature = "lang-rust")]
920 #[test]
921 fn rust_skips_strings_and_comments_but_reaches_macro_arguments() {
922 let found =
923 identifier_occurrences(Lang::Rust, RUST_SOURCE.as_bytes(), "widget_count").unwrap();
924 assert_eq!(
927 found.len(),
928 2,
929 "expected the definition and the macro-argument call, got {found:?}"
930 );
931 for occurrence in &found {
932 let before = &RUST_SOURCE[..occurrence.start_byte];
933 assert!(
934 !before.ends_with("/// doc ") && !before.ends_with("// "),
935 "occurrence at {} is inside a comment",
936 occurrence.start_byte
937 );
938 assert!(
939 !before.ends_with('"'),
940 "occurrence at {} is inside a string literal",
941 occurrence.start_byte
942 );
943 }
944 }
945
946 #[cfg(feature = "lang-rust")]
947 #[test]
948 fn replacing_rust_occurrences_leaves_prose_and_data_alone() {
949 let found =
950 identifier_occurrences(Lang::Rust, RUST_SOURCE.as_bytes(), "widget_count").unwrap();
951 let (out, replaced) = replace_occurrences(RUST_SOURCE, &found, "gadget_count");
952 assert_eq!(replaced, 2);
953 assert!(out.contains("fn gadget_count()"), "definition not renamed");
954 assert!(
955 out.contains("gadget_count())"),
956 "macro-argument call not renamed"
957 );
958 assert!(
959 out.contains("/// doc widget_count"),
960 "doc comment was renamed"
961 );
962 assert!(
963 out.contains("// widget_count comment"),
964 "line comment was renamed"
965 );
966 assert!(
967 out.contains("\"widget_count\""),
968 "string literal was renamed"
969 );
970 }
971
972 #[cfg(feature = "lang-python")]
973 #[test]
974 fn python_skips_strings_and_comments() {
975 let source = "def widget_count():\n # widget_count comment\n return \"widget_count\"\n\nwidget_count()\n";
976 let found = identifier_occurrences(Lang::Python, source.as_bytes(), "widget_count").unwrap();
977 assert_eq!(found.len(), 2, "got {found:?}");
978 let (out, replaced) = replace_occurrences(source, &found, "gadget_count");
979 assert_eq!(replaced, 2);
980 assert!(out.contains("def gadget_count()"));
981 assert!(out.contains("gadget_count()\n"));
982 assert!(out.contains("# widget_count comment"));
983 assert!(out.contains("\"widget_count\""));
984 }
985
986 #[cfg(feature = "lang-python")]
987 #[test]
988 fn python_callable_narrowing_keeps_method_calls_but_skips_attribute_reads() {
989 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";
990 let found = identifier_occurrences_for(
991 Lang::Python,
992 source.as_bytes(),
993 "widget_count",
994 RenameTarget::Callable,
995 )
996 .unwrap();
997 let (out, replaced) = replace_occurrences(source, &found, "gadget_count");
998
999 assert_eq!(replaced, 4, "got {found:?}\n{out}");
1000 assert!(out.contains("def gadget_count():"));
1001 assert!(out.contains("def gadget_count(self):"));
1002 assert!(out.contains("called = panel.gadget_count()"));
1003 assert!(out.contains("direct = gadget_count()"));
1004 assert!(out.contains("read = panel.widget_count\n"));
1005 }
1006
1007 #[cfg(feature = "lang-python")]
1011 #[test]
1012 fn python_narrowing_keeps_imported_module_attributes_including_bare_reads() {
1013 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";
1014 let found = identifier_occurrences_for(
1015 Lang::Python,
1016 source.as_bytes(),
1017 "widget_count",
1018 RenameTarget::Callable,
1019 )
1020 .unwrap();
1021 let (out, replaced) = replace_occurrences(source, &found, "gadget_count");
1022
1023 assert_eq!(replaced, 4, "got {found:?}\n{out}");
1024 assert!(out.contains("def gadget_count():"), "{out}");
1025 assert!(
1026 out.contains("module_read = mod.gadget_count\n"),
1027 "an imported-module read was dropped:\n{out}"
1028 );
1029 assert!(out.contains("module_call = mod.gadget_count()"), "{out}");
1030 assert!(
1031 out.contains("aliased_read = aliased.gadget_count"),
1032 "an aliased-import read was dropped:\n{out}"
1033 );
1034 assert!(
1035 out.contains("read = panel.widget_count\n"),
1036 "an instance attribute read was renamed:\n{out}"
1037 );
1038 }
1039
1040 #[cfg(feature = "lang-kotlin")]
1041 #[test]
1042 fn kotlin_callable_narrowing_keeps_method_calls_but_skips_navigation_reads() {
1043 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";
1044 let found = identifier_occurrences_for(
1045 Lang::Kotlin,
1046 source.as_bytes(),
1047 "widgetCount",
1048 RenameTarget::Callable,
1049 )
1050 .unwrap();
1051 let (out, replaced) = replace_occurrences(source, &found, "gadgetCount");
1052
1053 assert_eq!(replaced, 4, "got {found:?}\n{out}");
1054 assert!(out.contains("fun gadgetCount(): Int = 1"));
1055 assert!(out.contains("fun gadgetCount(): Int = 2"));
1056 assert!(out.contains("val called = panel.gadgetCount()"));
1057 assert!(out.contains("val direct = gadgetCount()"));
1058 assert!(out.contains("val read = panel.widgetCount\n"));
1059 }
1060
1061 #[cfg(feature = "lang-kotlin")]
1065 #[test]
1066 fn kotlin_narrowing_keeps_members_of_types_declared_in_the_file() {
1067 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";
1068 let found = identifier_occurrences_for(
1069 Lang::Kotlin,
1070 source.as_bytes(),
1071 "widgetCount",
1072 RenameTarget::Callable,
1073 )
1074 .unwrap();
1075 let (out, replaced) = replace_occurrences(source, &found, "gadgetCount");
1076
1077 assert_eq!(replaced, 4, "got {found:?}\n{out}");
1078 assert!(out.contains("fun gadgetCount(): Int = 2"), "{out}");
1079 assert!(out.contains("fun gadgetCount(): Int = 3"), "{out}");
1080 assert!(
1081 out.contains("val fromClass = Panel.gadgetCount\n"),
1082 "a companion member read was dropped:\n{out}"
1083 );
1084 assert!(
1085 out.contains("val fromObject = Registry.gadgetCount()"),
1086 "an object member call was dropped:\n{out}"
1087 );
1088 assert!(
1089 out.contains("val fromValue = panel.widgetCount\n"),
1090 "a value's member read was renamed:\n{out}"
1091 );
1092 }
1093
1094 #[cfg(feature = "lang-kotlin")]
1098 #[test]
1099 fn kotlin_narrowing_keeps_members_of_imported_names() {
1100 let source = "import widgets.Panel\n\
1101import widgets.Registry as ExternalRegistry\n\
1102\n\
1103val fromClass = Panel.widgetCount\n\
1104val fromAlias = ExternalRegistry.widgetCount()\n\
1105val fromValue = panel.widgetCount\n";
1106 let found = identifier_occurrences_for(
1107 Lang::Kotlin,
1108 source.as_bytes(),
1109 "widgetCount",
1110 RenameTarget::Callable,
1111 )
1112 .unwrap();
1113 let (out, replaced) = replace_occurrences(source, &found, "gadgetCount");
1114
1115 assert_eq!(replaced, 2, "got {found:?}\n{out}");
1116 assert!(
1117 out.contains("val fromClass = Panel.gadgetCount\n"),
1118 "{out}"
1119 );
1120 assert!(
1121 out.contains("val fromAlias = ExternalRegistry.gadgetCount()\n"),
1122 "{out}"
1123 );
1124 assert!(out.contains("val fromValue = panel.widgetCount\n"), "{out}");
1125 }
1126
1127 #[cfg(feature = "lang-typescript")]
1128 #[test]
1129 fn typescript_skips_strings_and_comments() {
1130 let source = "// widgetCount comment\nfunction widgetCount(): number { return 1; }\nconst label = \"widgetCount\";\nwidgetCount();\n";
1131 let found =
1132 identifier_occurrences(Lang::TypeScript, source.as_bytes(), "widgetCount").unwrap();
1133 assert_eq!(found.len(), 2, "got {found:?}");
1134 let (out, _) = replace_occurrences(source, &found, "gadgetCount");
1135 assert!(out.contains("function gadgetCount()"));
1136 assert!(out.contains("// widgetCount comment"));
1137 assert!(out.contains("\"widgetCount\""));
1138 }
1139
1140 #[cfg(feature = "lang-bash")]
1141 const BASH_SOURCE: &str = r#"widget_count() {
1142 echo widget_count
1143 local label="widget_count"
1144 # widget_count comment
1145 echo "$widget_count"
1146}
1147widget_count
1148"#;
1149
1150 #[cfg(feature = "lang-bash")]
1151 #[test]
1152 fn bash_renames_names_but_not_arguments_prose_or_data() {
1153 let found =
1154 identifier_occurrences(Lang::Bash, BASH_SOURCE.as_bytes(), "widget_count").unwrap();
1155 assert_eq!(found.len(), 3, "got {found:?}");
1158 let (out, replaced) = replace_occurrences(BASH_SOURCE, &found, "gadget_count");
1159 assert_eq!(replaced, 3);
1160 assert!(out.contains("gadget_count() {"), "definition not renamed");
1161 assert!(
1162 out.contains("echo \"$gadget_count\""),
1163 "expansion not renamed"
1164 );
1165 assert!(
1166 out.contains("}\ngadget_count\n"),
1167 "bare call not renamed:\n{out}"
1168 );
1169 assert!(
1170 out.contains("echo widget_count\n"),
1171 "an unquoted argument was renamed, which rewrites data:\n{out}"
1172 );
1173 assert!(out.contains("label=\"widget_count\""), "string was renamed");
1174 assert!(
1175 out.contains("# widget_count comment"),
1176 "comment was renamed"
1177 );
1178 }
1179
1180 #[cfg(feature = "lang-zig")]
1181 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";
1182
1183 #[cfg(feature = "lang-zig")]
1188 #[test]
1189 fn zig_callable_narrowing_keeps_namespace_members_but_skips_field_reads() {
1190 let found = identifier_occurrences_for(
1191 Lang::Zig,
1192 ZIG_MEMBER_SOURCE.as_bytes(),
1193 "widget_count",
1194 RenameTarget::Callable,
1195 )
1196 .unwrap();
1197 let (out, replaced) = replace_occurrences(ZIG_MEMBER_SOURCE, &found, "gadget_count");
1198
1199 assert_eq!(replaced, 5, "got {found:?}\n{out}");
1200 assert!(out.contains("pub fn gadget_count() u32"), "{out}");
1201 assert!(out.contains("return gadget_count() +"), "{out}");
1202 assert!(out.contains("m.gadget_count()"), "import call dropped:\n{out}");
1203 assert!(
1204 out.contains("m.gadget_count +"),
1205 "import read dropped, which breaks every cross-file reference:\n{out}"
1206 );
1207 assert!(
1208 out.contains("Panel.gadget_count;"),
1209 "container-type member dropped:\n{out}"
1210 );
1211 assert!(
1212 out.contains(" widget_count: u32 = 0,"),
1213 "a struct field declaration was renamed:\n{out}"
1214 );
1215 assert!(
1216 out.contains("p.widget_count +"),
1217 "a field read off a value was renamed:\n{out}"
1218 );
1219 assert!(
1220 out.contains("return self.widget_count;"),
1221 "a field read off self was renamed:\n{out}"
1222 );
1223 }
1224
1225 #[cfg(feature = "lang-zig")]
1231 #[test]
1232 fn zig_value_narrowing_keeps_namespace_members_and_drops_struct_fields() {
1233 let found = identifier_occurrences_for(
1234 Lang::Zig,
1235 ZIG_MEMBER_SOURCE.as_bytes(),
1236 "widget_count",
1237 RenameTarget::Value,
1238 )
1239 .unwrap();
1240 let (out, _) = replace_occurrences(ZIG_MEMBER_SOURCE, &found, "gadget_count");
1241
1242 assert!(
1243 out.contains("m.gadget_count +"),
1244 "an import-qualified const read was dropped:\n{out}"
1245 );
1246 assert!(
1247 out.contains("Panel.gadget_count;"),
1248 "a container-type const read was dropped:\n{out}"
1249 );
1250 assert!(
1251 out.contains("p.widget_count +"),
1252 "a struct field read was renamed by a const rename:\n{out}"
1253 );
1254 assert!(
1255 out.contains(" widget_count: u32 = 0,"),
1256 "the field declaration is not an indexed symbol and must not move:\n{out}"
1257 );
1258 }
1259
1260 #[cfg(feature = "lang-zig")]
1261 #[test]
1262 fn zig_skips_strings_and_comments() {
1263 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";
1264 let found = identifier_occurrences(Lang::Zig, source.as_bytes(), "widget_count").unwrap();
1265 assert_eq!(found.len(), 2, "got {found:?}");
1266 let (out, replaced) = replace_occurrences(source, &found, "gadget_count");
1267 assert_eq!(replaced, 2);
1268 assert!(out.contains("pub fn gadget_count()"), "definition not renamed");
1269 assert!(out.contains("return gadget_count();"), "call not renamed");
1270 assert!(
1271 out.contains("// widget_count comment"),
1272 "comment was renamed"
1273 );
1274 assert!(out.contains("\"widget_count\""), "string was renamed");
1275 }
1276
1277 #[cfg(feature = "lang-gdscript")]
1278 #[test]
1279 fn gdscript_renames_declaration_and_reference_but_not_prose() {
1280 let source = "# widget_count comment\nfunc widget_count():\n\tvar label = \"widget_count\"\n\treturn label\n\nfunc caller():\n\treturn widget_count()\n";
1281 let found =
1282 identifier_occurrences(Lang::GdScript, source.as_bytes(), "widget_count").unwrap();
1283 assert_eq!(found.len(), 2, "got {found:?}");
1286 let (out, replaced) = replace_occurrences(source, &found, "gadget_count");
1287 assert_eq!(replaced, 2);
1288 assert!(out.contains("func gadget_count():"), "definition not renamed");
1289 assert!(out.contains("return gadget_count()"), "call not renamed");
1290 assert!(
1291 out.contains("# widget_count comment"),
1292 "comment was renamed"
1293 );
1294 assert!(out.contains("\"widget_count\""), "string was renamed");
1295 }
1296
1297 #[cfg(feature = "lang-rust")]
1298 const RUST_FIELD_SOURCE: &str = r#"struct Meter { count: usize }
1299fn count() -> usize { 3 }
1300impl Meter {
1301 fn read(&self) -> usize { self.count }
1302 fn count(&self) -> usize { self.count }
1303}
1304fn use_it(m: &Meter) -> usize { m.count() + m.count + count() }
1305fn build() -> Meter { Meter { count: 1 } }
1306"#;
1307
1308 #[cfg(feature = "lang-rust")]
1309 #[test]
1310 fn renaming_a_rust_function_leaves_an_identically_named_field_alone() {
1311 let found =
1312 identifier_occurrences_for(Lang::Rust, RUST_FIELD_SOURCE.as_bytes(), "count", RenameTarget::Callable)
1313 .unwrap();
1314 let (out, _) = replace_occurrences(RUST_FIELD_SOURCE, &found, "tally");
1315 assert!(out.contains("fn tally() -> usize"), "free fn:\n{out}");
1317 assert!(out.contains("fn tally(&self)"), "inherent method:\n{out}");
1318 assert!(out.contains("m.tally()"), "method call:\n{out}");
1319 assert!(out.contains("+ tally()"), "free call:\n{out}");
1320 assert!(
1322 out.contains("struct Meter { count: usize }"),
1323 "field declaration was renamed:\n{out}"
1324 );
1325 assert!(
1326 out.contains("{ self.count }"),
1327 "field read was renamed:\n{out}"
1328 );
1329 assert!(
1330 out.contains("m.count +"),
1331 "field read was renamed:\n{out}"
1332 );
1333 assert!(
1334 out.contains("Meter { count: 1 }"),
1335 "struct literal field was renamed:\n{out}"
1336 );
1337 }
1338
1339 #[cfg(feature = "lang-rust")]
1340 #[test]
1341 fn an_unresolved_rust_target_keeps_the_pre_narrowing_behaviour() {
1342 let narrowed = identifier_occurrences_for(
1345 Lang::Rust,
1346 RUST_FIELD_SOURCE.as_bytes(),
1347 "count",
1348 RenameTarget::Callable,
1349 )
1350 .unwrap();
1351 let wide = identifier_occurrences(Lang::Rust, RUST_FIELD_SOURCE.as_bytes(), "count").unwrap();
1352 assert!(
1353 wide.len() > narrowed.len(),
1354 "narrowing dropped nothing: {} vs {}",
1355 wide.len(),
1356 narrowed.len()
1357 );
1358 }
1359
1360 #[cfg(feature = "lang-rust")]
1361 #[test]
1362 fn a_field_access_inside_a_macro_is_still_renamed() {
1363 let source = "struct Meter { count: usize }\nfn count() -> usize { 3 }\nfn f(m: &Meter) -> String { format!(\"{}\", m.count) }\n";
1370 let found =
1371 identifier_occurrences_for(Lang::Rust, source.as_bytes(), "count", RenameTarget::Callable)
1372 .unwrap();
1373 let (out, _) = replace_occurrences(source, &found, "tally");
1374 assert!(out.contains("m.tally)"), "expected the known over-rename:\n{out}");
1375 assert!(
1376 out.contains("struct Meter { count: usize }"),
1377 "the field declaration is outside the macro and must survive:\n{out}"
1378 );
1379 }
1380
1381 #[cfg(feature = "lang-gdscript")]
1382 #[test]
1383 fn renaming_a_gdscript_func_leaves_an_identically_named_var_declaration_alone() {
1384 let source = "func count():\n\tvar count = 1\n\treturn 2\n\nfunc caller():\n\treturn count()\n";
1387 let found =
1388 identifier_occurrences_for(Lang::GdScript, source.as_bytes(), "count", RenameTarget::Callable)
1389 .unwrap();
1390 let (out, _) = replace_occurrences(source, &found, "tally");
1391 assert!(out.contains("func tally():"), "declaration:\n{out}");
1392 assert!(out.contains("return tally()"), "call:\n{out}");
1393 assert!(
1394 out.contains("var count = 1"),
1395 "the local var declaration was renamed:\n{out}"
1396 );
1397 }
1398
1399 #[cfg(feature = "lang-gdscript")]
1400 #[test]
1401 fn a_gdscript_local_that_shadows_the_target_and_is_read_refuses() {
1402 let source = "func count():\n\tvar count = 1\n\treturn count\n\nfunc caller():\n\treturn count()\n";
1406 let err = identifier_occurrences_for(
1407 Lang::GdScript,
1408 source.as_bytes(),
1409 "count",
1410 RenameTarget::Callable,
1411 )
1412 .unwrap_err();
1413 let message = format!("{err:#}");
1414 assert!(message.contains("shadows it"), "{message}");
1415 assert!(message.contains("line 2"), "{message}");
1416 }
1417
1418 #[cfg(feature = "lang-gdscript")]
1419 #[test]
1420 fn a_gdscript_callee_is_never_ambiguous() {
1421 let source = "func count():\n\treturn 1\n\nfunc caller():\n\treturn count() + count()\n";
1424 let found = identifier_occurrences_for(
1425 Lang::GdScript,
1426 source.as_bytes(),
1427 "count",
1428 RenameTarget::Callable,
1429 )
1430 .unwrap();
1431 assert_eq!(found.len(), 3, "got {found:?}");
1432 }
1433
1434 #[cfg(feature = "lang-gdscript")]
1435 #[test]
1436 fn renaming_a_gdscript_var_leaves_the_function_declaration_alone() {
1437 let source = "var count = 1\nfunc count():\n\treturn count\n";
1439 let found =
1440 identifier_occurrences_for(Lang::GdScript, source.as_bytes(), "count", RenameTarget::Value)
1441 .unwrap();
1442 let (out, _) = replace_occurrences(source, &found, "tally");
1443 assert!(out.contains("var tally = 1"), "var declaration:\n{out}");
1444 assert!(
1445 out.contains("func count():"),
1446 "the function declaration was renamed:\n{out}"
1447 );
1448 }
1449
1450 #[cfg(feature = "lang-gdscript")]
1451 #[test]
1452 fn a_gdscript_parameter_is_a_binding_not_a_reference() {
1453 let shadowed = "func caller(count):\n\treturn count\n";
1456 let err = identifier_occurrences_for(
1457 Lang::GdScript,
1458 shadowed.as_bytes(),
1459 "count",
1460 RenameTarget::Callable,
1461 )
1462 .unwrap_err();
1463 assert!(format!("{err:#}").contains("shadows it"), "{err:#}");
1464
1465 let source = "func caller(count):\n\treturn 1\n";
1468 let found =
1469 identifier_occurrences_for(Lang::GdScript, source.as_bytes(), "count", RenameTarget::Callable)
1470 .unwrap();
1471 let (out, _) = replace_occurrences(source, &found, "tally");
1472 assert!(
1473 out.contains("func caller(count):"),
1474 "a parameter declaration was renamed:\n{out}"
1475 );
1476 }
1477
1478 #[cfg(feature = "lang-typescript")]
1479 const TS_PROPERTY_SOURCE: &str = r#"function beta(v: number) { return v; }
1480const keyed = { beta: 1 };
1481const shorthand = { beta };
1482class K { beta() { return 2; } }
1483const k = new K();
1484const read = k.beta() + keyed.beta + beta(3);
1485export { beta };
1486"#;
1487
1488 #[cfg(feature = "lang-typescript")]
1489 #[test]
1490 fn renaming_a_typescript_function_leaves_properties_alone() {
1491 let found = identifier_occurrences_for(
1492 Lang::TypeScript,
1493 TS_PROPERTY_SOURCE.as_bytes(),
1494 "beta",
1495 RenameTarget::Callable,
1496 )
1497 .unwrap();
1498 let (out, _) = replace_occurrences(TS_PROPERTY_SOURCE, &found, "gamma");
1499 assert!(out.contains("function gamma(v: number)"), "declaration:
1501{out}");
1502 assert!(out.contains("+ gamma(3)"), "call:
1503{out}");
1504 assert!(out.contains("export { gamma };"), "export:
1505{out}");
1506 assert!(out.contains("{ beta: 1 }"), "object key was renamed:
1508{out}");
1509 assert!(
1510 out.contains("class K { beta()"),
1511 "class method was renamed:
1512{out}"
1513 );
1514 assert!(out.contains("k.beta()"), "member call was renamed:
1515{out}");
1516 assert!(out.contains("keyed.beta"), "member read was renamed:
1517{out}");
1518 }
1519
1520 #[cfg(feature = "lang-typescript")]
1521 #[test]
1522 fn a_javascript_object_shorthand_is_expanded_rather_than_overwritten() {
1523 let found = identifier_occurrences_for(
1527 Lang::TypeScript,
1528 TS_PROPERTY_SOURCE.as_bytes(),
1529 "beta",
1530 RenameTarget::Callable,
1531 )
1532 .unwrap();
1533 let (out, _) = replace_occurrences(TS_PROPERTY_SOURCE, &found, "gamma");
1534 assert!(
1535 out.contains("const shorthand = { beta: gamma };"),
1536 "shorthand was not expanded:
1537{out}"
1538 );
1539 }
1540
1541 #[cfg(feature = "lang-typescript")]
1542 #[test]
1543 fn a_destructuring_pattern_is_renamed_in_place_not_expanded() {
1544 let source = "import * as mod from './mod';
1548const { beta } = mod;
1549beta();
1550";
1551 let found =
1552 identifier_occurrences_for(Lang::TypeScript, source.as_bytes(), "beta", RenameTarget::Callable)
1553 .unwrap();
1554 let (out, _) = replace_occurrences(source, &found, "gamma");
1555 assert!(out.contains("const { gamma } = mod;"), "{out}");
1556 assert!(!out.contains("beta: gamma"), "pattern was expanded:
1557{out}");
1558 }
1559
1560 #[cfg(feature = "lang-typescript")]
1561 #[test]
1562 fn a_typescript_type_rename_keeps_type_identifiers_and_drops_properties() {
1563 let source = "type Beta = number;
1564const o = { Beta: 1 };
1565const v: Beta = 1;
1566export type { Beta };
1567";
1568 let callable = identifier_occurrences_for(
1569 Lang::TypeScript,
1570 source.as_bytes(),
1571 "Beta",
1572 RenameTarget::Callable,
1573 )
1574 .unwrap();
1575 let typed =
1576 identifier_occurrences_for(Lang::TypeScript, source.as_bytes(), "Beta", RenameTarget::Type)
1577 .unwrap();
1578 assert!(
1579 typed.len() > callable.len(),
1580 "a type rename must reach type_identifier positions a callable rename does not: {typed:?} vs {callable:?}"
1581 );
1582 let (out, _) = replace_occurrences(source, &typed, "Gamma");
1583 assert!(out.contains("type Gamma = number;"), "{out}");
1584 assert!(out.contains("const v: Gamma = 1;"), "{out}");
1585 assert!(out.contains("{ Beta: 1 }"), "object key was renamed:
1586{out}");
1587 }
1588
1589 #[test]
1590 fn indexed_symbol_kinds_map_onto_what_a_grammar_can_check() {
1591 assert_eq!(RenameTarget::from_indexed_kind("function"), RenameTarget::Callable);
1592 assert_eq!(RenameTarget::from_indexed_kind("signal"), RenameTarget::Signal);
1593 assert_eq!(RenameTarget::from_indexed_kind("struct"), RenameTarget::Type);
1594 assert_eq!(RenameTarget::from_indexed_kind("class"), RenameTarget::Type);
1595 assert_eq!(RenameTarget::from_indexed_kind("variable"), RenameTarget::Value);
1596 assert_eq!(RenameTarget::from_indexed_kind("const"), RenameTarget::Value);
1597 assert_eq!(RenameTarget::from_indexed_kind("heading"), RenameTarget::Unresolved);
1599 assert_eq!(RenameTarget::from_indexed_kind(""), RenameTarget::Unresolved);
1600 assert_eq!(RenameTarget::default(), RenameTarget::Unresolved);
1601 }
1602
1603 #[test]
1604 fn a_name_that_only_appears_in_prose_has_no_occurrences() {
1605 #[cfg(feature = "lang-rust")]
1606 {
1607 let source = "// widget_count\nfn other() {}\n";
1608 let found =
1609 identifier_occurrences(Lang::Rust, source.as_bytes(), "widget_count").unwrap();
1610 assert!(found.is_empty(), "got {found:?}");
1611 }
1612 }
1613
1614 #[cfg(feature = "lang-markdown")]
1615 #[test]
1616 fn markdown_has_no_identifier_kinds() {
1617 assert!(identifier_node_kinds(Lang::Markdown).is_empty());
1618 assert!(
1619 identifier_occurrences(Lang::Markdown, b"# widget_count\n", "widget_count")
1620 .unwrap()
1621 .is_empty()
1622 );
1623 }
1624
1625 #[test]
1626 fn every_indexed_language_declares_its_identifier_kinds() {
1627 for lang in Lang::all() {
1630 let kinds = identifier_node_kinds(lang);
1631 if lang.name() == "markdown" {
1632 continue;
1633 }
1634 assert!(
1635 !kinds.is_empty(),
1636 "{} declares no identifier node kinds",
1637 lang.name()
1638 );
1639 let ts_lang = lang.tree_sitter_language();
1640 for kind in kinds {
1641 assert!(
1642 ts_lang.id_for_node_kind(kind, true) != 0,
1643 "{} declares node kind {kind:?}, which its grammar does not have",
1644 lang.name()
1645 );
1646 }
1647 }
1648 }
1649}