Skip to main content

diffler_core/syntax/
scope.rs

1//! The chain of definitions enclosing a line, from a grammar's tags query.
2
3use std::collections::HashSet;
4
5use tree_sitter::{Node, Query, QueryCursor, StreamingIterator, Tree};
6
7use crate::syntax::registry::{LangEntry, LanguageRegistry};
8use crate::syntax::{MAX_PARSE_BYTES, parse};
9
10#[derive(Debug, Clone, Default)]
11pub struct ScopeIndex {
12    defs: Vec<Def>,
13}
14
15#[derive(Debug, Clone)]
16struct Def {
17    start_row: usize,
18    end_row: usize,
19    name: String,
20}
21
22impl ScopeIndex {
23    pub fn is_empty(&self) -> bool {
24        self.defs.is_empty()
25    }
26
27    /// 0-based start rows of every definition, sorted and deduped.
28    pub fn def_starts(&self) -> Vec<usize> {
29        let mut rows: Vec<usize> = self.defs.iter().map(|d| d.start_row).collect();
30        rows.sort_unstable();
31        rows.dedup();
32        rows
33    }
34
35    /// 0-based inclusive row span of the definition called `name`.
36    pub fn def_span(&self, name: &str) -> Option<(usize, usize)> {
37        self.defs
38            .iter()
39            .find(|def| def.name == name)
40            .map(|def| (def.start_row, def.end_row))
41    }
42
43    /// The innermost definition enclosing `line` (0-based): its name and its
44    /// inclusive row span.
45    pub fn enclosing(&self, line: usize) -> Option<(&str, usize, usize)> {
46        self.defs
47            .iter()
48            .filter(|def| def.start_row <= line && line <= def.end_row)
49            .min_by_key(|def| def.end_row - def.start_row)
50            .map(|def| (def.name.as_str(), def.start_row, def.end_row))
51    }
52
53    /// Names of the definitions enclosing `line` (0-based), outermost first. A
54    /// line inside `class A` → `method` → body returns `["A", "method"]`.
55    pub fn crumbs(&self, line: usize) -> Vec<String> {
56        let mut hits: Vec<&Def> = self
57            .defs
58            .iter()
59            .filter(|d| d.start_row <= line && line <= d.end_row)
60            .collect();
61        hits.sort_by(|a, b| {
62            a.start_row
63                .cmp(&b.start_row)
64                .then(b.end_row.cmp(&a.end_row))
65        });
66        hits.into_iter().map(|d| d.name.clone()).collect()
67    }
68}
69
70impl LanguageRegistry {
71    /// Empty when the file has no grammar or tags query, is too large, or
72    /// fails to parse.
73    pub fn scope_index(&self, path: &str, content: &str) -> ScopeIndex {
74        Self::scope_index_as(self.for_file(path, content), content)
75    }
76
77    /// [`Self::scope_index`] with `content` parsed as `entry`'s language.
78    pub fn scope_index_as(entry: Option<&LangEntry>, content: &str) -> ScopeIndex {
79        if content.len() > MAX_PARSE_BYTES {
80            return ScopeIndex::default();
81        }
82        let Some(entry) = entry else {
83            return ScopeIndex::default();
84        };
85        let Some(query) = entry.tags() else {
86            return ScopeIndex::default();
87        };
88        let Some(tree) = parse(entry, content) else {
89            return ScopeIndex::default();
90        };
91        tag_pass(query, &tree, content).0
92    }
93}
94
95/// Definition spans, plus the start byte of every name the tags query
96/// captures, definitions and references alike.
97pub(crate) fn tag_pass(query: &Query, tree: &Tree, content: &str) -> (ScopeIndex, HashSet<usize>) {
98    let names = query.capture_names();
99    let bytes = content.as_bytes();
100    let mut cursor = QueryCursor::new();
101    let mut matches = cursor.matches(query, tree.root_node(), bytes);
102    let mut defs = Vec::new();
103    let mut captured = HashSet::new();
104    while let Some(m) = matches.next() {
105        let mut span: Option<(usize, usize)> = None;
106        let mut name: Option<String> = None;
107        let mut qualified: Option<String> = None;
108        for cap in m.captures {
109            let cname = names.get(cap.index as usize).copied().unwrap_or("");
110            if cname.starts_with("definition.") {
111                let node = whole_definition(cap.node);
112                span = Some((node.start_position().row, node.end_position().row));
113            } else if cname == "name" {
114                captured.insert(cap.node.start_byte());
115                name = cap.node.utf8_text(bytes).ok().map(str::to_owned);
116                qualified = cap
117                    .node
118                    .parent()
119                    .filter(|parent| parent.kind() == "qualified_identifier")
120                    .and_then(|parent| parent.utf8_text(bytes).ok())
121                    .map(str::to_owned);
122            }
123        }
124        if let (Some((start_row, end_row)), Some(name)) = (span, name) {
125            for name in std::iter::once(name).chain(qualified) {
126                defs.push(Def {
127                    start_row,
128                    end_row,
129                    name,
130                });
131            }
132        }
133    }
134    (ScopeIndex { defs }, captured)
135}
136
137/// The node a definition spans. C and C++ tag a function by its declarator,
138/// which holds only the signature, so we climb to the function definition
139/// around it to take in the body; a prototype has none and keeps its own.
140fn whole_definition(node: Node<'_>) -> Node<'_> {
141    let mut current = node;
142    while let Some(parent) = current.parent() {
143        match parent.kind() {
144            "function_definition" => return parent,
145            kind if kind.ends_with("declarator") => current = parent,
146            _ => break,
147        }
148    }
149    node
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn nested_python_scope_reads_class_then_method() {
158        let reg = LanguageRegistry::build();
159        let src = "class A:\n    def method(self):\n        x = 1\n        return x\n";
160        let crumbs = reg.scope_index("a.py", src).crumbs(2);
161        let names: Vec<&str> = crumbs.iter().map(String::as_str).collect();
162        assert_eq!(names, ["A", "method"]);
163    }
164
165    #[test]
166    fn rust_function_scope() {
167        let reg = LanguageRegistry::build();
168        let src = "fn outer() {\n    let y = 2;\n}\n";
169        let crumbs = reg.scope_index("a.rs", src).crumbs(1);
170        let names: Vec<&str> = crumbs.iter().map(String::as_str).collect();
171        assert_eq!(names, ["outer"]);
172    }
173
174    #[test]
175    fn scope_index_empty_for_unsupported_language() {
176        let reg = LanguageRegistry::build();
177        assert!(reg.scope_index("a.zzz-unknown", "whatever\n").is_empty());
178    }
179
180    #[test]
181    fn line_outside_any_definition_has_no_crumbs() {
182        let reg = LanguageRegistry::build();
183        let src = "import os\n\ndef f():\n    pass\n";
184        assert!(reg.scope_index("a.py", src).crumbs(0).is_empty());
185    }
186
187    #[test]
188    fn every_tagged_language_spans_a_whole_function() {
189        let reg = LanguageRegistry::build();
190        let samples: &[(&str, &str, &str, (usize, usize))] = &[
191            ("a.rs", "fn f() {\n    let x = 1;\n    x;\n}\n", "f", (0, 3)),
192            ("a.py", "def f():\n    x = 1\n    return x\n", "f", (0, 2)),
193            (
194                "a.js",
195                "function f() {\n  const x = 1;\n  return x;\n}\n",
196                "f",
197                (0, 3),
198            ),
199            (
200                "a.ts",
201                "function f(): number {\n  const x = 1;\n  return x;\n}\n",
202                "f",
203                (0, 3),
204            ),
205            (
206                "a.tsx",
207                "function f() {\n  const x = 1;\n  return x;\n}\n",
208                "f",
209                (0, 3),
210            ),
211            (
212                "a.go",
213                "package main\n\nfunc f() int {\n\tx := 1\n\treturn x\n}\n",
214                "f",
215                (2, 5),
216            ),
217            (
218                "a.c",
219                "static int f(int a,\n             int b) {\n    int x = a;\n    return x + b;\n}\n",
220                "f",
221                (0, 4),
222            ),
223            (
224                "a.cpp",
225                "static bool f(const int* a) {\n    int x = 1;\n    return x;\n}\n",
226                "f",
227                (0, 3),
228            ),
229            (
230                "A.java",
231                "class A {\n    int f() {\n        int x = 1;\n        return x;\n    }\n}\n",
232                "f",
233                (1, 4),
234            ),
235            (
236                "a.cs",
237                "class A {\n    int F() {\n        int x = 1;\n        return x;\n    }\n}\n",
238                "F",
239                (1, 4),
240            ),
241            ("a.rb", "def f\n  x = 1\n  x\nend\n", "f", (0, 3)),
242            (
243                "a.php",
244                "<?php\nfunction f() {\n    $x = 1;\n    return $x;\n}\n",
245                "f",
246                (1, 4),
247            ),
248            (
249                "a.lua",
250                "function f()\n  local x = 1\n  return x\nend\n",
251                "f",
252                (0, 3),
253            ),
254            (
255                "a.swift",
256                "func f() -> Int {\n    let x = 1\n    return x\n}\n",
257                "f",
258                (0, 3),
259            ),
260            (
261                "a.ex",
262                "defmodule A do\n  def f do\n    x = 1\n    x\n  end\nend\n",
263                "f",
264                (1, 4),
265            ),
266            (
267                "a.dart",
268                "int f() {\n  var x = 1;\n  return x;\n}\n",
269                "f",
270                (0, 3),
271            ),
272        ];
273        let wrong: Vec<String> = samples
274            .iter()
275            .filter_map(|(path, src, name, want)| {
276                let got = reg.scope_index(path, src).def_span(name);
277                (got != Some(*want)).then(|| format!("{path}: {got:?}, want {want:?}"))
278            })
279            .collect();
280        assert!(wrong.is_empty(), "{wrong:#?}");
281    }
282
283    #[test]
284    fn a_cpp_method_defined_outside_its_class_answers_to_its_qualified_name() {
285        let reg = LanguageRegistry::build();
286        let src = "void Cache::Link(int a) {\n    int x = a;\n    use(x);\n}\n";
287        let index = reg.scope_index("a.cpp", src);
288        assert_eq!(index.def_span("Cache::Link"), Some((0, 3)));
289        assert_eq!(index.def_span("Link"), Some((0, 3)));
290    }
291
292    #[test]
293    fn a_c_prototype_keeps_its_own_line() {
294        let reg = LanguageRegistry::build();
295        let src = "int f(int a);\n\nint g(void) {\n    return f(1);\n}\n";
296        let index = reg.scope_index("a.c", src);
297        assert_eq!(index.def_span("g"), Some((2, 4)));
298        assert_eq!(index.def_span("f"), Some((0, 0)));
299    }
300}