big_code_analysis/spaces/ast.rs
1//! Inherent and `Debug` impl blocks for [`super::Ast`].
2//!
3//! Split out of `spaces.rs` to keep that module focused on the public
4//! API type definitions. The blocks are moved verbatim; method
5//! resolution is by type, so `crate::spaces::Ast::parse` (and every
6//! other `Ast` method) resolves unchanged.
7
8use super::*;
9
10impl fmt::Debug for Ast {
11 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12 // The held parser owns a `tree_sitter::Tree` and a `Vec<u8>`;
13 // neither has a meaningful `Debug` projection (one is an opaque
14 // C handle, the other is raw source). Reporting language + name
15 // keeps the public `Ast: Debug` promise without forcing `Debug`
16 // onto every per-language `*Code` tag.
17 f.debug_struct("Ast")
18 .field("language", &self.language())
19 .field("name", &self.name)
20 .finish_non_exhaustive()
21 }
22}
23
24impl Ast {
25 /// Parse `source` into a reusable [`Ast`]. Equivalent to the parse half
26 /// of [`analyze`]: every [`Ast::metrics`] call on the returned handle
27 /// produces the same [`FuncSpace`] as a freshly-issued
28 /// `analyze(source, options)` would.
29 ///
30 /// # Errors
31 ///
32 /// Returns [`MetricsError::LanguageDisabled`] when the source language's
33 /// per-language Cargo feature is not enabled in this build.
34 pub fn parse(source: Source<'_>) -> Result<Self, MetricsError> {
35 let Source {
36 lang,
37 code,
38 name,
39 preproc_path,
40 preproc,
41 } = source;
42 let inner =
43 crate::langs::ast_parse_dispatch(lang, code.into_owned(), preproc_path, preproc)?;
44 Ok(Self { inner, name })
45 }
46
47 /// Adopt a caller-built [`tree_sitter::Tree`], reusing it instead of
48 /// running the bundled parser, with `name: Option<String>` carried
49 /// end-to-end.
50 ///
51 /// The supplied `tree` must have been produced from `code` with the
52 /// [`tree_sitter::Language`] returned by
53 /// [`LANG::tree_sitter_language`] for `lang`. A mismatch is not
54 /// `unsafe`, but it is a precondition violation, not merely a
55 /// garbage-in/garbage-out one: the walk slices `code` by node byte
56 /// ranges, so a tree built from *longer* source than `code` indexes
57 /// out of bounds and panics. Mismatches that stay in bounds yield
58 /// nonsensical metric values instead.
59 ///
60 /// # Errors
61 ///
62 /// Returns [`MetricsError::LanguageDisabled`] when `lang`'s
63 /// per-language Cargo feature is not enabled in this build.
64 pub fn from_tree_sitter(
65 lang: LANG,
66 tree: tree_sitter::Tree,
67 code: Vec<u8>,
68 name: Option<String>,
69 ) -> Result<Self, MetricsError> {
70 let inner = crate::langs::ast_from_tree_dispatch(lang, tree, code)?;
71 Ok(Self { inner, name })
72 }
73
74 /// Read `path`, detect its language, and parse it into a reusable
75 /// [`Ast`] — the file-backed counterpart to [`Ast::parse`].
76 ///
77 /// The file is read through the same text reader [`analyze`] uses, so
78 /// the bytes (after end-of-line normalization and UTF-8 BOM stripping)
79 /// are byte-identical and `from_path(p)?.metrics(opts)` equals
80 /// `analyze` over the same file. The detected language follows the same
81 /// extension / shebang / mode-line rules as the CLI and `analyze`.
82 ///
83 /// Unlike [`analyze`], `from_path` is *no-magic*: it does **not** skip
84 /// generated files — the caller asked for *this* file's tree, so a
85 /// generated file is parsed like any other. It also does not run the
86 /// C/C++ preprocessor pass (matching the in-memory parse path); the
87 /// tree reflects the source as written.
88 ///
89 /// # Errors
90 ///
91 /// Returns a [`FromPathError`](crate::FromPathError) variant for each
92 /// distinct failure: a real I/O fault
93 /// ([`Io`](crate::FromPathError::Io)), a non-UTF-8 path
94 /// ([`NonUtf8Path`](crate::FromPathError::NonUtf8Path)), an empty /
95 /// binary / non-UTF-8 file
96 /// ([`Unreadable`](crate::FromPathError::Unreadable)), an unrecognized
97 /// language ([`UnknownLanguage`](crate::FromPathError::UnknownLanguage)),
98 /// or a disabled-language build
99 /// ([`Parse`](crate::FromPathError::Parse)). `from_path` never silently
100 /// returns a tree-less success.
101 pub fn from_path(path: &std::path::Path) -> Result<Self, crate::FromPathError> {
102 use crate::FromPathError;
103
104 // The path doubles as the FuncSpace name (an identifier); reject a
105 // lossy conversion rather than corrupting it, matching `analyze`'s
106 // strict default.
107 let name = path.to_str().ok_or(FromPathError::NonUtf8Path)?.to_owned();
108 let code = crate::read_file_with_eol(path)
109 .map_err(FromPathError::Io)?
110 .ok_or(FromPathError::Unreadable)?;
111 let lang = crate::guess_language(&code, path)
112 .0
113 .ok_or(FromPathError::UnknownLanguage)?;
114 Ok(Self::parse(
115 Source::from_bytes(lang, code).with_name(Some(name)),
116 )?)
117 }
118
119 /// Run the metric walker against the held parse. Safe to call
120 /// repeatedly — the tree is reused.
121 ///
122 /// Two `metrics` calls with different [`MetricsOptions::with_only`]
123 /// selections walk the tree twice; the savings versus [`analyze`] come
124 /// from not re-parsing the source.
125 ///
126 /// # Errors
127 ///
128 /// The return type carries [`MetricsError::EmptyRoot`] for forward
129 /// compatibility, but the walker always pushes a synthetic top-level
130 /// [`SpaceKind::Unit`] [`FuncSpace`] before walking, so this method
131 /// does not return `Err` in practice today.
132 pub fn metrics(&self, options: MetricsOptions) -> Result<FuncSpace, MetricsError> {
133 self.inner.run_metrics(self.name.clone(), options)
134 }
135
136 /// Return every operator and operand of each space in the held parse.
137 ///
138 /// The top-level [`crate::Ops::name`] is the `Source::name` supplied
139 /// to [`Ast::parse`] / [`Ast::from_tree_sitter`] — carried explicitly
140 /// rather than derived from a filesystem path via lossy UTF-8
141 /// conversion, so [`crate::Ops::name_was_lossy`] is never set on this
142 /// path. Safe to call repeatedly; the tree is reused.
143 ///
144 /// # Errors
145 ///
146 /// The return type carries [`MetricsError::EmptyRoot`] for forward
147 /// compatibility, but the walker always pushes a synthetic top-level
148 /// space before walking, so this method does not return `Err` in
149 /// practice today (see the variant doc).
150 ///
151 /// # Examples
152 ///
153 /// ```
154 /// use big_code_analysis::{Ast, LANG, Source};
155 ///
156 /// let ops = Ast::parse(
157 /// Source::new(LANG::Cpp, b"int a = 42;")
158 /// .with_name(Some("foo.c".to_owned())),
159 /// )
160 /// .expect("cpp feature enabled")
161 /// .ops()
162 /// .expect("walker succeeds");
163 /// assert_eq!(ops.name.as_deref(), Some("foo.c"));
164 /// assert!(!ops.name_was_lossy);
165 /// ```
166 pub fn ops(&self) -> Result<crate::ops::Ops, MetricsError> {
167 self.inner.run_ops(self.name.clone())
168 }
169
170 /// Source language of the parsed tree.
171 #[must_use]
172 #[inline]
173 pub fn language(&self) -> LANG {
174 self.inner.language()
175 }
176
177 /// Source bytes the held tree was parsed from. For [`LANG::Cpp`] with
178 /// preprocessor inputs supplied to [`Ast::parse`], these are the
179 /// *expanded* bytes (see the type-level "C++ preprocessor" note).
180 #[must_use]
181 #[inline]
182 pub fn source(&self) -> &[u8] {
183 self.inner.code_bytes()
184 }
185
186 /// Display name carried through to [`FuncSpace::name`] by every
187 /// [`Ast::metrics`] call.
188 #[must_use]
189 #[inline]
190 pub fn name(&self) -> Option<&str> {
191 self.name.as_deref()
192 }
193
194 /// Borrow the underlying [`tree_sitter::Tree`] for callers that want
195 /// to drive their own traversal alongside the metric walker.
196 ///
197 /// The returned reference is valid only while `self` lives; nodes
198 /// obtained from it must be resolved against [`Ast::source`] (the
199 /// `tree_sitter::Tree` is lazy and lifetime-bound to that byte
200 /// buffer).
201 #[must_use]
202 #[inline]
203 pub fn as_tree_sitter(&self) -> &tree_sitter::Tree {
204 self.inner.ts_tree()
205 }
206
207 /// Strip non-doc comments from the held parse, returning the source
208 /// with those byte ranges removed. `None` when there is nothing to
209 /// strip. Safe to call repeatedly; the tree is reused.
210 ///
211 /// # Examples
212 ///
213 /// ```
214 /// use big_code_analysis::{Ast, LANG, Source};
215 ///
216 /// let stripped = Ast::parse(Source::new(LANG::Rust, b"// gone\nfn f() {}\n"))
217 /// .expect("rust feature enabled")
218 /// .strip_comments()
219 /// .expect("a comment was present");
220 /// assert!(!stripped.windows(2).any(|w| w == b"//"));
221 /// ```
222 #[must_use]
223 pub fn strip_comments(&self) -> Option<Vec<u8>> {
224 self.inner.run_strip_comments()
225 }
226
227 /// Detect the span of every function in the held parse. Safe to call
228 /// repeatedly; the tree is reused.
229 ///
230 /// # Examples
231 ///
232 /// ```
233 /// use big_code_analysis::{Ast, LANG, Source};
234 ///
235 /// let funcs = Ast::parse(Source::new(LANG::Rust, b"fn a() {}\nfn b() {}\n"))
236 /// .expect("rust feature enabled")
237 /// .functions();
238 /// assert_eq!(funcs.len(), 2);
239 /// ```
240 #[must_use]
241 pub fn functions(&self) -> Vec<crate::FunctionSpan> {
242 self.inner.run_functions()
243 }
244
245 /// Build the [`AstResponse`](crate::AstResponse) node tree for the held
246 /// parse under `cfg`. The `Source`-based counterpart of the deprecated
247 /// `AstCallback` dispatch. Safe to call repeatedly; the tree is reused.
248 ///
249 /// # Examples
250 ///
251 /// ```
252 /// use big_code_analysis::{Ast, AstCfg, LANG, Source};
253 ///
254 /// let resp = Ast::parse(Source::new(LANG::Rust, b"fn f() {}"))
255 /// .expect("rust feature enabled")
256 /// .dump(AstCfg {
257 /// id: String::new(),
258 /// language: "rust".to_owned(),
259 /// comment: false,
260 /// span: false,
261 /// });
262 /// assert_eq!(resp.language, "rust");
263 /// assert_eq!(resp.root.expect("root node").r#type, "source_file");
264 /// ```
265 #[must_use]
266 pub fn dump(&self, cfg: crate::AstCfg) -> crate::AstResponse {
267 self.inner.run_dump(cfg)
268 }
269
270 /// Count `(matching, total)` nodes in the held parse, where a node
271 /// matches when its kind is named in `filters` (the same vocabulary
272 /// the `bca count` CLI accepts — `all`, `call`, `comment`, `error`,
273 /// `string`, `function`, a numeric `kind_id`, or an exact
274 /// `node.kind()`). Safe to call repeatedly; the tree is reused.
275 ///
276 /// # Examples
277 ///
278 /// ```
279 /// use big_code_analysis::{Ast, LANG, Source};
280 ///
281 /// let (matching, total) = Ast::parse(Source::new(LANG::Rust, b"fn f() {}"))
282 /// .expect("rust feature enabled")
283 /// .count(&["function_item".to_owned()]);
284 /// assert_eq!(matching, 1);
285 /// assert!(total > matching);
286 /// ```
287 #[must_use]
288 pub fn count(&self, filters: &[String]) -> (usize, usize) {
289 self.inner.run_count(filters)
290 }
291
292 /// Find every node in the held parse whose kind is named in
293 /// `filters`. The returned [`Node`]s borrow the held tree, so they
294 /// must be resolved against [`Ast::source`]. Safe to call
295 /// repeatedly; the tree is reused.
296 ///
297 /// # Errors
298 ///
299 /// Currently infallible; the [`Result`] wrapper is reserved for a
300 /// future strict-parsing mode (matching the other `Ast` walkers).
301 pub fn find(&self, filters: &[String]) -> Result<Vec<Node<'_>>, MetricsError> {
302 self.inner.run_find(filters)
303 }
304
305 /// Collect every in-source suppression marker (`// bca: suppress …`)
306 /// in the held parse, sorted by line. Safe to call repeatedly; the
307 /// tree is reused.
308 #[must_use]
309 pub fn suppressions(&self) -> Vec<crate::SuppressionMarker> {
310 self.inner.run_suppressions()
311 }
312
313 /// Borrow the root [`Node`] of the held parse for callers that drive
314 /// their own traversal (e.g. rendering an AST dump). Nodes obtained
315 /// from it must be resolved against [`Ast::source`].
316 #[must_use]
317 #[inline]
318 pub fn root_node(&self) -> Node<'_> {
319 self.inner.root_node()
320 }
321}