Class: Ryac::Pipeline::Compactor

Inherits:
Object
  • Object
show all
Defined in:
lib/ryac/pipeline/compactor.rb,
sig/ryac/pipeline/compactor.rbs

Overview

Level 0: Compaction — full AST rebuild with no syntax optimizations. Removes comments and whitespace, joins with ";". Preserves original paren form. No renaming, no TypeProf needed. Self-contained — no shared mixins.

Constant Summary collapse

BINARY_OPERATORS =

Returns:

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

Returns:

  • (Set[Symbol])
%i[== === != =~ !~ <=>].to_set.freeze
OPERATOR_PRECEDENCE =

Returns:

  • (Hash[Symbol, Integer])
{
  :"|" => 1, :'^' => 1, :'&' => 2,
  :'<=>' => 3, :== => 3, :=== => 3, :!= => 3, :=~ => 3, :'!~' => 3,
  :> => 4, :>= => 4, :< => 4, :<= => 4,
  :<< => 5, :>> => 5,
  :+ => 6, :- => 6,
  :* => 7, :/ => 7, :% => 7,
  :** => 8
}.freeze
COMPOUND_WRITE_NODES =

Returns:

  • (Array[singleton(Prism::Node)])
[
  Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode,
  Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
  Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableAndWriteNode,
  Prism::ClassVariableOperatorWriteNode, Prism::ClassVariableOrWriteNode, Prism::ClassVariableAndWriteNode,
  Prism::ConstantOperatorWriteNode, Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode,
  Prism::ConstantPathOperatorWriteNode, Prism::ConstantPathOrWriteNode, Prism::ConstantPathAndWriteNode,
  Prism::CallOperatorWriteNode, Prism::CallOrWriteNode, Prism::CallAndWriteNode,
  Prism::IndexOperatorWriteNode, Prism::IndexOrWriteNode, Prism::IndexAndWriteNode,
].freeze
GROUP_MIN =

def self.x runs of GROUP_MIN or more become one class<<self block. Only CONSECUTIVE defs form a run — grouping across other statements would reorder their side effects.

Returns:

  • (Integer)
4
MODIFIER_KEYWORD_NODES =

--- Return / Break / Next ---

Returns:

  • (Array[singleton(Prism::Node)])
[Prism::IfNode, Prism::UnlessNode, Prism::WhileNode, Prism::UntilNode].freeze
PERCENT_SAFE_WORD =

