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