Class: Woods::Extractors::CallbackAnalyzer

Inherits:
Object
  • Object
show all
Defined in:
lib/woods/extractors/callback_analyzer.rb

Overview

Analyzes callback method bodies to detect side effects.

Given a model’s composite source code (with inlined concerns) and its callback metadata, this analyzer finds each callback method body and classifies its side effects: column writes, job enqueues, service calls, mailer triggers, and database reads.

Examples:

analyzer = CallbackAnalyzer.new(
  source_code: model_source,
  column_names: %w[email status name]
)
enriched = analyzer.analyze(callback_hash)
enriched[:side_effects][:columns_written] #=> ["email"]

Constant Summary collapse

DB_READ_METHODS =

Database query methods that indicate a read operation.

%w[find where pluck first last].freeze
SINGLE_COLUMN_WRITERS =

Methods that write a single column, taking column name as first argument.

%w[update_column write_attribute].freeze
MULTI_COLUMN_WRITERS =

Methods that write multiple columns via keyword arguments.

%w[update_columns assign_attributes].freeze
ASYNC_METHODS =

Async enqueue methods that indicate a job is being dispatched.

%w[perform_later perform_async perform_in perform_at].freeze
SINGLE_COLUMN_WRITER_PATTERNS =

Pre-compiled regex patterns (avoid dynamic regex construction in hot loops)

SINGLE_COLUMN_WRITERS.to_h do |w|
  [w, /\b#{Regexp.escape(w)}\s*\(?\s*[:'"](\w+)/]
end.freeze
MULTI_COLUMN_WRITER_PATTERNS =
MULTI_COLUMN_WRITERS.to_h do |w|
  [w, /\b#{Regexp.escape(w)}\s*\(([^)]+)\)/m]
end.freeze
ASYNC_PATTERN =
/(\w+(?:Job|Worker))\.(?:#{ASYNC_METHODS.map { |m| Regexp.escape(m) }.join('|')})/
DB_READ_PATTERNS =
DB_READ_METHODS.to_h do |m|
  [m, /\.#{Regexp.escape(m)}\b/]
end.freeze

Instance Method Summary collapse

Constructor Details

#initialize(source_code:, column_names: []) ⇒ CallbackAnalyzer

Returns a new instance of CallbackAnalyzer.

Parameters:

  • source_code (String)

    Composite model source (with inlined concerns)

  • column_names (Array<String>) (defaults to: [])

    Model’s database column names



54
55
56
57
58
59
60
# File 'lib/woods/extractors/callback_analyzer.rb', line 54

def initialize(source_code:, column_names: [])
  @source_code = source_code
  @column_names = column_names.map(&:to_s)
  @parser = Ast::Parser.new
  @operation_extractor = FlowAnalysis::OperationExtractor.new
  @parsed_root = safe_parse
end

Instance Method Details

#analyze(callback_hash) ⇒ Hash

Analyze a single callback and enrich it with side-effect data.

Finds the callback’s method body in the source, scans it for side effects, and returns the original callback hash with an added :side_effects key.

Parameters:

  • callback_hash (Hash)

    Callback metadata from ModelExtractor: { type:, filter:, kind:, conditions: }

Returns:

  • (Hash)

    The callback hash with an added :side_effects key



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/woods/extractors/callback_analyzer.rb', line 71

def analyze(callback_hash)
  filter = callback_hash[:filter].to_s
  method_node = find_method_node(filter)

  return callback_hash.merge(side_effects: empty_side_effects) if method_node.nil?

  method_source = method_source_from_node(method_node)
  return callback_hash.merge(side_effects: empty_side_effects) if method_source.nil?

  callback_hash.merge(
    side_effects: {
      columns_written: detect_columns_written(method_source),
      jobs_enqueued: detect_jobs_enqueued(method_source),
      services_called: detect_services_called(method_source),
      mailers_triggered: detect_mailers_triggered(method_source),
      database_reads: detect_database_reads(method_source),
      operations: extract_operations(method_node)
    }
  )
end