A word must survive %w/%i verbatim: no whitespace, brackets, or backslash (those are the notation's escapes).

Returns:

  • (Regexp)
/\A[^\s\[\]\\]+\z/
ASSIGNMENT_NODES =

Returns:

  • (Array[singleton(Prism::Node)])
[
  Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode,
  Prism::ClassVariableWriteNode, Prism::GlobalVariableWriteNode,
  Prism::ConstantWriteNode, Prism::ConstantPathWriteNode, Prism::MultiWriteNode,
  *COMPOUND_WRITE_NODES,
].freeze
PARENS_KEPT_NODES =

Match nodes keep parens like assignments do: an endless def body of (value in pattern) stripped bare rebinds the in to the def statement itself and no longer parses.

Returns:

  • (Array[singleton(Prism::Node)])
[*ASSIGNMENT_NODES, *AstUtils::MATCH_REBIND_NODES].freeze
PARENS_KEPT_CLASSES =

Returns:

  • (Set[singleton(Prism::Node)])
PARENS_KEPT_NODES.to_set.freeze
JUMP_NODES =

Returns:

  • (Array[singleton(Prism::Node)])
[Prism::ReturnNode, Prism::BreakNode, Prism::NextNode].freeze
LOGIC_LOOSE_OPERANDS =

Operand classes that bind at or below the keyword logic forms — bare under && or || they would re-associate or fail to parse. Derived from the paren-keeping and modifier-keyword families, so a class added there joins this rule with it.

Returns:

  • (Array[singleton(Prism::Node)])
[
  *PARENS_KEPT_NODES, *MODIFIER_KEYWORD_NODES,
  Prism::CaseNode, Prism::CaseMatchNode, Prism::ForNode, Prism::BeginNode,
  Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode,
  Prism::FlipFlopNode, Prism::LambdaNode
].freeze
LOGIC_LOOSE_OPERAND_CLASSES =

Returns:

  • (Set[singleton(Prism::Node)])
LOGIC_LOOSE_OPERANDS.to_set.freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.jump_with_value?(node) ⇒ Boolean

A jump is loose only when it carries a value: x&&return parses, x&&return 5 does not.

Parameters:

  • node (Prism::Node, nil)

Returns:

  • (Boolean)


1125
1126
1127
# File 'lib/ryac/pipeline/compactor.rb', line 1125

def self.jump_with_value?(node)
  JUMP_NODES.include?(node.class) && !node.arguments.nil? # steep:ignore NoMethod
end

.loose_logic_operand?(node, tight:) ⇒ Boolean

The one owner of "does this operand need parens under && / ||". ControlFlowSimplify asks the same question when it folds nested conditionals into the compacted dialect. An OrNode operand needs parens under && but not under ||. Prism node classes are concrete, so a class-keyed Set replaces an is_a? scan.

Parameters:

  • node (Prism::Node, nil)
  • tight: (Boolean)

Returns:

  • (Boolean)


1134
1135
1136
1137
1138
# File 'lib/ryac/pipeline/compactor.rb', line 1134

def self.loose_logic_operand?(node, tight:)
  LOGIC_LOOSE_OPERAND_CLASSES.include?(node.class) ||
    (tight && node.is_a?(Prism::OrNode)) ||
    jump_with_value?(node)
end

Instance Method Details

#bare_standard_error?(exceptions) ⇒ Boolean

Parameters:

  • exceptions (Array[Prism::node])

Returns:

  • (Boolean)


776
777
778
779
780
# File 'lib/ryac/pipeline/compactor.rb', line 776

def bare_standard_error?(exceptions)
  exceptions.size == 1 &&
    exceptions[0].is_a?(Prism::ConstantReadNode) &&
    exceptions[0].name == :StandardError
end

#binary_op_separator(recv_node, recv_wrapped, op) ⇒ String

Parameters:

  • recv_node (Prism::Node, nil)
  • recv_wrapped (Boolean)
  • op (Symbol)

Returns:

  • (String)


1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
# File 'lib/ryac/pipeline/compactor.rb', line 1057

def binary_op_separator(recv_node, recv_wrapped, op)
  return '' if recv_wrapped
  inner = AstUtils.unwrap_statements(recv_node)
  if op == :'!~'
    AstUtils.ends_with_name_char?(inner) ? ' ' : ''
  elsif op.to_s.start_with?('=') && inner.is_a?(Prism::CallNode) && inner.name.to_s.end_with?('?', '!')
    ' '
  else
    ''
  end
end

#binary_operator_call?(node) ⇒ Boolean

Parameters:

  • node (Prism::Node, nil)

Returns:

  • (Boolean)


1157
1158
1159
1160
# File 'lib/ryac/pipeline/compactor.rb', line 1157

def binary_operator_call?(node)
  node.is_a?(Prism::CallNode) && node.receiver &&
    node.arguments&.arguments&.size == 1 && BINARY_OPERATORS.include?(node.name)
end

#block_uses_it?(body) ⇒ Boolean

Parameters:

  • body (Prism::Node, nil)

Returns:

  • (Boolean)


1189
1190
1191
1192
1193
# File 'lib/ryac/pipeline/compactor.rb', line 1189

def block_uses_it?(body)
  return false unless body
  prism_traverse(body) { |n| return true if n.is_a?(Prism::ItLocalVariableReadNode) }
  false
end

#build_call_args(node, block_pass = nil) ⇒ String

--- Helpers ---

Parameters:

  • node (Prism::CallNode)
  • block_pass (String, nil) (defaults to: nil)

Returns:

  • (String)


925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
# File 'lib/ryac/pipeline/compactor.rb', line 925

def build_call_args(node, block_pass = nil)
  raw_args = node.arguments&.arguments || []
  args = [] #: Array[String]
  raw_args.each do |arg|
    case arg
    when Prism::SplatNode
      args << (arg.expression ? "*#{r_delimited(arg.expression)}" : '*')
    when Prism::KeywordHashNode
      arg.elements.each do |el|
        args << (el.is_a?(Prism::AssocSplatNode) ? "**#{r(el.value)}" : r_assoc(el))
      end
    when Prism::ForwardingArgumentsNode
      args << '...'
    else
      args << r_delimited(arg)
    end
  end
  args << block_pass if block_pass
  return '' if args.empty?
  "(#{args.join(',')})"
end

#build_def_params(params) ⇒ Array[String]

Parameters:

  • params (Prism::ParametersNode, nil)

Returns:

  • (Array[String])


947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
# File 'lib/ryac/pipeline/compactor.rb', line 947

def build_def_params(params)
  return [] unless params

  req = (params.requireds || []).map { |p|
    case p
    when Prism::RequiredParameterNode then p.name.to_s
    when Prism::MultiTargetNode then r_multi_target_param(p)
    else p.slice
    end
  }
  opt = (params.optionals || []).map { |p| "#{p.name}=#{r(p.value)}" }
  rest = if params.rest.is_a?(Prism::RestParameterNode)
    [params.rest.name ? "*#{params.rest.name}" : '*']
  else
    [] #: Array[String]
  end
  post = (params.posts || []).map { |p|
    p.is_a?(Prism::RequiredParameterNode) ? p.name.to_s : p.slice
  }
  req_kw = [] #: Array[String]
  opt_kw = [] #: Array[String]
  (params.keywords || []).each do |p|
    case p
    when Prism::RequiredKeywordParameterNode then req_kw << "#{p.name}:"
    when Prism::OptionalKeywordParameterNode
      val = r(p.value)
      sep = val.start_with?(':') ? ' ' : ''
      opt_kw << "#{p.name}:#{sep}#{val}"
    end
  end
  rest_kw = if params.keyword_rest.is_a?(Prism::KeywordRestParameterNode)
    [params.keyword_rest.name ? "**#{params.keyword_rest.name}" : '**']
  elsif params.keyword_rest.is_a?(Prism::NoKeywordsParameterNode)
    ['**nil']
  elsif params.keyword_rest.is_a?(Prism::ForwardingParameterNode)
    ['...']
  else
    [] #: Array[String]
  end
  block_param = if params.block.is_a?(Prism::BlockParameterNode)
    [params.block.name ? "&#{params.block.name}" : '&']
  else
    [] #: Array[String]
  end
  req + opt + rest + post + req_kw + opt_kw + rest_kw + block_param
end

#build_if_chain(node) ⇒ String

Parameters:

  • node (Prism::IfNode)

Returns:

  • (String)


1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
# File 'lib/ryac/pipeline/compactor.rb', line 1069

def build_if_chain(node)
  parts = [] #: Array[String]
  current = node
  keyword = "if"
  loop do
    inner = current.statements ? AstUtils.unwrap_statements(current.statements) : nil
    then_body = (inner.nil? || inner.is_a?(Prism::NilNode)) ? nil : r_stmt(current.statements)
    parts << "#{keyword} #{r(current.predicate)}#{fmt_body(then_body)}"
    subsequent = current.subsequent
    if subsequent.is_a?(Prism::IfNode)
      current = subsequent
      keyword = "elsif"
    else
      if subsequent.is_a?(Prism::ElseNode)
        inner = subsequent.statements ? AstUtils.unwrap_statements(subsequent.statements) : nil
        unless inner.nil? || inner.is_a?(Prism::NilNode)
          eb = r_stmt(subsequent.statements)
          parts << "else#{fmt_body(eb)}" if eb && !eb.empty?
        end
      end
      break
    end
  end
  parts.join(";") + ";end"
end

#build_lambda_params(block_params) ⇒ String

Parameters:

  • block_params (Prism::Node, nil)

Returns:

  • (String)


1009
1010
1011
1012
1013
1014
1015
# File 'lib/ryac/pipeline/compactor.rb', line 1009

def build_lambda_params(block_params)
  return "" unless block_params.is_a?(Prism::BlockParametersNode)
  inner = block_params.parameters
  return "()" unless inner
  all = build_def_params(inner)
  all.empty? ? "" : "(#{all.join(',')})"
end

#call(input_string) ⇒ String

Parameters:

  • input_string (String)

Returns:

  • (String)


33
34
35
36
37
# File 'lib/ryac/pipeline/compactor.rb', line 33

def call(input_string)
  @prism_ast = Prism.parse(input_string).value
  @standard_error_redefined = nil
  rebuild.join(";")
end

#call_target(node) ⇒ String

Parameters:

  • node (Prism::CallOperatorWriteNode, Prism::CallOrWriteNode, Prism::CallAndWriteNode)

Returns:

  • (String)


473
474
475
476
477
478
# File 'lib/ryac/pipeline/compactor.rb', line 473

def call_target(node)
  recv_str = r(node.receiver)
  base = node.read_name.to_s.delete_suffix('=')
  call_op = node.safe_navigation? ? '&.' : '.'
  "#{recv_str}#{call_op}#{base}"
end

#compound(target, op, node) ⇒ String

Parameters:

  • target (String, Symbol)
  • op (Symbol, String)
  • node (compound_write_node)

Returns:

  • (String)


471
# File 'lib/ryac/pipeline/compactor.rb', line 471

def compound(target, op, node) = "#{target}#{op}=#{r(node.value)}"

#escape_for_dquote(str) ⇒ String

Parameters:

  • str (String)

Returns:

  • (String)


684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
# File 'lib/ryac/pipeline/compactor.rb', line 684

def escape_for_dquote(str)
  result = +""
  i = 0
  while i < str.size
    c = str[i]
    case c
    when '\\' then result << '\\\\'
    when '"' then result << '\\"'
    when "\n" then result << '\\n'
    when "\t" then result << '\\t'
    when "\r" then result << '\\r'
    when "\0" then result << '\\0'
    when '#'
      if i + 1 < str.size && '{@$'.include?(str[i + 1]) # steep:ignore ArgumentTypeMismatch
        result << '\\#'
      else
        result << '#'
      end
    else
      result << c # steep:ignore ArgumentTypeMismatch
    end
    i += 1
  end
  result
end

#escape_regexp_slash(content) ⇒ String

Parameters:

  • content (String)

Returns:

  • (String)


509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
# File 'lib/ryac/pipeline/compactor.rb', line 509

def escape_regexp_slash(content)
  result = +""
  i = 0
  while i < content.size
    if content[i] == '\\'
      result << content[i] # steep:ignore ArgumentTypeMismatch
      i += 1
      result << content[i] if i < content.size # steep:ignore ArgumentTypeMismatch
      i += 1
    elsif content[i] == '/'
      result << '\\/'
      i += 1
    else
      result << content[i] # steep:ignore ArgumentTypeMismatch
      i += 1
    end
  end
  result
end

#extract_block_params(block) ⇒ Array[String]

Parameters:

  • block (Prism::BlockNode)

Returns:

  • (Array[String])


1017
1018
1019
1020
# File 'lib/ryac/pipeline/compactor.rb', line 1017

def extract_block_params(block)
  return [] unless block.parameters.is_a?(Prism::BlockParametersNode) && block.parameters.parameters
  build_def_params(block.parameters.parameters)
end

#flush_singleton_run(run) ⇒ Array[String]

Renders and clears the pending run of def self. statements: one class<<self group when the run is long enough to pay for the wrapper, the plain renders otherwise.

Parameters:

  • run (Array[Prism::DefNode])

Returns:

  • (Array[String])


180
181
182
183
184
185
186
187
188
189
190
# File 'lib/ryac/pipeline/compactor.rb', line 180

def flush_singleton_run(run)
  rendered = if run.size >= GROUP_MIN
               inner = run.map { |n| r_def(n, drop_self_receiver: true) }
               inner.reject! { |s| s.nil? || s.empty? }
               ["class<<self;#{inner.join(';')};end"]
             else
               run.map { |n| r(n) }.reject { |s| s.nil? || s.empty? }
             end
  run.clear
  rendered
end

#fmt_body(body) ⇒ String

Parameters:

  • body (String, nil)

Returns:

  • (String)


1155
# File 'lib/ryac/pipeline/compactor.rb', line 1155

def fmt_body(body) = body.nil? || body.empty? ? "" : ";#{body}"

#idx_target(node) ⇒ String

Parameters:

  • node (Prism::IndexOperatorWriteNode, Prism::IndexOrWriteNode, Prism::IndexAndWriteNode)

Returns:

  • (String)


480
481
482
483
# File 'lib/ryac/pipeline/compactor.rb', line 480

def idx_target(node)
  keys = (node.arguments&.arguments || []).map { |a| r_delimited(a) }.join(',')
  "#{r(node.receiver)}[#{keys}]"
end

#interp_part(part, escape_quotes: false) ⇒ String

Parameters:

  • part (Prism::Node)
  • escape_quotes: (Boolean) (defaults to: false)

Returns:

  • (String)


670
671
672
673
674
675
676
677
678
679
680
681
682
# File 'lib/ryac/pipeline/compactor.rb', line 670

def interp_part(part, escape_quotes: false)
  case part
  when Prism::StringNode
    if escape_quotes
      escape_for_dquote(part.unescaped)
    else
      part.content
    end
  when Prism::EmbeddedStatementsNode then "\#{#{r_stmt(part.statements)}}"
  when Prism::EmbeddedVariableNode then "\#{#{r(part.variable)}}"
  else "\#{#{r(part)}}"
  end
end

#logic_operand(operand, tight:) ⇒ String

Parameters:

  • operand (Prism::Node)
  • tight: (Boolean)

Returns:

  • (String)


742
743
744
745
746
747
# File 'lib/ryac/pipeline/compactor.rb', line 742

def logic_operand(operand, tight:)
  # Classified through source parens: `x&&(y||z)` arrives as a
  # ParenthesesNode operand whose parens must survive.
  inner = AstUtils.unwrap_statements(operand)
  Compactor.loose_logic_operand?(inner, tight: tight) ? "(#{r(inner)})" : r(operand)
end

#node_returns_bot?(node) ⇒ Boolean

Parameters:

  • node (Prism::Node)

Returns:

  • (Boolean)


1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
# File 'lib/ryac/pipeline/compactor.rb', line 1168

def node_returns_bot?(node)
  case node
  when Prism::ReturnNode, Prism::BreakNode, Prism::NextNode then true
  when Prism::CallNode
    # Only the Kernel spellings end the flow; `logger.raise(1)` is an
    # ordinary method call.
    node.receiver.nil? && (node.name == :raise || node.name == :fail)
  else false
  end
end

#percent_array_form(node) ⇒ String?

The %-spelling of an all-static word array, whatever the source wrote — bracket, %w/%i, or a %W/%I that never interpolates. nil when the elements aren't uniformly plain words; the caller keeps whichever spelling is shorter (bracket on ties, so single-char strings stay available to CharShorten).

Parameters:

  • node (Prism::ArrayNode)

Returns:

  • (String, nil)


546
547
548
549
550
551
552
553
554
555
556
557
# File 'lib/ryac/pipeline/compactor.rb', line 546

def percent_array_form(node)
  elements = node.elements
  words, kind = if elements.all? { |e| e.is_a?(Prism::SymbolNode) }
    [elements.map { |e| e.value.to_s }, 'i'] # steep:ignore NoMethod
  elsif elements.all? { |e| e.is_a?(Prism::StringNode) }
    [elements.map(&:unescaped), 'w'] # steep:ignore BlockTypeMismatch
  end
  return nil unless words&.any?
  return nil unless words.all? { |w| w.match?(PERCENT_SAFE_WORD) }

  "%#{kind}[#{words.join(' ')}]"
end

#prism_traverse(node) {|node| ... } ⇒ void

This method returns an undefined value.

Parameters:

  • node (Prism::Node, nil)

Yields:

  • (node)

Yield Parameters:

  • arg0 (Prism::Node)

Yield Returns:

  • (void)


1195
1196
1197
1198
1199
# File 'lib/ryac/pipeline/compactor.rb', line 1195

def prism_traverse(node, &block)
  return unless node
  yield node
  node.compact_child_nodes.each { |child| prism_traverse(child, &block) }
end

#r(node) ⇒ String

Parameters:

  • node (Prism::Node, nil)

Returns:

  • (String)


47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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
134
135
136
137
# File 'lib/ryac/pipeline/compactor.rb', line 47

def r(node)
  case node
  when Prism::CallNode then r_call(node)
  when Prism::DefNode then r_def(node)
  when Prism::YieldNode then r_yield(node)
  when Prism::IfNode then r_if(node)
  when Prism::UnlessNode then r_unless(node)
  when Prism::WhileNode then r_while(node)
  when Prism::CaseNode then r_case(node)
  when Prism::ReturnNode then r_return(node)
  when Prism::ClassNode then r_class(node)
  when Prism::ModuleNode then r_module(node)
  when Prism::AndNode then r_and(node)
  when Prism::FlipFlopNode then r_flipflop(node)
  when Prism::OrNode then r_or(node)
  when Prism::LocalVariableWriteNode then "#{node.name}=#{r(node.value)}"
  when Prism::InstanceVariableWriteNode, Prism::GlobalVariableWriteNode,
       Prism::ClassVariableWriteNode then "#{node.name}=#{r(node.value)}"
  when Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode,
       Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
       Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableAndWriteNode,
       Prism::ClassVariableOperatorWriteNode, Prism::ClassVariableOrWriteNode, Prism::ClassVariableAndWriteNode,
       Prism::ConstantOperatorWriteNode, Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode,
       Prism::ConstantPathOperatorWriteNode, Prism::ConstantPathOrWriteNode, Prism::ConstantPathAndWriteNode,
       Prism::CallOperatorWriteNode, Prism::CallOrWriteNode, Prism::CallAndWriteNode,
       Prism::IndexOperatorWriteNode, Prism::IndexOrWriteNode, Prism::IndexAndWriteNode
    r_compound_write(node)
  when Prism::ConstantPathNode
    node.parent ? "#{r(node.parent)}::#{node.name}" : "::#{node.name}"
  when Prism::ConstantWriteNode then "#{node.name}=#{r(node.value)}"
  when Prism::ConstantPathWriteNode then "#{r(node.target)}=#{r(node.value)}"
  when Prism::StringNode then r_string(node)
  when Prism::IntegerNode then node.value.to_s
  when Prism::FloatNode then r_float(node)
  when Prism::ArrayNode then r_array(node)
  when Prism::RangeNode then r_range(node)
  when Prism::SymbolNode then r_symbol(node)
  when Prism::HashNode then r_hash(node)
  when Prism::InterpolatedStringNode then r_interp_string(node)
  when Prism::InterpolatedSymbolNode then r_interp_symbol(node)
  when Prism::InterpolatedRegularExpressionNode then r_interp_regexp(node)
  when Prism::RegularExpressionNode then r_regexp(node)
  when Prism::StatementsNode then r_stmt(node)
  when Prism::ParenthesesNode then r_parens(node)
  when Prism::SuperNode then r_super(node)
  when Prism::BeginNode then r_begin(node)
  when Prism::RescueModifierNode then "(#{r(node.expression)} rescue #{r(node.rescue_expression)})"
  when Prism::BreakNode then r_break(node)
  when Prism::NextNode then r_next(node)
  when Prism::SplatNode then node.expression ? "*#{r_delimited(node.expression)}" : '*'
  when Prism::UntilNode then r_until(node)
  when Prism::AliasMethodNode then "alias #{r_alias_name(node.new_name)} #{r_alias_name(node.old_name)}"
  when Prism::UndefNode then "undef #{node.names.map { |n| r_alias_name(n) }.join(',')}"
  when Prism::LambdaNode then r_lambda(node)
  when Prism::PostExecutionNode then "END{#{r_stmt(node.statements)}}"
  when Prism::MultiWriteNode then r_multi_write(node)
  when Prism::MatchWriteNode then r(node.call)
  when Prism::DefinedNode then r_defined(node)
  when Prism::ForNode then r_for(node)
  when Prism::MatchRequiredNode then "#{r(node.value)}=>#{r_pattern(node.pattern)}"
  when Prism::MatchPredicateNode then "#{r(node.value)} in #{r_pattern(node.pattern)}"
  when Prism::SingletonClassNode then r_singleton_class(node)
  when Prism::CaseMatchNode then r_case_match(node)
  when Prism::ArrayPatternNode then r_array_pattern(node)
  when Prism::HashPatternNode then r_hash_pattern(node)
  when Prism::FindPatternNode then r_find_pattern(node)
  when Prism::AlternationPatternNode, Prism::CapturePatternNode then r_pattern(node)
  when Prism::PinnedVariableNode then "^#{r(node.variable)}"
  when Prism::PinnedExpressionNode then "^(#{r_delimited(node.expression)})"
  when Prism::ImplicitNode then r(node.value)
  when Prism::ShareableConstantNode then r(node.write)
  when Prism::CallTargetNode then "#{r(node.receiver)}.#{node.name.to_s.chomp('=')}"
  when Prism::KeywordHashNode then r_keyword_hash(node)
  # nil spelled as the empty grouped expression: same value, one byte
  # shorter. Pattern positions reject it and go through r_pattern.
  when Prism::NilNode then '()'
  when Prism::TrueNode, Prism::FalseNode, Prism::SelfNode,
       Prism::RedoNode, Prism::RetryNode, Prism::ForwardingSuperNode,
       Prism::ItLocalVariableReadNode, Prism::NumberedReferenceReadNode,
       Prism::SourceEncodingNode, Prism::SourceFileNode, Prism::SourceLineNode,
       Prism::LocalVariableReadNode, Prism::LocalVariableTargetNode,
       Prism::InstanceVariableReadNode, Prism::GlobalVariableReadNode,
       Prism::ClassVariableReadNode, Prism::ConstantReadNode,
       Prism::RationalNode, Prism::ImaginaryNode, Prism::MatchLastLineNode,
       Prism::InterpolatedMatchLastLineNode,
       Prism::BackReferenceReadNode, Prism::AliasGlobalVariableNode
    node.slice
  else
    raise Ryac::MinifyError, "Unknown node: #{node.class}"
  end
end

#r_alias_name(node) ⇒ String

A plain symbol drops its colon (alias a b); anything else — an interpolated symbol — must keep its own syntax.

Parameters:

  • node (Prism::Node)

Returns:

  • (String)


441
# File 'lib/ryac/pipeline/compactor.rb', line 441

def r_alias_name(node) = node.is_a?(Prism::SymbolNode) ? node.value.to_s : r(node)

#r_and(node) ⇒ String

Always the operator spelling. The keyword forms bind below assignment, so and/or accept operands &&/|| cannot take bare — assignments, jumps with a value, control expressions — and those operands are parenthesized here. Rendering the keyword would need the surrounding position to be known (z=(a and b) unwrapped is a different program), and the renderer is position-free.

Parameters:

  • node (Prism::AndNode)

Returns:

  • (String)


734
735
736
# File 'lib/ryac/pipeline/compactor.rb', line 734

def r_and(node)
  "#{logic_operand(node.left, tight: true)}&&#{logic_operand(node.right, tight: true)}"
end

#r_array(node) ⇒ String

Parameters:

  • node (Prism::ArrayNode)

Returns:

  • (String)


535
536
537
538
539
# File 'lib/ryac/pipeline/compactor.rb', line 535

def r_array(node)
  bracket = "[#{node.elements.map { r_delimited(_1) }.join(',')}]"
  percent = percent_array_form(node)
  percent && percent.bytesize < bracket.bytesize ? percent : bracket
end

#r_array_pattern(node) ⇒ String

--- Pattern matching ---

Parameters:

  • node (Prism::ArrayPatternNode)

Returns:

  • (String)


876
877
878
879
880
881
882
883
884
885
886
887
# File 'lib/ryac/pipeline/compactor.rb', line 876

def r_array_pattern(node)
  parts = node.requireds.map { |n| r_pattern(n) }
  if node.rest
    parts << if node.rest.is_a?(Prism::SplatNode) && node.rest.expression
      "*#{r(node.rest.expression)}"
    else
      '*'
    end
  end
  parts.concat(node.posts.map { |p| r_pattern(p) })
  "[#{parts.join(',')}]"
end

#r_assoc(element) ⇒ String

Parameters:

  • element (Prism::AssocNode)

Returns:

  • (String)


617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
# File 'lib/ryac/pipeline/compactor.rb', line 617

def r_assoc(element)
  key = element.key
  val = element.value
  val_str = r_delimited(val)
  if key.is_a?(Prism::SymbolNode) && key.value.to_s.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*\z/)
    key_name = key.value.to_s
    return "#{key_name}:" if val.is_a?(Prism::ImplicitNode)
    sep = val_str.start_with?(':') ? ': ' : ':'
    "#{key_name}#{sep}#{val_str}"
  else
    key_str = r(key)
    separator = if key.is_a?(Prism::SymbolNode) && !key.value.to_s.match?(/[a-zA-Z0-9_]\z/)
      ' =>'
    else
      '=>'
    end
    "#{key_str}#{separator}#{val_str}"
  end
