big_code_analysis_ast/macros/mod.rs
1//! The `mk_langs!` family that generates [`langs`](crate::langs), the
2//! [`with_any_parser!`](crate::with_any_parser) dispatch macro, and the
3//! kind-set aliases.
4
5// `get_language!` is invoked only from feature-gated arms in `mk_lang!`
6// (one arm per `LANG::*` variant whose per-language Cargo feature is
7// enabled). A build with `--no-default-features` and no language
8// feature has no remaining call sites; suppress the lint for that
9// pathological-but-valid configuration.
10#[allow(unused_macros)]
11macro_rules! get_language {
12 (tree_sitter_typescript) => {
13 tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()
14 };
15 (tree_sitter_tsx) => {
16 tree_sitter_typescript::LANGUAGE_TSX.into()
17 };
18 (tree_sitter_php) => {
19 tree_sitter_php::LANGUAGE_PHP.into()
20 };
21 ($name:ident) => {
22 $name::LANGUAGE.into()
23 };
24}
25
26macro_rules! mk_lang {
27 ( $( ($feature:literal, $camel:ident, $name:ident, $display: expr, $description:expr, $version:literal) ),* ) => {
28 /// The list of supported languages.
29 ///
30 /// Every variant is always defined regardless of the Cargo
31 /// feature set: per-language features only gate the grammar
32 /// crate references, never the enum surface itself. Disabled
33 /// variants surface at runtime as
34 /// [`crate::MetricsError::LanguageDisabled`] from every entry
35 /// point that returns a `Result`.
36 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
37 pub enum LANG {
38 $(
39 #[doc = $description]
40 $camel,
41 )*
42 }
43 impl LANG {
44 /// Return an iterator over the supported languages.
45 ///
46 /// # Examples
47 ///
48 /// ```
49 /// use big_code_analysis_ast::LANG;
50 ///
51 /// for lang in LANG::into_enum_iter() {
52 /// println!("{:?}", lang);
53 /// }
54 /// ```
55 pub fn into_enum_iter() -> impl Iterator<Item=LANG> {
56 use LANG::*;
57 [$( $camel, )*].into_iter()
58 }
59
60 /// Returns the name of a language as a `&str`.
61 ///
62 /// # Examples
63 ///
64 /// ```
65 /// use big_code_analysis_ast::LANG;
66 ///
67 /// println!("{}", LANG::Rust.name());
68 /// ```
69 pub fn name(&self) -> &'static str {
70 match self {
71 $(
72 LANG::$camel => $display,
73 )*
74 }
75 }
76
77 /// Returns the pinned tree-sitter grammar crate version that
78 /// backs this variant (e.g. `"0.25.1"` for [`LANG::Bash`]).
79 ///
80 /// The value mirrors the `=X.Y.Z` pin in the workspace
81 /// `Cargo.toml` and is independent of the per-language Cargo
82 /// feature: it is returned even for a variant whose feature is
83 /// disabled in the current build (a build-time constant, no
84 /// grammar crate reference). A drift test in `src/langs.rs`
85 /// asserts every value here matches the manifest pin.
86 ///
87 /// # Grammars vs. forks
88 ///
89 /// For languages backed by an upstream crates.io grammar
90 /// (`bash`, `rust`, `python`, `typescript`, …) this is the
91 /// exact upstream grammar version, so a consumer migrating
92 /// matchers off py-tree-sitter can line node-kind vocabularies
93 /// up against the same pin. For the vendored big-code-analysis
94 /// forks (`mozcpp`, `mozjs`, `tcl`, `ccomment`, `preproc`,
95 /// `kotlin`) the value is the **fork crate's** version
96 /// (published as `bca-tree-sitter-*` / `tree-sitter-kotlin-ng`),
97 /// not an upstream tree-sitter grammar semver — there is no
98 /// upstream release to compare against.
99 ///
100 /// This is part of the value-not-stable surface: the returned
101 /// version changes whenever the grammar pin is bumped.
102 #[must_use]
103 pub fn grammar_version(&self) -> &'static str {
104 match self {
105 $(
106 LANG::$camel => $version,
107 )*
108 }
109 }
110
111 /// Reports whether this variant's grammar crate is
112 /// compiled into the current build.
113 ///
114 /// Returns `false` for variants whose per-language Cargo
115 /// feature is disabled; calling
116 /// [`Self::tree_sitter_language`], `big_code_analysis::analyze`,
117 /// or any other dispatcher with such a variant will
118 /// return [`crate::MetricsError::LanguageDisabled`].
119 #[must_use]
120 pub fn is_enabled(&self) -> bool {
121 match self {
122 $(
123 #[cfg(feature = $feature)]
124 LANG::$camel => true,
125 #[cfg(not(feature = $feature))]
126 LANG::$camel => false,
127 )*
128 }
129 }
130
131 // Returns a tree-sitter language paired with this variant,
132 // or `Err(LanguageDisabled)` when the matching Cargo
133 // feature is off. This is the internal entry point used
134 // by `Tree::new` to construct a parser; the public
135 // counterpart is `tree_sitter_language`.
136 pub(crate) fn get_ts_language(&self) -> Result<Language, crate::MetricsError> {
137 match self {
138 $(
139 #[cfg(feature = $feature)]
140 LANG::$camel => Ok(get_language!($name)),
141 #[cfg(not(feature = $feature))]
142 LANG::$camel => Err(crate::MetricsError::LanguageDisabled(*self)),
143 )*
144 }
145 }
146
147 /// Returns the [`tree_sitter::Language`] grammar used by
148 /// this variant.
149 ///
150 /// Useful when feeding a caller-built
151 /// [`tree_sitter::Parser`] into the
152 /// `big_code_analysis::Ast::from_tree_sitter` entry point — the
153 /// language returned here is the one the metric walker
154 /// expects for `kind_id` matching, so the trees agree
155 /// structurally.
156 ///
157 /// This method is part of the value-not-stable surface:
158 /// the underlying `tree-sitter-*` grammar pin may bump
159 /// in any minor release, which can change `Language`
160 /// equality on the caller side.
161 ///
162 /// # Errors
163 ///
164 /// Returns [`crate::MetricsError::LanguageDisabled`] when
165 /// the variant's per-language Cargo feature is not
166 /// enabled in the current build (see the `[features]`
167 /// table in the root `Cargo.toml`).
168 ///
169 /// # Examples
170 ///
171 /// ```
172 /// use big_code_analysis_ast::LANG;
173 ///
174 /// let _lang = LANG::Rust.tree_sitter_language().expect("rust feature enabled");
175 /// ```
176 pub fn tree_sitter_language(&self) -> Result<::tree_sitter::Language, crate::MetricsError> {
177 self.get_ts_language()
178 }
179 }
180
181 /// Renders the language's canonical lowercase slug, identical to
182 /// [`LANG::name`].
183 ///
184 /// Every variant has a distinct slug, so `Display` is injective
185 /// and a `Display` → [`FromStr`](std::str::FromStr) round-trip
186 /// returns the original variant (see the round-trip test in
187 /// `src/langs.rs`). The slug is the single canonical identifier
188 /// used across every surface (CLI JSON, web `/metrics`, the
189 /// Python bindings): it contains no punctuation and is always a
190 /// valid `FromStr` lookup token.
191 impl ::std::fmt::Display for LANG {
192 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
193 f.write_str(self.name())
194 }
195 }
196
197 /// Parses a [`LANG`] from its [`Display`](std::fmt::Display)
198 /// spelling (the canonical lowercase [`LANG::name`] slug, e.g.
199 /// `"rust"`, `"cpp"`, `"csharp"`, `"tsx"`).
200 ///
201 /// Matching is case-sensitive and exact, mirroring
202 /// `big_code_analysis::Metric`'s `FromStr`: only the canonical
203 /// lowercase slug is accepted. File extensions and emacs modes
204 /// are deliberately *not* accepted here — use
205 /// [`get_from_ext`](crate::get_from_ext) /
206 /// [`get_from_emacs_mode`](crate::get_from_emacs_mode) for those.
207 ///
208 /// Every variant has a distinct slug, so this is the exact
209 /// inverse of [`Display`](std::fmt::Display): the round-trip
210 /// `LANG::from_str(&lang.to_string())` returns the original
211 /// variant for every `LANG`.
212 impl ::std::str::FromStr for LANG {
213 type Err = $crate::macros::ParseLangError;
214
215 fn from_str(s: &str) -> Result<Self, Self::Err> {
216 LANG::into_enum_iter()
217 .find(|lang| lang.name() == s)
218 .ok_or_else(|| $crate::macros::ParseLangError::new(s))
219 }
220 }
221 };
222}
223
224/// Error returned by [`LANG`](crate::LANG)'s
225/// [`FromStr`](std::str::FromStr) impl when the input is not a
226/// recognised language name.
227///
228/// Holds the offending input verbatim so wrapper layers can format
229/// their own user-facing message; mirrors
230/// `big_code_analysis::ParseMetricError`.
231#[derive(Debug, Clone, PartialEq, Eq)]
232pub struct ParseLangError(String);
233
234impl ParseLangError {
235 // Constructor kept `pub(crate)` so the macro-generated `FromStr`
236 // impl in `crate::langs` can build the error without exposing the
237 // private field across module boundaries.
238 pub(crate) fn new(input: &str) -> Self {
239 Self(input.to_owned())
240 }
241
242 /// The rejected input that failed to parse as a language name.
243 ///
244 /// Lets callers recover the offending string programmatically
245 /// rather than scraping it out of the [`Display`](std::fmt::Display)
246 /// output. Mirrors
247 /// `big_code_analysis::ParseMetricError::input`.
248 #[must_use]
249 pub fn input(&self) -> &str {
250 &self.0
251 }
252}
253
254impl ::std::fmt::Display for ParseLangError {
255 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
256 write!(f, "unknown language: {}", self.0)
257 }
258}
259
260impl ::std::error::Error for ParseLangError {}
261
262macro_rules! mk_action {
263 ( $( ($feature:literal, $camel:ident, $parser:ident) ),* ) => {
264 /// A parsed tree plus its source bytes for a language chosen at
265 /// runtime — one variant per [`LANG`], each holding that
266 /// language's `Parser<XCode>`. The public seam is
267 /// `big_code_analysis::Ast`; this enum is the language-dispatched carrier
268 /// it wraps, and the value a caller matches on to reach a
269 /// concrete parser (`with_any_parser!`).
270 ///
271 /// Every variant exists regardless of the Cargo feature set: a
272 /// `Parser<XCode>` *type* needs no grammar crate, only
273 /// [`Self::parse`] / [`Self::from_tree`] do, and those are the
274 /// arms that are feature-gated. A disabled language is therefore
275 /// never *constructed* — the constructors return
276 /// `Err(LanguageDisabled)` for it — but it can always be *named*,
277 /// which is what lets `with_any_parser!` be written once without
278 /// any `cfg` of its own (#1376).
279 ///
280 /// The cost is real and was priced: because the match is total
281 /// over all 25 variants, every generic walk is monomorphised for
282 /// every language whatever features were selected. Measured on a
283 /// `--no-default-features --features rust` build, `metrics_inner`
284 /// and `ops_inner` each get 25 instantiations where the
285 /// pre-split crate got one.
286 ///
287 /// **DECIDED (#1376): it stays.** Reversing it means moving the
288 /// dispatch macro into `big-code-analysis`, so the `cfg`s are
289 /// evaluated in the crate whose features they name — a
290 /// `#[cfg]` inside a `#[macro_export]` macro is evaluated in the
291 /// *invoking* crate, which is the whole reason this shape exists
292 /// — and that buys an eighth site enumerating every language,
293 /// permanently. A visitor trait does not help: one defined here
294 /// can only bound its type parameter by traits this crate can
295 /// name, so it hands back a `ParserTrait` where the metric walks
296 /// need the root's `MetricSuite`, and Rust cannot let a caller
297 /// supply that bound. Nothing in this workspace pays the cost —
298 /// the CLI, the server and the Python bindings all build
299 /// `all-languages` — and what it costs is compile time and rlib
300 /// size, not shipped binary size, since unreferenced
301 /// instantiations are dropped at link time. Revisit only if a
302 /// consumer of the narrow feature set actually appears.
303 pub enum AnyParser {
304 $(
305 #[doc = concat!("The `", stringify!($camel), "` parser.")]
306 $camel($parser),
307 )*
308 }
309
310 impl AnyParser {
311 /// Parse `source` as `lang`.
312 ///
313 /// `Parser::new` keys the C-family macro-expansion lookup off
314 /// the caller-supplied path; callers analysing in-memory
315 /// snippets pass `None` and get the empty `Path` (`""`),
316 /// which the lookup ignores. That path never leaks into a
317 /// display name — `Ast` carries the name separately.
318 /// `source` is taken by value so an owned buffer moves
319 /// straight into the parser instead of being copied.
320 ///
321 /// # Errors
322 ///
323 /// `MetricsError::LanguageDisabled` when `lang`'s Cargo
324 /// feature is not enabled in this build.
325 pub fn parse(
326 lang: LANG,
327 source: Vec<u8>,
328 preproc_path: Option<&Path>,
329 preproc: Option<Arc<PreprocResults>>,
330 ) -> Result<Self, MetricsError> {
331 let preproc_path = preproc_path.unwrap_or(Path::new(""));
332 match lang {
333 $(
334 #[cfg(feature = $feature)]
335 LANG::$camel => Ok(AnyParser::$camel($parser::new(source, preproc_path, preproc))),
336 #[cfg(not(feature = $feature))]
337 LANG::$camel => {
338 let _ = (source, preproc_path, preproc);
339 Err(MetricsError::LanguageDisabled(lang))
340 },
341 )*
342 }
343 }
344
345 /// Adopt a caller-built [`tree_sitter::Tree`] produced from
346 /// `source` with `lang`'s grammar.
347 ///
348 /// # Errors
349 ///
350 /// `MetricsError::LanguageDisabled` when `lang`'s Cargo
351 /// feature is not enabled in this build.
352 pub fn from_tree(
353 lang: LANG,
354 tree: ::tree_sitter::Tree,
355 source: Vec<u8>,
356 ) -> Result<Self, MetricsError> {
357 match lang {
358 $(
359 #[cfg(feature = $feature)]
360 LANG::$camel => Ok(AnyParser::$camel($parser::from_tree(tree, source))),
361 #[cfg(not(feature = $feature))]
362 LANG::$camel => {
363 let _ = (tree, source);
364 Err(MetricsError::LanguageDisabled(lang))
365 },
366 )*
367 }
368 }
369
370 /// The language this parser was built for.
371 #[must_use]
372 pub fn language(&self) -> LANG {
373 match self {
374 $( AnyParser::$camel(_) => LANG::$camel, )*
375 }
376 }
377
378 /// The bytes the tree was parsed from — after `#define`
379 /// expansion for the C family, so not necessarily the bytes
380 /// handed to [`Self::parse`].
381 #[must_use]
382 pub fn code(&self) -> &[u8] {
383 $crate::with_any_parser!(self, |p| p.code())
384 }
385
386 /// The held [`tree_sitter::Tree`].
387 #[must_use]
388 pub fn ts_tree(&self) -> &::tree_sitter::Tree {
389 $crate::with_any_parser!(self, |p| p.ts_tree())
390 }
391
392 /// The root [`Node`] of the held tree.
393 #[must_use]
394 pub fn root_node(&self) -> Node<'_> {
395 $crate::with_any_parser!(self, |p| p.root())
396 }
397
398 /// The source with non-doc comments removed, or `None` when
399 /// there was nothing to strip.
400 #[must_use]
401 pub fn strip_comments(&self) -> Option<Vec<u8>> {
402 $crate::with_any_parser!(self, |p| $crate::comment_rm::rm_comments(p))
403 }
404
405 /// The AST dump under `cfg`.
406 #[must_use]
407 pub fn dump(&self, cfg: $crate::ast::AstCfg) -> $crate::ast::AstResponse {
408 $crate::with_any_parser!(self, |p| $crate::ast::dump_inner(p, cfg))
409 }
410
411 /// `(matching, total)` node counts for `filters` — the same
412 /// vocabulary [`ParserTrait::filters`] accepts.
413 #[must_use]
414 pub fn count(&self, filters: &[String]) -> (usize, usize) {
415 $crate::with_any_parser!(self, |p| $crate::count::count(p, filters))
416 }
417
418 /// Every node matching `filters`, in source order. The nodes
419 /// borrow the held tree.
420 ///
421 /// # Errors
422 ///
423 /// Currently infallible; the `Result` is reserved for a
424 /// future strict-parsing mode.
425 pub fn find(&self, filters: &[String]) -> Result<Vec<Node<'_>>, MetricsError> {
426 $crate::with_any_parser!(self, |p| $crate::find::find(p, filters))
427 }
428 }
429 };
430}
431
432/// Dispatches over every [`AnyParser`] variant, binding the concrete
433/// `Parser<XCode>` to `$p` and evaluating `$body` once per arm.
434///
435/// Written out by hand rather than generated inside `mk_action!` so the
436/// arm list stays a plain match: a variant missing here is a
437/// non-exhaustive-match compile error, which is the whole guarantee.
438/// Every arm is unconditional — see the [`AnyParser`] docs for why no
439/// `cfg` is needed. Add a line here when `mk_langs!` gains a language.
440///
441/// [`AnyParser`]: crate::langs::AnyParser
442#[macro_export]
443macro_rules! with_any_parser {
444 ($any:expr, |$p:ident| $body:expr) => {
445 match $any {
446 $crate::langs::AnyParser::Javascript($p) => $body,
447 $crate::langs::AnyParser::Mozjs($p) => $body,
448 $crate::langs::AnyParser::Java($p) => $body,
449 $crate::langs::AnyParser::Go($p) => $body,
450 $crate::langs::AnyParser::Kotlin($p) => $body,
451 $crate::langs::AnyParser::Lua($p) => $body,
452 $crate::langs::AnyParser::Rust($p) => $body,
453 $crate::langs::AnyParser::Tcl($p) => $body,
454 $crate::langs::AnyParser::Irules($p) => $body,
455 $crate::langs::AnyParser::C($p) => $body,
456 $crate::langs::AnyParser::Cpp($p) => $body,
457 $crate::langs::AnyParser::Mozcpp($p) => $body,
458 $crate::langs::AnyParser::Objc($p) => $body,
459 $crate::langs::AnyParser::Csharp($p) => $body,
460 $crate::langs::AnyParser::Elixir($p) => $body,
461 $crate::langs::AnyParser::Python($p) => $body,
462 $crate::langs::AnyParser::Tsx($p) => $body,
463 $crate::langs::AnyParser::Typescript($p) => $body,
464 $crate::langs::AnyParser::Bash($p) => $body,
465 $crate::langs::AnyParser::Ccomment($p) => $body,
466 $crate::langs::AnyParser::Preproc($p) => $body,
467 $crate::langs::AnyParser::Perl($p) => $body,
468 $crate::langs::AnyParser::Php($p) => $body,
469 $crate::langs::AnyParser::Ruby($p) => $body,
470 $crate::langs::AnyParser::Groovy($p) => $body,
471 }
472 };
473}
474
475macro_rules! mk_extensions {
476 ( $( ($camel:ident, [ $( $ext:ident ),* ]) ),* ) => {
477 /// Detects the language associated to the input file extension.
478 ///
479 /// # Examples
480 ///
481 /// ```
482 /// use big_code_analysis_ast::get_from_ext;
483 ///
484 /// let ext = "rs";
485 ///
486 /// get_from_ext(ext).unwrap();
487 /// ```
488 pub fn get_from_ext(ext: &str) -> Option<LANG>{
489 match ext {
490 $(
491 $(
492 stringify!($ext) => Some(LANG::$camel),
493 )*
494 )*
495 _ => None,
496 }
497 }
498
499 impl LANG {
500 /// Returns the file extensions recognised for this language.
501 ///
502 /// The returned list is the same one consulted by
503 /// [`get_from_ext`] and [`crate::get_language_for_file`].
504 /// Helper variants without user-facing files (`Ccomment`,
505 /// `Preproc`) return an empty slice.
506 ///
507 /// # Examples
508 ///
509 /// ```
510 /// use big_code_analysis_ast::LANG;
511 ///
512 /// assert!(LANG::Rust.extensions().contains(&"rs"));
513 /// ```
514 #[must_use]
515 pub fn extensions(&self) -> &'static [&'static str] {
516 match self {
517 $(
518 LANG::$camel => &[ $( stringify!($ext), )* ],
519 )*
520 }
521 }
522 }
523 };
524}
525
526macro_rules! mk_emacs_mode {
527 ( $( ($camel:ident, [ $( $emacs_mode:expr ),* ]) ),* ) => {
528 /// Detects the language associated to the input `Emacs` mode.
529 ///
530 /// An `Emacs` mode is used to detect a language according to
531 /// particular text-information contained in a file.
532 ///
533 /// # Examples
534 ///
535 /// ```
536 /// use big_code_analysis_ast::get_from_emacs_mode;
537 ///
538 /// let emacs_mode = "rust";
539 ///
540 /// get_from_emacs_mode(emacs_mode).unwrap();
541 /// ```
542 pub fn get_from_emacs_mode(mode: &str) -> Option<LANG>{
543 match mode {
544 $(
545 $(
546 $emacs_mode => Some(LANG::$camel),
547 )*
548 )*
549 _ => None,
550 }
551 }
552 };
553}
554
555macro_rules! mk_code {
556 ( $( ($camel:ident, $code:ident, $parser:ident, $name:ident, $docname:expr) ),* ) => {
557 $(
558 #[doc = concat!("Per-language code type tag for ", $docname, "; carries no data.")]
559 pub struct $code { _guard: (), }
560
561 impl LanguageInfo for $code {
562 type BaseLang = $camel;
563
564 fn lang() -> LANG {
565 LANG::$camel
566 }
567 }
568
569 #[doc = "The `"]
570 #[doc = $docname]
571 #[doc = "` language parser."]
572 pub type $parser = Parser<$code>;
573 )*
574 };
575}
576
577macro_rules! mk_langs {
578 ( $( ($feature:literal, $camel:ident, $description: expr, $display: expr, $code:ident, $parser:ident, $name:ident, [ $( $ext:ident ),* ], [ $( $emacs_mode:expr ),* ], $version:literal) ),* ) => {
579 mk_lang!($( ($feature, $camel, $name, $display, $description, $version) ),*);
580 mk_action!($( ($feature, $camel, $parser) ),*);
581 mk_extensions!($( ($camel, [ $( $ext ),* ]) ),*);
582 mk_emacs_mode!($( ($camel, [ $( $emacs_mode ),* ]) ),*);
583 mk_code!($( ($camel, $code, $parser, $name, stringify!($camel)) ),*);
584 };
585}
586
587mod kind_sets;
588
589// The kind-set macros are `#[macro_export]`ed (they live at the crate
590// root for `big-code-analysis`'s metric modules); re-exported here so
591// the `crate::macros::<name>` spelling the classifiers use keeps working.
592pub use crate::{
593 csharp_invocation_expr_kinds, csharp_paren_expr_kinds, csharp_prefix_unary_expr_kinds,
594 csharp_var_decl_kinds, csharp_var_declarator_kinds, go_bool_terminal_kinds, with_any_parser,
595};
596pub(crate) use {
597 get_language, mk_action, mk_code, mk_emacs_mode, mk_extensions, mk_lang, mk_langs,
598};