Module: Audition::Rewriters::MagicComments

Defined in:
lib/audition/rewriters.rb

Overview

-- magic comments ----------------------------------------------

Defined Under Namespace

Classes: ConstantValues

Constant Summary collapse

SCV_LINE =
"# shareable_constant_value: literal\n"
FSL_LINE =
"# frozen_string_literal: true\n"

Class Method Summary collapse

Class Method Details

.comment_plan(file, line) ⇒ Object



137
138
139
140
141
142
143
144
145
146
# File 'lib/audition/rewriters.rb', line 137

def self.comment_plan(file, line)
  offset = file.magic_insertion_offset
  {
    edit: Autofix.new(start_offset: offset,
      end_offset: offset,
      replacement: line,
      safety: :unsafe),
    covers: ["mutable-constants"]
  }
end

.constant_collector(file) ⇒ Object



148
149
150
151
152
# File 'lib/audition/rewriters.rb', line 148

def self.constant_collector(file)
  collector = ConstantValues.new
  collector.visit(file.root)
  collector
end

.deep_literal?(node, classifier) ⇒ Boolean

Bare literals only: [...].freeze is a method call, and under the magic comment Ruby raises for it at assignment because the shallowly-frozen value is unshareable (verified on 4.0 with jwt's NAMED_CURVES).

Returns:

  • (Boolean)


117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/audition/rewriters.rb', line 117

def self.deep_literal?(node, classifier)
  case node
  when Prism::ArrayNode
    node.elements.all? do |element|
      deep_literal?(element, classifier)
    end
  when Prism::HashNode, Prism::KeywordHashNode
    node.elements.all? do |element|
      element.is_a?(Prism::AssocNode) &&
        deep_literal?(element.key, classifier) &&
        deep_literal?(element.value, classifier)
    end
  when Prism::CallNode
    false
  else
    %i[shareable mutable_string]
      .include?(classifier.classify(node))
  end
end

.mutates_own_constant?(file, assigned) ⇒ Boolean

Returns:

  • (Boolean)


103
104
105
106
107
108
109
110
111
# File 'lib/audition/rewriters.rb', line 103

def self.mutates_own_constant?(file, assigned)
  mutated = file.mutated_constants
  assigned.any? do |name|
    bare = name.split("::").last
    mutated.any? do |m|
      m == name || m.split("::").last == bare
    end
  end
end

.plan(file, findings) ⇒ Object

shareable_constant_value: literal raises at load for non-literal constant values (verified on 4.0), so it is only planned when every constant assignment in the file is a literal all the way down: an array literal holding a local or a call (Racc-generated parser tables are the canonical case) becomes an unshareable value that the magic comment rejects at load time. Otherwise, if the flagged values are all strings, frozen_string_literal covers them.



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
# File 'lib/audition/rewriters.rb', line 67

def self.plan(file, findings)
  return nil if file.shareable_constants?
  return nil if findings.none? { |f| f.check == "mutable-constants" }

  classifier = Static::LiteralClassifier.new(
    frozen_string_literal: file.frozen_string_literal?
  )
  collector = constant_collector(file)
  # A constant assigned here and mutated here is a
  # deliberate accumulator (sinatra's PARAMS_CONFIG);
  # freezing the file's constants would raise at the
  # mutation site.
  return nil if mutates_own_constant?(file, collector.names)

  values = collector.values
  kinds = values.map { |v| classifier.classify(v) }
  flagged = kinds.reject do |k|
    %i[shareable unknown].include?(k)
  end
  scv_ok = values.any? &&
    values.all? { |value| deep_literal?(value, classifier) }

  # Both branches demand something to fix: with no constant
  # values (a lone constant-mutation finding) or nothing
  # flagged, a comment would freeze unrelated code while
  # fixing nothing. An explicit `frozen_string_literal:
  # false` is an author opt-out and is never overridden.
  if scv_ok
    comment_plan(file, SCV_LINE)
  elsif flagged.any? &&
      file.magic_comment("frozen_string_literal").nil? &&
      flagged.all? { |k| k == :mutable_string }
    comment_plan(file, FSL_LINE)
  end
end