end

#r_begin(node) ⇒ String

--- Error handling ---

Parameters:

  • node (Prism::BeginNode)

Returns:

  • (String)


751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
# File 'lib/ryac/pipeline/compactor.rb', line 751

def r_begin(node)
  parts = ["begin#{fmt_body(r_stmt(node.statements))}"]
  rescue_node = node.rescue_clause
  while rescue_node
    rp = ';rescue'
    exceptions = rescue_node.exceptions || []
    # `rescue StandardError` is what a bare rescue already means —
    # unless the program redefines StandardError, when the bare form
    # would resolve differently.
    unless exceptions.empty? || (bare_standard_error?(exceptions) && !standard_error_redefined?)
      rp += " #{exceptions.map { |e| r(e) }.join(',')}"
    end
    if rescue_node.reference && rescue_var_used?(rescue_node)
      # the reference is a name-bearing target node in valid rescue clauses
      rp += "=>#{rescue_node.reference.name}" # steep:ignore NoMethod
    end
    parts << "#{rp}#{fmt_body(r_stmt(rescue_node.statements))}"
    rescue_node = rescue_node.subsequent
  end
  parts << ";else#{fmt_body(r_stmt(node.else_clause.statements))}" if node.else_clause
  parts << ";ensure#{fmt_body(r_stmt(node.ensure_clause.statements))}" if node.ensure_clause
  parts << ';end'
  parts.join
