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