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}