Class: RailsRoutesAnalyzer::RouteAnalysis

Inherits:
Object
  • Object
show all
Defined in:
lib/rails_routes_analyzer/route_analysis.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app: Rails.application, verbose: false, only_only: false, only_except: false) ⇒ RouteAnalysis

Returns a new instance of RouteAnalysis.



14
15
16
17
18
19
20
21
# File 'lib/rails_routes_analyzer/route_analysis.rb', line 14

def initialize(app: Rails.application, verbose: false, only_only: false, only_except: false)
  self.app         = app
  self.verbose     = verbose
  self.only_only   = only_only
  self.only_except = only_except

  analyze!
end

Instance Attribute Details

#appObject

Returns the value of attribute app.



11
12
13
# File 'lib/rails_routes_analyzer/route_analysis.rb', line 11

def app
  @app
end

#only_exceptObject

Returns the value of attribute only_except.



11
12
13
# File 'lib/rails_routes_analyzer/route_analysis.rb', line 11

def only_except
  @only_except
end

#only_onlyObject

Returns the value of attribute only_only.



11
12
13
# File 'lib/rails_routes_analyzer/route_analysis.rb', line 11

def only_only
  @only_only
end

#route_callsObject

Returns the value of attribute route_calls.



12
13
14
# File 'lib/rails_routes_analyzer/route_analysis.rb', line 12

def route_calls
  @route_calls
end

#route_linesObject

Returns the value of attribute route_lines.



12
13
14
# File 'lib/rails_routes_analyzer/route_analysis.rb', line 12

def route_lines
  @route_lines
end

#route_logObject

Returns the value of attribute route_log.



12
13
14
# File 'lib/rails_routes_analyzer/route_analysis.rb', line 12

def route_log
  @route_log
end

#verboseObject

Returns the value of attribute verbose.



11
12
13
# File 'lib/rails_routes_analyzer/route_analysis.rb', line 11

def verbose
  @verbose
end

Instance Method Details

#all_unique_issues_file_namesObject



134
135
136
# File 'lib/rails_routes_analyzer/route_analysis.rb', line 134

def all_unique_issues_file_names
  issues.map(&:full_filename).uniq.sort
end

#analyse_route_call(**kwargs) ⇒ Object



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/rails_routes_analyzer/route_analysis.rb', line 68

def analyse_route_call(**kwargs)
  controller_class_name = "#{kwargs[:controller_name]}_controller".camelize

  opts = kwargs.merge(controller_class_name: controller_class_name)

  route_call = RouteCall.new(opts)
  route_calls << route_call

  controller = nil
  begin
    controller = Object.const_get(controller_class_name)
  rescue LoadError, RuntimeError, NameError => e
    route_call.add_issue RouteIssue::NoController.new(error: e.message)
    return
  end

  if controller.nil?
    route_call.add_issue RouteIssue::NoController.new(error: "#{controller_class_name} is nil")
    return
  end

  analyze_action_availability(controller, route_call, **opts)
end

#analyze!Object



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/rails_routes_analyzer/route_analysis.rb', line 39

def analyze!
  clear_data
  prepare_for_analysis

  RouteInterceptor.route_data.each do |(file_location, route_creation_method, controller_name), action_names|
    analyse_route_call(
      file_location:         file_location,
      route_creation_method: route_creation_method,
      controller_name:       controller_name,
      action_names:          action_names.uniq.sort,
    )
  end

  route_log.concat RouteInterceptor.route_log
  generate_route_lines
end

#analyze_action_availability(controller, route_call, **opts) ⇒ Object

Checks which if any actions referred to by the route don’t exist.



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/rails_routes_analyzer/route_analysis.rb', line 93

