Module: Audition::Rewriters::WriteOnce

Defined in:
lib/audition/rewriters.rb

Overview

A variable written exactly once, at a scope where constant assignment is legal, with every other reference a plain read in the same file, converts mechanically to a frozen constant. Unsafe because cross-file readers are invisible.

Defined Under Namespace

Classes: Variables

Class Method Summary collapse

Class Method Details

.class_variables(file) ⇒ Object



260
261
262
263
264
265
266
# File 'lib/audition/rewriters.rb', line 260

def self.class_variables(file)
  collector = Variables.new(:cvar)
  collector.visit(file.root)
  convert(collector, file) do |name|
    name.delete_prefix("@@").upcase
  end
end

.convert(collector, file) ⇒ Object



268
269
270
271
272
273
274
275
276
277
278
279
280
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
309
310
311
312
313
314
315
316
317
318
319
320
321
322
# File 'lib/audition/rewriters.rb', line 268

def self.convert(collector, file)
  classifier = Static::LiteralClassifier.new(
    frozen_string_literal: file.frozen_string_literal?
  )
  edits = []
  collector.groups.each do |name, ops|
    writes = ops.select { |op| op[:kind] == :write }
    next unless writes.size == 1 && writes[0][:assignable]
    next unless (ops - writes).all? { |op| op[:kind] == :read }

    write = writes[0][:node]
    shareable = classifier.classify(write.value) == :shareable
    # A mutable value that would get deep-frozen must never be
    # the receiver of a call afterwards: `X[k] = v`, `X << v`
    # and friends would raise FrozenError at runtime. Reads of
    # immutable values are safe anywhere.
    unless shareable
      mutated = (ops - writes).any? do |op|
        collector.receiver_reads.include?(op[:node].object_id)
      end
      next if mutated
    end

    constant = yield(name.split("/").last)
    next unless constant.match?(/\A[A-Z][A-Z0-9_]*\z/)
    next if collector.taken_constants.include?(constant)

    edits << Autofix.new(
      start_offset: write.name_loc.start_offset,
      end_offset: write.name_loc.end_offset,
      replacement: constant,
      safety: :unsafe
    )
    unless shareable
      value = write.value
      source = value.location.slice
      edits << Autofix.new(
        start_offset: value.location.start_offset,
        end_offset: value.location.end_offset,
        replacement: "Ractor.make_shareable(#{source})",
        safety: :unsafe
      )
    end
    (ops - writes).each do |op|
      node = op[:node]
      edits << Autofix.new(
        start_offset: node.location.start_offset,
        end_offset: node.location.end_offset,
        replacement: constant,
        safety: :unsafe
      )
    end
  end
  edits
end

.globals(file) ⇒ Object



252
253
254
255
256
257
258
# File 'lib/audition/rewriters.rb', line 252

def self.globals(file)
  collector = Variables.new(:gvar)
  collector.visit(file.root)
  convert(collector, file) do |name|
    name.delete_prefix("$").upcase
  end
end

.plan(file, findings) ⇒ Object



241
242
243
244
245
246
247
248
249
250
# File 'lib/audition/rewriters.rb', line 241

def self.plan(file, findings)
  edits = []
  if findings.any? { |f| f.check == "global-variables" }
    edits += globals(file)
  end
  if findings.any? { |f| f.check == "class-variables" }
    edits += class_variables(file)
  end
  edits
end