end

#r_binary_op(node) ⇒ String

Parameters:

  • node (Prism::CallNode)

Returns:

  • (String)


221
222
223
224
225
226
227
228
# File 'lib/ryac/pipeline/compactor.rb', line 221

def r_binary_op(node)
  recv_str, recv_wrapped = wrap_operand(node.receiver, node.name, :left)
  # binary operator calls always carry exactly one argument
  arg_str, = wrap_operand(node.arguments.arguments.first, node.name, :right) # steep:ignore NoMethod
  op = node.name
  sep = binary_op_separator(node.receiver, recv_wrapped, op)
  "#{recv_str}#{sep}#{op}#{arg_str}"
end

#r_block_call(node) ⇒ String?

Parameters:

  • node (Prism::CallNode)

Returns:

  • (String, nil)


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

def r_block_call(node)
  method_name = node.name
  call_op = node.safe_navigation? ? '&.' : '.'
  block = node.block
  if block.is_a?(Prism::BlockNode)
    block_params = extract_block_params(block)
    uses_numbered = block_params.any? { |p| p.match?(/^_\d+$/) }
    block_params_str = if block_params.empty? || uses_numbered || block_uses_it?(block.body)
      ""
    else
      "|#{block_params.join(',')}|"
    end
    "#{recv_with_op(node.receiver, call_op)}#{method_name}#{build_call_args(node)}{#{block_params_str}#{r_stmt(block.body)}}"
  elsif block.is_a?(Prism::BlockArgumentNode)
    bp = block.expression ? "&#{r(block.expression)}" : "&"
    "#{recv_with_op(node.receiver, call_op)}#{method_name}#{build_call_args(node, bp)}"
  end
end

#r_break(node) ⇒ String

Parameters:

  • node (Prism::BreakNode)

Returns:

  • (String)


437
# File 'lib/ryac/pipeline/compactor.rb', line 437

def r_break(node) = r_jump('break', node.arguments)

#r_call(node) ⇒ String

--- Method calls ---

Parameters:

  • node (Prism::CallNode)

Returns:

  • (String)


194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/ryac/pipeline/compactor.rb', line 194

