rails-route-checker 0.1.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 +7 -0
- data/Gemfile +2 -0
- data/exe/rails-route-checker +29 -0
- data/lib/rails-route-checker.rb +7 -0
- data/lib/rails-route-checker/app_interface.rb +134 -0
- data/lib/rails-route-checker/config_file.rb +27 -0
- data/lib/rails-route-checker/loaded_app.rb +108 -0
- data/lib/rails-route-checker/runner.rb +54 -0
- data/lib/rails-route-checker/version.rb +3 -0
- data/rails-route-checker.gemspec +30 -0
- metadata +96 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ea424b37eac8e092071553a987c58ee05c0fae83
|
4
|
+
data.tar.gz: 935ad9981a24ff5f65ac447c44dcaacb54d98d32
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f8ffc446b5200c1f96c2f1b658ea132ad8aca579b6a2b30a9480c1a49e98504acd10791559aec379b19accf0f88789af7df8d3d2bd5121081289d1c54f3bcd43
|
7
|
+
data.tar.gz: 49a04e76dde512d40127684c8c0127a216755f32e8240b68de4799b3cc1c6e334b240e850507846500a1840ae76fab419ec71ff5606ae783d777e608d1c2efa2
|
data/Gemfile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rails-route-checker'
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
options = {}
|
7
|
+
OptionParser.new do |parser|
|
8
|
+
parser.banner = 'Usage: rails-route-checker [options]'
|
9
|
+
|
10
|
+
parser.on('-c', '--config CONFIG_FILE', 'Path to config file') do |path|
|
11
|
+
unless File.exist?(path)
|
12
|
+
puts 'Config file does not exist'
|
13
|
+
exit 1
|
14
|
+
end
|
15
|
+
|
16
|
+
options[:config_file] = path
|
17
|
+
end
|
18
|
+
|
19
|
+
parser.on('-h', '--help', 'Prints this help') do
|
20
|
+
puts parser
|
21
|
+
exit
|
22
|
+
end
|
23
|
+
end.parse!
|
24
|
+
|
25
|
+
options[:config_file] = '.rails-route-checker.yml' if File.exist?('.rails-route-checker.yml') && !options[:config_file]
|
26
|
+
|
27
|
+
rrc = RailsRouteChecker::Runner.new(options)
|
28
|
+
puts rrc.output
|
29
|
+
exit rrc.issues? ? 1 : 0
|
@@ -0,0 +1,134 @@
|
|
1
|
+
module RailsRouteChecker
|
2
|
+
class AppInterface
|
3
|
+
def initialize(**opts)
|
4
|
+
@options = opts
|
5
|
+
end
|
6
|
+
|
7
|
+
def routes_without_actions
|
8
|
+
loaded_app.routes.map do |r|
|
9
|
+
controller = r.requirements[:controller]
|
10
|
+
action = r.requirements[:action]
|
11
|
+
|
12
|
+
next if options[:ignored_controllers].include?(controller)
|
13
|
+
next if controller_information.key?(controller) && controller_information[controller][:actions].include?(action)
|
14
|
+
|
15
|
+
{
|
16
|
+
controller: controller,
|
17
|
+
action: action
|
18
|
+
}
|
19
|
+
end.compact
|
20
|
+
end
|
21
|
+
|
22
|
+
def undefined_path_method_calls
|
23
|
+
generate_undef_view_path_calls + generate_undef_controller_path_calls
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :options
|
29
|
+
|
30
|
+
def loaded_app
|
31
|
+
@loaded_app ||= RailsRouteChecker::LoadedApp.new
|
32
|
+
end
|
33
|
+
|
34
|
+
def controller_information
|
35
|
+
@controller_information ||= loaded_app.controller_information.reject do |path, _|
|
36
|
+
options[:ignored_controllers].include?(path)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def generate_undef_view_path_calls
|
41
|
+
files = `find app -type f -iregex '.*\\.haml' -or -iregex '.*\\.erb' -or -iregex '.*\\.html'`.split("\n")
|
42
|
+
files.map do |filename|
|
43
|
+
controller = controller_from_view_file(filename)
|
44
|
+
|
45
|
+
defined_variables = []
|
46
|
+
|
47
|
+
File.read(filename).each_line.each_with_index.map do |line, line_num|
|
48
|
+
next if line =~ /^\s*-\s*#/
|
49
|
+
skip_first = false
|
50
|
+
if line =~ /^\s*-/
|
51
|
+
line_match = line.match(/^\s*-\s*([a-zA-Z0-9_]+_(?:path|url))\s*=/)
|
52
|
+
defined_variables << line_match[1] if line_match
|
53
|
+
skip_first = true
|
54
|
+
end
|
55
|
+
|
56
|
+
matches = line.scan(/(([a-zA-Z][a-zA-Z0-9_]*)_(?:path|url))[^a-z0-9_]/)
|
57
|
+
matches.shift if skip_first
|
58
|
+
ignores = line.scan(/(([a-zA-Z][a-zA-Z0-9_]*)_(?:path|url))(?: =|[!:])/).map(&:first)
|
59
|
+
ignores += line.scan(/[.@:_'"]([a-zA-Z][a-zA-Z0-9_]+_(?:path|url))[^a-z0-9_]/).map(&:first)
|
60
|
+
|
61
|
+
matches.reject! { |match| ignores.include?(match[0]) }
|
62
|
+
|
63
|
+
matches.map do |match|
|
64
|
+
next if match_in_whitelist?(filename, match)
|
65
|
+
next if match_defined_in_view?(controller, defined_variables, match)
|
66
|
+
{ file: filename, line: line_num + 1, method: match[0] }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end.flatten.compact
|
70
|
+
end
|
71
|
+
|
72
|
+
def generate_undef_controller_path_calls
|
73
|
+
`find app/controllers -type f -iregex '.*\\.rb'`.split("\n").map do |filename|
|
74
|
+
controller = controller_from_ruby_file(filename)
|
75
|
+
|
76
|
+
File.read(filename).each_line.each_with_index.map do |line, line_num|
|
77
|
+
next if line =~ /^\s*#/
|
78
|
+
next if line =~ /^\s*def\s/
|
79
|
+
|
80
|
+
matches = line.scan(/(([a-zA-Z][a-zA-Z0-9_]*)_(?:path|url))[^a-z0-9_]/)
|
81
|
+
ignores = line.scan(/(([a-zA-Z][a-zA-Z0-9_]*)_(?:path|url))(?: =|[!:])/).map(&:first)
|
82
|
+
ignores += line.scan(/[.@:_'"]([a-zA-Z][a-zA-Z0-9_]+_(?:path|url))[^a-z0-9_]/).map(&:first)
|
83
|
+
|
84
|
+
matches.reject! { |match| ignores.include?(match[0]) }
|
85
|
+
|
86
|
+
matches.map do |match|
|
87
|
+
next if match_in_whitelist?(filename, match)
|
88
|
+
next if match_defined_in_ruby?(controller, match)
|
89
|
+
{ file: filename, line: line_num + 1, method: match[0] }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end.flatten.compact
|
93
|
+
end
|
94
|
+
|
95
|
+
def match_in_whitelist?(filename, match)
|
96
|
+
full_match, possible_route_name = match
|
97
|
+
return true if options[:ignored_paths].include?(possible_route_name)
|
98
|
+
(options[:ignored_path_whitelist][filename] || []).include?(full_match)
|
99
|
+
end
|
100
|
+
|
101
|
+
def match_defined_in_view?(controller, defined_variables, match)
|
102
|
+
full_match, possible_route_name = match
|
103
|
+
return true if loaded_app.all_route_names.include?(possible_route_name)
|
104
|
+
return true if defined_variables.include?(full_match)
|
105
|
+
controller && controller[:helpers].include?(full_match)
|
106
|
+
end
|
107
|
+
|
108
|
+
def match_defined_in_ruby?(controller, match)
|
109
|
+
full_match, possible_route_name = match
|
110
|
+
return true if loaded_app.all_route_names.include?(possible_route_name)
|
111
|
+
controller && controller[:instance_methods].include?(full_match)
|
112
|
+
end
|
113
|
+
|
114
|
+
def controller_from_view_file(filename)
|
115
|
+
split_path = filename.split('/')
|
116
|
+
possible_controller_path = split_path[(split_path.index('app') + 2)..-2]
|
117
|
+
|
118
|
+
controller = nil
|
119
|
+
while controller.nil? && possible_controller_path.any?
|
120
|
+
if controller_information.include?(possible_controller_path.join('/'))
|
121
|
+
controller = controller_information[possible_controller_path.join('/')]
|
122
|
+
else
|
123
|
+
possible_controller_path = possible_controller_path[0..-2]
|
124
|
+
end
|
125
|
+
end
|
126
|
+
controller || controller_information['application']
|
127
|
+
end
|
128
|
+
|
129
|
+
def controller_from_ruby_file(filename)
|
130
|
+
controller_name = (filename.match(%r{app/controllers/(.*)_controller.rb}) || [])[1] || 'application'
|
131
|
+
controller_information[controller_name]
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module RailsRouteChecker
|
2
|
+
class ConfigFile
|
3
|
+
def initialize(filename)
|
4
|
+
@filename = filename
|
5
|
+
end
|
6
|
+
|
7
|
+
def config
|
8
|
+
@config ||= begin
|
9
|
+
hash = load_yaml_file
|
10
|
+
{
|
11
|
+
ignored_controllers: hash['ignored_controllers'] || [],
|
12
|
+
ignored_paths: hash['ignored_paths'] || [],
|
13
|
+
ignored_path_whitelist: hash['ignored_path_whitelist'] || []
|
14
|
+
}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :filename
|
21
|
+
|
22
|
+
def load_yaml_file
|
23
|
+
require 'yaml'
|
24
|
+
YAML.safe_load(File.read(filename))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module RailsRouteChecker
|
2
|
+
class LoadedApp
|
3
|
+
def initialize
|
4
|
+
@app = suppress_output do
|
5
|
+
app_base_path = Dir.pwd
|
6
|
+
require_relative "#{app_base_path}/config/boot"
|
7
|
+
require_relative "#{Dir.pwd}/config/environment"
|
8
|
+
|
9
|
+
a = Rails.application
|
10
|
+
a.eager_load!
|
11
|
+
attempt_to_load_default_controllers
|
12
|
+
a.reload_routes!
|
13
|
+
Rails::Engine.subclasses.each(&:eager_load!)
|
14
|
+
|
15
|
+
a
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def routes
|
20
|
+
return @routes if defined?(@routes)
|
21
|
+
|
22
|
+
@routes = app.routes.routes.reject do |r|
|
23
|
+
reject_route?(r)
|
24
|
+
end.uniq
|
25
|
+
|
26
|
+
return @routes unless @app.config.respond_to?(:assets)
|
27
|
+
|
28
|
+
use_spec = defined?(ActionDispatch::Journey::Route) || defined?(Journey::Route)
|
29
|
+
@routes.reject do |route|
|
30
|
+
path = use_spec ? route.path.spec.to_s : route.path
|
31
|
+
path =~ /^#{app.config.assets.prefix}/
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def all_route_names
|
36
|
+
@all_route_names ||= app.routes.routes.map(&:name).compact
|
37
|
+
end
|
38
|
+
|
39
|
+
def controller_information
|
40
|
+
@controller_information ||= ActionController::Base.descendants.map do |controller|
|
41
|
+
next if controller.controller_path.start_with?('rails/')
|
42
|
+
|
43
|
+
instance_methods = (controller.instance_methods.map(&:to_s) + controller.private_instance_methods.map(&:to_s))
|
44
|
+
|
45
|
+
[
|
46
|
+
controller.controller_path,
|
47
|
+
{
|
48
|
+
helpers: controller.helpers.methods.map(&:to_s),
|
49
|
+
actions: controller.action_methods.to_a,
|
50
|
+
instance_methods: instance_methods.compact.uniq
|
51
|
+
}
|
52
|
+
]
|
53
|
+
end.compact.to_h
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
attr_reader :app
|
59
|
+
|
60
|
+
def suppress_output
|
61
|
+
begin
|
62
|
+
original_stderr = $stderr.clone
|
63
|
+
original_stdout = $stdout.clone
|
64
|
+
$stderr.reopen(File.new('/dev/null', 'w'))
|
65
|
+
$stdout.reopen(File.new('/dev/null', 'w'))
|
66
|
+
retval = yield
|
67
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
68
|
+
$stdout.reopen(original_stdout)
|
69
|
+
$stderr.reopen(original_stderr)
|
70
|
+
raise e
|
71
|
+
ensure
|
72
|
+
$stdout.reopen(original_stdout)
|
73
|
+
$stderr.reopen(original_stderr)
|
74
|
+
end
|
75
|
+
retval
|
76
|
+
end
|
77
|
+
|
78
|
+
def attempt_to_load_default_controllers
|
79
|
+
# rubocop:disable Lint/HandleExceptions
|
80
|
+
begin
|
81
|
+
::Rails::InfoController
|
82
|
+
rescue NameError # ignored
|
83
|
+
end
|
84
|
+
begin
|
85
|
+
::Rails::WelcomeController
|
86
|
+
rescue NameError # ignored
|
87
|
+
end
|
88
|
+
begin
|
89
|
+
::Rails::MailersController
|
90
|
+
rescue NameError # ignored
|
91
|
+
end
|
92
|
+
# rubocop:enable Lint/HandleExceptions
|
93
|
+
end
|
94
|
+
|
95
|
+
def reject_route?(route)
|
96
|
+
return true if route.name.nil? && route.requirements.blank?
|
97
|
+
return true if route.app.is_a?(ActionDispatch::Routing::Mapper::Constraints) &&
|
98
|
+
route.app.app.respond_to?(:call)
|
99
|
+
return true if route.app.is_a?(ActionDispatch::Routing::Redirect)
|
100
|
+
|
101
|
+
controller = route.requirements[:controller]
|
102
|
+
action = route.requirements[:action]
|
103
|
+
return true unless controller && action
|
104
|
+
return true if controller.start_with?('rails/')
|
105
|
+
false
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module RailsRouteChecker
|
2
|
+
class Runner
|
3
|
+
def initialize(**opts)
|
4
|
+
@options = { ignored_controllers: [], ignored_paths: [], ignored_path_whitelist: {} }
|
5
|
+
@options.merge!(RailsRouteChecker::ConfigFile.new(opts[:config_file]).config) if opts[:config_file]
|
6
|
+
@options.merge!(opts)
|
7
|
+
end
|
8
|
+
|
9
|
+
def issues
|
10
|
+
@issues ||= {
|
11
|
+
missing_actions: app_interface.routes_without_actions,
|
12
|
+
missing_routes: app_interface.undefined_path_method_calls
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
def issues?
|
17
|
+
issues.values.flatten(1).count > 1
|
18
|
+
end
|
19
|
+
|
20
|
+
def output
|
21
|
+
output_lines = []
|
22
|
+
output_lines += missing_actions_output if issues[:missing_actions].any?
|
23
|
+
if issues[:missing_routes].any?
|
24
|
+
output_lines << "\n" if output_lines.any?
|
25
|
+
output_lines += missing_routes_output
|
26
|
+
end
|
27
|
+
output_lines = ['All good in the hood'] if output_lines.empty?
|
28
|
+
output_lines.join("\n")
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def app_interface
|
34
|
+
@app_interface ||= RailsRouteChecker::AppInterface.new(@options)
|
35
|
+
end
|
36
|
+
|
37
|
+
def missing_actions_output
|
38
|
+
[
|
39
|
+
"The following #{issues[:missing_actions].count} routes are defined, " \
|
40
|
+
'but have no corresponding controller action.',
|
41
|
+
'If you have recently added a route to routes.rb, make sure a matching action exists in the controller.',
|
42
|
+
'If you have recently removed a controller action, also remove the route in routes.rb.',
|
43
|
+
*issues[:missing_actions].map { |r| " - #{r[:controller]}##{r[:action]}" }
|
44
|
+
]
|
45
|
+
end
|
46
|
+
|
47
|
+
def missing_routes_output
|
48
|
+
[
|
49
|
+
"The following #{issues[:missing_routes].count} url and path methods don't correspond to any route.",
|
50
|
+
*issues[:missing_routes].map { |line| " - #{line[:file]}:L#{line[:line]} - call to #{line[:method]}" }
|
51
|
+
]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'rails-route-checker/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'rails-route-checker'
|
8
|
+
spec.version = RailsRouteChecker::VERSION
|
9
|
+
spec.authors = ['Dave Allie']
|
10
|
+
spec.email = ['dave@daveallie.com']
|
11
|
+
|
12
|
+
spec.summary = 'Blah'
|
13
|
+
spec.description = 'Blah'
|
14
|
+
spec.homepage = 'https://github.com/daveallie/rails-route-checker'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
spec.files = Dir['exe/*'] + Dir['lib/**/*'] +
|
18
|
+
['Gemfile', 'rails-route-checker.gemspec']
|
19
|
+
|
20
|
+
# `git ls-files -z`.split("\x0").reject do |f|
|
21
|
+
# f.match(%r{^(test|spec|features)/})
|
22
|
+
# end
|
23
|
+
spec.bindir = 'exe'
|
24
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
25
|
+
spec.require_paths = ['lib']
|
26
|
+
|
27
|
+
spec.add_development_dependency 'bundler', '~> 1.15'
|
28
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
29
|
+
spec.add_development_dependency 'rubocop', '~> 0.51'
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rails-route-checker
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dave Allie
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-10-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.15'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.15'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rubocop
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.51'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.51'
|
55
|
+
description: Blah
|
56
|
+
email:
|
57
|
+
- dave@daveallie.com
|
58
|
+
executables:
|
59
|
+
- rails-route-checker
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- Gemfile
|
64
|
+
- exe/rails-route-checker
|
65
|
+
- lib/rails-route-checker.rb
|
66
|
+
- lib/rails-route-checker/app_interface.rb
|
67
|
+
- lib/rails-route-checker/config_file.rb
|
68
|
+
- lib/rails-route-checker/loaded_app.rb
|
69
|
+
- lib/rails-route-checker/runner.rb
|
70
|
+
- lib/rails-route-checker/version.rb
|
71
|
+
- rails-route-checker.gemspec
|
72
|
+
homepage: https://github.com/daveallie/rails-route-checker
|
73
|
+
licenses:
|
74
|
+
- MIT
|
75
|
+
metadata: {}
|
76
|
+
post_install_message:
|
77
|
+
rdoc_options: []
|
78
|
+
require_paths:
|
79
|
+
- lib
|
80
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
requirements: []
|
91
|
+
rubyforge_project:
|
92
|
+
rubygems_version: 2.6.12
|
93
|
+
signing_key:
|
94
|
+
specification_version: 4
|
95
|
+
summary: Blah
|
96
|
+
test_files: []
|