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
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}