module ArchSpec::RailsReflector
The built-in Rails producer. archspec reflect boots the application in a separate process and records what Rails itself resolved: association targets and generated methods, custom validator classes, and the effects conditional concern callbacks had on each consumer. Normal analysis never requires Rails.
Constants
- ASSOCIATIONS
- GENERATED_ASSOCIATIONS
-
Macros that declare associations under derived names.
- MODULE_NAME
-
Module#name, immune to classes that redefine
namewith other meanings. - Site
- VALIDATIONS
Public Instance Methods
# File lib/archspec/rails_reflector.rb, line 42 def associations(facts, models, sites = macro_sites(facts.graph, models)) graph = facts.graph reflections = models.flat_map(&:reflect_on_all_associations) .uniq { |reflection| [reflection.active_record.name, reflection.name] } reflections.sort_by { |reflection| [reflection.active_record.name.to_s, reflection.name.to_s] }.each do |reflection| model = reflection.active_record next if model.name.nil? || graph.constants_named(model.name).empty? ancestors = graph.ancestor_names(model.name).first | Set[model.name] candidates = sites.fetch([reflection.macro, reflection.name.to_s], []).select { |site| ancestors.include?(site.owner) } source = source_node(graph, model, candidates) next unless source if candidates.size != 1 facts.gap(source: source, location: source.location, message: "association #{model.name}.#{reflection.name}: no unique literal declaration") next end site = candidates.first generated = model.generated_association_methods.instance_methods(false).map(&:to_s) names = [reflection.name.to_s, "#{reflection.name}="] & generated facts.methods(owner: source, location: site.location, names: names.sort) unless names.empty? if reflection.polymorphic? facts.gap(source: source, location: site.location, message: "polymorphic association #{model.name}.#{reflection.name}") next end begin facts.reference(source: source, target: reflection.klass, location: site.location) rescue NameError, ArgumentError, ActiveRecord::ActiveRecordError, Error => error facts.gap(source: source, location: site.location, message: "unresolved association #{model.name}.#{reflection.name}: #{error.message.lines.first.strip}") end end end
Source
# File lib/archspec/rails_reflector.rb, line 24 def boot(root) environment = File.join(root, 'config/environment.rb') raise Error, "no #{environment} found; the rails producer requires a Rails application" unless File.file?(environment) ENV['RAILS_ENV'] ||= 'development' require environment eager_load_application end
# File lib/archspec/rails_reflector.rb, line 34 def capture(facts, models: active_record_models, validated: validated_classes) sites = macro_sites(facts.graph, models | validated) associations(facts, models, sites) validators(facts, validated, sites) concerns(facts) facts end
Records every Rails fact for the loaded application.
Source
# File lib/archspec/rails_reflector.rb, line 115 def concerns(facts) graph = facts.graph callbacks = graph.conditional_callbacks return if callbacks.empty? unsettled = callbacks.map(&:concern).uniq.reject { |concern| runtime_constant(concern) }.to_set consumers(graph, callbacks).each do |node, applicable| consumer = runtime_constant(node.name) if consumer unsettled.merge(observe_mixins(facts, node, consumer, applicable)) observe_methods(facts, node, consumer, applicable) else unsettled.merge(applicable.map(&:concern)) end end callbacks.each do |callback| next if unsettled.include?(callback.concern) source = graph.constants_named(callback.concern).find { |constant| constant.path == callback.path } facts.resolve(source: source, location: callback.location, message: "conditional #{callback.kind} callback", consumers: graph.consumers_of(callback.concern)) end end
Conditional concern callbacks are gaps in static analysis. Once the application is loaded, each consumer shows which candidate mixins and methods it actually received. An effect is recorded only when exactly one candidate explains it; the gap is resolved only when every consumer could be inspected and every observed effect was attributed.
# File lib/archspec/rails_reflector.rb, line 80 def validators(facts, classes, sites = macro_sites(facts.graph, classes)) graph = facts.graph classes.to_h { |klass| [MODULE_NAME.bind_call(klass), klass] }.sort.each do |class_name, klass| next if class_name.nil? || graph.constants_named(class_name).empty? ancestors = graph.ancestor_names(class_name).first | Set[class_name] klass.validators.map(&:class).uniq.map { |validator| MODULE_NAME.bind_call(validator) }.compact.sort.each do |name| next if name.start_with?('ActiveModel::', 'ActiveRecord::') candidates = VALIDATIONS.flat_map { |macro| sites.fetch([macro, :validator], []) }.select do |site| ancestors.include?(site.owner) && validator_key?(site.name, name) end if candidates.empty? next if referenced?(graph, ancestors, name) source = source_node(graph, klass, []) if source facts.gap(source: source, location: source.location, message: "validator #{name} on #{class_name}: no literal declaration") end next end candidates.each do |site| source = site_source(graph, klass, site) facts.reference(source: source, target: name, location: site.location) if source end end end end
A validation such as validates :email, email: true names its validator class only by convention. The loaded model knows the class Rails resolved; the literal validates call supplies the location.