Module: Ryac::AstUtils

Defined in:
lib/ryac/ast_utils.rb,
sig/ryac/ast_utils.rbs

Constant Summary collapse

MIDDLE_METHODS =

Returns:

  • (Array[Symbol])
i[+ - * / ** % ^ > < <= >= <=> == === != & | << >> =~ !~].freeze
TERNARY_SAFE_PREV =

Bytes before which a ternary does NOT need wrapping parens

Returns:

  • (Array[Integer])
[';'.ord, "\n".ord, "\r".ord, '('.ord, '['.ord, '{'.ord].freeze
COMPARISON_PREV =

Returns:

  • (Array[Integer])
['<'.ord, '>'.ord, '!'.ord, '='.ord].freeze
PIPE_BREAK_BYTES =

Returns:

  • (Array[Integer])
[';'.ord, "\n".ord, '{'.ord].freeze
MATCH_REBIND_NODES =

Expressions that rebind to the surrounding statement when their parens are stripped: def f =expr in pat parses as (def f =expr) in pat. Every "needs parens" list must include these.

Returns:

  • (Array[untyped])
[Prism::MatchPredicateNode, Prism::MatchRequiredNode].freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.can_omit_parens?(node, method_name = nil) ⇒ Boolean

Returns:

  • (Boolean)


230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/ryac/ast_utils.rb', line 230

def can_omit_parens?(node, method_name = nil)
  return false if has_block?(node)
  # `.()` proc-call sugar has no message; its parens ARE the call.
  return false if node.message_loc.nil?
  raw_args = node.arguments&.arguments || []
  positional_args = raw_args.reject { |a| a.is_a?(Prism::KeywordHashNode) }
  return false if positional_args.empty?
  return false if raw_args.any? { |a| a.is_a?(Prism::ForwardingArgumentsNode) }
  return false if node.name == :[] || node.name == :[]=
  name_to_check = method_name || node.name.to_s
  return false if name_to_check.end_with?('?')
  return false if first_arg_starts_with_brace?(positional_args.first)
  return false if first_arg_is_regex?(positional_args.first)
  return false if raw_args.any? { |a| contains_bare_block?(a) }

  true
end

.contains_bare_block?(node) ⇒ Boolean

Returns:

  • (Boolean)


252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/ryac/ast_utils.rb', line 252

def contains_bare_block?(node)
  case node
  when Prism::CallNode
    return true if node.block.is_a?(Prism::BlockNode)
    return true if node.receiver && contains_bare_block?(node.receiver)
    return false if node.opening_loc
    node.arguments&.arguments&.any? { |a| contains_bare_block?(a) } || false
  when Prism::ParenthesesNode, Prism::ArrayNode, Prism::HashNode, Prism::LambdaNode
    false
  when Prism::KeywordHashNode
    node.elements.any? { |e| e.is_a?(Prism::AssocNode) && contains_bare_block?(e.value) }
  when Prism::SplatNode
    contains_bare_block?(node.expression)
  else
    false
  end
end

.each_node(node) {|node| ... } ⇒ Object

Depth-first over every node. The shared primitive for analyses that need the whole tree without caring about scope or nesting context.

Yields:

  • (node)


272
273
274
275
276
# File 'lib/ryac/ast_utils.rb', line 272

def self.each_node(node, &block)
  return unless node.is_a?(Prism::Node)
  yield node
  node.compact_child_nodes.each { |child| each_node(child, &block) }
end

.ends_with_method_suffix?(node) ⇒ Boolean

Does this node's output end with ? or ! method suffix? Recursively follows rightmost sub-expressions.

Returns:

  • (Boolean)


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
# File 'lib/ryac/ast_utils.rb', line 137

def ends_with_method_suffix?(node)
  inner = unwrap_statements(node)
  case inner
  when Prism::OrNode, Prism::AndNode
    ends_with_method_suffix?(inner.right)
  when Prism::CallNode
    return false if inner.block
    return false if inner.opening_loc && inner.name != :[]=
    if middle_method?(inner.name)
      arg = inner.arguments&.arguments&.first
      arg ? ends_with_method_suffix?(arg) : false
    elsif inner.name == :[]=
      last_arg = inner.arguments&.arguments&.last
      last_arg ? ends_with_method_suffix?(last_arg) : false
    elsif inner.receiver && !inner.arguments && (inner.name == :! || inner.name == :~)
      # Unary prefix operators (!x, ~x) — output ends with operand, not suffix
      ends_with_method_suffix?(inner.receiver)
    else
      inner.name.to_s.end_with?('?', '!')
    end
  when Prism::SymbolNode
    !inner.closing_loc && inner.value.to_s.end_with?('?', '!')
  when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode,
       Prism::ClassVariableWriteNode, Prism::GlobalVariableWriteNode,
       Prism::ConstantWriteNode, Prism::ConstantPathWriteNode,
       Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode,
       Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
       Prism::ClassVariableOrWriteNode, Prism::ClassVariableAndWriteNode,
       Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableAndWriteNode,
       Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode
    ends_with_method_suffix?(inner.value)
  else
    false
  end
