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