Class: Audition::Static::GraphAudit

Inherits:
Object
  • Object
show all
Defined in:
lib/audition/static/graph_audit.rb

Overview

Whole-program semantic checks backed by the rubydex graph. rubydex resolves state to its true owner, so an ivar written in the class body, in def self.x, and inside class << self, across several files, unifies into one declaration owned by the singleton class. Per-file AST visitors cannot see that.

Constant Summary collapse

CVAR_WHY =
"Class variables cannot be accessed from non-main Ractors " \
"at all; both reads and writes raise " \
"Ractor::IsolationError (\"can not access class variables " \
"from non-main Ractors\")."
CVAR_FIX =
"Replace with a deeply frozen constant, per-instance " \
"state, or Ractor-local storage (Ractor.current[:key], " \
"Ractor.store_if_absent)."
STATE_WHY =
"This instance variable lives on the class/module object, " \
"which is shared across Ractors; non-main Ractors raise " \
"Ractor::IsolationError when writing it, and when reading " \
"it while it holds a non-shareable value (verified on " \
"Ruby 4.0)."
STATE_FIX =
"Precompute and freeze the value at load time (for " \
"per-subclass values, in the inherited hook). For " \
"collections, rebuild and refreeze on write, " \
"Rails-style copy-on-write: self.list = " \
"(list + [item]).freeze; never mutate in place. As a " \
"last resort use Ractor.store_if_absent for lazy " \
"initialization or per-Ractor state in " \
"Ractor.current[:key]."
FROZEN_MEMO_WHY =
"Every write memoizes a shareable (frozen) value, so " \
"non-main Ractors can read it once it has been " \
"computed; only the first write must happen on the " \
"main Ractor, or it raises Ractor::IsolationError. " \
"This is the pattern Rails core uses for its own " \
"memoized class state."
FROZEN_MEMO_FIX =
"Warm the cache at boot, before spawning Ractors: call " \
"the memoizing method from an initializer or on_load " \
"hook. If the value genuinely must be computed at " \
"runtime, proxy the write to the main Ractor or use " \
"Ractor.store_if_absent."

Instance Method Summary collapse

Instance Method Details

#analyze_paths(paths) ⇒ Array<Finding>

rubydex's index_all descends directories but skips bare file lists, so files are fed through index_source individually.

Parameters:

  • paths (Array<String>)

    files to index and audit

Returns:



69
70
71
72
73
74
75
76
77
# File 'lib/audition/static/graph_audit.rb', line 69

def analyze_paths(paths)
  sources = {}
  paths.each do |path|
    sources[path] = File.read(path)
  rescue SystemCallError
    next
  end
  analyze_sources(sources)
end

#analyze_sources(sources) ⇒ Array<Finding>

Parameters:

  • sources (Hash{String => String})

    path => source

Returns:



54
55
56
57
58
59
60
61
62
# File 'lib/audition/static/graph_audit.rb', line 54

def analyze_sources(sources)
  graph = Rubydex::Graph.new
  sources.each do |path, code|
    graph.index_source(path, code, "ruby")
  end
  @sources = sources
  @frozen_memos = frozen_memo_map(sources)
  audit(graph)
end