end

.ends_with_name_char?(node) ⇒ Boolean

Does this node's string representation end with a character that could form name! or name? if ! or ? is appended? Used for spacing before !~ operator and ternary ?/:. Recursively follows rightmost sub-expressions for compound nodes.

Returns:

  • (Boolean)


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
# File 'lib/ryac/ast_utils.rb', line 69

def ends_with_name_char?(node)
  inner = unwrap_statements(node)
  case inner
  # Empty parens unwrap to nothing; the rendered `()` ends with ')'.
  when nil
    false
  # nil renders as `()` — ends with ')', not a name char.
  when Prism::NilNode
    false
  # Terminals: definitely safe (end with delimiter/sigil/literal)
  when Prism::ParenthesesNode,
       Prism::StringNode, Prism::InterpolatedStringNode,
       Prism::InterpolatedSymbolNode,
       Prism::RegularExpressionNode, Prism::InterpolatedRegularExpressionNode,
       Prism::ArrayNode, Prism::HashNode, Prism::LambdaNode,
       Prism::InstanceVariableReadNode, Prism::ClassVariableReadNode,
       Prism::GlobalVariableReadNode,
       Prism::IntegerNode, Prism::FloatNode, Prism::RationalNode, Prism::ImaginaryNode,
       Prism::DefinedNode, Prism::RescueModifierNode
    false
  # Bare symbol :name ends with name char; quoted :"name" ends with delimiter
  when Prism::SymbolNode
    !inner.closing_loc
  # Compound: follow rightmost sub-expression
  when Prism::OrNode, Prism::AndNode
    ends_with_name_char?(inner.right)
  when Prism::RangeNode
    inner.right ? ends_with_name_char?(inner.right) : false
  when Prism::CallNode
    return false if inner.block
    if middle_method?(inner.name)
      # Binary op: a+b → ends with right operand
      arg = inner.arguments&.arguments&.first
      arg ? ends_with_name_char?(arg) : true
    elsif inner.name == :[]=
      # Index assignment: recv[k]=val → ends with value
      last_arg = inner.arguments&.arguments&.last
      last_arg ? ends_with_name_char?(last_arg) : false
    elsif inner.receiver && !inner.arguments && (inner.name == :! || inner.name == :~)
      # Unary prefix operators: !x, ~x → output ends with operand
      ends_with_name_char?(inner.receiver)
    elsif inner.opening_loc
      false # foo(args) → ends with )
    elsif inner.name.to_s.end_with?('?', '!')
      false
    else
      true # bare method: foo.bar → ends with ident char
    end
  when Prism::YieldNode, Prism::SuperNode
    inner.arguments.nil?
  # Assignment: var=value → ends with value
  when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode,
       Prism::ClassVariableWriteNode, Prism::GlobalVariableWriteNode,
       Prism::ConstantWriteNode, Prism::ConstantPathWriteNode
    ends_with_name_char?(inner.value)
  when Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode,
       Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
       Prism::ClassVariableOrWriteNode, Prism::ClassVariableAndWriteNode,
       Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableAndWriteNode,
       Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode
    ends_with_name_char?(inner.value)
  else
    true # conservative: local var, const, nil, true, false, self, etc.
  end
end

.first_arg_is_regex?(arg) ⇒ Boolean

Returns:

  • (Boolean)


248
249
250
# File 'lib/ryac/ast_utils.rb', line 248

def first_arg_is_regex?(arg)
  arg.is_a?(Prism::RegularExpressionNode) || arg.is_a?(Prism::InterpolatedRegularExpressionNode)
end

.first_arg_starts_with_brace?(arg) ⇒ Boolean

Returns:

  • (Boolean)


57
58
59
60
61
62
63
# File 'lib/ryac/ast_utils.rb', line 57

def first_arg_starts_with_brace?(arg)
  inner = unwrap_statements(arg)
  return true if inner.is_a?(Prism::HashNode)
  return first_arg_starts_with_brace?(inner.receiver) if inner.is_a?(Prism::CallNode) && inner.receiver

  false