def r_call(node)
  return r_binary_op(node) if AstUtils.middle_method?(node.name)
  # has_block? guarantees a BlockNode/BlockArgumentNode, so r_block_call
  # cannot actually return nil here.
  return r_block_call(node) if AstUtils.has_block?(node) # steep:ignore ReturnTypeMismatch
  return r_index_access(node) if node.name == :'[]'
  return r_index_assign(node) if node.name == :'[]='
  return r_unary(node, '!') if node.name == :'!'
  return r_unary(node, node.name == :'-@' ? '-' : '+') if node.name == :'-@' || node.name == :'+@'
  call_op = node.safe_navigation? ? '&.' : '.'
  # `x.call(args)` has proc-call sugar: `x.(args)` dispatches to the
  # exact same method on any receiver. Erasing the message this early
  # is sound only because :call sits in EXCLUDED_METHODS — no renamer
  # ever needs to find these sites by name.
  if node.name == :call && node.receiver
    raw_args = node.arguments&.arguments || []
    return "#{recv_with_op(node.receiver, call_op)}(#{raw_args.map { |a| r_delimited(a) }.join(',')})"
  end
  method_name = node.name
  if setter_call?(node)
    # setter_call? implies exactly one argument is present
    "#{recv_with_op(node.receiver, call_op)}#{method_name}#{r(node.arguments.arguments.first)}" # steep:ignore NoMethod
  else
    "#{recv_with_op(node.receiver, call_op)}#{method_name}#{build_call_args(node)}"
  end
end

#r_case(node) ⇒ String

Parameters:

  • node (Prism::CaseNode)

Returns:

  • (String)


369
370
371
372
373
374
375
376
377
378
379
380
# File 'lib/ryac/pipeline/compactor.rb', line 369

def r_case(node)
  predicate = node.predicate ? " #{r(node.predicate)}" : ''
  body = "case#{predicate};" + node.conditions.map { |wn|
    conditions = wn.conditions.map { |c| r_delimited(c) }.join(',')
    "when #{conditions}#{fmt_body(r_stmt(wn.statements))}"
  }.join(";")
  if node.else_clause
    eb = node.else_clause.statements ? r_stmt(node.else_clause.statements) : nil
    body += ";else#{fmt_body(eb)}"
  end
  body + ";end"
end

#r_case_match(node) ⇒ String

Parameters:

  • node (Prism::CaseMatchNode)

Returns:

  • (String)


382
383
384
385
386
387
388
389
390
391
# File 'lib/ryac/pipeline/compactor.rb', line 382

def r_case_match(node)
  body = "case #{r(node.predicate)};"
  body += node.conditions.map { |in_node|
    "in #{r_in_pattern(in_node.pattern)}#{fmt_body(r_stmt(in_node.statements))}"
  }.join(";")
  if node.else_clause
    body += ";else#{fmt_body(r_stmt(node.else_clause.statements))}"
  end
  body + ";end"
end

#r_class(node) ⇒ String

--- Classes / Modules ---

Parameters:

  • node (Prism::ClassNode)

Returns:

  • (String)


712
713
714
715
716
# File 'lib/ryac/pipeline/compactor.rb', line 712

def r_class(node)
  class_name = node.constant_path.slice
  superclass_str = node.superclass ? "<#{r(node.superclass)}" : ''
  "class #{class_name}#{superclass_str}#{fmt_body(r_stmt(node.body))};end"
end

#r_compound_write(node) ⇒ String

--- Compound assignments ---

Parameters:

  • node (compound_write_node)

Returns:

  • (String)


455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
# File 'lib/ryac/pipeline/compactor.rb', line 455

def r_compound_write(node)
  target = case node
  when Prism::ConstantPathOperatorWriteNode, Prism::ConstantPathOrWriteNode, Prism::ConstantPathAndWriteNode
    r(node.target)
  when Prism::CallOperatorWriteNode, Prism::CallOrWriteNode, Prism::CallAndWriteNode
    call_target(node)
  when Prism::IndexOperatorWriteNode, Prism::IndexOrWriteNode, Prism::IndexAndWriteNode
    idx_target(node)
  else
    node.name
  end
  # binary_operator exists exactly on the *Operator* variants selected here
  op = node.class.name.include?('Operator') ? node.binary_operator : (node.class.name.include?('Or') ? '||' : '&&') # steep:ignore NoMethod
  compound(target, op, node)
end

#r_def(node, drop_self_receiver: false) ⇒ String

drop_self_receiver: the def sits directly inside class<<self, so its written self. disappears; anything nested in its body renders through the plain path and keeps its own receiver.

Parameters:

  • node (Prism::DefNode)
  • drop_self_receiver: (Boolean) (defaults to: false)

Returns:

  • (String)


281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/ryac/pipeline/compactor.rb', line 281

def r_def(node, drop_self_receiver: false)
  method_name = node.name
  receiver_prefix = if node.receiver
    if drop_self_receiver && node.receiver.is_a?(Prism::SelfNode)
      ""
    else
      "#{r(node.receiver)}."
    end
  else
    ""
  end
  all_params = build_def_params(node.parameters)
  body_node = node.body
  if body_node.is_a?(Prism::StatementsNode) && body_node.body.size == 1 &&
      body_node.body.first.is_a?(Prism::ParenthesesNode)
    body_node = body_node.body.first.body # steep:ignore NoMethod
  end
  # A sole-nil body and an empty body are the same method, and the
  # empty spelling is what `def f =()` parses back into — emitting it
  # directly keeps re-minification at a fixed point.
  if body_node.is_a?(Prism::StatementsNode) && body_node.body.size == 1 &&
      body_node.body.first.is_a?(Prism::NilNode)
    body_node = nil
  end
  body = r_stmt(body_node)
  params_str = all_params.empty? ? "" : "(#{all_params.join(',')})"
  "def #{receiver_prefix}#{method_name}#{params_str}#{fmt_body(body)};end"
end

#r_defined(node) ⇒ String

Parameters:

  • node (Prism::DefinedNode)

Returns:

  • (String)


816
817
818
# File 'lib/ryac/pipeline/compactor.rb', line 816

def r_defined(node)
  "defined?(#{r(node.value)})"
end

#r_delimited(node) ⇒ String

Parameters:

  • node (Prism::Node)

Returns:

  • (String)


590
591
592
593
# File 'lib/ryac/pipeline/compactor.rb', line 590

def r_delimited(node)
  inner = AstUtils.unwrap_statements(node)
  inner.is_a?(Prism::RangeNode) ? r_range(inner, bare: true) : r(node)
end

#r_find_pattern(node) ⇒ String

Parameters:

  • node (Prism::FindPatternNode)

Returns:

  • (String)


903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
# File 'lib/ryac/pipeline/compactor.rb', line 903

def r_find_pattern(node)
  parts = [] #: Array[String]
  if node.left
    parts << if node.left.is_a?(Prism::SplatNode) && node.left.expression
      "*#{r(node.left.expression)}"
    else
      '*'
    end
  end
  parts.concat(node.requireds.map { |n| r_pattern(n) })
  if node.right
    parts << if node.right.is_a?(Prism::SplatNode) && node.right.expression
      "*#{r(node.right.expression)}"
    else
      '*'
    end
  end
  "[#{parts.join(',')}]"
end

#r_flipflop(node) ⇒ String

A flip-flop keeps its operands parenthesized — their own rendering, not a slice of the source, so interior spacing compacts like everything else.

Parameters:

  • node (Prism::FlipFlopNode)

Returns:

  • (String)


598
599
600
601
602
603
# File 'lib/ryac/pipeline/compactor.rb', line 598

def r_flipflop(node)
  op = node.exclude_end? ? '...' : '..'
  left = node.left ? "(#{r(node.left)})" : ''
  right = node.right ? "(#{r(node.right)})" : ''
  "#{left}#{op}#{right}"
end

#r_float(node) ⇒ String

The shortest spelling that denotes the same decimal value: such a respelling parses to the identical double, so only the bytes change. One shape covers plain and exponent forms — pull the digits together, fold leading/trailing zeros into the exponent — and the final to_f comparison is a belt-and-suspenders check.

Parameters:

  • node (Prism::FloatNode)

Returns:

  • (String)


564
565
566
567
568
569
570
571
572
573
574
575
576
577
# File 'lib/ryac/pipeline/compactor.rb', line 564

def r_float(node)
  s = node.value.to_s
  m = s.match(/\A(-?)(\d+)\.(\d+)(?:e([+-]?\d+))?\z/)
  return s unless m

  frac = m[3].to_s
  exp = m[4].to_i - frac.size
  digits = "#{m[2]}#{frac}".sub(/\A0+/, '')
  digits = digits.sub(/0+\z/) { |z| exp += z.size; '' }
  return s if digits.empty? || exp == 0

  cand = "#{m[1]}#{digits}e#{exp}"
  cand.size < s.size && cand.to_f == node.value ? cand : s
end

#r_for(node) ⇒ String

Parameters:

  • node (Prism::ForNode)

Returns:

  • (String)


426
427
428
429
430
# File 'lib/ryac/pipeline/compactor.rb', line 426

