Skip to main content

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}