end

.has_block?(node) ⇒ Boolean

Returns:

  • (Boolean)


35
36
37
# File 'lib/ryac/ast_utils.rb', line 35

def has_block?(node)
  node.block != nil
end

.line_col_key(node) ⇒ Object

The line/column coordinate space (syntax_data and friends) — kept separate from byte-offset location_key on purpose.



316
317
318
319
# File 'lib/ryac/ast_utils.rb', line 316

def self.line_col_key(node)
  loc = node.location
  [loc.start_line, loc.start_column]
end

.location_key(node) ⇒ Object

The single place the analysis and the patchers agree on what "the same piece of source" is: every map between phases is keyed by this. Prism nodes only — TypeOracle converts its own tree's nodes before keying, so the seam between the two trees stays inside the oracle.

Byte offsets, because that is the coordinate system patches are applied in — deriving a separate line/column space for the join only gave the two sides a way to disagree.



309
310
311
312
# File 'lib/ryac/ast_utils.rb', line 309

def self.location_key(node)
  loc = node.location
  [loc.start_offset, loc.end_offset]
end

.logical_op?(node) ⇒ Boolean

Returns:

  • (Boolean)


31
32
33
# File 'lib/ryac/ast_utils.rb', line 31

def logical_op?(node)
  node.is_a?(Prism::OrNode) || node.is_a?(Prism::AndNode)
end

.match_rebind?(node) ⇒ Boolean

Returns:

  • (Boolean)


283
284
285
# File 'lib/ryac/ast_utils.rb', line 283

def self.match_rebind?(node)
  MATCH_REBIND_NODES.any? { |t| node.is_a?(t) }
end

.middle_method?(method) ⇒ Boolean

Returns:

  • (Boolean)


27
28
29
# File 'lib/ryac/ast_utils.rb', line 27

def middle_method?(method)
  MIDDLE_METHODS.include?(method)
end

.modifier_conditional?(node) ⇒ Boolean

Returns:

  • (Boolean)


183
184
185
186
187
188
189
# File 'lib/ryac/ast_utils.rb', line 183

def modifier_conditional?(node)
  case node
  when Prism::IfNode then node.if_keyword_loc && !node.end_keyword_loc
  when Prism::UnlessNode then node.keyword_loc && !node.end_keyword_loc
  else false
  end
end

.modifier_control_flow?(node) ⇒ Boolean

Returns:

  • (Boolean)


179
180
181
# File 'lib/ryac/ast_utils.rb', line 179

def modifier_control_flow?(node)
  modifier_conditional?(node) || modifier_loop?(node)
end

.modifier_loop?(node) ⇒ Boolean

Returns:

  • (Boolean)


191
192
193
194
195
196
# File 'lib/ryac/ast_utils.rb', line 191

def modifier_loop?(node)
  case node
  when Prism::WhileNode, Prism::UntilNode then !node.closing_loc
  else false
  end
end

.needs_ternary_q_space?(node) ⇒ Boolean

Needs space before ternary ?? True when condition ends with: name char, ?, !, or digit.

Returns:

  • (Boolean)


175
176
177
# File 'lib/ryac/ast_utils.rb', line 175

def needs_ternary_q_space?(node)
  ends_with_name_char?(node) || ends_with_method_suffix?(node)
end

.setter_def_name?(name) ⇒ Boolean

Returns:

  • (Boolean)


45
46
47
48
# File 'lib/ryac/ast_utils.rb', line 45

def setter_def_name?(name)
  s = name.to_s
  s.end_with?('=') && !%w[== != <= >= ===].include?(s)
end

.shorthand_pun_values(node) ⇒ Object

The shorthand-pun values of a hash-ish node: implicit assoc values that are NOT plain local reads ({ label: } calling method label). The one definition of "pun" shared by the keyword and method exclusion passes.



290
291
292
293
294
295
296
297
298
299
# File 'lib/ryac/ast_utils.rb', line 290

def self.shorthand_pun_values(node)
  return [] unless node.is_a?(Prism::KeywordHashNode) || node.is_a?(Prism::HashNode)

  node.elements.filter_map do |el|
    next unless el.is_a?(Prism::AssocNode) && el.value.is_a?(Prism::ImplicitNode)

    inner = el.value.value
    inner unless inner.is_a?(Prism::LocalVariableReadNode)
  end
end

.simple_negatable?(node) ⇒ Boolean

Returns:

  • (Boolean)


50
51
52
53
54
55
# File 'lib/ryac/ast_utils.rb', line 50

