Skip to main content

tsift_graph/
rename.rs

1//! Identifier occurrence collection for symbol renames.
2//!
3//! A rename used to be a substring scan with an identifier-boundary guard. That
4//! shape cannot tell an identifier from the same characters inside a string
5//! literal or a comment, so `rename_symbol` silently rewrote both — a string
6//! literal is data, and rewriting it changes behaviour rather than names.
7//!
8//! Here the walk is restricted to the node kinds that *are* identifiers in each
9//! grammar. Comments and string bodies are different node kinds, so they drop
10//! out by construction; there is no comment or string special case below, and a
11//! new quoting or comment form cannot reintroduce the bug.
12//!
13//! A kind filter alone is still coarser than the language: several grammars
14//! spell two unrelated declarations with the same node kind — a Rust struct
15//! field and a method call are both `field_identifier`, a GDScript `func` and a
16//! local `var` are both `name`. Where the *position* in the tree separates
17//! them, [`RenameTarget`] carries what the index resolved the symbol to be and
18//! the walk drops occurrences that cannot be that thing. Where position does
19//! not separate them, the occurrence is kept: under-renaming leaves a caller
20//! pointing at a name that no longer exists, which is worse than the
21//! over-renaming it would avoid.
22
23use crate::lang::Lang;
24use anyhow::Result;
25use tree_sitter::{Node, Parser};
26
27/// What the index resolved the rename target to be.
28///
29/// Grammars distinguish a declaration from a reference far more often than they
30/// distinguish two same-named declarations, so this is the only input that lets
31/// the walk tell `fn count()` from `struct S { count: usize }`. It comes from
32/// the indexed symbol's kind, and [`RenameTarget::Unresolved`] — the default
33/// when nothing resolved — accepts every identifier kind, which is exactly the
34/// behaviour before this existed.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
36pub enum RenameTarget {
37    /// A function or method.
38    Callable,
39    /// A GDScript `signal`. Declared and connected by name, but never a
40    /// function, and the two have distinct declaration nodes.
41    Signal,
42    /// A type-level name: struct, enum, trait, class, interface, type alias.
43    Type,
44    /// A value binding: const, static, or variable.
45    Value,
46    /// Unresolved, or an indexed kind that maps to none of the above.
47    #[default]
48    Unresolved,
49}
50
51impl RenameTarget {
52    /// Map an indexed symbol kind onto what the grammar can check.
53    ///
54    /// The input strings are the capture names in `Lang::symbol_query`, so an
55    /// unrecognized one means a new capture was added without deciding what it
56    /// is. That falls to `Unresolved`, which is the permissive answer — a new
57    /// symbol kind must not silently start dropping occurrences.
58    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" | "record" | "delegate" => Self::Type,
65            "const" | "static" | "variable" | "property" | "enum_member" => Self::Value,
66            _ => Self::Unresolved,
67        }
68    }
69}
70
71/// The byte span of one identifier occurrence, as a half-open range.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub struct IdentifierOccurrence {
74    pub start_byte: usize,
75    pub end_byte: usize,
76    /// This occurrence is a JS-like object-literal shorthand (`{ beta }`),
77    /// where the one token is both the property name and a read of the binding.
78    /// Overwriting the span would silently rename the property too, so the
79    /// splice writes `beta: newName` and both survive.
80    pub expands_shorthand_key: bool,
81}
82
83/// Node kinds that carry a bare identifier in this language's grammar.
84///
85/// An empty slice means the language has no identifier concept a rename could
86/// target (Markdown), which callers must treat as "not renamable" rather than
87/// as "no occurrences found".
88pub fn identifier_node_kinds(lang: Lang) -> &'static [&'static str] {
89    match lang {
90        // Rust identifiers inside macro arguments live under an opaque
91        // `token_tree`, but they are still named `identifier` nodes, so a
92        // `foo()` call inside `assert_eq!`/`format!` is reached by this walk.
93        #[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        // Bash has no separate identifier node: a command name and a function
122        // name are both `word`, and an expansion is `variable_name`. `word` is
123        // also every unquoted argument, so kind alone is not enough here —
124        // `occurrence_is_renamable` narrows it to the name positions.
125        #[cfg(feature = "lang-bash")]
126        Lang::Bash => &["word", "variable_name"],
127        // Go splits selectors and struct-field/method names into
128        // `field_identifier`, and type positions into `type_identifier`; a bare
129        // reference or declaration is `identifier`. `package_identifier` is
130        // deliberately absent — renaming a package is a directory move.
131        #[cfg(feature = "lang-go")]
132        Lang::Go => &["identifier", "type_identifier", "field_identifier"],
133        #[cfg(feature = "lang-csharp")]
134        Lang::CSharp => &["identifier"],
135        // GDScript splits the two: `name` is the declared name of a statement
136        // or block, `identifier` is every reference to one.
137        #[cfg(feature = "lang-gdscript")]
138        Lang::GdScript => &["identifier", "name"],
139        // Markdown has headings, not identifiers; `rename_heading` is its kind.
140        #[cfg(feature = "lang-markdown")]
141        Lang::Markdown => &[],
142    }
143}
144
145/// Whether an identifier-kind node sits in a *naming* position.
146///
147/// For most grammars the node kind settles it, and this is unconditionally
148/// true. Bash is the exception that forces the check to exist: a bare `word`
149/// is the function name in `deploy() { … }`, the command name in `deploy`,
150/// **and** every unquoted argument, so `echo deploy` would otherwise have a
151/// rename rewrite an argument that is data. Restricting `word` to the
152/// declaration and command-name positions keeps arguments out, the same way
153/// the kind filter keeps strings and comments out for every other language.
154fn occurrence_is_renamable(lang: Lang, node: Node) -> bool {
155    match lang {
156        #[cfg(feature = "lang-bash")]
157        Lang::Bash => {
158            if node.kind() != "word" {
159                // `variable_name` is only ever a variable, in an assignment or
160                // an expansion.
161                return true;
162            }
163            node.parent().is_some_and(|parent| {
164                matches!(parent.kind(), "function_definition" | "command_name")
165            })
166        }
167        _ => {
168            let _ = node;
169            true
170        }
171    }
172}
173
174/// Whether an identifier-kind node could be *this* symbol.
175///
176/// Only positions the grammar makes unambiguous are ruled out. Anything the
177/// tree cannot attribute — a bare `count` reference in GDScript, a Rust
178/// `x.count()` where `count` might be an inherent method or a trait method on
179/// something else — is kept, because dropping it silently breaks a caller.
180#[allow(unused_variables)]
181fn occurrence_matches_target(
182    lang: Lang,
183    node: Node,
184    source: &[u8],
185    target: RenameTarget,
186) -> bool {
187    if target == RenameTarget::Unresolved {
188        return true;
189    }
190    match lang {
191        #[cfg(feature = "lang-rust")]
192        Lang::Rust => rust_occurrence_matches_target(node, target),
193        #[cfg(feature = "lang-python")]
194        Lang::Python => python_occurrence_matches_target(node, source, target),
195        #[cfg(feature = "lang-gdscript")]
196        Lang::GdScript => gdscript_occurrence_matches_target(node, target),
197        #[cfg(feature = "lang-typescript")]
198        Lang::TypeScript | Lang::Tsx => js_like_occurrence_matches_target(node, target),
199        #[cfg(feature = "lang-javascript")]
200        Lang::JavaScript | Lang::Jsx => js_like_occurrence_matches_target(node, target),
201        #[cfg(feature = "lang-kotlin")]
202        Lang::Kotlin => kotlin_occurrence_matches_target(node, source, target),
203        #[cfg(feature = "lang-zig")]
204        Lang::Zig => zig_occurrence_matches_target(node, source, target),
205        #[cfg(feature = "lang-go")]
206        Lang::Go => go_occurrence_matches_target(node, source, target),
207        #[cfg(feature = "lang-csharp")]
208        Lang::CSharp => csharp_occurrence_matches_target(node, target),
209        _ => {
210            let _ = node;
211            true
212        }
213    }
214}
215
216#[cfg(feature = "lang-csharp")]
217fn csharp_occurrence_matches_target(node: Node, target: RenameTarget) -> bool {
218    let Some(parent) = node.parent() else {
219        return true;
220    };
221
222    let declaration_target = match parent.kind() {
223        "method_declaration" | "local_function_statement" => Some(RenameTarget::Callable),
224        "class_declaration"
225        | "struct_declaration"
226        | "interface_declaration"
227        | "enum_declaration"
228        | "record_declaration"
229        | "delegate_declaration" => Some(RenameTarget::Type),
230        "property_declaration" | "enum_member_declaration" => Some(RenameTarget::Value),
231        _ => None,
232    };
233    if let Some(declaration_target) = declaration_target {
234        return target == declaration_target;
235    }
236
237    if parent.kind() == "invocation_expression"
238        && parent
239            .child_by_field_name("function")
240            .is_some_and(|function| function.id() == node.id())
241    {
242        return target == RenameTarget::Callable;
243    }
244
245    if parent.kind() == "member_access_expression"
246        && parent
247            .child_by_field_name("name")
248            .is_some_and(|name| name.id() == node.id())
249    {
250        let is_call = parent.parent().is_some_and(|call| {
251            call.kind() == "invocation_expression"
252                && call
253                    .child_by_field_name("function")
254                    .is_some_and(|function| function.id() == parent.id())
255        });
256        return match target {
257            RenameTarget::Callable => is_call,
258            RenameTarget::Value => !is_call,
259            RenameTarget::Type | RenameTarget::Signal | RenameTarget::Unresolved => true,
260        };
261    }
262
263    true
264}
265
266/// Go spells a struct field declaration, a field read, a method name, and a
267/// package-qualified reference all as `field_identifier` (`#goindex`).
268///
269/// The declaration position is decidable and is never a `Lang::symbol_query`
270/// capture, so it is ruled out outright. For a selector, the receiver settles
271/// it: a package name this file imported reaches a package-level declaration
272/// and must be renamed; anything else is a value, where only the callee
273/// position of a call can still be the function being renamed.
274#[cfg(feature = "lang-go")]
275fn go_occurrence_matches_target(node: Node, source: &[u8], target: RenameTarget) -> bool {
276    let Some(parent) = node.parent() else {
277        return true;
278    };
279    match parent.kind() {
280        // A struct field declaration. `Lang::symbol_query` captures struct
281        // *type* names, never their field names, so this can never be the
282        // symbol a resolved rename selected.
283        "field_declaration" => !parent
284            .children_by_field_name("name", &mut parent.walk())
285            .any(|name| name.id() == node.id()),
286        "selector_expression" => {
287            if parent
288                .child_by_field_name("field")
289                .is_none_or(|field| field.id() != node.id())
290            {
291                return true;
292            }
293            if go_receiver_is_imported_package(parent, source) {
294                return true;
295            }
296            target == RenameTarget::Callable
297                && parent.parent().is_some_and(|call| {
298                    call.kind() == "call_expression"
299                        && call
300                            .child_by_field_name("function")
301                            .is_some_and(|function| function.id() == parent.id())
302                })
303        }
304        _ => true,
305    }
306}
307
308/// Whether the receiver of a `selector_expression` is a package name bound by
309/// this file's imports.
310///
311/// `import "net/http"` binds `http`; `import h "net/http"` binds `h`. A local
312/// variable that shadows an imported package resolves to "package" here and
313/// keeps the occurrence — the over-renaming direction, which this module
314/// prefers because an extra rename is visible and a dropped one is not.
315#[cfg(feature = "lang-go")]
316fn go_receiver_is_imported_package(selector: Node, source: &[u8]) -> bool {
317    let Some(mut operand) = selector.child_by_field_name("operand") else {
318        return false;
319    };
320    while operand.kind() == "selector_expression" {
321        let Some(inner) = operand.child_by_field_name("operand") else {
322            return false;
323        };
324        operand = inner;
325    }
326    if operand.kind() != "identifier" && operand.kind() != "package_identifier" {
327        return false;
328    }
329    let Ok(name) = operand.utf8_text(source) else {
330        return false;
331    };
332    go_file_imports_package(selector, name, source)
333}
334
335#[cfg(feature = "lang-go")]
336fn go_file_imports_package(node: Node, name: &str, source: &[u8]) -> bool {
337    let mut root = node;
338    while let Some(parent) = root.parent() {
339        root = parent;
340    }
341    let mut found = false;
342    go_walk_import_specs(root, source, &mut |bound| {
343        if bound == name {
344            found = true;
345        }
346    });
347    found
348}
349
350/// Call `visit` with the local name each `import_spec` in the tree binds.
351#[cfg(feature = "lang-go")]
352fn go_walk_import_specs(node: Node, source: &[u8], visit: &mut impl FnMut(&str)) {
353    if node.kind() == "import_spec" {
354        if let Some(alias) = node.child_by_field_name("name")
355            && let Ok(text) = alias.utf8_text(source)
356        {
357            visit(text);
358            return;
359        }
360        if let Some(path) = node.child_by_field_name("path")
361            && let Ok(text) = path.utf8_text(source)
362        {
363            let trimmed = text.trim_matches('"');
364            if let Some(last) = trimmed.rsplit('/').next() {
365                visit(last);
366            }
367        }
368        return;
369    }
370    let mut cursor = node.walk();
371    for child in node.children(&mut cursor) {
372        go_walk_import_specs(child, source, visit);
373    }
374}
375
376/// Python uses `identifier` for both a binding and the attribute in `obj.name`.
377/// The attribute cannot be a module-level binding, except in two positions:
378/// methods are indexed as callables, so `obj.name()` is a real rename target,
379/// and `mod.name` is the module-level binding itself when `mod` is a module
380/// this file imported. Dropping that second case is a silent under-rename —
381/// `import mod` is half of how Python spells a cross-module reference, and the
382/// rename runs across files.
383#[cfg(feature = "lang-python")]
384fn python_occurrence_matches_target(node: Node, source: &[u8], target: RenameTarget) -> bool {
385    let Some(attribute) = node.parent().filter(|parent| parent.kind() == "attribute") else {
386        return true;
387    };
388    if attribute
389        .child_by_field_name("attribute")
390        .is_none_or(|name| name.id() != node.id())
391    {
392        return true;
393    }
394    if python_receiver_is_imported_module(attribute, source) {
395        return true;
396    }
397
398    target == RenameTarget::Callable
399        && attribute.parent().is_some_and(|call| {
400            call.kind() == "call"
401                && call
402                    .child_by_field_name("function")
403                    .is_some_and(|function| function.id() == attribute.id())
404        })
405}
406
407/// Whether the receiver of this `attribute` is a module bound by `import`.
408///
409/// Only `import mod` / `import pkg.mod as alias` bind a name that is reached
410/// with a dot; `from mod import name` binds `name` directly and never produces
411/// an attribute position. A chained receiver (`pkg.sub.name`) is resolved by
412/// walking to the root of the chain, which is the imported name.
413///
414/// A local variable that shadows an imported module resolves to "module" here
415/// and keeps the occurrence. That is the over-renaming direction, which this
416/// module prefers: an extra rename is visible, a dropped one is not.
417#[cfg(feature = "lang-python")]
418fn python_receiver_is_imported_module(attribute: Node, source: &[u8]) -> bool {
419    let Some(mut object) = attribute.child_by_field_name("object") else {
420        return false;
421    };
422    while object.kind() == "attribute" {
423        let Some(inner) = object.child_by_field_name("object") else {
424            return false;
425        };
426        object = inner;
427    }
428    if object.kind() != "identifier" {
429        return false;
430    }
431    let Ok(name) = object.utf8_text(source) else {
432        return false;
433    };
434    python_file_imports_module(attribute, name, source)
435}
436
437/// Whether the file holding `node` binds `name` with an `import` statement.
438#[cfg(feature = "lang-python")]
439fn python_file_imports_module(node: Node, name: &str, source: &[u8]) -> bool {
440    let mut root = node;
441    while let Some(parent) = root.parent() {
442        root = parent;
443    }
444    let mut cursor = root.walk();
445    let mut descend = true;
446    loop {
447        if descend {
448            let current = cursor.node();
449            if current.kind() == "import_statement"
450                && python_import_binds(current, name, source)
451            {
452                return true;
453            }
454            if cursor.goto_first_child() {
455                continue;
456            }
457        }
458        if cursor.goto_next_sibling() {
459            descend = true;
460            continue;
461        }
462        if !cursor.goto_parent() {
463            return false;
464        }
465        descend = false;
466    }
467}
468
469/// The name one `import_statement` clause binds: the alias when there is one,
470/// otherwise the first segment of the dotted path — `import pkg.mod` binds
471/// `pkg`, not `mod`.
472#[cfg(feature = "lang-python")]
473fn python_import_binds(import: Node, name: &str, source: &[u8]) -> bool {
474    let mut cursor = import.walk();
475    import.named_children(&mut cursor).any(|clause| {
476        let bound = match clause.kind() {
477            "aliased_import" => clause.child_by_field_name("alias"),
478            "dotted_name" => clause.named_child(0),
479            _ => None,
480        };
481        bound.is_some_and(|bound| bound.utf8_text(source).is_ok_and(|text| text == name))
482    })
483}
484
485/// Kotlin's first `navigation_expression` identifier is the receiver binding;
486/// every later identifier is a member. A member is a rename target in the callee
487/// position, because Kotlin indexes methods as callables, and whenever the
488/// receiver is a type declared in this file or a name bound by an import —
489/// `Panel.widgetCount` reaches a companion member and `Registry.widgetCount` an
490/// `object` member, both of which the index holds as declarations, so dropping
491/// them is an under-rename.
492#[cfg(feature = "lang-kotlin")]
493fn kotlin_occurrence_matches_target(node: Node, source: &[u8], target: RenameTarget) -> bool {
494    let Some(navigation) = node
495        .parent()
496        .filter(|parent| parent.kind() == "navigation_expression")
497    else {
498        return true;
499    };
500    if node.prev_named_sibling().is_none() {
501        return true;
502    }
503    if kotlin_receiver_is_namespace(navigation, source) {
504        return true;
505    }
506
507    target == RenameTarget::Callable
508        && navigation.parent().is_some_and(|call| {
509            call.kind() == "call_expression"
510                && call
511                    .named_child(0)
512                    .is_some_and(|function| function.id() == navigation.id())
513        })
514}
515
516/// Whether the receiver of this `navigation_expression` is a namespace: a type
517/// declared in this file or a name bound by an import.
518#[cfg(feature = "lang-kotlin")]
519fn kotlin_receiver_is_namespace(navigation: Node, source: &[u8]) -> bool {
520    let mut receiver = navigation;
521    while receiver.kind() == "navigation_expression" {
522        let Some(inner) = receiver.named_child(0) else {
523            return false;
524        };
525        receiver = inner;
526    }
527    if receiver.kind() != "identifier" {
528        return false;
529    }
530    let Ok(name) = receiver.utf8_text(source) else {
531        return false;
532    };
533    kotlin_file_declares_type(navigation, name, source)
534        || kotlin_file_imports_name(navigation, name, source)
535}
536
537/// Whether the file holding `node` declares a class, interface, or object named
538/// `name`.
539#[cfg(feature = "lang-kotlin")]
540fn kotlin_file_declares_type(node: Node, name: &str, source: &[u8]) -> bool {
541    let mut root = node;
542    while let Some(parent) = root.parent() {
543        root = parent;
544    }
545    let mut cursor = root.walk();
546    let mut descend = true;
547    loop {
548        if descend {
549            let current = cursor.node();
550            if matches!(
551                current.kind(),
552                "class_declaration" | "object_declaration" | "interface_declaration"
553            ) && current
554                .child_by_field_name("name")
555                .and_then(|declared| declared.utf8_text(source).ok())
556                == Some(name)
557            {
558                return true;
559            }
560            if cursor.goto_first_child() {
561                continue;
562            }
563        }
564        if cursor.goto_next_sibling() {
565            descend = true;
566            continue;
567        }
568        if !cursor.goto_parent() {
569            return false;
570        }
571        descend = false;
572    }
573}
574
575/// Whether the file holding `node` imports a declaration under `name`.
576///
577/// A Kotlin import binds the last path segment unless an `as` alias is present.
578/// Wildcard imports do not prove which names they bind, so they remain
579/// deliberately unresolved.
580#[cfg(feature = "lang-kotlin")]
581fn kotlin_file_imports_name(node: Node, name: &str, source: &[u8]) -> bool {
582    let mut root = node;
583    while let Some(parent) = root.parent() {
584        root = parent;
585    }
586    let mut cursor = root.walk();
587    let mut descend = true;
588    loop {
589        if descend {
590            let current = cursor.node();
591            if current.kind() == "import" && kotlin_import_binds(current, name, source) {
592                return true;
593            }
594            if cursor.goto_first_child() {
595                continue;
596            }
597        }
598        if cursor.goto_next_sibling() {
599            descend = true;
600            continue;
601        }
602        if !cursor.goto_parent() {
603            return false;
604        }
605        descend = false;
606    }
607}
608
609#[cfg(feature = "lang-kotlin")]
610fn kotlin_import_binds(import: Node, name: &str, source: &[u8]) -> bool {
611    let mut cursor = import.walk();
612    let children = import.named_children(&mut cursor).collect::<Vec<_>>();
613    if let Some(alias) = children
614        .get(1)
615        .filter(|child| child.kind() == "identifier")
616    {
617        return alias
618            .utf8_text(source)
619            .is_ok_and(|bound_name| bound_name == name);
620    }
621    children
622        .first()
623        .filter(|path| matches!(path.kind(), "identifier" | "qualified_identifier"))
624        .and_then(|path| path.utf8_text(source).ok())
625        .and_then(|path| path.rsplit('.').next())
626        .is_some_and(|bound_name| bound_name == name)
627}
628
629/// Zig spells a struct field declaration and every member access with the same
630/// flat `identifier` kind as a binding, so position is the only separator.
631///
632/// The member of `x.name` is *not* treated the way Python and Kotlin members
633/// are, because Zig has no import-into-namespace form: `@import("m.zig").name`
634/// and `Type.name` are the only ways to reach another declaration, and both are
635/// `field_expression` members. Dropping them by position would leave every
636/// cross-file reference of a renamed `const`, type, or non-called function
637/// pointing at a name that no longer exists. So a member is kept whenever its
638/// receiver chain roots in a *namespace* — an `@import` binding or a container
639/// type — and dropped only when the receiver is an ordinary value, where the
640/// member is a struct field. The callee exception applies there for the same
641/// reason it does elsewhere: Zig indexes methods as `function_declaration`.
642#[cfg(feature = "lang-zig")]
643fn zig_occurrence_matches_target(node: Node, source: &[u8], target: RenameTarget) -> bool {
644    let Some(parent) = node.parent() else {
645        return true;
646    };
647    match parent.kind() {
648        // `container_field` is a struct/enum/union field declaration. No
649        // capture in `Lang::symbol_query` produces one, so it can never be the
650        // symbol a resolved rename selected.
651        "container_field" => parent
652            .child_by_field_name("name")
653            .is_none_or(|name| name.id() != node.id()),
654        "field_expression" => {
655            if parent
656                .child_by_field_name("member")
657                .is_none_or(|member| member.id() != node.id())
658            {
659                return true;
660            }
661            if zig_receiver_is_namespace(parent, source) {
662                return true;
663            }
664            target == RenameTarget::Callable
665                && parent.parent().is_some_and(|call| {
666                    call.kind() == "call_expression"
667                        && call
668                            .child_by_field_name("function")
669                            .is_some_and(|function| function.id() == parent.id())
670                })
671        }
672        _ => true,
673    }
674}
675
676/// Whether the receiver of `field_expression` is a namespace rather than a value.
677///
678/// `@import("m.zig").name` is a namespace outright. An identifier receiver is a
679/// namespace when this file binds it to an `@import` or to a container type —
680/// `const m = @import("m.zig")`, `const Panel = struct { ... }` — because a Zig
681/// container type doubles as the namespace holding its declarations. A chained
682/// receiver (`m.Sub.name`) is resolved by walking to the root of the chain.
683///
684/// Anything this cannot prove is *not* a namespace, which is the conservative
685/// answer only because the caller's fallback for a value receiver still keeps
686/// the callee position. A receiver whose binding lives in another file resolves
687/// to `false` here; that case is the struct-field reading it is indistinguishable
688/// from, and the call site is still renamed.
689#[cfg(feature = "lang-zig")]
690fn zig_receiver_is_namespace(field_expression: Node, source: &[u8]) -> bool {
691    let Some(mut object) = field_expression.child_by_field_name("object") else {
692        return false;
693    };
694    while object.kind() == "field_expression" {
695        let Some(inner) = object.child_by_field_name("object") else {
696            return false;
697        };
698        object = inner;
699    }
700    match object.kind() {
701        "builtin_function" => zig_is_import_builtin(object, source),
702        "identifier" => object
703            .utf8_text(source)
704            .is_ok_and(|name| zig_file_binds_namespace(field_expression, name, source)),
705        _ => false,
706    }
707}
708
709/// Whether this `builtin_function` node is an `@import(...)` call.
710#[cfg(feature = "lang-zig")]
711fn zig_is_import_builtin(builtin: Node, source: &[u8]) -> bool {
712    let mut cursor = builtin.walk();
713    builtin.named_children(&mut cursor).any(|child| {
714        child.kind() == "builtin_identifier"
715            && child.utf8_text(source).is_ok_and(|text| text == "@import")
716    })
717}
718
719/// Whether the file holding `node` binds `name` to an `@import` or a container
720/// type declaration.
721///
722/// Only whole-file scanning can answer this, and it runs once per *matching*
723/// occurrence — the walk has already filtered to identifiers whose text is the
724/// symbol being renamed — so it is bounded by the number of member positions
725/// that spell the renamed name, not by the file's identifier count.
726#[cfg(feature = "lang-zig")]
727fn zig_file_binds_namespace(node: Node, name: &str, source: &[u8]) -> bool {
728    let mut root = node;
729    while let Some(parent) = root.parent() {
730        root = parent;
731    }
732    let mut cursor = root.walk();
733    let mut descend = true;
734    loop {
735        if descend {
736            let current = cursor.node();
737            if current.kind() == "variable_declaration"
738                && zig_declaration_binds_namespace(current, name, source)
739            {
740                return true;
741            }
742            if cursor.goto_first_child() {
743                continue;
744            }
745        }
746        if cursor.goto_next_sibling() {
747            descend = true;
748            continue;
749        }
750        if !cursor.goto_parent() {
751            return false;
752        }
753        descend = false;
754    }
755}
756
757/// Whether one `variable_declaration` binds `name` to a namespace value.
758#[cfg(feature = "lang-zig")]
759fn zig_declaration_binds_namespace(declaration: Node, name: &str, source: &[u8]) -> bool {
760    let mut cursor = declaration.walk();
761    let children: Vec<Node> = declaration.named_children(&mut cursor).collect();
762    let binds_name = children.iter().any(|child| {
763        child.kind() == "identifier" && child.utf8_text(source).is_ok_and(|text| text == name)
764    });
765    if !binds_name {
766        return false;
767    }
768    children.iter().any(|child| match child.kind() {
769        "builtin_function" => zig_is_import_builtin(*child, source),
770        // A Zig container type is also the namespace holding its declarations,
771        // so `Panel.method` reaches a `function_declaration` the index has.
772        "struct_declaration" | "enum_declaration" | "union_declaration"
773        | "opaque_declaration" => true,
774        _ => false,
775    })
776}
777
778/// The JS-like grammars spell every property `property_identifier`, whether it
779/// is an object-literal key, a class method, or a member access. None of those
780/// is the module-level binding a rename resolves to — `Lang::symbol_query`
781/// indexes `function_declaration`, `class_declaration`, and arrow-valued
782/// `variable_declarator`, and nothing else — so a resolved rename must leave
783/// them alone.
784#[cfg(any(feature = "lang-typescript", feature = "lang-javascript"))]
785fn js_like_occurrence_matches_target(node: Node, target: RenameTarget) -> bool {
786    match node.kind() {
787        "property_identifier" => false,
788        "type_identifier" => target == RenameTarget::Type,
789        _ => true,
790    }
791}
792
793/// Whether this occurrence must be written as `key: replacement`.
794///
795/// `{ beta }` is one token doing two jobs: it names the property *and* reads
796/// the binding. Overwriting the span renames the property as a side effect;
797/// skipping it leaves a read of a name that no longer exists. Expanding to
798/// `beta: gamma` is the only spelling where both stay correct, and it is
799/// exactly what the shorthand desugars to.
800#[allow(unused_variables)]
801fn occurrence_expands_shorthand_key(lang: Lang, node: Node, target: RenameTarget) -> bool {
802    if target == RenameTarget::Unresolved {
803        return false;
804    }
805    match lang {
806        #[cfg(feature = "lang-typescript")]
807        Lang::TypeScript | Lang::Tsx => js_like_shorthand_key(node),
808        #[cfg(feature = "lang-javascript")]
809        Lang::JavaScript | Lang::Jsx => js_like_shorthand_key(node),
810        _ => false,
811    }
812}
813
814/// An object-literal shorthand, and deliberately *not* a destructuring pattern.
815///
816/// `const { beta } = mod` is `shorthand_property_identifier_pattern`: there the
817/// token reads a property off `mod` and declares a local of the same name, so
818/// the correct rewrite depends on whether `mod` is the module whose export was
819/// renamed — which is the common case, and which plain span renaming already
820/// gets right. Expanding it would be wrong for that case, so it is left alone.
821#[cfg(any(feature = "lang-typescript", feature = "lang-javascript"))]
822fn js_like_shorthand_key(node: Node) -> bool {
823    node.kind() == "shorthand_property_identifier"
824        && node.parent().is_some_and(|parent| parent.kind() == "object")
825}
826
827/// Rust spells three unrelated things `field_identifier`: a struct field
828/// declaration, a field read, and the method in `x.method()`. The first two
829/// cannot be a function, and the third must stay, or renaming a method would
830/// leave every call site broken.
831#[cfg(feature = "lang-rust")]
832fn rust_occurrence_matches_target(node: Node, target: RenameTarget) -> bool {
833    let parent_kind = node.parent().map(|parent| parent.kind()).unwrap_or("");
834    match node.kind() {
835        "field_identifier" => {
836            // `x.count()` parses as a `call_expression` whose `function` is the
837            // `field_expression` holding this node. Every other position — a
838            // `field_declaration`, a `field_initializer`, a bare `x.count` read
839            // — is a field, which a function/type/value rename must not touch.
840            target == RenameTarget::Callable && parent_kind == "field_expression" && {
841                node.parent()
842                    .and_then(|field_expression| {
843                        let call = field_expression.parent()?;
844                        (call.kind() == "call_expression"
845                            && call.child_by_field_name("function")?.id() == field_expression.id())
846                        .then_some(())
847                    })
848                    .is_some()
849            }
850        }
851        "shorthand_field_identifier" => target == RenameTarget::Value,
852        // `S { count }` desugars to `count: count`, so the identifier names a
853        // *field* as well as reading a binding. Renaming only the read would
854        // change the field too, so a function or type rename skips it; a value
855        // rename keeps the pre-existing behaviour.
856        "identifier" if parent_kind == "shorthand_field_initializer" => {
857            matches!(target, RenameTarget::Value)
858        }
859        "type_identifier" => target == RenameTarget::Type,
860        _ => true,
861    }
862}
863
864/// GDScript spells every declared name `name`, from `func` to a local `var`,
865/// and every reference `identifier`. The declaration node therefore says which
866/// kind of thing is being declared, and a rename of one kind must not rewrite
867/// another's declaration.
868#[cfg(feature = "lang-gdscript")]
869fn gdscript_occurrence_matches_target(node: Node, target: RenameTarget) -> bool {
870    let parent_kind = node.parent().map(|parent| parent.kind()).unwrap_or("");
871    match node.kind() {
872        "name" => {
873            let declares: &[&str] = match target {
874                RenameTarget::Callable => &["function_definition"],
875                RenameTarget::Signal => &["signal_statement"],
876                RenameTarget::Type => &["class_definition", "class_name_statement", "enum_definition"],
877                RenameTarget::Value => &[
878                    "variable_statement",
879                    "const_statement",
880                    "export_variable_statement",
881                    "onready_variable_statement",
882                ],
883                RenameTarget::Unresolved => return true,
884            };
885            declares.contains(&parent_kind)
886        }
887        // A parameter is a fresh binding that shadows, never a reference to the
888        // module-level symbol being renamed.
889        "identifier" if parent_kind == "parameters" => false,
890        _ => true,
891    }
892}
893
894/// Every occurrence of `name` that is a real identifier node, in source order.
895///
896/// Returns an empty vector when the name never appears as an identifier, which
897/// is distinct from it appearing only inside strings or comments — both look
898/// the same to the caller, and both mean "there is nothing here to rename".
899pub fn identifier_occurrences(
900    lang: Lang,
901    source: &[u8],
902    name: &str,
903) -> Result<Vec<IdentifierOccurrence>> {
904    identifier_occurrences_for(lang, source, name, RenameTarget::Unresolved)
905}
906
907/// The same walk, narrowed to occurrences that could be `target`.
908pub fn identifier_occurrences_for(
909    lang: Lang,
910    source: &[u8],
911    name: &str,
912    target: RenameTarget,
913) -> Result<Vec<IdentifierOccurrence>> {
914    let kinds = identifier_node_kinds(lang);
915    if kinds.is_empty() || name.is_empty() {
916        return Ok(Vec::new());
917    }
918
919    let ts_lang = lang.tree_sitter_language();
920    let mut parser = Parser::new();
921    parser.set_language(&ts_lang)?;
922    let tree = parser
923        .parse(source, None)
924        .ok_or_else(|| anyhow::anyhow!("parse failed"))?;
925
926    let mut occurrences = Vec::new();
927    // A declaration of the same name that the target narrowing rejected, and
928    // that *shadows* the target rather than merely coexisting with it.
929    let mut shadowing_declaration_line: Option<usize> = None;
930    // A reference the grammar cannot attribute to either one.
931    let mut saw_ambiguous_reference = false;
932    let mut cursor = tree.walk();
933    let mut descend = true;
934    loop {
935        if descend {
936            let node = cursor.node();
937            if kinds.contains(&node.kind())
938                && node.utf8_text(source).is_ok_and(|it| it == name)
939                && occurrence_is_renamable(lang, node)
940            {
941                if occurrence_matches_target(lang, node, source, target) {
942                    occurrences.push(IdentifierOccurrence {
943                        start_byte: node.start_byte(),
944                        end_byte: node.end_byte(),
945                        expands_shorthand_key: occurrence_expands_shorthand_key(
946                            lang, node, target,
947                        ),
948                    });
949                    saw_ambiguous_reference |= occurrence_is_ambiguous_reference(lang, node, target);
950                } else if shadowing_declaration_line.is_none()
951                    && occurrence_shadows_target(lang, node, target)
952                {
953                    shadowing_declaration_line = Some(node.start_position().row + 1);
954                }
955            }
956            if cursor.goto_first_child() {
957                continue;
958            }
959        }
960        if cursor.goto_next_sibling() {
961            descend = true;
962            continue;
963        }
964        if !cursor.goto_parent() {
965            break;
966        }
967        descend = false;
968    }
969
970    // A pre-order walk already yields these in source order, but nested
971    // grammars can nest an identifier inside another identifier-kind node, and
972    // every caller splices spans left to right.
973    occurrences.sort_by_key(|occurrence| (occurrence.start_byte, occurrence.end_byte));
974    occurrences.dedup();
975
976    // Narrowing a declaration out while still rewriting references to it would
977    // produce a file where the declaration keeps the old name and a read of it
978    // carries the new one — internally inconsistent, and worse than either
979    // renaming both or renaming neither. Where the grammar cannot separate the
980    // two, refuse and name the shadow, the same way an unattributable
981    // cross-file reference refuses instead of guessing.
982    if let Some(line) = shadowing_declaration_line
983        && saw_ambiguous_reference
984    {
985        anyhow::bail!(
986            "rename_symbol refuses {name:?}: a same-named declaration on line {line} shadows it, and a bare reference cannot say which one it belongs to"
987        );
988    }
989    Ok(occurrences)
990}
991
992/// A rejected declaration that *shadows* the rename target inside this file.
993///
994/// Two Rust `field_identifier` positions are not shadows: a field and a
995/// function are reached through different syntax, so no reference is ambiguous.
996/// A GDScript local `var` is a shadow: within its scope a bare `count` is the
997/// variable, not the function, and the grammar spells both the same.
998fn occurrence_shadows_target(lang: Lang, node: Node, target: RenameTarget) -> bool {
999    match lang {
1000        #[cfg(feature = "lang-gdscript")]
1001        Lang::GdScript => {
1002            if target != RenameTarget::Callable {
1003                return false;
1004            }
1005            let parent_kind = node.parent().map(|parent| parent.kind()).unwrap_or("");
1006            match node.kind() {
1007                "name" => matches!(
1008                    parent_kind,
1009                    "variable_statement"
1010                        | "const_statement"
1011                        | "export_variable_statement"
1012                        | "onready_variable_statement"
1013                ),
1014                "identifier" => parent_kind == "parameters",
1015                _ => false,
1016            }
1017        }
1018        _ => {
1019            let _ = (node, target);
1020            false
1021        }
1022    }
1023}
1024
1025/// A kept occurrence that a shadowing declaration would make ambiguous.
1026///
1027/// A callee is never ambiguous — `count()` is the function whatever else is in
1028/// scope. A bare read is, because it could be either.
1029fn occurrence_is_ambiguous_reference(lang: Lang, node: Node, target: RenameTarget) -> bool {
1030    match lang {
1031        #[cfg(feature = "lang-gdscript")]
1032        Lang::GdScript => {
1033            if target != RenameTarget::Callable || node.kind() != "identifier" {
1034                return false;
1035            }
1036            let parent_kind = node.parent().map(|parent| parent.kind()).unwrap_or("");
1037            !matches!(parent_kind, "call" | "attribute_call" | "base_call")
1038        }
1039        _ => {
1040            let _ = (node, target);
1041            false
1042        }
1043    }
1044}
1045
1046/// Splice `replacement` over every occurrence span, returning the new source
1047/// and the number of substitutions.
1048pub fn replace_occurrences(
1049    source: &str,
1050    occurrences: &[IdentifierOccurrence],
1051    replacement: &str,
1052) -> (String, usize) {
1053    let mut out = String::with_capacity(source.len());
1054    let mut last = 0usize;
1055    let mut replaced = 0usize;
1056    for occurrence in occurrences {
1057        if occurrence.start_byte < last {
1058            // Overlapping spans would corrupt the splice; the first one wins.
1059            continue;
1060        }
1061        out.push_str(&source[last..occurrence.start_byte]);
1062        if occurrence.expands_shorthand_key {
1063            // `{ beta }` becomes `{ beta: gamma }`: the property keeps its name,
1064            // the value follows the rename.
1065            out.push_str(&source[occurrence.start_byte..occurrence.end_byte]);
1066            out.push_str(": ");
1067        }
1068        out.push_str(replacement);
1069        last = occurrence.end_byte;
1070        replaced += 1;
1071    }
1072    out.push_str(&source[last..]);
1073    (out, replaced)
1074}
1075
1076#[cfg(test)]
1077mod tests {
1078    use super::*;
1079
1080    #[cfg(feature = "lang-csharp")]
1081    #[test]
1082    fn csharp_callable_rename_skips_properties_strings_and_comments() {
1083        let source = r#"class Counter {
1084    static int WidgetCount() => 3;
1085    static int Caller() => WidgetCount();
1086    // WidgetCount stays prose.
1087    static string Label = "WidgetCount";
1088}
1089class Data {
1090    public int WidgetCount { get; set; }
1091    public int Read() => this.WidgetCount;
1092}
1093"#;
1094        let found = identifier_occurrences_for(
1095            Lang::CSharp,
1096            source.as_bytes(),
1097            "WidgetCount",
1098            RenameTarget::Callable,
1099        )
1100        .unwrap();
1101        let (out, replaced) = replace_occurrences(source, &found, "GadgetCount");
1102
1103        assert_eq!(replaced, 2, "got {found:?}\n{out}");
1104        assert!(out.contains("static int GadgetCount()"), "{out}");
1105        assert!(out.contains("=> GadgetCount();"), "{out}");
1106        assert!(out.contains("int WidgetCount { get; set; }"), "{out}");
1107        assert!(out.contains("this.WidgetCount"), "{out}");
1108        assert!(out.contains("// WidgetCount stays prose."), "{out}");
1109        assert!(out.contains("\"WidgetCount\""), "{out}");
1110    }
1111
1112    #[cfg(feature = "lang-rust")]
1113    const RUST_SOURCE: &str = r#"/// doc widget_count
1114fn widget_count() -> usize { 3 }
1115
1116fn describe() -> String {
1117    // widget_count comment
1118    let label = "widget_count";
1119    format!("{label}: {}", widget_count())
1120}
1121"#;
1122
1123    #[cfg(feature = "lang-rust")]
1124    #[test]
1125    fn rust_skips_strings_and_comments_but_reaches_macro_arguments() {
1126        let found =
1127            identifier_occurrences(Lang::Rust, RUST_SOURCE.as_bytes(), "widget_count").unwrap();
1128        // The definition and the call inside `format!` — not the doc comment,
1129        // the line comment, or the string literal.
1130        assert_eq!(
1131            found.len(),
1132            2,
1133            "expected the definition and the macro-argument call, got {found:?}"
1134        );
1135        for occurrence in &found {
1136            let before = &RUST_SOURCE[..occurrence.start_byte];
1137            assert!(
1138                !before.ends_with("/// doc ") && !before.ends_with("// "),
1139                "occurrence at {} is inside a comment",
1140                occurrence.start_byte
1141            );
1142            assert!(
1143                !before.ends_with('"'),
1144                "occurrence at {} is inside a string literal",
1145                occurrence.start_byte
1146            );
1147        }
1148    }
1149
1150    #[cfg(feature = "lang-rust")]
1151    #[test]
1152    fn replacing_rust_occurrences_leaves_prose_and_data_alone() {
1153        let found =
1154            identifier_occurrences(Lang::Rust, RUST_SOURCE.as_bytes(), "widget_count").unwrap();
1155        let (out, replaced) = replace_occurrences(RUST_SOURCE, &found, "gadget_count");
1156        assert_eq!(replaced, 2);
1157        assert!(out.contains("fn gadget_count()"), "definition not renamed");
1158        assert!(
1159            out.contains("gadget_count())"),
1160            "macro-argument call not renamed"
1161        );
1162        assert!(
1163            out.contains("/// doc widget_count"),
1164            "doc comment was renamed"
1165        );
1166        assert!(
1167            out.contains("// widget_count comment"),
1168            "line comment was renamed"
1169        );
1170        assert!(
1171            out.contains("\"widget_count\""),
1172            "string literal was renamed"
1173        );
1174    }
1175
1176    #[cfg(feature = "lang-python")]
1177    #[test]
1178    fn python_skips_strings_and_comments() {
1179        let source = "def widget_count():\n    # widget_count comment\n    return \"widget_count\"\n\nwidget_count()\n";
1180        let found = identifier_occurrences(Lang::Python, source.as_bytes(), "widget_count").unwrap();
1181        assert_eq!(found.len(), 2, "got {found:?}");
1182        let (out, replaced) = replace_occurrences(source, &found, "gadget_count");
1183        assert_eq!(replaced, 2);
1184        assert!(out.contains("def gadget_count()"));
1185        assert!(out.contains("gadget_count()\n"));
1186        assert!(out.contains("# widget_count comment"));
1187        assert!(out.contains("\"widget_count\""));
1188    }
1189
1190    #[cfg(feature = "lang-python")]
1191    #[test]
1192    fn python_callable_narrowing_keeps_method_calls_but_skips_attribute_reads() {
1193        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";
1194        let found = identifier_occurrences_for(
1195            Lang::Python,
1196            source.as_bytes(),
1197            "widget_count",
1198            RenameTarget::Callable,
1199        )
1200        .unwrap();
1201        let (out, replaced) = replace_occurrences(source, &found, "gadget_count");
1202
1203        assert_eq!(replaced, 4, "got {found:?}\n{out}");
1204        assert!(out.contains("def gadget_count():"));
1205        assert!(out.contains("def gadget_count(self):"));
1206        assert!(out.contains("called = panel.gadget_count()"));
1207        assert!(out.contains("direct = gadget_count()"));
1208        assert!(out.contains("read = panel.widget_count\n"));
1209    }
1210
1211    /// The attribute rule must not swallow `mod.name`. `import mod` is half of
1212    /// how Python spells a cross-module reference, and the rename is cross-file,
1213    /// so dropping it renames the definition and leaves every reader broken.
1214    #[cfg(feature = "lang-python")]
1215    #[test]
1216    fn python_narrowing_keeps_imported_module_attributes_including_bare_reads() {
1217        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";
1218        let found = identifier_occurrences_for(
1219            Lang::Python,
1220            source.as_bytes(),
1221            "widget_count",
1222            RenameTarget::Callable,
1223        )
1224        .unwrap();
1225        let (out, replaced) = replace_occurrences(source, &found, "gadget_count");
1226
1227        assert_eq!(replaced, 4, "got {found:?}\n{out}");
1228        assert!(out.contains("def gadget_count():"), "{out}");
1229        assert!(
1230            out.contains("module_read = mod.gadget_count\n"),
1231            "an imported-module read was dropped:\n{out}"
1232        );
1233        assert!(out.contains("module_call = mod.gadget_count()"), "{out}");
1234        assert!(
1235            out.contains("aliased_read = aliased.gadget_count"),
1236            "an aliased-import read was dropped:\n{out}"
1237        );
1238        assert!(
1239            out.contains("read = panel.widget_count\n"),
1240            "an instance attribute read was renamed:\n{out}"
1241        );
1242    }
1243
1244    #[cfg(feature = "lang-kotlin")]
1245    #[test]
1246    fn kotlin_callable_narrowing_keeps_method_calls_but_skips_navigation_reads() {
1247        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";
1248        let found = identifier_occurrences_for(
1249            Lang::Kotlin,
1250            source.as_bytes(),
1251            "widgetCount",
1252            RenameTarget::Callable,
1253        )
1254        .unwrap();
1255        let (out, replaced) = replace_occurrences(source, &found, "gadgetCount");
1256
1257        assert_eq!(replaced, 4, "got {found:?}\n{out}");
1258        assert!(out.contains("fun gadgetCount(): Int = 1"));
1259        assert!(out.contains("fun gadgetCount(): Int = 2"));
1260        assert!(out.contains("val called = panel.gadgetCount()"));
1261        assert!(out.contains("val direct = gadgetCount()"));
1262        assert!(out.contains("val read = panel.widgetCount\n"));
1263    }
1264
1265    /// A receiver that names a declared type is a namespace, not a value, so its
1266    /// member is a declaration the index holds. Dropping it renames the
1267    /// companion/object declaration and leaves the qualified access behind.
1268    #[cfg(feature = "lang-kotlin")]
1269    #[test]
1270    fn kotlin_narrowing_keeps_members_of_types_declared_in_the_file() {
1271        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";
1272        let found = identifier_occurrences_for(
1273            Lang::Kotlin,
1274            source.as_bytes(),
1275            "widgetCount",
1276            RenameTarget::Callable,
1277        )
1278        .unwrap();
1279        let (out, replaced) = replace_occurrences(source, &found, "gadgetCount");
1280
1281        assert_eq!(replaced, 4, "got {found:?}\n{out}");
1282        assert!(out.contains("fun gadgetCount(): Int = 2"), "{out}");
1283        assert!(out.contains("fun gadgetCount(): Int = 3"), "{out}");
1284        assert!(
1285            out.contains("val fromClass = Panel.gadgetCount\n"),
1286            "a companion member read was dropped:\n{out}"
1287        );
1288        assert!(
1289            out.contains("val fromObject = Registry.gadgetCount()"),
1290            "an object member call was dropped:\n{out}"
1291        );
1292        assert!(
1293            out.contains("val fromValue = panel.widgetCount\n"),
1294            "a value's member read was renamed:\n{out}"
1295        );
1296    }
1297
1298    /// Imports bind external declarations into the local namespace. Qualified
1299    /// reads through the imported name or alias must survive callable narrowing,
1300    /// even when they are not immediately called.
1301    #[cfg(feature = "lang-kotlin")]
1302    #[test]
1303    fn kotlin_narrowing_keeps_members_of_imported_names() {
1304        let source = "import widgets.Panel\n\
1305import widgets.Registry as ExternalRegistry\n\
1306\n\
1307val fromClass = Panel.widgetCount\n\
1308val fromAlias = ExternalRegistry.widgetCount()\n\
1309val fromValue = panel.widgetCount\n";
1310        let found = identifier_occurrences_for(
1311            Lang::Kotlin,
1312            source.as_bytes(),
1313            "widgetCount",
1314            RenameTarget::Callable,
1315        )
1316        .unwrap();
1317        let (out, replaced) = replace_occurrences(source, &found, "gadgetCount");
1318
1319        assert_eq!(replaced, 2, "got {found:?}\n{out}");
1320        assert!(
1321            out.contains("val fromClass = Panel.gadgetCount\n"),
1322            "{out}"
1323        );
1324        assert!(
1325            out.contains("val fromAlias = ExternalRegistry.gadgetCount()\n"),
1326            "{out}"
1327        );
1328        assert!(out.contains("val fromValue = panel.widgetCount\n"), "{out}");
1329    }
1330
1331    #[cfg(feature = "lang-typescript")]
1332    #[test]
1333    fn typescript_skips_strings_and_comments() {
1334        let source = "// widgetCount comment\nfunction widgetCount(): number { return 1; }\nconst label = \"widgetCount\";\nwidgetCount();\n";
1335        let found =
1336            identifier_occurrences(Lang::TypeScript, source.as_bytes(), "widgetCount").unwrap();
1337        assert_eq!(found.len(), 2, "got {found:?}");
1338        let (out, _) = replace_occurrences(source, &found, "gadgetCount");
1339        assert!(out.contains("function gadgetCount()"));
1340        assert!(out.contains("// widgetCount comment"));
1341        assert!(out.contains("\"widgetCount\""));
1342    }
1343
1344    #[cfg(feature = "lang-bash")]
1345    const BASH_SOURCE: &str = r#"widget_count() {
1346  echo widget_count
1347  local label="widget_count"
1348  # widget_count comment
1349  echo "$widget_count"
1350}
1351widget_count
1352"#;
1353
1354    #[cfg(feature = "lang-bash")]
1355    #[test]
1356    fn bash_renames_names_but_not_arguments_prose_or_data() {
1357        let found =
1358            identifier_occurrences(Lang::Bash, BASH_SOURCE.as_bytes(), "widget_count").unwrap();
1359        // The definition, the `$widget_count` expansion, and the bare call —
1360        // not the `echo widget_count` argument, the string, or the comment.
1361        assert_eq!(found.len(), 3, "got {found:?}");
1362        let (out, replaced) = replace_occurrences(BASH_SOURCE, &found, "gadget_count");
1363        assert_eq!(replaced, 3);
1364        assert!(out.contains("gadget_count() {"), "definition not renamed");
1365        assert!(
1366            out.contains("echo \"$gadget_count\""),
1367            "expansion not renamed"
1368        );
1369        assert!(
1370            out.contains("}\ngadget_count\n"),
1371            "bare call not renamed:\n{out}"
1372        );
1373        assert!(
1374            out.contains("echo widget_count\n"),
1375            "an unquoted argument was renamed, which rewrites data:\n{out}"
1376        );
1377        assert!(out.contains("label=\"widget_count\""), "string was renamed");
1378        assert!(
1379            out.contains("# widget_count comment"),
1380            "comment was renamed"
1381        );
1382    }
1383
1384    #[cfg(feature = "lang-zig")]
1385    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";
1386
1387    /// The member positions Zig cannot narrow by the callee rule alone. A field
1388    /// read off a value is dropped; a namespace member is kept whether or not
1389    /// it is called, because `@import(...)` and a container type are the only
1390    /// ways Zig reaches another declaration.
1391    #[cfg(feature = "lang-zig")]
1392    #[test]
1393    fn zig_callable_narrowing_keeps_namespace_members_but_skips_field_reads() {
1394        let found = identifier_occurrences_for(
1395            Lang::Zig,
1396            ZIG_MEMBER_SOURCE.as_bytes(),
1397            "widget_count",
1398            RenameTarget::Callable,
1399        )
1400        .unwrap();
1401        let (out, replaced) = replace_occurrences(ZIG_MEMBER_SOURCE, &found, "gadget_count");
1402
1403        assert_eq!(replaced, 5, "got {found:?}\n{out}");
1404        assert!(out.contains("pub fn gadget_count() u32"), "{out}");
1405        assert!(out.contains("return gadget_count() +"), "{out}");
1406        assert!(out.contains("m.gadget_count()"), "import call dropped:\n{out}");
1407        assert!(
1408            out.contains("m.gadget_count +"),
1409            "import read dropped, which breaks every cross-file reference:\n{out}"
1410        );
1411        assert!(
1412            out.contains("Panel.gadget_count;"),
1413            "container-type member dropped:\n{out}"
1414        );
1415        assert!(
1416            out.contains("    widget_count: u32 = 0,"),
1417            "a struct field declaration was renamed:\n{out}"
1418        );
1419        assert!(
1420            out.contains("p.widget_count +"),
1421            "a field read off a value was renamed:\n{out}"
1422        );
1423        assert!(
1424            out.contains("return self.widget_count;"),
1425            "a field read off self was renamed:\n{out}"
1426        );
1427    }
1428
1429    /// A `const` rename keeps the namespace members — those name a module-level
1430    /// declaration in another file — and drops the struct field: no capture in
1431    /// `Lang::symbol_query` produces a `container_field`, so a field is never the
1432    /// symbol a resolved rename selected, and a field read off a value receiver
1433    /// is the one member position the grammar does attribute.
1434    #[cfg(feature = "lang-zig")]
1435    #[test]
1436    fn zig_value_narrowing_keeps_namespace_members_and_drops_struct_fields() {
1437        let found = identifier_occurrences_for(
1438            Lang::Zig,
1439            ZIG_MEMBER_SOURCE.as_bytes(),
1440            "widget_count",
1441            RenameTarget::Value,
1442        )
1443        .unwrap();
1444        let (out, _) = replace_occurrences(ZIG_MEMBER_SOURCE, &found, "gadget_count");
1445
1446        assert!(
1447            out.contains("m.gadget_count +"),
1448            "an import-qualified const read was dropped:\n{out}"
1449        );
1450        assert!(
1451            out.contains("Panel.gadget_count;"),
1452            "a container-type const read was dropped:\n{out}"
1453        );
1454        assert!(
1455            out.contains("p.widget_count +"),
1456            "a struct field read was renamed by a const rename:\n{out}"
1457        );
1458        assert!(
1459            out.contains("    widget_count: u32 = 0,"),
1460            "the field declaration is not an indexed symbol and must not move:\n{out}"
1461        );
1462    }
1463
1464    #[cfg(feature = "lang-zig")]
1465    #[test]
1466    fn zig_skips_strings_and_comments() {
1467        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";
1468        let found = identifier_occurrences(Lang::Zig, source.as_bytes(), "widget_count").unwrap();
1469        assert_eq!(found.len(), 2, "got {found:?}");
1470        let (out, replaced) = replace_occurrences(source, &found, "gadget_count");
1471        assert_eq!(replaced, 2);
1472        assert!(out.contains("pub fn gadget_count()"), "definition not renamed");
1473        assert!(out.contains("return gadget_count();"), "call not renamed");
1474        assert!(
1475            out.contains("// widget_count comment"),
1476            "comment was renamed"
1477        );
1478        assert!(out.contains("\"widget_count\""), "string was renamed");
1479    }
1480
1481    #[cfg(feature = "lang-gdscript")]
1482    #[test]
1483    fn gdscript_renames_declaration_and_reference_but_not_prose() {
1484        let source = "# widget_count comment\nfunc widget_count():\n\tvar label = \"widget_count\"\n\treturn label\n\nfunc caller():\n\treturn widget_count()\n";
1485        let found =
1486            identifier_occurrences(Lang::GdScript, source.as_bytes(), "widget_count").unwrap();
1487        // GDScript names a declaration with `name` and every reference with
1488        // `identifier`; the rename has to reach both kinds.
1489        assert_eq!(found.len(), 2, "got {found:?}");
1490        let (out, replaced) = replace_occurrences(source, &found, "gadget_count");
1491        assert_eq!(replaced, 2);
1492        assert!(out.contains("func gadget_count():"), "definition not renamed");
1493        assert!(out.contains("return gadget_count()"), "call not renamed");
1494        assert!(
1495            out.contains("# widget_count comment"),
1496            "comment was renamed"
1497        );
1498        assert!(out.contains("\"widget_count\""), "string was renamed");
1499    }
1500
1501    #[cfg(feature = "lang-rust")]
1502    const RUST_FIELD_SOURCE: &str = r#"struct Meter { count: usize }
1503fn count() -> usize { 3 }
1504impl Meter {
1505    fn read(&self) -> usize { self.count }
1506    fn count(&self) -> usize { self.count }
1507}
1508fn use_it(m: &Meter) -> usize { m.count() + m.count + count() }
1509fn build() -> Meter { Meter { count: 1 } }
1510"#;
1511
1512    #[cfg(feature = "lang-rust")]
1513    #[test]
1514    fn renaming_a_rust_function_leaves_an_identically_named_field_alone() {
1515        let found =
1516            identifier_occurrences_for(Lang::Rust, RUST_FIELD_SOURCE.as_bytes(), "count", RenameTarget::Callable)
1517                .unwrap();
1518        let (out, _) = replace_occurrences(RUST_FIELD_SOURCE, &found, "tally");
1519        // Renamed: the free fn, the inherent method, the method call, the call.
1520        assert!(out.contains("fn tally() -> usize"), "free fn:\n{out}");
1521        assert!(out.contains("fn tally(&self)"), "inherent method:\n{out}");
1522        assert!(out.contains("m.tally()"), "method call:\n{out}");
1523        assert!(out.contains("+ tally()"), "free call:\n{out}");
1524        // Untouched: every position that is a field, not a function.
1525        assert!(
1526            out.contains("struct Meter { count: usize }"),
1527            "field declaration was renamed:\n{out}"
1528        );
1529        assert!(
1530            out.contains("{ self.count }"),
1531            "field read was renamed:\n{out}"
1532        );
1533        assert!(
1534            out.contains("m.count +"),
1535            "field read was renamed:\n{out}"
1536        );
1537        assert!(
1538            out.contains("Meter { count: 1 }"),
1539            "struct literal field was renamed:\n{out}"
1540        );
1541    }
1542
1543    #[cfg(feature = "lang-rust")]
1544    #[test]
1545    fn an_unresolved_rust_target_keeps_the_pre_narrowing_behaviour() {
1546        // With no resolved symbol there is nothing to narrow by, and dropping
1547        // occurrences on a guess would silently under-rename.
1548        let narrowed = identifier_occurrences_for(
1549            Lang::Rust,
1550            RUST_FIELD_SOURCE.as_bytes(),
1551            "count",
1552            RenameTarget::Callable,
1553        )
1554        .unwrap();
1555        let wide = identifier_occurrences(Lang::Rust, RUST_FIELD_SOURCE.as_bytes(), "count").unwrap();
1556        assert!(
1557            wide.len() > narrowed.len(),
1558            "narrowing dropped nothing: {} vs {}",
1559            wide.len(),
1560            narrowed.len()
1561        );
1562    }
1563
1564    #[cfg(feature = "lang-rust")]
1565    #[test]
1566    fn a_field_access_inside_a_macro_is_still_renamed() {
1567        // Known limitation, pinned rather than left as folklore. tree-sitter
1568        // parses macro arguments as an opaque `token_tree`, so `m.count`
1569        // inside `format!` is a bare `identifier` with no `field_expression`
1570        // around it — the position rule has nothing to read. Over-renaming is
1571        // the deliberate side to err on: the alternative is dropping the real
1572        // call sites inside macros that the walk exists to reach.
1573        let source = "struct Meter { count: usize }\nfn count() -> usize { 3 }\nfn f(m: &Meter) -> String { format!(\"{}\", m.count) }\n";
1574        let found =
1575            identifier_occurrences_for(Lang::Rust, source.as_bytes(), "count", RenameTarget::Callable)
1576                .unwrap();
1577        let (out, _) = replace_occurrences(source, &found, "tally");
1578        assert!(out.contains("m.tally)"), "expected the known over-rename:\n{out}");
1579        assert!(
1580            out.contains("struct Meter { count: usize }"),
1581            "the field declaration is outside the macro and must survive:\n{out}"
1582        );
1583    }
1584
1585    #[cfg(feature = "lang-gdscript")]
1586    #[test]
1587    fn renaming_a_gdscript_func_leaves_an_identically_named_var_declaration_alone() {
1588        // The local is declared but never read by name, so nothing here is
1589        // ambiguous and the rename can proceed.
1590        let source = "func count():\n\tvar count = 1\n\treturn 2\n\nfunc caller():\n\treturn count()\n";
1591        let found =
1592            identifier_occurrences_for(Lang::GdScript, source.as_bytes(), "count", RenameTarget::Callable)
1593                .unwrap();
1594        let (out, _) = replace_occurrences(source, &found, "tally");
1595        assert!(out.contains("func tally():"), "declaration:\n{out}");
1596        assert!(out.contains("return tally()"), "call:\n{out}");
1597        assert!(
1598            out.contains("var count = 1"),
1599            "the local var declaration was renamed:\n{out}"
1600        );
1601    }
1602
1603    #[cfg(feature = "lang-gdscript")]
1604    #[test]
1605    fn a_gdscript_local_that_shadows_the_target_and_is_read_refuses() {
1606        // Renaming the `func` but not the shadowing `var` while still rewriting
1607        // `return count` would leave the declaration on the old name and its
1608        // read on the new one. Refusing names the shadow instead of guessing.
1609        let source = "func count():\n\tvar count = 1\n\treturn count\n\nfunc caller():\n\treturn count()\n";
1610        let err = identifier_occurrences_for(
1611            Lang::GdScript,
1612            source.as_bytes(),
1613            "count",
1614            RenameTarget::Callable,
1615        )
1616        .unwrap_err();
1617        let message = format!("{err:#}");
1618        assert!(message.contains("shadows it"), "{message}");
1619        assert!(message.contains("line 2"), "{message}");
1620    }
1621
1622    #[cfg(feature = "lang-gdscript")]
1623    #[test]
1624    fn a_gdscript_callee_is_never_ambiguous() {
1625        // A call site is the function whatever else is in scope, so a shadow
1626        // that is only ever *called* is not a reason to refuse.
1627        let source = "func count():\n\treturn 1\n\nfunc caller():\n\treturn count() + count()\n";
1628        let found = identifier_occurrences_for(
1629            Lang::GdScript,
1630            source.as_bytes(),
1631            "count",
1632            RenameTarget::Callable,
1633        )
1634        .unwrap();
1635        assert_eq!(found.len(), 3, "got {found:?}");
1636    }
1637
1638    #[cfg(feature = "lang-gdscript")]
1639    #[test]
1640    fn renaming_a_gdscript_var_leaves_the_function_declaration_alone() {
1641        // The mirror case: the same two `name` nodes, the other target kind.
1642        let source = "var count = 1\nfunc count():\n\treturn count\n";
1643        let found =
1644            identifier_occurrences_for(Lang::GdScript, source.as_bytes(), "count", RenameTarget::Value)
1645                .unwrap();
1646        let (out, _) = replace_occurrences(source, &found, "tally");
1647        assert!(out.contains("var tally = 1"), "var declaration:\n{out}");
1648        assert!(
1649            out.contains("func count():"),
1650            "the function declaration was renamed:\n{out}"
1651        );
1652    }
1653
1654    #[cfg(feature = "lang-gdscript")]
1655    #[test]
1656    fn a_gdscript_parameter_is_a_binding_not_a_reference() {
1657        // The parameter shadows, and `return count` reads it, so this refuses
1658        // rather than renaming the read out from under the declaration.
1659        let shadowed = "func caller(count):\n\treturn count\n";
1660        let err = identifier_occurrences_for(
1661            Lang::GdScript,
1662            shadowed.as_bytes(),
1663            "count",
1664            RenameTarget::Callable,
1665        )
1666        .unwrap_err();
1667        assert!(format!("{err:#}").contains("shadows it"), "{err:#}");
1668
1669        // With nothing reading the parameter, the declaration is simply left
1670        // alone: it is a fresh binding, never a reference to our function.
1671        let source = "func caller(count):\n\treturn 1\n";
1672        let found =
1673            identifier_occurrences_for(Lang::GdScript, source.as_bytes(), "count", RenameTarget::Callable)
1674                .unwrap();
1675        let (out, _) = replace_occurrences(source, &found, "tally");
1676        assert!(
1677            out.contains("func caller(count):"),
1678            "a parameter declaration was renamed:\n{out}"
1679        );
1680    }
1681
1682    #[cfg(feature = "lang-typescript")]
1683    const TS_PROPERTY_SOURCE: &str = r#"function beta(v: number) { return v; }
1684const keyed = { beta: 1 };
1685const shorthand = { beta };
1686class K { beta() { return 2; } }
1687const k = new K();
1688const read = k.beta() + keyed.beta + beta(3);
1689export { beta };
1690"#;
1691
1692    #[cfg(feature = "lang-typescript")]
1693    #[test]
1694    fn renaming_a_typescript_function_leaves_properties_alone() {
1695        let found = identifier_occurrences_for(
1696            Lang::TypeScript,
1697            TS_PROPERTY_SOURCE.as_bytes(),
1698            "beta",
1699            RenameTarget::Callable,
1700        )
1701        .unwrap();
1702        let (out, _) = replace_occurrences(TS_PROPERTY_SOURCE, &found, "gamma");
1703        // Renamed: the declaration, the call, and the export specifier.
1704        assert!(out.contains("function gamma(v: number)"), "declaration:
1705{out}");
1706        assert!(out.contains("+ gamma(3)"), "call:
1707{out}");
1708        assert!(out.contains("export { gamma };"), "export:
1709{out}");
1710        // Untouched: every property position.
1711        assert!(out.contains("{ beta: 1 }"), "object key was renamed:
1712{out}");
1713        assert!(
1714            out.contains("class K { beta()"),
1715            "class method was renamed:
1716{out}"
1717        );
1718        assert!(out.contains("k.beta()"), "member call was renamed:
1719{out}");
1720        assert!(out.contains("keyed.beta"), "member read was renamed:
1721{out}");
1722    }
1723
1724    #[cfg(feature = "lang-typescript")]
1725    #[test]
1726    fn a_javascript_object_shorthand_is_expanded_rather_than_overwritten() {
1727        // `{ beta }` names the property and reads the binding. Overwriting the
1728        // span would rename the property too; skipping it would leave a read of
1729        // a name that no longer exists.
1730        let found = identifier_occurrences_for(
1731            Lang::TypeScript,
1732            TS_PROPERTY_SOURCE.as_bytes(),
1733            "beta",
1734            RenameTarget::Callable,
1735        )
1736        .unwrap();
1737        let (out, _) = replace_occurrences(TS_PROPERTY_SOURCE, &found, "gamma");
1738        assert!(
1739            out.contains("const shorthand = { beta: gamma };"),
1740            "shorthand was not expanded:
1741{out}"
1742        );
1743    }
1744
1745    #[cfg(feature = "lang-typescript")]
1746    #[test]
1747    fn a_destructuring_pattern_is_renamed_in_place_not_expanded() {
1748        // `const { beta } = mod` reads a property off `mod`. When `mod` is the
1749        // module whose export was renamed — the common case — renaming the span
1750        // is exactly right, and expanding it would be wrong.
1751        let source = "import * as mod from './mod';
1752const { beta } = mod;
1753beta();
1754";
1755        let found =
1756            identifier_occurrences_for(Lang::TypeScript, source.as_bytes(), "beta", RenameTarget::Callable)
1757                .unwrap();
1758        let (out, _) = replace_occurrences(source, &found, "gamma");
1759        assert!(out.contains("const { gamma } = mod;"), "{out}");
1760        assert!(!out.contains("beta: gamma"), "pattern was expanded:
1761{out}");
1762    }
1763
1764    #[cfg(feature = "lang-typescript")]
1765    #[test]
1766    fn a_typescript_type_rename_keeps_type_identifiers_and_drops_properties() {
1767        let source = "type Beta = number;
1768const o = { Beta: 1 };
1769const v: Beta = 1;
1770export type { Beta };
1771";
1772        let callable = identifier_occurrences_for(
1773            Lang::TypeScript,
1774            source.as_bytes(),
1775            "Beta",
1776            RenameTarget::Callable,
1777        )
1778        .unwrap();
1779        let typed =
1780            identifier_occurrences_for(Lang::TypeScript, source.as_bytes(), "Beta", RenameTarget::Type)
1781                .unwrap();
1782        assert!(
1783            typed.len() > callable.len(),
1784            "a type rename must reach type_identifier positions a callable rename does not: {typed:?} vs {callable:?}"
1785        );
1786        let (out, _) = replace_occurrences(source, &typed, "Gamma");
1787        assert!(out.contains("type Gamma = number;"), "{out}");
1788        assert!(out.contains("const v: Gamma = 1;"), "{out}");
1789        assert!(out.contains("{ Beta: 1 }"), "object key was renamed:
1790{out}");
1791    }
1792
1793    #[test]
1794    fn indexed_symbol_kinds_map_onto_what_a_grammar_can_check() {
1795        assert_eq!(RenameTarget::from_indexed_kind("function"), RenameTarget::Callable);
1796        assert_eq!(RenameTarget::from_indexed_kind("signal"), RenameTarget::Signal);
1797        assert_eq!(RenameTarget::from_indexed_kind("struct"), RenameTarget::Type);
1798        assert_eq!(RenameTarget::from_indexed_kind("class"), RenameTarget::Type);
1799        assert_eq!(RenameTarget::from_indexed_kind("variable"), RenameTarget::Value);
1800        assert_eq!(RenameTarget::from_indexed_kind("const"), RenameTarget::Value);
1801        // An unrecognized kind must be permissive, never silently narrowing.
1802        assert_eq!(RenameTarget::from_indexed_kind("heading"), RenameTarget::Unresolved);
1803        assert_eq!(RenameTarget::from_indexed_kind(""), RenameTarget::Unresolved);
1804        assert_eq!(RenameTarget::default(), RenameTarget::Unresolved);
1805    }
1806
1807    #[test]
1808    fn a_name_that_only_appears_in_prose_has_no_occurrences() {
1809        #[cfg(feature = "lang-rust")]
1810        {
1811            let source = "// widget_count\nfn other() {}\n";
1812            let found =
1813                identifier_occurrences(Lang::Rust, source.as_bytes(), "widget_count").unwrap();
1814            assert!(found.is_empty(), "got {found:?}");
1815        }
1816    }
1817
1818    #[cfg(feature = "lang-markdown")]
1819    #[test]
1820    fn markdown_has_no_identifier_kinds() {
1821        assert!(identifier_node_kinds(Lang::Markdown).is_empty());
1822        assert!(
1823            identifier_occurrences(Lang::Markdown, b"# widget_count\n", "widget_count")
1824                .unwrap()
1825                .is_empty()
1826        );
1827    }
1828
1829    #[test]
1830    fn every_indexed_language_declares_its_identifier_kinds() {
1831        // A `Lang` variant added with no entry here would silently return an
1832        // empty set and make every rename in that language a no-op.
1833        for lang in Lang::all() {
1834            let kinds = identifier_node_kinds(lang);
1835            if lang.name() == "markdown" {
1836                continue;
1837            }
1838            assert!(
1839                !kinds.is_empty(),
1840                "{} declares no identifier node kinds",
1841                lang.name()
1842            );
1843            let ts_lang = lang.tree_sitter_language();
1844            for kind in kinds {
1845                assert!(
1846                    ts_lang.id_for_node_kind(kind, true) != 0,
1847                    "{} declares node kind {kind:?}, which its grammar does not have",
1848                    lang.name()
1849                );
1850            }
1851        }
1852    }
1853}