def r_for(node)
  idx = node.index
  target = idx.is_a?(Prism::LocalVariableTargetNode) ? idx.name.to_s : idx.slice
  "for #{target} in #{r(node.collection)}#{fmt_body(r_stmt(node.statements))};end"
end

#r_hash(node) ⇒ String

Parameters:

  • node (Prism::HashNode)

Returns:

  • (String)


605
606
607
608
609
# File 'lib/ryac/pipeline/compactor.rb', line 605

def r_hash(node)
  "{" + node.elements.map { |e|
    e.is_a?(Prism::AssocSplatNode) ? "**#{r(e.value)}" : r_assoc(e)
  }.join(',') + "}"
end

#r_hash_pattern(node) ⇒ String

Parameters:

  • node (Prism::HashPatternNode)

Returns:

  • (String)


889
890
891
892
893
894
895
896
897
898
899
900
901
# File 'lib/ryac/pipeline/compactor.rb', line 889

def r_hash_pattern(node)
  # hash-pattern keys are always plain SymbolNodes
  parts = node.elements.map { |a| "#{a.key.value}: #{r_pattern(a.value)}" } # steep:ignore NoMethod
  if node.rest
    case node.rest
    when Prism::AssocSplatNode
      parts << (node.rest.value ? "**#{r(node.rest.value)}" : '**')
    when Prism::NoKeywordsParameterNode
      parts << '**nil'
    end
  end
  "{#{parts.join(',')}}"
end

#r_if(node) ⇒ String

Parameters:

  • node (Prism::IfNode)

Returns:

  • (String)


324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/ryac/pipeline/compactor.rb', line 324

def r_if(node)
  then_body = r_stmt(node.statements)
  else_node = node.subsequent
  else_body = case else_node
  when nil then nil
  when Prism::IfNode then r(else_node)
  when Prism::ElseNode
    inner = else_node.statements ? AstUtils.unwrap_statements(else_node.statements) : nil
    (inner.nil? || inner.is_a?(Prism::NilNode)) ? nil : r_stmt(else_node.statements)
  end

  if else_body.nil?
    "if #{r(node.predicate)};#{then_body || ''};end"
  elsif else_node.is_a?(Prism::IfNode)
    build_if_chain(node)
  else
    "if #{r(node.predicate)};#{then_body || ''};else;#{else_body};end"
  end
end

#r_in_pattern(pattern) ⇒ String

in PATTERN if GUARD parses with the guard as an If/Unless node wrapping the pattern — rendering that as a statement-if corrupts the clause into in if guard;pattern;end. Guards cannot nest, so the wrapped node is always a plain pattern.

Parameters:

  • pattern (Prism::Node)

Returns:

  • (String)


397
398
399
400
401
402
403
404
405
406
# File 'lib/ryac/pipeline/compactor.rb', line 397

def r_in_pattern(pattern)
  case pattern
  when Prism::IfNode, Prism::UnlessNode
    keyword = pattern.is_a?(Prism::IfNode) ? 'if' : 'unless'
    # a guard's IfNode always wraps a non-empty statements body
    "#{r_pattern(pattern.statements.body[0])} #{keyword} #{r(pattern.predicate)}" # steep:ignore NoMethod
  else
    r_pattern(pattern)
  end
end

#r_index_access(node) ⇒ String

Parameters:

  • node (Prism::CallNode)

Returns:

  • (String)


249
250
251
252
253
254
255
256
257
# File 'lib/ryac/pipeline/compactor.rb', line 249

def r_index_access(node)
  raw_args = node.arguments&.arguments || []
  args_str = raw_args.map { |a| r_delimited(a) }.join(',')
  if node.safe_navigation?
    "#{r(node.receiver)}&.[](#{args_str})"
  else
    "#{r(node.receiver)}[#{args_str}]"
  end
end

#r_index_assign(node) ⇒ String

Parameters:

  • node (Prism::CallNode)

Returns:

  • (String)


259
260
261
262
263
264
265
266
267
# File 'lib/ryac/pipeline/compactor.rb', line 259

def r_index_assign(node)
  raw_args = node.arguments&.arguments || []
  if node.safe_navigation?
    "#{r(node.receiver)}&.[]=(#{raw_args.map { |a| r_delimited(a) }.join(',')})"
  else
    keys = raw_args[0..-2].map { |a| r_delimited(a) }.join(',') # steep:ignore NoMethod
    "#{r(node.receiver)}[#{keys}]=#{r(raw_args.last)}"
  end
end

#r_interp_regexp(node) ⇒ String

Parameters:

  • node (Prism::InterpolatedRegularExpressionNode)

Returns:

  • (String)


658
659
660
661
662
663
664
665
666
667
668
# File 'lib/ryac/pipeline/compactor.rb', line 658

def r_interp_regexp(node)
  needs_slash_escape = node.opening != '/'
  parts = node.parts.map { |p|
    if needs_slash_escape && p.is_a?(Prism::StringNode)
      escape_regexp_slash(p.content)
    else
      interp_part(p)
    end
  }
  "/" + parts.join + "/#{node.closing[1..]}"
end

#r_interp_string(node) ⇒ String

--- Interpolation ---

Parameters:

  • node (Prism::InterpolatedStringNode)

Returns:

  • (String)


649
650
651
652
# File 'lib/ryac/pipeline/compactor.rb', line 649

def r_interp_string(node)
  needs_escape = node.opening != '"'
  "\"" + node.parts.map { |p| interp_part(p, escape_quotes: needs_escape) }.join + "\""
end

#r_interp_symbol(node) ⇒ String

Parameters:

  • node (Prism::InterpolatedSymbolNode)

Returns:

  • (String)


654
655
656
# File 'lib/ryac/pipeline/compactor.rb', line 654

def r_interp_symbol(node)
  ":\"" + node.parts.map { |p| interp_part(p) }.join + "\""
end

#r_jump(keyword, arguments) ⇒ String

Parameters:

  • keyword (String)
  • arguments (Prism::ArgumentsNode, nil)

Returns:

  • (String)


444
445
446
447
448
449
450
451
# File 'lib/ryac/pipeline/compactor.rb', line 444

def r_jump(keyword, arguments)
  args = arguments&.arguments
  return keyword if args.nil? || args.empty?
  result = args.map { |a| r_delimited(a) }.join(',')
  first = args.first
  unwrapped = first.is_a?(Prism::ParenthesesNode) ? AstUtils.unwrap_statements(first) : first
  MODIFIER_KEYWORD_NODES.any? { |t| unwrapped.is_a?(t) } ? "#{keyword}(#{result})" : "#{keyword} #{result}"
end

#r_keyword_hash(node) ⇒ String

Parameters:

  • node (Prism::KeywordHashNode)

Returns:

  • (String)


611
612
613
614
615
# File 'lib/ryac/pipeline/compactor.rb', line 611

def r_keyword_hash(node)
  node.elements.map { |e|
    e.is_a?(Prism::AssocSplatNode) ? "**#{r(e.value)}" : r_assoc(e)
  }.join(',')
end

#r_lambda(node) ⇒ String

Parameters:

  • node (Prism::LambdaNode)

Returns:

  • (String)


310
311
312
313
314
# File 'lib/ryac/pipeline/compactor.rb', line 310

def r_lambda(node)
  params_str = build_lambda_params(node.parameters)
  body = r_stmt(node.body)
  "->#{params_str}{#{body}}"
end

#r_module(node) ⇒ String

Parameters:

  • node (Prism::ModuleNode)

Returns:

  • (String)


718
719
720
# File 'lib/ryac/pipeline/compactor.rb', line 718

def r_module(node)
  "module #{node.constant_path.slice}#{fmt_body(r_stmt(node.body))};end"
end

#r_multi_target(target) ⇒ String

Parameters:

  • target (Prism::Node)

Returns:

  • (String)


843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
# File 'lib/ryac/pipeline/compactor.rb', line 843

