big_code_analysis_ast/parser.rs
1// Per-language metric and AST modules deliberately consume the macro-
2// generated tree-sitter token enums via `use crate::*` and `use Foo::*`
3// inside match expressions — explicit imports would list dozens of
4// variants per arm and obscure the per-language token sets that are the
5// point of these files. Allowed at the module level rather than per
6// function so the per-language impl blocks stay readable.
7#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
8
9//! [`Parser<T>`](Parser) and the node [`Filter`]s `count` / `find` accept.
10
11use std::marker::PhantomData;
12use std::path::Path;
13use std::sync::Arc;
14
15use crate::checker::Checker;
16use crate::node::Ancestors;
17
18use crate::alterator::Alterator;
19use crate::getter::Getter;
20
21use crate::c_macro;
22use crate::langs::*;
23use crate::node::{Node, Tree};
24use crate::preproc::{PreprocResults, visible_macros};
25use crate::traits::*;
26
27/// Parsed source plus the tree-sitter `Tree` for a given language `T`.
28///
29/// Construct with [`Parser::new`] and feed the result into the metric,
30/// alterator, or AST-dump entry points. The type parameter `T` is one
31/// of the language code tags (`RustCode`, `PythonCode`, etc.) declared
32/// by the internal `mk_code!` macro.
33#[derive(Debug)]
34pub struct Parser<T: LanguageInfo + Alterator + Checker + Getter> {
35 code: Vec<u8>,
36 tree: Tree,
37 phantom: PhantomData<T>,
38}
39
40/// A single node-matching predicate. The `'a` bound lets a predicate
41/// borrow the parser's source buffer, which the `"function"` and
42/// `"string"` filters need to answer for a language whose functions or
43/// string literals are identified by their text rather than their kind
44/// (#1162, #1381).
45///
46/// The `Ancestors` argument is the chain the caller descended through.
47/// Both of those predicates ask about an enclosing construct, and
48/// resolving one from the node alone costs [`Node::parent`]'s
49/// `O(depth)` *per lookup* — which over a whole walk is the quadratic
50/// #1052 and #1122 warn about. The higher-ranked bound ties the chain's
51/// tree lifetime to the node's, so a predicate cannot be handed the
52/// ancestry of a different tree.
53type FilterFn<'a> = dyn for<'t, 'c> Fn(&Node<'t>, Ancestors<'t, 'c>) -> bool + 'a;
54
55/// Collection of node-matching predicates the `find` and `count` walks
56/// apply to each node they visit.
57pub struct Filter<'a> {
58 filters: Vec<Box<FilterFn<'a>>>,
59}
60
61impl Filter<'_> {
62 /// Returns `true` if *any* of the configured predicates matches
63 /// `node`, reached through `ancestors`.
64 ///
65 /// A walker that maintains an ancestor chain should pass it. The
66 /// `"function"` and `"string"` predicates ask about an enclosing
67 /// construct, and off [`Ancestors::unknown`] each such lookup is
68 /// [`Node::parent`]'s `O(depth)` — which made `count --type string`
69 /// quadratic in nesting depth until `find` and `count` threaded a
70 /// real chain (#1381).
71 #[must_use]
72 pub fn any<'t>(&self, node: &Node<'t>, ancestors: Ancestors<'t, '_>) -> bool {
73 for f in &self.filters {
74 if f(node, ancestors) {
75 return true;
76 }
77 }
78 false
79 }
80}
81
82#[inline]
83fn get_fake_code<T: LanguageInfo>(
84 code: &[u8],
85 path: &Path,
86 pr: Option<Arc<PreprocResults>>,
87) -> Option<Vec<u8>> {
88 if let Some(pr) = pr {
89 match T::lang() {
90 // The C-family languages share the preprocessor
91 // macro-replacement pass: `C` (#721), upstream `Cpp`, and the
92 // `Mozcpp` Gecko dialect (#720) all need `#define` expansion.
93 LANG::C | LANG::Cpp | LANG::Mozcpp => {
94 let macros = visible_macros(path, &pr.files);
95 c_macro::replace(code, ¯os)
96 }
97 _ => None,
98 }
99 } else {
100 None
101 }
102}
103
104impl<T: 'static + LanguageInfo + Alterator + Checker + Getter> ParserTrait for Parser<T> {
105 type Checker = T;
106 type Getter = T;
107
108 fn new(code: Vec<u8>, path: &Path, pr: Option<Arc<PreprocResults>>) -> Self {
109 let fake_code = get_fake_code::<T>(&code, path, pr);
110 let code = if let Some(fake) = fake_code {
111 fake
112 } else {
113 code
114 };
115
116 let tree = Tree::new::<T>(&code);
117
118 Self {
119 code,
120 tree,
121 phantom: PhantomData,
122 }
123 }
124
125 #[inline]
126 fn root(&self) -> Node<'_> {
127 self.tree.get_root()
128 }
129
130 #[inline]
131 fn code(&self) -> &[u8] {
132 &self.code
133 }
134
135 fn filters(&self, requested: &[String]) -> Filter<'_> {
136 // Borrowed by the `"call"`, `"function"` and `"string"` arms below, which
137 // is why `Filter` carries a lifetime.
138 let code = self.code();
139 let mut res: Vec<Box<FilterFn<'_>>> = Vec::new();
140 for f in requested {
141 let f = f.as_str();
142 match f {
143 "all" => res.push(Box::new(|_: &Node, _| -> bool { true })),
144 // `is_call_with_code`, not `is_call`: Perl parses a
145 // call's name and an auto-quoted hash key as bareword
146 // calls, and only the chain tells those from a call of
147 // their own (#1580). `is_comment` / `is_error` take
148 // `&Node` and nothing else, so no language *can* make
149 // them text-dependent.
150 "call" => res.push(Box::new(move |node: &Node, ancestors| {
151 T::is_call_with_code(node, code, ancestors)
152 })),
153 "comment" => res.push(Box::new(|node: &Node, _| T::is_comment(node))),
154 "error" => res.push(Box::new(|node: &Node, _| T::is_error(node))),
155 // `is_string_with_code`, not `is_string`: a Tcl-family
156 // `braced_word` is a string literal in a value position
157 // and a `proc` / `when` body everywhere else, and only
158 // the bytes of the enclosing command's leading word
159 // separate the two (#1381). This arm is the only caller
160 // of either spelling in the workspace, so the byte-less
161 // one now serves purely as the per-language kind table
162 // that each override narrows.
163 //
164 // This is also the arm that makes the chain load-bearing
165 // rather than merely cheaper. `braced_word` is *every*
166 // Tcl block and list literal, so the candidate set is
167 // dense, and the Tcl override asks about the enclosing
168 // command — roughly ten ancestor lookups per candidate.
169 // Off an unknown chain each is `Node::parent`'s
170 // `O(depth)`, which took `bca find --type string` on 8 KB
171 // of nested braces from 5 ms to 823 ms before the chain
172 // was threaded here.
173 "string" => res.push(Box::new(move |node: &Node, ancestors| {
174 T::is_string_with_code(node, code, ancestors)
175 })),
176 // The JS-family `is_func` and Elixir's `is_func_with_code`
177 // consult the chain to tell a named function from a
178 // closure and a `def` `Call` from any other (#1088,
179 // #1162). Both answer the same off an unknown chain —
180 // only the cost differs, and `find` / `count` now supply
181 // a real one.
182 "function" => res.push(Box::new(move |node: &Node, ancestors| {
183 T::is_func_with_code(node, code, ancestors)
184 })),
185 _ => {
186 if let Ok(n) = f.parse::<u16>() {
187 // A numeric `-t`/`--type` value matches by raw
188 // tree-sitter `kind_id` rather than node-type name.
189 // This is an escape hatch for grammar inspection
190 // (matching a kind that has no stable name, or one
191 // the string path mis-resolves), but `kind_id` is an
192 // index into the grammar's symbol table and is *not*
193 // stable across grammar versions — the same number
194 // names different nodes after a bump, and `-t 0` is
195 // the end/ERROR sentinel. Documented as unstable in
196 // big-code-analysis-book/src/commands/nodes.md; the
197 // string (`kind()`) path below is the supported one.
198 res.push(Box::new(move |node: &Node, _| -> bool {
199 node.kind_id() == n
200 }));
201 } else {
202 // Exact match on `node.kind()` — the CLI documents
203 // `find <NODE>` / `count <NODE_TYPE>` as searching
204 // for a specific node type, not a substring (see
205 // big-code-analysis-book/src/commands/nodes.md and
206 // issue #293).
207 let f = f.to_owned();
208 res.push(Box::new(move |node: &Node, _| -> bool { node.kind() == f }));
209 }
210 }
211 }
212 }
213 if res.is_empty() {
214 res.push(Box::new(|_: &Node, _| -> bool { true }));
215 }
216
217 Filter { filters: res }
218 }
219}
220
221impl<T: 'static + LanguageInfo + Alterator + Checker + Getter> Parser<T> {
222 /// Builds a [`Parser`] from a pre-parsed [`tree_sitter::Tree`]
223 /// and the matching source bytes.
224 ///
225 /// Use this when the caller already drives `tree-sitter` for
226 /// other purposes (e.g. an editor doing incremental reparsing)
227 /// and wants the metric walker to reuse the parse instead of
228 /// running its own. The standard byte-based entry point
229 /// remains [`ParserTrait::new`].
230 ///
231 /// The supplied `tree` must have been produced from `code` with
232 /// the tree-sitter language matching `T` — typically obtained
233 /// via [`crate::LANG::tree_sitter_language`]. A mismatch is
234 /// not `unsafe`, but metric values will be nonsensical because
235 /// the tree's `kind_id` values will not correspond to the per-
236 /// language enum the metric `compute` functions match on.
237 #[must_use]
238 pub fn from_tree(tree: tree_sitter::Tree, code: Vec<u8>) -> Self {
239 Self {
240 code,
241 tree: Tree::from_ts_tree(tree),
242 phantom: PhantomData,
243 }
244 }
245
246 /// Borrow the underlying [`tree_sitter::Tree`] for callers that
247 /// want to drive their own traversal alongside the metric walker.
248 ///
249 /// `Parser` is `pub` in this crate but carries no stability
250 /// promise (see the crate root); the stable spelling of this
251 /// accessor is `big_code_analysis::Ast::as_tree_sitter`.
252 #[must_use]
253 pub fn ts_tree(&self) -> &tree_sitter::Tree {
254 self.tree.as_ts_tree()
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use crate::count::count;
261 use crate::langs::PythonParser;
262 use crate::traits::ParserTrait;
263 use std::path::PathBuf;
264
265 #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "python"))]
266 fn parse_python(source: &str) -> PythonParser {
267 PythonParser::new(source.as_bytes().to_vec(), &PathBuf::from("t.py"), None)
268 }
269
270 #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "python"))]
271 fn count_kind(source: &str, filter: &str) -> usize {
272 count(&parse_python(source), &[filter.to_string()]).0
273 }
274
275 // Regression for #293: a named filter that is not a hardcoded
276 // keyword (`all`/`call`/`comment`/`error`/`string`/`function`) and
277 // not a numeric `kind_id` must match `node.kind()` exactly, not via
278 // substring containment.
279
280 #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "python"))]
281 #[test]
282 fn get_filters_exact_match_hits_named_kind() {
283 // Python's `if`/`elif`/`else` clauses each appear as their own
284 // `if_statement` / `elif_clause` / `else_clause` nodes. Filter
285 // `if_statement` should match exactly one node here.
286 let src = "if x:\n pass\nelif y:\n pass\nelse:\n pass\n";
287 assert_eq!(count_kind(src, "if_statement"), 1);
288 }
289
290 #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "python"))]
291 #[test]
292 fn get_filters_no_substring_match() {
293 // Filter `expression` must not match `expression_statement`,
294 // `binary_expression`, etc. Under the old substring behaviour
295 // every expression-bearing node would count; under exact-match
296 // there is no node literally named `expression` in this source.
297 let src = "x = 1 + 2\ny = foo(3)\n";
298 assert_eq!(
299 count_kind(src, "expression"),
300 0,
301 "exact match must not collapse all *_expression kinds"
302 );
303 // Sanity: `assignment` is a real node kind in tree-sitter-python
304 // and matches exactly twice (one per statement).
305 assert_eq!(count_kind(src, "assignment"), 2);
306 }
307
308 #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "python"))]
309 #[test]
310 fn get_filters_unknown_kind_returns_empty() {
311 // A filter that names no real node kind matches nothing — the
312 // previous substring behaviour would still return 0 here, but
313 // pinning it guards against a future regression that re-enables
314 // fuzzy matching.
315 let src = "x = 1\n";
316 assert_eq!(count_kind(src, "definitely_not_a_python_kind"), 0);
317 }
318
319 #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "python"))]
320 #[test]
321 fn get_filters_empty_request_matches_every_node() {
322 // Requesting nothing means "match everything": `filters` falls
323 // back to a match-all predicate when no arm pushed one, because
324 // a `Filter` holding no predicates would make `Filter::any`
325 // return `false` for every node and `bca find` / `bca count`
326 // silently report zero on a bare invocation.
327 //
328 // Asserting against `"all"` rather than a hand-counted total is
329 // what makes this able to fail: the two paths push the identical
330 // closure, so dropping the fallback sends the empty request to 0
331 // while `"all"` stays put. A `> 0` assertion could not tell the
332 // two apart from a source that simply had nodes.
333 let src = "if x:\n pass\nelse:\n y = foo(1 + 2)\n";
334 let parser = parse_python(src);
335 let everything = count(&parser, &["all".to_string()]).0;
336 let unfiltered = count(&parser, &[]).0;
337
338 assert!(
339 everything > 1,
340 "fixture must hold several nodes or this asserts nothing; got {everything}"
341 );
342 assert_eq!(
343 unfiltered, everything,
344 "an empty filter request must match what `all` matches"
345 );
346 }
347}