Class: Greenroom::CLI::Check::Analyzer

Inherits:
Object
  • Object
show all
Defined in:
lib/greenroom/cli/check.rb

Defined Under Namespace

Classes: Experiment, Scope

Constant Summary collapse

SourceError =
Class.new(StandardError)

Instance Method Summary collapse

Constructor Details

#initialize(before, after) ⇒ Analyzer

Returns a new instance of Analyzer.



112
113
114
115
116
117
# File 'lib/greenroom/cli/check.rb', line 112

def initialize(before, after)
  # Syntax trees omit source locations. Therefore, comment and white
  # space positions do not affect the acceptance result.
  @before = parse(before)
  @after = parse(after)
end

Instance Method Details

#checkObject



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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
172
173
174
175
176
177
178
179
180
# File 'lib/greenroom/cli/check.rb', line 119

def check
  return reject("Unsupported Ruby method or class syntax is present.") if unsupported?(@before) || unsupported?(@after)

  before_scopes = scopes(@before)
  after_scopes = scopes(@after)
  return reject("The class and module structure changed.") unless before_scopes.keys == after_scopes.keys
  return reject("The class or module header changed.") unless before_scopes.all? { |name, scope| same_header?(scope, after_scopes.fetch(name)) }
  return reject("A top-level statement changed.") unless top_level(@before) == top_level(@after)

  changed = []
  added = []
  before_scopes.each do |name, before_scope|
    after_scope = after_scopes.fetch(name)
    removed_names = before_scope.methods.keys - after_scope.methods.keys
    return reject("An existing method was removed.") unless removed_names.empty?

    common_names = before_scope.methods.keys & after_scope.methods.keys
    common_names.each do |method_name|
      changed << [name, before_scope.methods.fetch(method_name), after_scope.methods.fetch(method_name)] unless same_ast?(before_scope.methods.fetch(method_name), after_scope.methods.fetch(method_name))
    end
    (after_scope.methods.keys - before_scope.methods.keys).each do |method_name|
      added << [name, after_scope.methods.fetch(method_name)]
    end
  end

  return reject("Exactly one existing method must change.") unless changed.one?
  return reject("Exactly one candidate method must be added.") unless added.one?

  scope_name, before_method, after_method = changed.first
  added_scope_name, candidate = added.first
  return reject("The candidate method must be in the changed class.") unless scope_name == added_scope_name
  return reject("The candidate method must be private.") unless private_method?(after_scopes.fetch(scope_name), candidate)
  return reject("An existing method visibility changed.") unless same_visibilities?(before_scopes.fetch(scope_name), after_scopes.fetch(scope_name))
  return reject("Another class body statement changed.") unless unchanged_statements?(before_scopes.fetch(scope_name), after_scopes.fetch(scope_name), candidate)
  return reject("The changed method signature must stay the same.") unless same_ast?(before_method.children[1], after_method.children[1])
  other_scope_changed = before_scopes.any? do |name, before_scope|
    next false if name == scope_name

    after_scope = after_scopes.fetch(name)
    !unchanged_statements?(before_scope, after_scope)
  end
  return reject("Another class or module changed.") if other_scope_changed

  science = science_blocks(after_method)
  unless science
    shape_error = science_shape_error(after_method)
    return reject(shape_error || "The changed method body must contain one Scientist.run call.")
  end

  use_block, try_block, _compare_block, experiment, science_statement = science
  return reject("The use block must match the old method body.") unless matching_span?(before_method, after_method, science_statement, use_block)
  # Tests found behavior changes when statements move into a block.
  # A return in the moved part bypasses compare and publish.
  # A binding sees the block's local variables. Tests found the same
  # behavior for yield, rescue, ensure, __method__, local variables,
  # and constant references, so those forms remain valid.
  return reject("A return in a use or try block is unsafe.") if unsafe_return?(use_block) || unsafe_return?(try_block)
  return reject("A binding in a use or try block is unsafe.") if binding_call?(use_block) || binding_call?(try_block)

  candidate_name = candidate.children[0]
  Result.new(true, "Accepted experiment #{experiment} with candidate #{candidate_name}.", experiment, candidate_name.to_s)
end

#experimentsObject

The judge uses the same structural relation that the merge gate verifies. A second parser could accept a shape that the gate rejects.



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/greenroom/cli/check.rb', line 184

def experiments
  scopes(@after).values.flat_map do |scope|
    scope.methods.values.filter_map do |method|
      science = science_blocks(method)
      next unless science

      use_block, try_block, _compare_block, name = science
      try_body = block_body(try_block)
      next unless try_body&.send_type? && try_body.receiver.nil?

      candidate = scope.methods[try_body.method_name]
      Experiment.new(name, method, use_block, candidate) if candidate
    end
  end
end