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