rails_routes_analyzer 1.0.4 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.autotest +3 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +141 -0
- data/LICENSE.txt +1 -1
- data/README.md +45 -13
- data/Rakefile +1 -1
- data/lib/rails_routes_analyzer.rb +7 -6
- data/lib/rails_routes_analyzer/action_analysis.rb +276 -0
- data/lib/rails_routes_analyzer/gem_manager.rb +52 -0
- data/lib/rails_routes_analyzer/parameter_handler.rb +41 -0
- data/lib/rails_routes_analyzer/rails_routes_analyzer.rb +69 -30
- data/lib/rails_routes_analyzer/railtie.rb +11 -0
- data/lib/rails_routes_analyzer/route_analysis.rb +104 -67
- data/lib/rails_routes_analyzer/route_call.rb +65 -0
- data/lib/rails_routes_analyzer/route_file_annotator.rb +117 -37
- data/lib/rails_routes_analyzer/route_interceptor.rb +12 -13
- data/lib/rails_routes_analyzer/route_issue.rb +4 -109
- data/lib/rails_routes_analyzer/route_issue/base.rb +68 -0
- data/lib/rails_routes_analyzer/route_issue/no_action.rb +37 -0
- data/lib/rails_routes_analyzer/route_issue/no_controller.rb +30 -0
- data/lib/rails_routes_analyzer/route_issue/resources.rb +133 -0
- data/lib/rails_routes_analyzer/route_line.rb +85 -0
- data/lib/rails_routes_analyzer/version.rb +1 -1
- data/lib/tasks/rails_routes_analyzer.rake +28 -50
- data/rails_routes_analyzer.gemspec +13 -4
- metadata +129 -18
@@ -0,0 +1,52 @@
|
|
1
|
+
module RailsRoutesAnalyzer
|
2
|
+
module GemManager
|
3
|
+
extend self
|
4
|
+
|
5
|
+
# Replaces gem locations in file paths with the name of the gem.
|
6
|
+
#
|
7
|
+
# @param location [String] full path to a ruby source file.
|
8
|
+
# @return [String] path to ruby source file with gem location replaced.
|
9
|
+
def clean_gem_path(location)
|
10
|
+
location.gsub(gem_path_prefix_cleanup_regex) do |val|
|
11
|
+
gem_path_prefix_replacements[val] || val
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Identifies a gem based on a location from a backtrace.
|
16
|
+
#
|
17
|
+
# @param location [String] full path to a source file possibly in a gem.
|
18
|
+
# @return [String] name of a gem.
|
19
|
+
def identify_gem(location)
|
20
|
+
gem_locations[location[gem_locations_regexp]]
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# @return { String => String } mapping of gem path prefix to gem name.
|
26
|
+
def gem_path_prefix_replacements
|
27
|
+
@gem_path_prefix_replacements ||=
|
28
|
+
Gem.loaded_specs.values.each_with_object({}) do |spec, sum|
|
29
|
+
path = spec.full_gem_path.sub %r{/?\z}, '/'
|
30
|
+
sum[path] = "#{spec.name} @ "
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [Regexp] a regexp that matches all gem paths.
|
35
|
+
def gem_path_prefix_cleanup_regex
|
36
|
+
@gem_path_prefix_cleanup_regex ||=
|
37
|
+
/\A#{Regexp.union(gem_path_prefix_replacements.keys)}/
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return {String=>String} mapping of gem path to gem name.
|
41
|
+
def gem_locations
|
42
|
+
@gem_locations ||= Gem.loaded_specs.values.each_with_object({}) do |spec, sum|
|
43
|
+
sum[spec.full_gem_path] = spec.name
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [Regexp] a regexp covering paths of all available gems.
|
48
|
+
def gem_locations_regexp
|
49
|
+
@gem_locations_regexp ||= /\A#{Regexp.union(gem_locations.keys)}/
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module RailsRoutesAnalyzer
|
2
|
+
module ParameterHandler
|
3
|
+
def self.params_for_route_analysis(env = ENV)
|
4
|
+
{
|
5
|
+
only_only: env['ONLY_ONLY'].present?,
|
6
|
+
only_except: env['ONLY_EXCEPT'].present?,
|
7
|
+
verbose: env['ROUTES_VERBOSE'].present?,
|
8
|
+
}
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.params_for_annotate(env = ENV, extras = [])
|
12
|
+
params_for_route_analysis.merge(
|
13
|
+
try_to_fix: false,
|
14
|
+
allow_deleting: false,
|
15
|
+
force_overwrite: env['ROUTES_FORCE'].present? || extras.include?('force'),
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.file_to_annotate(env = ENV)
|
20
|
+
env['ROUTES_FILE']
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.params_for_fix(env = ENV, extras = [])
|
24
|
+
params_for_route_analysis.merge(
|
25
|
+
try_to_fix: true,
|
26
|
+
allow_deleting: true,
|
27
|
+
force_overwrite: env['ROUTES_FORCE'].present? || extras.include?('force'),
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.params_for_action_analysis(env = ENV, extras = [])
|
32
|
+
{
|
33
|
+
report_duplicates: env['ROUTES_DUPLICATES'].present? || extras.include?('duplicates'),
|
34
|
+
report_gems: env['ROUTES_GEMS'].present? || extras.include?('gems'),
|
35
|
+
report_modules: env['ROUTES_MODULES'].present? || extras.include?('modules'),
|
36
|
+
full_path: env['ROUTES_FULL_PATH'].present? || extras.include?('full'),
|
37
|
+
metadata: env['ROUTES_METADATA'].present? || extras.include?('metadata'),
|
38
|
+
}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -1,45 +1,84 @@
|
|
1
|
-
|
1
|
+
require_relative 'gem_manager'
|
2
2
|
|
3
3
|
module RailsRoutesAnalyzer
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
MULTI_METHODS = %w(resource resources).freeze
|
6
|
+
SINGLE_METHODS = %w(match get head post patch put delete options root).freeze
|
7
|
+
ROUTE_METHODS = (MULTI_METHODS + SINGLE_METHODS).freeze
|
8
|
+
|
9
|
+
class << self
|
10
|
+
# Converts Rails.root-relative filenames to be absolute.
|
11
|
+
def get_full_filename(filename)
|
12
|
+
return filename.to_s if filename.to_s.starts_with?('/')
|
13
|
+
Rails.root.join(filename).to_s
|
8
14
|
end
|
9
|
-
end
|
10
15
|
|
11
|
-
|
12
|
-
|
13
|
-
|
16
|
+
# Shortens full file path, replacing Rails.root and gem path with
|
17
|
+
# appropriate short prefixes to make the file names look good.
|
18
|
+
def sanitize_source_location(source_location, full_path: false)
|
19
|
+
source_location.dup.tap do |clean_location|
|
20
|
+
unless full_path
|
21
|
+
clean_location.gsub! "#{Rails.root}/", './'
|
14
22
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
23
|
+
clean_location.replace GemManager.clean_gem_path(clean_location)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
19
27
|
|
20
|
-
|
28
|
+
def routes_dead(env)
|
29
|
+
params = RailsRoutesAnalyzer::ParameterHandler.params_for_route_analysis(env)
|
30
|
+
analysis = RailsRoutesAnalyzer::RouteAnalysis.new(params)
|
21
31
|
|
22
|
-
|
23
|
-
|
24
|
-
end
|
32
|
+
analysis.print_report
|
33
|
+
end
|
25
34
|
|
26
|
-
|
27
|
-
|
28
|
-
|
35
|
+
def routes_dead_annotate(env)
|
36
|
+
routes_dead_annotate_common(env)
|
37
|
+
end
|
29
38
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
action_methods = controller_class.action_methods
|
39
|
+
def routes_dead_annotate_inplace(env, extras)
|
40
|
+
routes_dead_annotate_common(env, extras, inplace: true)
|
41
|
+
end
|
34
42
|
|
35
|
-
|
36
|
-
|
37
|
-
|
43
|
+
def routes_dead_fix(env)
|
44
|
+
routes_dead_fix_common(env)
|
45
|
+
end
|
38
46
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
47
|
+
def routes_dead_fix_inplace(env, extras)
|
48
|
+
routes_dead_fix_common(env, extras, inplace: true)
|
49
|
+
end
|
50
|
+
|
51
|
+
def routes_actions_missing_route(env, extras)
|
52
|
+
routes_actions_common(env, extras, report_routed: false)
|
53
|
+
end
|
54
|
+
|
55
|
+
def routes_actions_list_all(env, extras)
|
56
|
+
routes_actions_common(env, extras, report_routed: true)
|
57
|
+
end
|
58
|
+
|
59
|
+
protected
|
60
|
+
|
61
|
+
def routes_dead_common(env, params, **opts)
|
62
|
+
annotator = RailsRoutesAnalyzer::RouteFileAnnotator.new(params)
|
63
|
+
routes_file = RailsRoutesAnalyzer::ParameterHandler.file_to_annotate(env)
|
64
|
+
|
65
|
+
annotator.annotate_routes_file(routes_file, **opts)
|
66
|
+
end
|
67
|
+
|
68
|
+
def routes_dead_annotate_common(env, extras = [], **opts)
|
69
|
+
params = RailsRoutesAnalyzer::ParameterHandler.params_for_annotate(env, extras)
|
70
|
+
routes_dead_common(env, params, **opts)
|
71
|
+
end
|
72
|
+
|
73
|
+
def routes_dead_fix_common(env, extras = [], **opts)
|
74
|
+
params = RailsRoutesAnalyzer::ParameterHandler.params_for_fix(env, extras)
|
75
|
+
routes_dead_common(env, params, **opts)
|
76
|
+
end
|
77
|
+
|
78
|
+
def routes_actions_common(env, extras, **opts)
|
79
|
+
params = RailsRoutesAnalyzer::ParameterHandler.params_for_action_analysis(env, extras)
|
80
|
+
analysis = RailsRoutesAnalyzer::ActionAnalysis.new(params.merge(opts))
|
81
|
+
analysis.print_report
|
43
82
|
end
|
44
83
|
end
|
45
84
|
|
@@ -1,9 +1,15 @@
|
|
1
|
+
require_relative 'route_line'
|
2
|
+
require_relative 'route_call'
|
3
|
+
require_relative 'route_issue'
|
4
|
+
require_relative 'route_interceptor'
|
5
|
+
|
1
6
|
module RailsRoutesAnalyzer
|
2
7
|
|
8
|
+
RESOURCE_ACTIONS = [:index, :create, :new, :show, :update, :destroy, :edit].freeze
|
9
|
+
|
3
10
|
class RouteAnalysis
|
4
11
|
attr_accessor :app, :verbose, :only_only, :only_except
|
5
|
-
attr_accessor :route_log
|
6
|
-
attr_writer :all_issues
|
12
|
+
attr_accessor :route_log, :route_lines, :route_calls
|
7
13
|
|
8
14
|
def initialize(app: Rails.application, verbose: false, only_only: false, only_except: false)
|
9
15
|
self.app = app
|
@@ -14,7 +20,13 @@ module RailsRoutesAnalyzer
|
|
14
20
|
analyze!
|
15
21
|
end
|
16
22
|
|
17
|
-
def
|
23
|
+
def clear_data
|
24
|
+
self.route_lines = []
|
25
|
+
self.route_calls = []
|
26
|
+
self.route_log = []
|
27
|
+
end
|
28
|
+
|
29
|
+
def prepare_for_analysis
|
18
30
|
app.eager_load! # all controller classes need to be loaded
|
19
31
|
|
20
32
|
::ActionDispatch::Routing::Mapper::Mapping.prepend RouteInterceptor
|
@@ -22,110 +34,135 @@ module RailsRoutesAnalyzer
|
|
22
34
|
RouteInterceptor.route_log.clear
|
23
35
|
|
24
36
|
app.reload_routes!
|
37
|
+
end
|
25
38
|
|
26
|
-
|
39
|
+
def analyze!
|
40
|
+
clear_data
|
41
|
+
prepare_for_analysis
|
27
42
|
|
28
43
|
RouteInterceptor.route_data.each do |(file_location, route_creation_method, controller_name), action_names|
|
29
|
-
|
30
|
-
|
31
|
-
action_names = action_names.uniq.sort
|
32
|
-
|
33
|
-
opts = {
|
44
|
+
analyse_route_call(
|
34
45
|
file_location: file_location,
|
35
46
|
route_creation_method: route_creation_method,
|
36
47
|
controller_name: controller_name,
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
controller = nil
|
42
|
-
begin
|
43
|
-
controller = Object.const_get(controller_class_name)
|
44
|
-
rescue LoadError, RuntimeError, NameError => e
|
45
|
-
all_issues << RouteIssue.new(opts.merge(type: :no_controller, error: e.message))
|
46
|
-
next
|
47
|
-
end
|
48
|
+
action_names: action_names.uniq.sort,
|
49
|
+
)
|
50
|
+
end
|
48
51
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
end
|
52
|
+
route_log.concat RouteInterceptor.route_log
|
53
|
+
generate_route_lines
|
54
|
+
end
|
53
55
|
|
54
|
-
|
55
|
-
|
56
|
+
def generate_route_lines
|
57
|
+
calls_per_line = route_calls.group_by do |record|
|
58
|
+
[record.full_filename, record.line_number]
|
59
|
+
end
|
56
60
|
|
57
|
-
|
58
|
-
|
59
|
-
|
61
|
+
calls_per_line.each do |(full_filename, line_number), records|
|
62
|
+
route_lines << RouteLine.new(full_filename: full_filename,
|
63
|
+
line_number: line_number,
|
64
|
+
records: records)
|
65
|
+
end
|
66
|
+
end
|
60
67
|
|
61
|
-
|
62
|
-
|
63
|
-
if missing.present?
|
64
|
-
all_issues << RouteIssue.new(opts.merge(type: :no_action, missing_actions: missing))
|
65
|
-
end
|
68
|
+
def analyse_route_call(**kwargs)
|
69
|
+
controller_class_name = "#{kwargs[:controller_name]}_controller".camelize
|
66
70
|
|
67
|
-
|
68
|
-
end
|
71
|
+
opts = kwargs.merge(controller_class_name: controller_class_name)
|
69
72
|
|
70
|
-
|
73
|
+
route_call = RouteCall.new(opts)
|
74
|
+
route_calls << route_call
|
71
75
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
76
|
+
controller = nil
|
77
|
+
begin
|
78
|
+
controller = Object.const_get(controller_class_name)
|
79
|
+
rescue LoadError, RuntimeError, NameError => e
|
80
|
+
route_call.add_issue RouteIssue::NoController.new(error: e.message)
|
81
|
+
return
|
82
|
+
end
|
78
83
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
+
if controller.nil?
|
85
|
+
route_call.add_issue RouteIssue::NoController.new(error: "#{controller_class_name} is nil")
|
86
|
+
return
|
87
|
+
end
|
88
|
+
|
89
|
+
analyze_action_availability(controller, route_call, **opts)
|
90
|
+
end
|
84
91
|
|
85
|
-
|
86
|
-
|
92
|
+
# Checks which if any actions referred to by the route don't exist.
|
93
|
+
def analyze_action_availability(controller, route_call, **opts)
|
94
|
+
present, missing = opts[:action_names].partition { |name| controller.action_methods.include?(name.to_s) }
|
95
|
+
|
96
|
+
route_call[:present_actions] = present if present.any?
|
97
|
+
|
98
|
+
if SINGLE_METHODS.include?(opts[:route_creation_method])
|
99
|
+
# NOTE a single call like 'get' can add multiple actions if called in a loop
|
100
|
+
if missing.present?
|
101
|
+
route_call.add_issue RouteIssue::NoAction.new(missing_actions: missing)
|
87
102
|
end
|
103
|
+
return
|
104
|
+
end
|
88
105
|
|
89
|
-
|
106
|
+
return if missing.empty? # Everything is perfect, all routes match an action
|
107
|
+
|
108
|
+
if present.sort == RESOURCE_ACTIONS.sort
|
109
|
+
# Should happen only if RESOURCE_ACTIONS doesn't match which actions rails supports
|
110
|
+
raise "shouldn't get all methods being present and yet some missing at the same time: #{present.inspect} #{missing.inspect}"
|
90
111
|
end
|
91
112
|
|
92
|
-
|
93
|
-
|
113
|
+
suggested_param = resource_route_suggested_param(present)
|
114
|
+
|
115
|
+
route_call.add_issue RouteIssue::Resources.new(suggested_param: suggested_param)
|
94
116
|
end
|
95
117
|
|
96
|
-
def
|
97
|
-
|
118
|
+
def resource_route_suggested_param(present)
|
119
|
+
if (present.size < 4 || only_only) && !only_except
|
120
|
+
"only: [#{present.sort.map { |x| ":#{x}" }.join(', ')}]"
|
121
|
+
else
|
122
|
+
"except: [#{(RESOURCE_ACTIONS - present).sort.map { |x| ":#{x}" }.join(', ')}]"
|
123
|
+
end
|
98
124
|
end
|
99
125
|
|
100
126
|
def issues
|
101
|
-
|
127
|
+
route_calls.select(&:issue?)
|
102
128
|
end
|
103
129
|
|
104
130
|
def non_issues
|
105
|
-
|
131
|
+
route_calls.reject(&:issue?)
|
106
132
|
end
|
107
133
|
|
108
134
|
def all_unique_issues_file_names
|
109
|
-
|
110
|
-
end
|
111
|
-
|
112
|
-
def unique_issues_file_names
|
113
|
-
issues.map { |issue| issue.full_filename }.uniq.sort
|
135
|
+
issues.map(&:full_filename).uniq.sort
|
114
136
|
end
|
115
137
|
|
116
|
-
def
|
117
|
-
|
138
|
+
def route_calls_for_file_name(full_filename)
|
139
|
+
route_calls.select { |record| record.full_filename == full_filename.to_s }
|
118
140
|
end
|
119
141
|
|
120
142
|
def implemented_routes
|
121
143
|
Set.new.tap do |implemented_routes|
|
122
|
-
|
123
|
-
|
124
|
-
implemented_routes << [
|
144
|
+
route_calls.each do |route_call|
|
145
|
+
(route_call.present_actions || []).each do |action|
|
146
|
+
implemented_routes << [route_call.controller_class_name, action]
|
125
147
|
end
|
126
148
|
end
|
127
149
|
end
|
128
150
|
end
|
151
|
+
|
152
|
+
def route_lines_for_file(full_filename)
|
153
|
+
route_lines.select { |line| line.full_filename == full_filename.to_s }
|
154
|
+
end
|
155
|
+
|
156
|
+
def print_report
|
157
|
+
if issues.empty?
|
158
|
+
puts "No route issues found"
|
159
|
+
return
|
160
|
+
end
|
161
|
+
|
162
|
+
issues.each do |issue|
|
163
|
+
puts issue.human_readable_error(verbose: verbose)
|
164
|
+
end
|
165
|
+
end
|
129
166
|
end
|
130
167
|
|
131
168
|
end
|