def r_multi_target(target)
  case target
  when Prism::LocalVariableTargetNode, Prism::InstanceVariableTargetNode,
       Prism::ClassVariableTargetNode, Prism::GlobalVariableTargetNode
    target.name.to_s
  when Prism::ConstantTargetNode
    target.name.to_s
  when Prism::ConstantPathTargetNode
    target.parent ? "#{r(target.parent)}::#{target.name}" : "::#{target.name}"
  when Prism::MultiTargetNode
    parts = target.lefts.map { |t| r_multi_target(t) }
    if target.rest
      case target.rest
      when Prism::SplatNode
        parts << (target.rest.expression ? "*#{r_multi_target(target.rest.expression)}" : '*')
      when Prism::ImplicitRestNode
        parts << '*'
      end
    end
    parts.concat(target.rights.map { |t| r_multi_target(t) })
    parts.join(',')
  when Prism::IndexTargetNode
    args = target.arguments&.arguments || []
    "#{r(target.receiver)}[#{args.map { |a| r(a) }.join(',')}]"
  when Prism::SplatNode
    target.expression ? "*#{r_multi_target(target.expression)}" : '*'
  else
    r(target)
  end
end

#r_multi_target_param(target) ⇒ String

Parameters:

  • target (Prism::MultiTargetNode)

Returns:

  • (String)


994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
# File 'lib/ryac/pipeline/compactor.rb', line 994

def r_multi_target_param(target)
  parts = target.lefts.map { |t|
    case t
    when Prism::RequiredParameterNode then t.name.to_s
    when Prism::MultiTargetNode then r_multi_target_param(t)
    else t.slice
    end
  }
  if target.rest
    parts << (target.rest.is_a?(Prism::SplatNode) && target.rest.expression ? "*#{target.rest.expression.slice}" : '*')
  end
  parts.concat(target.rights.map { |t| t.respond_to?(:name) ? t.name.to_s : t.slice }) # steep:ignore NoMethod
  "(#{parts.join(',')})"
end

#r_multi_write(node) ⇒ String

--- Multi-write ---

Parameters:

  • node (Prism::MultiWriteNode)

Returns:

  • (String)


822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
# File 'lib/ryac/pipeline/compactor.rb', line 822

def r_multi_write(node)
  targets = node.lefts.map { |t| r_multi_target(t) }
  if node.rest
    case node.rest
    when Prism::SplatNode
      targets << (node.rest.expression ? "*#{r_multi_target(node.rest.expression)}" : '*')
    when Prism::ImplicitRestNode
      targets << '*'
    end
  end
  targets.concat(node.rights.map { |t| r_multi_target(t) })
  value = node.value
  sep = targets.last&.end_with?('*') ? ' ' : ''
  if value.is_a?(Prism::ArrayNode) && (value.opening_loc.nil? || value.elements.size >= 2)
    elements = value.elements.map { |e| r(e) }
    "#{targets.join(',')}#{sep}=#{elements.join(',')}"
  else
    "#{targets.join(',')}#{sep}=#{r(value)}"
  end
end

#r_next(node) ⇒ String

Parameters:

  • node (Prism::NextNode)

Returns:

  • (String)


442
# File 'lib/ryac/pipeline/compactor.rb', line 442

def r_next(node) = r_jump('next', node.arguments)

#r_or(node) ⇒ String

Parameters:

  • node (Prism::OrNode)

Returns:

  • (String)


738
739
740
# File 'lib/ryac/pipeline/compactor.rb', line 738

def r_or(node)
  "#{logic_operand(node.left, tight: false)}||#{logic_operand(node.right, tight: false)}"
end

#r_parens(node) ⇒ String

Parameters:

  • node (Prism::ParenthesesNode)

Returns:

  • (String)


1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
# File 'lib/ryac/pipeline/compactor.rb', line 1140

def r_parens(node)
  body = r_stmt(node.body)
  # Empty parens ARE the nil literal this renderer emits — keeping
  # them (instead of collapsing to '') is what makes a second pass
  # over already-minified source round-trip.
  return '()' if body.empty?
  inner = AstUtils.unwrap_statements(node)
  keep = (node.body.is_a?(Prism::StatementsNode) && node.body.body.size > 1) ||
         PARENS_KEPT_CLASSES.include?(inner.class) ||
         # `(return 5)` bare after `&&` (or another operator) fails
         # to parse; a valueless jump is fine bare.
         Compactor.jump_with_value?(inner)
  keep ? "(#{body})" : body
end

#r_pattern(node) ⇒ String

Patterns are their own little language: nil there is the nil pattern and () is a syntax error, so pattern positions dispatch through here. Anything that reads the same in both languages — other literals, constants, pins, capture targets — falls through to r.

Parameters:

  • node (Prism::Node)

Returns:

  • (String)


413
414
415
416
417
418
419
420
421
422
423
424
# File 'lib/ryac/pipeline/compactor.rb', line 413

def r_pattern(node)
  case node
  when Prism::NilNode then 'nil'
  when Prism::ParenthesesNode
    # pattern parens always wrap exactly one pattern
    inner = AstUtils.unwrap_statements(node) #: Prism::Node
    "(#{r_pattern(inner)})"
  when Prism::AlternationPatternNode then "#{r_pattern(node.left)} | #{r_pattern(node.right)}"
  when Prism::CapturePatternNode then "#{r_pattern(node.value)}=>#{r_multi_target(node.target)}"
  else r(node)
  end
end

#r_range(node, bare: false) ⇒ String

Parenthesized by default: .. binds below almost everything, so in an unknown position the parens are what keep the parse. A position with its own delimiters (argument lists, subscripts, array and hash literals, when conditions, splats, pins) takes the bare form.

Parameters:

  • node (Prism::RangeNode)
  • bare: (Boolean) (defaults to: false)

Returns:

  • (String)


583
584
585
586
587
588
# File 'lib/ryac/pipeline/compactor.rb', line 583

def r_range(node, bare: false)
  op = node.exclude_end? ? '...' : '..'
  left = node.left ? r(node.left) : ''
  right = node.right ? r(node.right) : ''
  bare ? "#{left}#{op}#{right}" : "(#{left}#{op}#{right})"
end

#r_regexp(node) ⇒ String

Parameters:

  • node (Prism::RegularExpressionNode)

Returns:

  • (String)


502
503
504
505
506
507
# File 'lib/ryac/pipeline/compactor.rb', line 502

def r_regexp(node)
  content = node.content
  content = escape_regexp_slash(content) unless node.opening == '/'
  flags = node.closing[1..]
  "/#{content}/#{flags}"
end

#r_return(node) ⇒ String

Parameters:

  • node (Prism::ReturnNode)

Returns:

  • (String)


436
# File 'lib/ryac/pipeline/compactor.rb', line 436

def r_return(node) = r_jump('return', node.arguments)

#r_singleton_class(node) ⇒ String

Parameters:

  • node (Prism::SingletonClassNode)

Returns:

  • (String)


722
723
724
# File 'lib/ryac/pipeline/compactor.rb', line 722

def r_singleton_class(node)
  "class<<#{r(node.expression)}#{fmt_body(r_stmt(node.body))};end"
end

#r_stmt(nodes) ⇒ String

Parameters:

  • nodes (Prism::Node, nil)

Returns:

  • (String)


139
140
141
142
143
144
145
146
147
148
# File 'lib/ryac/pipeline/compactor.rb', line 139

def r_stmt(nodes)
  return '' if nodes.nil?

  if nodes.is_a?(Prism::StatementsNode)
    render_statement_list(nodes.body).join(';')
  else
    result = r(nodes)
    result.nil? || result.empty? ? '' : result
  end
end

#r_string(node) ⇒ String

--- Strings ---

Parameters:

  • node (Prism::StringNode)

Returns:

  • (String)


639
640
641
642
643
644
645
# File 'lib/ryac/pipeline/compactor.rb', line 639

def r_string(node)
  if node.opening == '"' || node.opening.nil?
    "\"#{node.content}\""
  else
    node.unescaped.inspect
  end
end

#r_super(node) ⇒ String

--- Other ---

Parameters:

  • node (Prism::SuperNode)

Returns:

  • (String)


810
811
812
813
814
# File 'lib/ryac/pipeline/compactor.rb', line 810

def r_super(node)
  args = node.arguments&.arguments&.map { |a| r_delimited(a) } || []
  args_str = args.join(',')
  args_str.empty? ? 'super()' : "super(#{args_str})"
end

#r_symbol(node) ⇒ String

--- Literals ---

Parameters:

  • node (Prism::SymbolNode)

Returns:

  • (String)


487
488
489
490
491
492
493
494
495
496
497
498
499
500
# File 'lib/ryac/pipeline/compactor.rb', line 487

def r_symbol(node)
  sym = node.value.to_sym # steep:ignore NoMethod
  name = sym.to_s
  if name.match?(/\A[a-zA-Z_]\w*[?!=]?\z/) || BINARY_OPERATORS.include?(sym) || %i[[] []=].include?(sym)
    ":#{name}"
  elsif name.start_with?('$') && !name.include?('\\')
    # A backslash never survives the bare form — `:$\` does not parse —
    # so those fall through to the quoted form, where the raw content
    # (Prism's value keeps escapes as written) reproduces itself.
    ":#{name}"
  else
    ":\"#{name.gsub('"', '\\"')}\""
  end
end

#r_unary(node, op) ⇒ String

Parameters:

  • node (Prism::CallNode)
  • op (String)

Returns:

  • (String)


269
270
271
272
273
274
# File 'lib/ryac/pipeline/compactor.rb', line 269

def r_unary(node, op)
  recv_str = r(node.receiver)
  inner = AstUtils.unwrap_statements(node.receiver)
  needs_wrap = binary_operator_call?(inner) || AstUtils.logical_op?(inner)
  needs_wrap ? "#{op}(#{recv_str})" : "#{op}#{recv_str}"
end

#r_unless(node) ⇒ String

Parameters:

  • node (Prism::UnlessNode)

Returns:

  • (String)


344
345
346
347
# File 'lib/ryac/pipeline/compactor.rb', line 344

def r_unless(node)
  then_body = r_stmt(node.statements)
  "unless #{r(node.predicate)};#{then_body || ''};end"
end

#r_until(node) ⇒ String

Parameters:

  • node (Prism::UntilNode)

Returns:

  • (String)


359
360
361
362
363
364
365
366
367
# File 'lib/ryac/pipeline/compactor.rb', line 359

def r_until(node)
  cond = r(node.predicate)
  body = r_stmt(node.statements)
  if node.begin_modifier?
    "#{body} until #{cond}"
  else
    "until #{cond}#{fmt_body(body)};end"
  end
end

#r_while(node) ⇒ String

Parameters:

  • node (Prism::WhileNode)

Returns:

  • (String)


349
350
351
352
353
354
355
356
357
# File 'lib/ryac/pipeline/compactor.rb', line 349

def r_while(node)
  cond = r(node.predicate)
  body = r_stmt(node.statements)
  if node.begin_modifier?
    "#{body} while #{cond}"
  else
    "while #{cond}#{fmt_body(body)};end"
  end
end

#r_yield(node) ⇒ String

--- Control flow ---

Parameters:

  • node (Prism::YieldNode)

Returns:

  • (String)


318
319
320
321
322
# File 'lib/ryac/pipeline/compactor.rb', line 318

def r_yield(node)
  args = node.arguments&.arguments || []
  return 'yield' if args.empty?
  "yield(#{args.map { |a| r_delimited(a) }.join(',')})"
end

#rebuild ⇒ Array[String]

--- Dispatch ---

Returns:

  • (Array[String])


43
44
45
# File 'lib/ryac/pipeline/compactor.rb', line 43

def rebuild
  render_statement_list(@prism_ast.statements.body)
end

#recv_with_op(recv_node, call_op) ⇒ String

Parameters:

  • recv_node (Prism::Node, nil)
  • call_op (String)

Returns:

  • (String)


1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
# File 'lib/ryac/pipeline/compactor.rb', line 1022

def recv_with_op(recv_node, call_op)
  return '' if recv_node.nil?
  recv_str = r(recv_node)
  inner = AstUtils.unwrap_statements(recv_node)
  # Unary calls must keep their parens under a chain: `(-vec).to_a`
  # stripped to `-vec.to_a` rebinds as `-(vec.to_a)` — String#-@ even
  # makes that run silently instead of failing.
  needs_wrap = binary_operator_call?(inner) || AstUtils.logical_op?(inner) ||
               inner.is_a?(Prism::TrueNode) || inner.is_a?(Prism::FalseNode) ||
               (inner.is_a?(Prism::CallNode) && %i[! -@ +@].include?(inner.name))
  needs_wrap ? "(#{recv_str})#{call_op}" : "#{recv_str}#{call_op}"
end

#render_statement_list(stmts) ⇒ Array[String]

Parameters:

  • stmts (Array[Prism::Node])

Returns:

  • (Array[String])


159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/ryac/pipeline/compactor.rb', line 159

def render_statement_list(stmts)
  results = [] #: Array[String]
  run = [] #: Array[Prism::DefNode]
  stmts.each do |subnode|
    if singleton_self_def?(subnode)
      # @type var subnode: Prism::DefNode
      run << subnode
      next
    end
    results.concat(flush_singleton_run(run))
    result = r(subnode)
    results << result unless result.nil? || result.empty?
    break if node_returns_bot?(subnode)
  end
  results.concat(flush_singleton_run(run))
  results
end

#rescue_var_used?(rescue_node) ⇒ Boolean

Parameters:

  • rescue_node (Prism::RescueNode)

Returns:

  • (Boolean)


1179
1180
1181
1182
1183
1184
1185
1186
1187
# File 'lib/ryac/pipeline/compactor.rb', line 1179

def rescue_var_used?(rescue_node)
  # only called when the (name-bearing) reference is present
  var = rescue_node.reference.name # steep:ignore NoMethod
  return false unless rescue_node.statements
  prism_traverse(rescue_node.statements) { |n|
    return true if n.is_a?(Prism::LocalVariableReadNode) && n.name == var
  }
  false
end

#setter_call?(node) ⇒ Boolean

Parameters:

  • node (Prism::CallNode)

Returns:

  • (Boolean)


1162
1163
1164
1165
1166
# File 'lib/ryac/pipeline/compactor.rb', line 1162

def setter_call?(node)
  name = node.name.to_s
  node.receiver && name.end_with?('=') && name.size > 1 &&
    !%w[== != <= >= ===].include?(name) && name != '[]='
end

#singleton_self_def?(node) ⇒ Boolean

Parameters:

  • node (Prism::Node)

Returns:

  • (Boolean)


155
156
157
# File 'lib/ryac/pipeline/compactor.rb', line 155

def singleton_self_def?(node)
  node.is_a?(Prism::DefNode) && node.receiver.is_a?(Prism::SelfNode)
end

#standard_error_redefined? ⇒ Boolean

Lazy: only a literal rescue StandardError clause ever needs the answer, so most programs never pay for the walk. Any definition-ish node whose written name is StandardError counts — over-matching a nested Foo::StandardError only forgoes a few bytes.

Returns:

  • (Boolean)


786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
# File 'lib/ryac/pipeline/compactor.rb', line 786

def standard_error_redefined?
  # a non-nil memo is always one of the two bools
  return @standard_error_redefined unless @standard_error_redefined.nil? # steep:ignore ReturnTypeMismatch

  found = false
  AstUtils.each_node(@prism_ast) do |node|
    case node
    when Prism::ClassNode
      path = node.constant_path
      name = path.is_a?(Prism::ConstantReadNode) || path.is_a?(Prism::ConstantPathNode) ? path.name : nil
      found = true if name == :StandardError
    when Prism::ConstantWriteNode, Prism::ConstantTargetNode
      found = true if node.name == :StandardError
    when Prism::ConstantPathWriteNode
      found = true if node.target.name == :StandardError
    when Prism::ConstantPathTargetNode
      found = true if node.name == :StandardError
    end
  end
  @standard_error_redefined = found
end

#wrap_operand(node, parent_op, side) ⇒ [String, bool]

Parameters:

  • node (Prism::Node, nil)
  • parent_op (Symbol)
  • side (Symbol)

Returns:

  • ([String, bool])


1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
# File 'lib/ryac/pipeline/compactor.rb', line 1035

def wrap_operand(node, parent_op, side)
  return ['', true] if node.nil?
  str = r(node)
  inner = AstUtils.unwrap_statements(node)
  return ["(#{str})", true] if AstUtils.logical_op?(inner)
  # ** binds tighter than unary minus: `(-a)**b` must not become -a**b.
  if parent_op == :** && inner.is_a?(Prism::CallNode) && %i[-@ +@].include?(inner.name)
    return ["(#{str})", true]
  end
  if binary_operator_call?(inner)
    pp = OPERATOR_PRECEDENCE[parent_op]
    # binary_operator_call? has established inner is a CallNode
    cp = OPERATOR_PRECEDENCE[inner.name] # steep:ignore NoMethod
    if pp && cp
      return ["(#{str})", true] if cp < pp
      return ["(#{str})", true] if cp == pp && side == :right
      return ["(#{str})", true] if cp == pp && NON_ASSOCIATIVE_OPS.include?(parent_op)
    end
  end
  [str, false]
end