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_variables(file) ⇒ Object
- .convert(collector, file) ⇒ Object
- .globals(file) ⇒ Object
- .plan(file, findings) ⇒ Object
Class Method Details
.class_variables(file) ⇒ Object
798 799 800 801 802 803 804 |
# File 'lib/audition/rewriters.rb', line 798 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
806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 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 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 |
# File 'lib/audition/rewriters.rb', line 806 def self.convert(collector, file) classifier = Static::LiteralClassifier.new( frozen_string_literal: file.frozen_string_literal? ) # The same bare @@name under two namespaces of one file is # usually same-file inheritance sharing one runtime # variable; converting either copy strands the other's # readers with a NameError. = collector.groups.keys .map { |key| key.split("/").last } .tally planned = [] bundles = [] collector.groups.each do |name, ops| next if [name.split("/").last] > 1 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 # A constructed object may gain singleton methods or be # mutated later (sinatra's @@eats_errors); wrapping it # in make_shareable freezes it and those break. next if Memoization.constructor?(write.value, classifier) 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. The same # goes for reads that escape into a local (`list = $x; # list << h`) or a call argument, where an alias can be # mutated out of sight. Reads of immutable values are # safe anywhere. unless shareable escapes = (ops - writes).any? do |op| id = op[:node].object_id collector.receiver_reads.include?(id) || collector.escaping_reads.include?(id) end next if escapes end constant = yield(name.split("/").last) next unless constant.match?(/\A[A-Z][A-Z0-9_]*\z/) next if collector.taken_constants.include?(constant) # `$max` and `$MAX` both map to MAX; only the first may # take the name, the second stays a variable. target = [name.rpartition("/").first, constant] next if planned.include?(target) planned << target 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 if value.is_a?(Prism::ArrayNode) && value.opening_loc.nil? source = "[#{source}]" end 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 bundles << Rewriters.bundle(ops, edits) end bundles.compact end |
.globals(file) ⇒ Object
790 791 792 793 794 795 796 |
# File 'lib/audition/rewriters.rb', line 790 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
779 780 781 782 783 784 785 786 787 788 |
# File 'lib/audition/rewriters.rb', line 779 def self.plan(file, findings) bundles = [] if findings.any? { |f| f.check == "global-variables" } bundles += globals(file) end if findings.any? { |f| f.check == "class-variables" } bundles += class_variables(file) end bundles end |