def simple_negatable?(node)
  inner = unwrap_statements(node)
  return false if logical_op?(inner)

  inner.is_a?(Prism::CallNode) ? !middle_method?(inner.name) : true
end

.single_statement_body?(body) ⇒ Boolean

Returns:

  • (Boolean)


39
40
41
42
43
# File 'lib/ryac/ast_utils.rb', line 39

def single_statement_body?(body)
  return false if body.nil?
  return body.body.size == 1 if body.is_a?(Prism::StatementsNode)
  true
end

.symbol_arguments(call_node) ⇒ Object

The plain-symbol arguments of a call, as symbols — how attr_*, private_constant and friends name their subjects.



9
10
11
12
13
# File 'lib/ryac/ast_utils.rb', line 9

def symbol_arguments(call_node)
  call_node.arguments&.arguments&.filter_map { |arg|
    arg.unescaped.to_sym if arg.is_a?(Prism::SymbolNode)
  } || []
end

.ternary_needs_parens?(node, source) ⇒ Boolean

Returns:

  • (Boolean)


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
# File 'lib/ryac/ast_utils.rb', line 203

def ternary_needs_parens?(node, source)
  start = node.location.start_offset
  return false if start == 0
  prev = source.getbyte(start - 1)
  return false if TERNARY_SAFE_PREV.include?(prev)
  if prev == '='.ord
    return false if start < 2
    prev2 = source.getbyte(start - 2)
    return COMPARISON_PREV.include?(prev2)
  end
  if prev == '|'.ord
    return true if start < 3
    return true if source.getbyte(start - 2) == '|'.ord
    pos = start - 2
    while pos >= 0
      byte = source.getbyte(pos)
      if byte == '|'.ord
        return !(pos > 0 && source.getbyte(pos - 1) == '{'.ord)
      end
      break if PIPE_BREAK_BYTES.include?(byte)
      pos -= 1
    end
    return true
  end
  true
end

.unwrap_statements(node) ⇒ Object



15
16
17
18
19
20
21
22
23
# File 'lib/ryac/ast_utils.rb', line 15

def unwrap_statements(node)
  return node unless node
  node = node.body if node.is_a?(Prism::ParenthesesNode)
  if node.is_a?(Prism::StatementsNode) && node.body.size == 1
    node.body.first
  else
    node
  end
end

Instance Method Details

#self?.can_omit_parens? ⇒ Boolean

Parameters:

  • node (Prism::CallNode)
  • method_name (String, nil)

Returns:

  • (Boolean)


24
# File 'sig/ryac/ast_utils.rbs', line 24

def self?.can_omit_parens?: (Prism::CallNode node, ?String? method_name) -> bool

#self?.contains_bare_block? ⇒ Boolean

Parameters:

  • node (Prism::Node, nil)

Returns:

  • (Boolean)


26
# File 'sig/ryac/ast_utils.rbs', line 26

def self?.contains_bare_block?: (Prism::Node? node) -> bool

#self?.each_node {|arg0| ... } ⇒ void

This method returns an undefined value.

Parameters:

  • node (Prism::Node, nil)

Yields:

Yield Parameters:

  • arg0 (Prism::Node)

Yield Returns:

  • (void)


30
# File 'sig/ryac/ast_utils.rbs', line 30

def self?.each_node: (Prism::Node? node) { (Prism::Node) -> void } -> void

#self?.ends_with_method_suffix? ⇒ Boolean

Parameters:

  • node (Prism::Node)

Returns:

  • (Boolean)


18
# File 'sig/ryac/ast_utils.rbs', line 18

def self?.ends_with_method_suffix?: (Prism::Node node) -> bool

#self?.ends_with_name_char? ⇒ Boolean

Parameters:

  • node (Prism::Node, nil)

Returns:

  • (Boolean)


17
# File 'sig/ryac/ast_utils.rbs', line 17

def self?.ends_with_name_char?: (Prism::Node? node) -> bool

#self?.first_arg_is_regex? ⇒ Boolean

Parameters:

  • arg (Prism::Node)

Returns:

  • (Boolean)


25
# File 'sig/ryac/ast_utils.rbs', line 25

def self?.first_arg_is_regex?: (Prism::Node arg) -> bool

#self?.first_arg_starts_with_brace? ⇒ Boolean

Parameters:

  • arg (Prism::Node)

Returns:

  • (Boolean)


16
# File 'sig/ryac/ast_utils.rbs', line 16

def self?.first_arg_starts_with_brace?: (Prism::Node arg) -> bool

#self?.has_block? ⇒ Boolean

Parameters:

  • node (Prism::CallNode)

Returns:

  • (Boolean)


