rails-route-checker 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []