Skip to main content

big_code_analysis_ast/checker/
ruby.rs

1//! `Checker` implementation for Ruby.
2#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
3
4use super::*;
5
6/// Whether `node` — a `Block` / `DoBlock` — is the body of a stabby
7/// lambda (`->(z) { … }` / `->(z) do … end`), which parses as a `Lambda`
8/// node that CONTAINS the block. The `Lambda` wrapper already counts as
9/// the closure (#465) and opens the function space (#1257), so
10/// classifying the body again would double-count one lambda as two
11/// closures, or open a phantom nested Function space. The keyword forms
12/// `lambda { }` / `proc { }` and iterator blocks (`[1].each { |x| x }`)
13/// parse as a `Call` carrying the block argument — the parent is not a
14/// `Lambda` — so they still classify exactly once.
15///
16/// One helper shared by [`Checker::is_closure`],
17/// [`Checker::is_func_space_with_code`], and the cognitive lambda-nesting
18/// arm (`crate::metrics::cognitive`'s Ruby impl), so the closure count,
19/// the space tree, and the nesting surcharge cannot drift apart
20/// (`.claude/rules/grammar-dispatch.md` §6).
21///
22/// The parent comes off the caller's chain: all consumers run per node
23/// from a walk, and `Node::parent` costs `O(depth)` because
24/// `tree_sitter` resolves it by descending from the root (#1088).
25#[must_use]
26pub fn is_stabby_lambda_body<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
27    ancestors
28        .parent(node)
29        .is_some_and(|parent| parent.kind_id() == Ruby::Lambda)
30}
31
32impl Checker for RubyCode {
33    fn is_comment(node: &Node) -> bool {
34        node.kind_id() == Ruby::Comment
35    }
36
37    // Over-approximates for `Block` / `DoBlock`: a stabby lambda's own
38    // body block is NOT a space of its own (the wrapping `Lambda` is),
39    // but telling the two apart needs the parent and this spelling has
40    // no ancestor access. The walker promotes through the
41    // [`Checker::is_func_space_with_code`] override below, which
42    // carries the lambda-body gate (#1257).
43    fn is_func_space(node: &Node) -> bool {
44        matches!(
45            node.kind_id().into(),
46            Ruby::Program
47                | Ruby::Method
48                | Ruby::SingletonMethod
49                | Ruby::Lambda
50                | Ruby::Block
51                | Ruby::DoBlock
52                | Ruby::Class
53                | Ruby::SingletonClass
54                | Ruby::Module
55        )
56    }
57
58    // Derives from `is_func_space` so the kind set lives in one place;
59    // this override only subtracts the stabby-lambda body blocks the
60    // byte-less spelling over-approximates on.
61    fn is_func_space_with_code<'a>(
62        node: &Node<'a>,
63        _code: &[u8],
64        ancestors: Ancestors<'a, '_>,
65    ) -> bool {
66        Self::is_func_space(node)
67            && !(matches!(node.kind_id().into(), Ruby::Block | Ruby::DoBlock)
68                && is_stabby_lambda_body(node, ancestors))
69    }
70
71    fn is_func<'a>(node: &Node<'a>, _ancestors: Ancestors<'a, '_>) -> bool {
72        matches!(node.kind_id().into(), Ruby::Method | Ruby::SingletonMethod)
73    }
74
75    // Ruby is the one non-JS grammar whose closure test is not
76    // answerable from the node's own kind: a `Block` / `DoBlock` counts
77    // only when it is not a stabby lambda's own body (#465, see
78    // `is_stabby_lambda_body`).
79    fn is_closure<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
80        match node.kind_id().into() {
81            Ruby::Lambda => true,
82            Ruby::Block | Ruby::DoBlock => !is_stabby_lambda_body(node, ancestors),
83            _ => false,
84        }
85    }
86
87    // tree-sitter-ruby 0.23.1 emits four aliased visible variants of the
88    // `call` rule (`Call`, `Call2`, `Call3`, `Call4`); `Call5` ("_call")
89    // is the hidden inner production and does not surface.
90    fn is_call(node: &Node) -> bool {
91        matches!(
92            node.kind_id().into(),
93            Ruby::Call | Ruby::Call2 | Ruby::Call3 | Ruby::Call4
94        )
95    }
96
97    fn is_non_arg(node: &Node) -> bool {
98        // `PIPE` is included because block parameter lists are delimited
99        // by `|` rather than parentheses (e.g. `[1,2,3].each { |x| … }`).
100        matches!(
101            node.kind_id().into(),
102            Ruby::LPAREN
103                | Ruby::LPAREN2
104                | Ruby::RPAREN
105                | Ruby::RPAREN2
106                | Ruby::COMMA
107                | Ruby::SEMI
108                | Ruby::PIPE
109        )
110    }
111
112    impl_simple_is_string!(
113        Ruby,
114        String,
115        ChainedString,
116        BareString,
117        Subshell,
118        Regex,
119        HeredocBody,
120        DelimitedSymbol,
121        SimpleSymbol,
122        StringArray,
123        SymbolArray,
124        Character,
125    );
126
127    // tree-sitter-ruby exposes `elsif` as its own named clause node, so the
128    // dedicated-clause-node strategy applies here (same as Lua/Bash/PHP).
129    impl_is_else_if_clause!(Ruby, Elsif);
130}