12
# File 'sig/ryac/ast_utils.rbs', line 12

def self?.has_block?: (Prism::CallNode node) -> bool

#self?.line_col_key ⇒ line_col_key

Parameters:

  • node (Prism::Node)

Returns:



32
# File 'sig/ryac/ast_utils.rbs', line 32

def self?.line_col_key: (Prism::Node node) -> line_col_key

#self?.location_key ⇒ location_key

Parameters:

  • node (Prism::Node)

Returns:



31
# File 'sig/ryac/ast_utils.rbs', line 31

def self?.location_key: (Prism::Node node) -> location_key

#self?.logical_op? ⇒ Boolean

Parameters:

  • node (Prism::Node, nil)

Returns:

  • (Boolean)


11
# File 'sig/ryac/ast_utils.rbs', line 11

def self?.logical_op?: (Prism::Node? node) -> bool

#self?.match_rebind? ⇒ Boolean

Parameters:

  • node (Prism::Node)

Returns:

  • (Boolean)


28
# File 'sig/ryac/ast_utils.rbs', line 28

def self?.match_rebind?: (Prism::Node node) -> bool

#self?.middle_method? ⇒ Boolean

Parameters:

  • method (Symbol)

Returns:

  • (Boolean)


10
# File 'sig/ryac/ast_utils.rbs', line 10

def self?.middle_method?: (Symbol method) -> bool

#self?.modifier_conditional? ⇒ Boolean?

Parameters:

  • node (Prism::Node)

Returns:

  • (Boolean, nil)


21
# File 'sig/ryac/ast_utils.rbs', line 21

def self?.modifier_conditional?: (Prism::Node node) -> bool?

#self?.modifier_control_flow? ⇒ Boolean

Parameters:

  • node (Prism::Node)

Returns:

  • (Boolean)


20
# File 'sig/ryac/ast_utils.rbs', line 20

def self?.modifier_control_flow?: (Prism::Node node) -> bool

#self?.modifier_loop? ⇒ Boolean

Parameters:

  • node (Prism::Node)

Returns:

  • (Boolean)


22
# File 'sig/ryac/ast_utils.rbs', line 22

def self?.modifier_loop?: (Prism::Node node) -> bool

#self?.needs_ternary_q_space? ⇒ Boolean

Parameters:

  • node (Prism::Node)

Returns:

  • (Boolean)


19
# File 'sig/ryac/ast_utils.rbs', line 19

def self?.needs_ternary_q_space?: (Prism::Node node) -> bool

#self?.setter_def_name? ⇒ Boolean

Parameters:

  • name (Symbol)

Returns:

  • (Boolean)


14
# File 'sig/ryac/ast_utils.rbs', line 14

def self?.setter_def_name?: (Symbol name) -> bool

#self?.shorthand_pun_values ⇒ Array[Prism::Node]

Parameters:

  • node (Prism::Node)

Returns:

  • (Array[Prism::Node])


29
# File 'sig/ryac/ast_utils.rbs', line 29

def self?.shorthand_pun_values: (Prism::Node node) -> Array[Prism::Node]

#self?.simple_negatable? ⇒ Boolean

Parameters:

  • node (Prism::Node)

Returns:

  • (Boolean)


15
# File 'sig/ryac/ast_utils.rbs', line 15

def self?.simple_negatable?: (Prism::Node node) -> bool

#self?.single_statement_body? ⇒ Boolean

Parameters:

  • body (Prism::Node, nil)

Returns:

  • (Boolean)


13
# File 'sig/ryac/ast_utils.rbs', line 13

def self?.single_statement_body?: (Prism::Node? body) -> bool

#self?.symbol_arguments ⇒ Array[Symbol]

Parameters:

  • call_node (Prism::CallNode)

Returns:

  • (Array[Symbol])


3
# File 'sig/ryac/ast_utils.rbs', line 3

def self?.symbol_arguments: (Prism::CallNode call_node) -> Array[Symbol]

#self?.ternary_needs_parens? ⇒ Boolean

Parameters:

  • node (Prism::Node)
  • source (String)

Returns:

  • (Boolean)


23
# File 'sig/ryac/ast_utils.rbs', line 23

def self?.ternary_needs_parens?: (Prism::Node node, String source) -> bool

#self?.unwrap_statements ⇒ Prism::Node?

Parameters:

  • node (Prism::Node, nil)

Returns:

  • (Prism::Node, nil)


9
# File 'sig/ryac/ast_utils.rbs', line 9

def self?.unwrap_statements: (Prism::Node? node) -> Prism::Node?