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.
@@ -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,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
@@ -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,7 @@
1
+ require 'rails-route-checker/app_interface'
2
+ require 'rails-route-checker/config_file'
3
+ require 'rails-route-checker/loaded_app'
4
+ require 'rails-route-checker/runner'
5
+ require 'rails-route-checker/version'
6
+
7
+ module RailsRouteChecker; end
@@ -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,3 @@
1
+ module RailsRouteChecker
2
+ VERSION = '0.1.0'.freeze
3
+ 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: []