Class: Audition::Static::LiteralClassifier
- Inherits:
-
Object
- Object
- Audition::Static::LiteralClassifier
- Defined in:
- lib/audition/static/literal_classifier.rb
Overview
Classifies a Prism expression node by Ractor shareability:
:shareable proven deeply shareable
:mutable_string unfrozen String literal
:mutable_container Array/Hash/Set literal or constructor
:mutable_call unfrozen String, Regexp, or sentinel
Object returned by a call (`.tr`,
`format`, `Regexp.new`, `Object.new`)
:fresh_container unfrozen Array or Hash a core method
allocates on any receiver (`.map`,
`.keys`, `.merge`, `X + [..]`, `.dup`)
:unshareable_object a Method or UnboundMethod, never
shareable even frozen
:shallow_freeze frozen container with mutable elements
:sync_primitive Mutex/Queue/... constructor
:proc lambda or proc
:default_proc Hash.new with a block; the block
survives .freeze and stays unshareable
:instance_new unfrozen instance of an arbitrary class
:opaque_call method call with an unprovable return
:shallow_opaque frozen at the top, but what it holds
is an unprovable call result
:unknown cannot tell statically
Constant Summary collapse
- SYNC_PRIMITIVES =
%w[ Mutex Monitor Queue SizedQueue ConditionVariable Thread::Mutex Thread::Queue Thread::SizedQueue Thread::ConditionVariable ].freeze
- SHAREABLE_FACTORIES =
%w[Struct Class Module].freeze
- SENTINEL_FACTORIES =
Sentinels:
NOT_GIVEN = Object.newraises when read from a worker until frozen, and a frozen bare Object is shareable (verified on Ruby 4.0.6). BasicObject has no #freeze. %w[Object BasicObject].freeze
- UNFREEZABLE_COLLECTIONS =
Concurrent::Map defines no #freeze, so make_shareable raises NoMethodError on it; a constant holding one can never be shared (verified on 4.0.6 with concurrent-ruby).
%w[Concurrent::Map].freeze
- STRING_ONLY_METHODS =
Calls returning a fresh, unfrozen String or Regexp;
# frozen_string_literal: truecovers literals only. Both shapes (.trandRegexp.new) turn up in constants in the wild. These names belong to String alone in core, so any receiver qualifies. %i[ tr tr_s gsub sub squeeze strip lstrip rstrip chomp chop center ljust rjust encode scrub unicode_normalize b ].freeze
- STRINGIFIERS =
Fresh Strings off a literal receiver:
to_son anything but a String (String#to_s returns self),inspecton any literal, and a Regexp literal's source. %i[to_s inspect].freeze
- REGEXP_LITERAL_METHODS =
%i[source to_s inspect].freeze
- STRING_LITERAL_METHODS =
Unambiguous only on a String literal receiver: Symbols and numbers define these too and return shareable values.
%i[ + * % upcase downcase capitalize swapcase reverse dup succ next +@ ].freeze
- ARRAY_CHAIN_METHODS =
Array methods that hand back another Array, so a
.joinat the end of the chain still joins the literal it started from ([MAJOR, MINOR, PRE].compact.join(".")). %i[ compact map collect flatten uniq sort sort_by reverse reject select filter flat_map take drop rotate shuffle grep grep_v first last ].freeze
- FORMATTERS =
%i[format sprintf].freeze
- REGEXP_FACTORIES =
%i[new union compile].freeze
- FILE_PATH_METHODS =
File methods returning a fresh path String.
%i[ expand_path join dirname basename absolute_path realpath ].freeze
- SHAREABLE_RETURNS =
Calls returning a shareable primitive on any receiver.
%i[ to_i to_int to_f to_r to_c to_sym size length count bytesize ord hash ].freeze
- FRESH_ARRAY_METHODS =
Return-type contracts of core methods, executed against the running Ruby by literal_classifier_spec so no entry can drift from what Ruby does. Each list names methods whose result, on every core receiver that defines them, is a newly allocated object of the stated kind.
%i[ map collect flat_map collect_concat filter_map grep grep_v partition sort sort_by uniq flatten reverse rotate shuffle zip product take drop take_while drop_while entries keys values values_at ].freeze
- FRESH_HASH_METHODS =
%i[ merge invert transform_values transform_keys tally group_by except ].freeze
- FRESH_CONTAINER_METHODS =
Array on an Array or Range, Hash on a Hash; fresh either way.
%i[select filter reject compact].freeze
- FRESH_STRING_ARRAYS =
Arrays of freshly allocated Strings: a
.freezeon the result is shallow. %i[chars lines split scan].freeze
- SHAREABLE_ELEMENT_ARRAYS =
Arrays of Integers: a
.freezeon the result is enough. %i[bytes codepoints].freeze
- METHOD_OBJECTS =
Method objects are unshareable and freezing does not help (verified on Ruby 4.0.6).
%i[ method instance_method public_method public_instance_method ].freeze
- COMPARISONS =
Booleans, nil, or an Integer on any receiver.
%i[== != < > <= >= <=> === !].freeze
- ARITHMETIC =
Numeric on a Numeric receiver, whatever the operand.
%i[+ - * / % ** << >> & | ^].freeze
- OPERAND_TYPED =
Numeric on any receiver given a numeric literal operand: no core container accepts a number there. Array and String repeat with
*, String formats with%, Array appends with<<, so those three stay unproven. (ARITHMETIC - %i[* % <<]).freeze
- SET_OPERATORS =
Operators whose result takes its type from a literal operand:
X + [..]is an Array,X + "s"a String. %i[+ - | &].freeze
- NUMERIC_LITERALS =
[ Prism::IntegerNode, Prism::FloatNode, Prism::RationalNode, Prism::ImaginaryNode ].freeze
- SORBET_CASTS =
Sorbet's inline casts, which return their value argument.
%i[let cast must].freeze
Instance Method Summary collapse
-
#array_root(node) ⇒ Object
The Array literal a call chain starts from, when every link keeps it an Array; nil for any other receiver.
-
#begin_value(node) ⇒ Object
A begin block's value is its last statement.
-
#cast_value(node) ⇒ Object
The value inside a Sorbet inline cast, or nil.
-
#classify(node) ⇒ Symbol
Classification, see class docs.
-
#const_name(node) ⇒ Object
::MutexandMutexare the same constant for matching purposes; the leading colons are stripped. -
#fresh_elements(node) ⇒ Object
Whether a fresh container's elements are provably mutable (split strings), provably shareable (bytes), or unknown.
-
#fresh_string_owner(node) ⇒ Object
The core class whose method a fresh-string call names, for the finding's display: Array#join, Symbol#to_s, String#tr.
-
#fresh_type(node) ⇒ Object
What a fresh-container call allocates, for display.
-
#frozen_kind(value) ⇒ Object
What
value.freezewould classify as, for the check to choose a plain.freezeover a deep wrap: :shareable when every element is provably shareable, :shallow_freeze when one is provably mutable, :unknown otherwise. -
#initialize(frozen_string_literal:) ⇒ LiteralClassifier
constructor
A new instance of LiteralClassifier.
-
#unwrap(node) ⇒ Object
The value an expression hands back once begin blocks and inline casts are peeled off.
Constructor Details
#initialize(frozen_string_literal:) ⇒ LiteralClassifier
Returns a new instance of LiteralClassifier.
135 136 137 |
# File 'lib/audition/static/literal_classifier.rb', line 135 def initialize(frozen_string_literal:) @frozen_string_literal = frozen_string_literal end |
Instance Method Details
#array_root(node) ⇒ Object
The Array literal a call chain starts from, when every link keeps it an Array; nil for any other receiver.
240 241 242 243 244 245 246 247 248 249 250 251 252 253 |
# File 'lib/audition/static/literal_classifier.rb', line 240 def array_root(node) loop do case node when Prism::ArrayNode return node when Prism::CallNode return nil unless ARRAY_CHAIN_METHODS.include?(node.name) node = node.receiver else return nil end end end |
#begin_value(node) ⇒ Object
A begin block's value is its last statement. A rescue, else, or ensure clause can supply a different one, so only the plain form resolves.
191 192 193 194 195 196 197 |
# File 'lib/audition/static/literal_classifier.rb', line 191 def begin_value(node) return node unless node.is_a?(Prism::BeginNode) && node.rescue_clause.nil? && node.else_clause.nil? && node.ensure_clause.nil? node.statements&.body&.last || node end |
#cast_value(node) ⇒ Object
The value inside a Sorbet inline cast, or nil. The cast returns its argument, so fixes and type names belong on the value, not the cast.
213 214 215 216 217 218 219 |
# File 'lib/audition/static/literal_classifier.rb', line 213 def cast_value(node) return nil unless node.is_a?(Prism::CallNode) return nil unless const_name(node.receiver) == "T" && SORBET_CASTS.include?(node.name) node.arguments&.arguments&.first end |
#classify(node) ⇒ Symbol
Returns classification, see class docs.
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 |
# File 'lib/audition/static/literal_classifier.rb', line 141 def classify(node) node = begin_value(node) case node when Prism::IntegerNode, Prism::FloatNode, Prism::RationalNode, Prism::ImaginaryNode, Prism::SymbolNode, Prism::InterpolatedSymbolNode, Prism::TrueNode, Prism::FalseNode, Prism::NilNode, Prism::RegularExpressionNode, Prism::InterpolatedRegularExpressionNode :shareable when Prism::StringNode @frozen_string_literal ? :shareable : :mutable_string when Prism::InterpolatedStringNode classify_interpolated_string(node) when Prism::ArrayNode, Prism::HashNode, Prism::KeywordHashNode container_kind(node) when Prism::RangeNode ends = [node.left, node.right].compact if ends.all? { |n| classify(n) == :shareable } :shareable else :unknown end when Prism::LambdaNode :proc when Prism::CallNode classify_call(node) when Prism::IfNode ternary_kind(node) else :unknown end end |
#const_name(node) ⇒ Object
::Mutex and Mutex are the same constant for matching
purposes; the leading colons are stripped.
201 202 203 204 205 206 207 208 |
# File 'lib/audition/static/literal_classifier.rb', line 201 def const_name(node) case node when Prism::ConstantReadNode node.name.to_s when Prism::ConstantPathNode node.location.slice.delete_prefix("::") end end |
#fresh_elements(node) ⇒ Object
Whether a fresh container's elements are provably mutable (split strings), provably shareable (bytes), or unknown.
294 295 296 297 298 299 |
# File 'lib/audition/static/literal_classifier.rb', line 294 def fresh_elements(node) return :mutable if FRESH_STRING_ARRAYS.include?(node.name) return :shareable if SHAREABLE_ELEMENT_ARRAYS.include?(node.name) :unknown end |
#fresh_string_owner(node) ⇒ Object
The core class whose method a fresh-string call names, for the finding's display: Array#join, Symbol#to_s, String#tr.
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 |
# File 'lib/audition/static/literal_classifier.rb', line 257 def fresh_string_owner(node) receiver = node.receiver if node.name == :join && array_root(receiver) "Array" elsif receiver.is_a?(Prism::SymbolNode) "Symbol" elsif receiver.is_a?(Prism::RegularExpressionNode) "Regexp" elsif NUMERIC_LITERALS.any? { |type| receiver.is_a?(type) } "Integer" elsif receiver.is_a?(Prism::ArrayNode) "Array" elsif receiver.is_a?(Prism::HashNode) "Hash" else "String" end end |
#fresh_type(node) ⇒ Object
What a fresh-container call allocates, for display.
277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/audition/static/literal_classifier.rb', line 277 def fresh_type(node) name = node.name return "Hash" if FRESH_HASH_METHODS.include?(name) return "copy" if name == :dup if name == :each_with_object return literal_container_type(first_argument(node)) end if SET_OPERATORS.include?(name) return literal_container_type(first_argument(node)) end return "container" if FRESH_CONTAINER_METHODS.include?(name) "Array" end |
#frozen_kind(value) ⇒ Object
What value.freeze would classify as, for the check to
choose a plain .freeze over a deep wrap: :shareable
when every element is provably shareable, :shallow_freeze
when one is provably mutable, :unknown otherwise.
225 226 227 228 229 230 231 232 233 234 235 236 |
# File 'lib/audition/static/literal_classifier.rb', line 225 def frozen_kind(value) case value when Prism::ArrayNode, Prism::HashNode, Prism::KeywordHashNode deep_classify(value.elements) when Prism::CallNode elements = set_elements(value) elements ? deep_classify(elements) : :unknown else :unknown end end |
#unwrap(node) ⇒ Object
The value an expression hands back once begin blocks and inline casts are peeled off.
178 179 180 181 182 183 184 185 186 |
# File 'lib/audition/static/literal_classifier.rb', line 178 def unwrap(node) loop do inner = begin_value(node) inner = cast_value(inner) || inner break node if inner.equal?(node) node = inner end end |