Skip to main content

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    cpp_bool_terminal_kinds, csharp_bool_terminal_kinds, csharp_invocation_expr_kinds,
594    csharp_paren_expr_kinds, csharp_prefix_unary_expr_kinds, csharp_var_decl_kinds,
595    csharp_var_declarator_kinds, elixir_bool_terminal_kinds, go_bool_terminal_kinds,
596    groovy_bool_terminal_kinds, irules_bool_terminal_kinds, java_bool_terminal_kinds,
597    javascript_bool_terminal_kinds, kotlin_bool_terminal_kinds, lua_bool_terminal_kinds,
598    mozjs_bool_terminal_kinds, perl_bool_terminal_kinds, php_bool_terminal_kinds,
599    python_bool_terminal_kinds, ruby_bool_terminal_kinds, rust_bool_terminal_kinds,
600    tcl_bool_terminal_kinds, tsx_bool_terminal_kinds, typescript_bool_terminal_kinds,
601    with_any_parser,
602};
603pub(crate) use {
604    get_language, mk_action, mk_code, mk_emacs_mode, mk_extensions, mk_lang, mk_langs,
605};