def analyze_action_availability(controller, route_call, **opts)
  present, missing = opts[:action_names].partition { |name| controller.action_methods.include?(name.to_s) }

  route_call[:present_actions] = present if present.any?

  if SINGLE_METHODS.include?(opts[:route_creation_method])
    # NOTE a single call like 'get' can add multiple actions if called in a loop
    if missing.present?
      route_call.add_issue RouteIssue::NoAction.new(missing_actions: missing)
    end
    return
  end

  return if missing.empty? # Everything is perfect, all routes match an action

  if present.sort == RESOURCE_ACTIONS.sort
    # Should happen only if RESOURCE_ACTIONS doesn't match which actions rails supports
    raise "shouldn't get all methods being present and yet some missing at the same time: #{present.inspect} #{missing.inspect}"
  end

  suggested_param = resource_route_suggested_param(present)

  route_call.add_issue RouteIssue::Resources.new(suggested_param: suggested_param)
end

#clear_dataObject



23
24
25
26
27
# File 'lib/rails_routes_analyzer/route_analysis.rb', line 23

def clear_data
  self.route_lines = []
  self.route_calls = []
  self.route_log   = []
end

#generate_route_linesObject



56
57
58
59
60
61
62
63
64
65
66
# File 'lib/rails_routes_analyzer/route_analysis.rb', line 56

def generate_route_lines
  calls_per_line = route_calls.group_by do |record|
    [record.full_filename, record.line_number]
  end

  calls_per_line.each do |(full_filename, line_number), records|
    route_lines << RouteLine.new(full_filename: full_filename,
                                 line_number:   line_number,
                                 records:       records)
  end
end

#implemented_routesObject



142
143
144
145
146
147
148
149
150
# File 'lib/rails_routes_analyzer/route_analysis.rb', line 142

def implemented_routes
  Set.new.tap do |implemented_routes|
    route_calls.each do |route_call|
      (route_call.present_actions || []).each do |action|
        implemented_routes << [route_call.controller_class_name, action]
      end
    end
  end
end

#issuesObject



126
127
128
# File 'lib/rails_routes_analyzer/route_analysis.rb', line 126

def issues
  route_calls.select(&:issue?)
end

#non_issuesObject



130
131
132
# File 'lib/rails_routes_analyzer/route_analysis.rb', line 130

def non_issues
  route_calls.reject(&:issue?)
end

#prepare_for_analysisObject



29
30
31
32
33
34
35
36
37
# File 'lib/rails_routes_analyzer/route_analysis.rb', line 29

def prepare_for_analysis
  app.eager_load! # all controller classes need to be loaded

  ::ActionDispatch::Routing::Mapper::Mapping.prepend RouteInterceptor

  RouteInterceptor.route_log.clear

  app.reload_routes!
end


156
157
158
159
160
161
162
163
164
165
# File 'lib/rails_routes_analyzer/route_analysis.rb', line 156

def print_report
  if issues.empty?
    puts "No route issues found"
    return
  end

  issues.each do |issue|
    puts issue.human_readable_error(verbose: verbose)
  end
end

#resource_route_suggested_param(present) ⇒ Object



118
119
120
121
122
123
124
# File 'lib/rails_routes_analyzer/route_analysis.rb', line 118

def resource_route_suggested_param(present)
  if (present.size < 4 || only_only) && !only_except
    "only: [#{present.sort.map { |x| ":#{x}" }.join(', ')}]"
  else
    "except: [#{(RESOURCE_ACTIONS - present).sort.map { |x| ":#{x}" }.join(', ')}]"
  end
end

#route_calls_for_file_name(full_filename) ⇒ Object



138
139
140
# File 'lib/rails_routes_analyzer/route_analysis.rb', line 138

def route_calls_for_file_name(full_filename)
  route_calls.select { |record| record.full_filename == full_filename.to_s }
end

#route_lines_for_file(full_filename) ⇒ Object



152
153
154
# File 'lib/rails_routes_analyzer/route_analysis.rb', line 152

def route_lines_for_file(full_filename)
  route_lines.select { |line| line.full_filename == full_filename.to_s }
end