route_guard 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/.gitignore +13 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +173 -0
- data/README.md +57 -0
- data/Rakefile +10 -0
- data/bin/route_guard +14 -0
- data/docs/publishing.md +76 -0
- data/docs/usage.md +81 -0
- data/lib/route_guard/analyzer.rb +69 -0
- data/lib/route_guard/cli.rb +123 -0
- data/lib/route_guard/configuration.rb +38 -0
- data/lib/route_guard/formatter/ci.rb +20 -0
- data/lib/route_guard/formatter/html.rb +297 -0
- data/lib/route_guard/formatter/json.rb +61 -0
- data/lib/route_guard/formatter/terminal.rb +95 -0
- data/lib/route_guard/inspector.rb +58 -0
- data/lib/route_guard/models/issue.rb +30 -0
- data/lib/route_guard/models/report.rb +29 -0
- data/lib/route_guard/models/route.rb +42 -0
- data/lib/route_guard/railtie.rb +12 -0
- data/lib/route_guard/route_loader.rb +110 -0
- data/lib/route_guard/route_tracker.rb +141 -0
- data/lib/route_guard/rules/base.rb +163 -0
- data/lib/route_guard/rules/complexity.rb +62 -0
- data/lib/route_guard/rules/duplicate_helpers.rb +43 -0
- data/lib/route_guard/rules/duplicate_resources.rb +56 -0
- data/lib/route_guard/rules/duplicate_routes.rb +40 -0
- data/lib/route_guard/rules/shadowed_routes.rb +78 -0
- data/lib/route_guard/rules/statistics.rb +81 -0
- data/lib/route_guard/rules/unreachable_routes.rb +42 -0
- data/lib/route_guard/rules/unused_routes.rb +66 -0
- data/lib/route_guard/version.rb +5 -0
- data/lib/route_guard.rb +14 -0
- data/lib/tasks/route_guard.rake +30 -0
- metadata +195 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module RouteGuard
|
|
6
|
+
module Rules
|
|
7
|
+
class Statistics < Base
|
|
8
|
+
def analyze(routes, report)
|
|
9
|
+
return [] if routes.empty?
|
|
10
|
+
|
|
11
|
+
# Calculate metrics
|
|
12
|
+
total_routes = routes.length
|
|
13
|
+
|
|
14
|
+
# REST Resources: count unique controller resource names or use RouteTracker if available
|
|
15
|
+
resource_calls = RouteTracker.resource_calls
|
|
16
|
+
rest_resources = if resource_calls.any?
|
|
17
|
+
resource_calls.map { |c| c[:args].first }.uniq.size
|
|
18
|
+
else
|
|
19
|
+
# Fallback: estimate from controller count
|
|
20
|
+
routes.map(&:controller).compact.uniq.size
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Namespaces: count of unique directories in controller names
|
|
24
|
+
namespaces = routes.map(&:controller).compact
|
|
25
|
+
.select { |c| c.include?("/") }
|
|
26
|
+
.map { |c| c.split("/")[0...-1].join("/") }
|
|
27
|
+
.uniq.size
|
|
28
|
+
|
|
29
|
+
# Scopes: count of unique non-parameter, non-wildcard first-level path prefixes
|
|
30
|
+
prefixes = routes.map { |r| r.path.split("/")[1] }
|
|
31
|
+
.compact
|
|
32
|
+
.reject { |p| p.start_with?(":") || p.start_with?("*") }
|
|
33
|
+
.uniq
|
|
34
|
+
scopes = prefixes.size
|
|
35
|
+
|
|
36
|
+
# Wildcards: routes containing '*'
|
|
37
|
+
wildcards = routes.select { |r| r.path.include?("*") }.size
|
|
38
|
+
|
|
39
|
+
# Nesting depth: count of dynamic parameters (:id, etc.) in route path
|
|
40
|
+
depths = routes.map { |r| r.path.scan(/:\w+/).size }
|
|
41
|
+
max_depth = depths.max || 0
|
|
42
|
+
avg_depth = (depths.sum.to_f / total_routes).round(2)
|
|
43
|
+
|
|
44
|
+
# Most common controller
|
|
45
|
+
controller_counts = Hash.new(0)
|
|
46
|
+
routes.each { |r| controller_counts[r.controller] += 1 if r.controller }
|
|
47
|
+
most_common_controller = nil
|
|
48
|
+
most_common_count = 0
|
|
49
|
+
if controller_counts.any?
|
|
50
|
+
raw_controller, most_common_count = controller_counts.max_by { |_, count| count }
|
|
51
|
+
most_common_controller = camelize_controller(raw_controller)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Store in report stats (will be updated with issues later)
|
|
55
|
+
report.stats = {
|
|
56
|
+
total_routes: total_routes,
|
|
57
|
+
rest_resources: rest_resources,
|
|
58
|
+
namespaces: namespaces,
|
|
59
|
+
scopes: scopes,
|
|
60
|
+
wildcards: wildcards,
|
|
61
|
+
duplicate_paths: 0, # Will be set by analyzer after running all rules
|
|
62
|
+
shadowed_routes: 0, # Will be set by analyzer
|
|
63
|
+
average_nesting_depth: avg_depth,
|
|
64
|
+
maximum_nesting_depth: max_depth,
|
|
65
|
+
most_common_controller: most_common_controller,
|
|
66
|
+
most_common_count: most_common_count
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
[] # Statistics rule does not generate issues itself
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def camelize_controller(name)
|
|
75
|
+
return nil unless name
|
|
76
|
+
name.to_s.split("/").map { |part| part.split("_").map(&:capitalize).join }.join("::") + "Controller"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "../models/issue"
|
|
5
|
+
|
|
6
|
+
module RouteGuard
|
|
7
|
+
module Rules
|
|
8
|
+
class UnreachableRoutes < Base
|
|
9
|
+
def analyze(routes, report)
|
|
10
|
+
issues = []
|
|
11
|
+
catch_alls = []
|
|
12
|
+
|
|
13
|
+
routes.each do |route|
|
|
14
|
+
matching_catch_all = catch_alls.find do |ca|
|
|
15
|
+
verb_matches?(ca.verb, route.verb)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
if matching_catch_all
|
|
19
|
+
issues << Models::Issue.new(
|
|
20
|
+
rule_name: :unreachable_routes,
|
|
21
|
+
severity: :error,
|
|
22
|
+
message: "Unreachable Route: #{route} is unreachable because it comes after catch-all wildcard #{matching_catch_all}.",
|
|
23
|
+
route: route,
|
|
24
|
+
related_routes: [matching_catch_all]
|
|
25
|
+
)
|
|
26
|
+
elsif catch_all_wildcard?(route)
|
|
27
|
+
catch_alls << route
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
issues
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def catch_all_wildcard?(route)
|
|
37
|
+
path = route.path
|
|
38
|
+
path == "/*path" || path == "*path" || path.match?(/\A\/?\*[a-zA-Z_]+\z/)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "../models/issue"
|
|
5
|
+
|
|
6
|
+
module RouteGuard
|
|
7
|
+
module Rules
|
|
8
|
+
class UnusedRoutes < Base
|
|
9
|
+
def analyze(routes, report)
|
|
10
|
+
issues = []
|
|
11
|
+
return issues unless defined?(Rails)
|
|
12
|
+
|
|
13
|
+
routes.each do |route|
|
|
14
|
+
next if route.controller.nil? || route.action.nil?
|
|
15
|
+
next if route.internal?
|
|
16
|
+
|
|
17
|
+
controller_class_name = camelize_controller(route.controller)
|
|
18
|
+
|
|
19
|
+
begin
|
|
20
|
+
klass = if controller_class_name.respond_to?(:safe_constantize)
|
|
21
|
+
controller_class_name.safe_constantize
|
|
22
|
+
else
|
|
23
|
+
Object.const_get(controller_class_name) rescue nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if klass
|
|
27
|
+
if klass.respond_to?(:action_methods)
|
|
28
|
+
unless klass.action_methods.include?(route.action.to_s)
|
|
29
|
+
issues << Models::Issue.new(
|
|
30
|
+
rule_name: :unused_routes,
|
|
31
|
+
severity: :warning,
|
|
32
|
+
message: "Unused Route: Action '#{route.action}' is missing in #{controller_class_name}.",
|
|
33
|
+
route: route
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
else
|
|
38
|
+
issues << Models::Issue.new(
|
|
39
|
+
rule_name: :unused_routes,
|
|
40
|
+
severity: :warning,
|
|
41
|
+
message: "Unused Route: Controller #{controller_class_name} does not exist.",
|
|
42
|
+
route: route
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
rescue NameError, LoadError
|
|
46
|
+
issues << Models::Issue.new(
|
|
47
|
+
rule_name: :unused_routes,
|
|
48
|
+
severity: :warning,
|
|
49
|
+
message: "Unused Route: Controller #{controller_class_name} could not be loaded.",
|
|
50
|
+
route: route
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
issues
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def camelize_controller(name)
|
|
61
|
+
return nil unless name
|
|
62
|
+
name.to_s.split("/").map { |part| part.split("_").map(&:capitalize).join }.join("::") + "Controller"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
data/lib/route_guard.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "route_guard/version"
|
|
4
|
+
require_relative "route_guard/configuration"
|
|
5
|
+
require_relative "route_guard/models/route"
|
|
6
|
+
require_relative "route_guard/models/issue"
|
|
7
|
+
require_relative "route_guard/models/report"
|
|
8
|
+
require_relative "route_guard/route_tracker"
|
|
9
|
+
require_relative "route_guard/route_loader"
|
|
10
|
+
require_relative "route_guard/analyzer"
|
|
11
|
+
require_relative "route_guard/inspector"
|
|
12
|
+
require_relative "route_guard/cli"
|
|
13
|
+
|
|
14
|
+
require_relative "route_guard/railtie" if defined?(Rails::Railtie)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :routes do
|
|
4
|
+
desc "Lint Rails routes and detect shadowing, duplicates, etc."
|
|
5
|
+
task lint: :environment do
|
|
6
|
+
require "route_guard"
|
|
7
|
+
config = RouteGuard::Configuration.new
|
|
8
|
+
inspector = RouteGuard::Inspector.new(config)
|
|
9
|
+
report = inspector.run
|
|
10
|
+
inspector.format(report, :terminal)
|
|
11
|
+
|
|
12
|
+
exit(1) if report.errors.any?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
desc "Run RouteGuard doctor mode on Rails routes"
|
|
16
|
+
task doctor: :environment do
|
|
17
|
+
require "route_guard"
|
|
18
|
+
config = RouteGuard::Configuration.new
|
|
19
|
+
config.strict = true
|
|
20
|
+
config.fail_on_warning = true
|
|
21
|
+
inspector = RouteGuard::Inspector.new(config)
|
|
22
|
+
report = inspector.run
|
|
23
|
+
inspector.format(report, :terminal)
|
|
24
|
+
|
|
25
|
+
exit(1) if report.errors.any? || report.warnings.any?
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
desc "Lint Rails routes using RouteGuard"
|
|
30
|
+
task route_guard: ["routes:lint"]
|
metadata
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: route_guard
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Amritesh Tiwari
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-08-04 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rainbow
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '3.1'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '3.1'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: thor
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.2'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.2'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: actionpack
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '6.1'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '6.1'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: railties
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '6.1'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '6.1'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: bundler
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '2.4'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '2.4'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: rake
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '13.0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '13.0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: rspec
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '3.12'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '3.12'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: simplecov
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '0.22'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '0.22'
|
|
125
|
+
description: Analyze Rails routes for shadowing, duplicates, unreachable routes, and
|
|
126
|
+
maintainability metrics.
|
|
127
|
+
email:
|
|
128
|
+
- t.amritesh801@gmail.com
|
|
129
|
+
executables:
|
|
130
|
+
- route_guard
|
|
131
|
+
extensions: []
|
|
132
|
+
extra_rdoc_files: []
|
|
133
|
+
files:
|
|
134
|
+
- ".gitignore"
|
|
135
|
+
- Gemfile
|
|
136
|
+
- Gemfile.lock
|
|
137
|
+
- README.md
|
|
138
|
+
- Rakefile
|
|
139
|
+
- bin/route_guard
|
|
140
|
+
- docs/publishing.md
|
|
141
|
+
- docs/usage.md
|
|
142
|
+
- lib/route_guard.rb
|
|
143
|
+
- lib/route_guard/analyzer.rb
|
|
144
|
+
- lib/route_guard/cli.rb
|
|
145
|
+
- lib/route_guard/configuration.rb
|
|
146
|
+
- lib/route_guard/formatter/ci.rb
|
|
147
|
+
- lib/route_guard/formatter/html.rb
|
|
148
|
+
- lib/route_guard/formatter/json.rb
|
|
149
|
+
- lib/route_guard/formatter/terminal.rb
|
|
150
|
+
- lib/route_guard/inspector.rb
|
|
151
|
+
- lib/route_guard/models/issue.rb
|
|
152
|
+
- lib/route_guard/models/report.rb
|
|
153
|
+
- lib/route_guard/models/route.rb
|
|
154
|
+
- lib/route_guard/railtie.rb
|
|
155
|
+
- lib/route_guard/route_loader.rb
|
|
156
|
+
- lib/route_guard/route_tracker.rb
|
|
157
|
+
- lib/route_guard/rules/base.rb
|
|
158
|
+
- lib/route_guard/rules/complexity.rb
|
|
159
|
+
- lib/route_guard/rules/duplicate_helpers.rb
|
|
160
|
+
- lib/route_guard/rules/duplicate_resources.rb
|
|
161
|
+
- lib/route_guard/rules/duplicate_routes.rb
|
|
162
|
+
- lib/route_guard/rules/shadowed_routes.rb
|
|
163
|
+
- lib/route_guard/rules/statistics.rb
|
|
164
|
+
- lib/route_guard/rules/unreachable_routes.rb
|
|
165
|
+
- lib/route_guard/rules/unused_routes.rb
|
|
166
|
+
- lib/route_guard/version.rb
|
|
167
|
+
- lib/tasks/route_guard.rake
|
|
168
|
+
homepage: https://github.com/amriteshtiwari/RouteGuard
|
|
169
|
+
licenses:
|
|
170
|
+
- MIT
|
|
171
|
+
metadata:
|
|
172
|
+
allowed_push_host: https://rubygems.org
|
|
173
|
+
homepage_uri: https://github.com/amriteshtiwari/RouteGuard
|
|
174
|
+
source_code_uri: https://github.com/amriteshtiwari/RouteGuard
|
|
175
|
+
changelog_uri: https://github.com/amriteshtiwari/RouteGuard/blob/main/CHANGELOG.md
|
|
176
|
+
post_install_message:
|
|
177
|
+
rdoc_options: []
|
|
178
|
+
require_paths:
|
|
179
|
+
- lib
|
|
180
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
181
|
+
requirements:
|
|
182
|
+
- - ">="
|
|
183
|
+
- !ruby/object:Gem::Version
|
|
184
|
+
version: 3.2.0
|
|
185
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
186
|
+
requirements:
|
|
187
|
+
- - ">="
|
|
188
|
+
- !ruby/object:Gem::Version
|
|
189
|
+
version: '0'
|
|
190
|
+
requirements: []
|
|
191
|
+
rubygems_version: 3.5.3
|
|
192
|
+
signing_key:
|
|
193
|
+
specification_version: 4
|
|
194
|
+
summary: A production-ready static analysis tool for Rails routes.
|
|
195
|
+
test_files: []
|