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



573
574
575
576
577
578
579
# File 'lib/audition/rewriters.rb', line 573

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



581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
# File 'lib/audition/rewriters.rb', line 581

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]
    kind = classifier.classify(write.value)
    next if kind == :sync_primitive

    shareable = kind == :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



565
566
567
568
569
570
571
# File 'lib/audition/rewriters.rb', line 565

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



554
555
556
557
558
559
560
561
562
563
# File 'lib/audition/rewriters.rb', line 554

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