rigortype 0.1.9 → 0.1.11
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/README.md +1 -1
- data/lib/rigor/analysis/baseline.rb +51 -15
- data/lib/rigor/analysis/runner.rb +67 -9
- data/lib/rigor/analysis/worker_session.rb +13 -4
- data/lib/rigor/cache/rbs_descriptor.rb +21 -2
- data/lib/rigor/cache/rbs_environment.rb +2 -1
- data/lib/rigor/cli/annotate_command.rb +57 -7
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/coverage_command.rb +126 -0
- data/lib/rigor/cli/coverage_renderer.rb +162 -0
- data/lib/rigor/cli/coverage_report.rb +75 -0
- data/lib/rigor/cli/mcp_command.rb +70 -0
- data/lib/rigor/cli.rb +88 -5
- data/lib/rigor/environment/rbs_loader.rb +46 -5
- data/lib/rigor/environment/reporters.rb +3 -2
- data/lib/rigor/environment.rb +159 -4
- data/lib/rigor/inference/def_return_typer.rb +98 -0
- data/lib/rigor/inference/expression_typer.rb +143 -12
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +5 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +62 -15
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +115 -7
- data/lib/rigor/inference/precision_scanner.rb +131 -0
- data/lib/rigor/inference/statement_evaluator.rb +26 -2
- data/lib/rigor/mcp/loop.rb +43 -0
- data/lib/rigor/mcp/server.rb +263 -0
- data/lib/rigor/mcp.rb +16 -0
- data/lib/rigor/plugin/base.rb +28 -5
- data/lib/rigor/plugin/manifest.rb +33 -5
- data/lib/rigor/plugin/registry.rb +21 -0
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +51 -0
- data/lib/rigor/sig_gen/generator.rb +150 -75
- data/lib/rigor/type/combinator.rb +57 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +190 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +189 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +81 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +142 -0
- data/plugins/rigor-actioncable/lib/rigor-actioncable.rb +3 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +178 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +310 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +76 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +177 -0
- data/plugins/rigor-actionmailer/lib/rigor-actionmailer.rb +3 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +589 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +150 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +123 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +247 -0
- data/plugins/rigor-actionpack/lib/rigor-actionpack.rb +3 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +114 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +177 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +65 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +117 -0
- data/plugins/rigor-activejob/lib/rigor-activejob.rb +3 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +273 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/inflector.rb +114 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +561 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +194 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +240 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +94 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +514 -0
- data/plugins/rigor-activerecord/lib/rigor-activerecord.rb +8 -0
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +182 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +78 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +162 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +43 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +170 -0
- data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +8 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +34 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor-activesupport-core-ext.rb +20 -0
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +463 -0
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +108 -0
- data/plugins/rigor-devise/lib/rigor-devise.rb +8 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +285 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +124 -0
- data/plugins/rigor-dry-schema/lib/rigor-dry-schema.rb +8 -0
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +116 -0
- data/plugins/rigor-dry-struct/lib/rigor-dry-struct.rb +8 -0
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +341 -0
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +120 -0
- data/plugins/rigor-dry-types/lib/rigor-dry-types.rb +8 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +120 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +85 -0
- data/plugins/rigor-dry-validation/lib/rigor-dry-validation.rb +7 -0
- data/plugins/rigor-dry-validation/sig/dry_validation.rbs +25 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +177 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +242 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +56 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +174 -0
- data/plugins/rigor-factorybot/lib/rigor-factorybot.rb +3 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +409 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +114 -0
- data/plugins/rigor-graphql/lib/rigor-graphql.rb +8 -0
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +124 -0
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +111 -0
- data/plugins/rigor-hanami/lib/rigor-hanami.rb +3 -0
- data/plugins/rigor-hanami/sig/hanami_action.rbs +78 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +302 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +72 -0
- data/plugins/rigor-minitest/lib/rigor-minitest.rb +3 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +194 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +140 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +65 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +130 -0
- data/plugins/rigor-pundit/lib/rigor-pundit.rb +3 -0
- data/plugins/rigor-rails/lib/rigor-rails.rb +31 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +277 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +108 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +138 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +167 -0
- data/plugins/rigor-rails-i18n/lib/rigor-rails-i18n.rb +3 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +161 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +103 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +490 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +158 -0
- data/plugins/rigor-rails-routes/lib/rigor-rails-routes.rb +3 -0
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +163 -0
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +24 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +110 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +200 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +170 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +233 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +190 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +188 -0
- data/plugins/rigor-rspec/lib/rigor-rspec.rb +3 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +128 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +60 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +75 -0
- data/plugins/rigor-rspec-rails/lib/rigor-rspec-rails.rb +3 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +266 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +113 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor-shoulda-matchers.rb +3 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +152 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +190 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +61 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +124 -0
- data/plugins/rigor-sidekiq/lib/rigor-sidekiq.rb +3 -0
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +85 -0
- data/plugins/rigor-sinatra/lib/rigor-sinatra.rb +8 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +108 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +250 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +95 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +226 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +28 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +154 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +100 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +323 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +660 -0
- data/plugins/rigor-sorbet/lib/rigor-sorbet.rb +3 -0
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +209 -0
- data/plugins/rigor-statesman/lib/rigor-statesman.rb +8 -0
- data/plugins/rigor-typescript-utility-types/lib/rigor/plugin/typescript_utility_types.rb +163 -0
- data/plugins/rigor-typescript-utility-types/lib/rigor-typescript-utility-types.rb +9 -0
- data/sig/rigor/analysis/baseline.rbs +39 -0
- data/sig/rigor/environment.rbs +3 -2
- data/sig/rigor/type.rbs +4 -0
- data/sig/rigor.rbs +2 -0
- metadata +180 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Plugin
|
|
7
|
+
class RailsRoutes < Rigor::Plugin::Base
|
|
8
|
+
# Walks a parsed file's AST looking for `*_path` /
|
|
9
|
+
# `*_url` calls and validates each against the
|
|
10
|
+
# plugin's {HelperTable}. Emits info diagnostics for
|
|
11
|
+
# recognised helpers and error diagnostics for typos /
|
|
12
|
+
# arity mismatches.
|
|
13
|
+
module Analyzer
|
|
14
|
+
DID_YOU_MEAN_DISTANCE = 3
|
|
15
|
+
|
|
16
|
+
# Built-in Rails helpers we don't want to flag as
|
|
17
|
+
# unknown. The plugin's HelperTable describes
|
|
18
|
+
# user-declared routes; Rails ships built-in helpers
|
|
19
|
+
# (`url_for`, `polymorphic_path`, …) the plugin
|
|
20
|
+
# deliberately ignores.
|
|
21
|
+
BUILTIN_PASSTHROUGH = %w[
|
|
22
|
+
url_for_path url_for_url
|
|
23
|
+
polymorphic_path polymorphic_url
|
|
24
|
+
].freeze
|
|
25
|
+
|
|
26
|
+
Diagnostic = Struct.new(:path, :line, :column, :severity, :rule, :message, keyword_init: true)
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
# @param path [String] file being analysed
|
|
31
|
+
# @param root [Prism::Node]
|
|
32
|
+
# @param helper_table [HelperTable]
|
|
33
|
+
# @return [Array<Diagnostic>]
|
|
34
|
+
def diagnose(path:, root:, helper_table:)
|
|
35
|
+
diagnostics = []
|
|
36
|
+
walk(root) do |call_node|
|
|
37
|
+
name = call_node.name.to_s
|
|
38
|
+
next unless name.end_with?("_path") || name.end_with?("_url")
|
|
39
|
+
next if BUILTIN_PASSTHROUGH.include?(name)
|
|
40
|
+
|
|
41
|
+
entry = helper_table.find(name)
|
|
42
|
+
if entry
|
|
43
|
+
diagnostics << info_diagnostic(path, call_node, entry)
|
|
44
|
+
arity_diagnostic = arity_check(path, call_node, entry, helper_table)
|
|
45
|
+
diagnostics << arity_diagnostic if arity_diagnostic
|
|
46
|
+
else
|
|
47
|
+
diagnostics << unknown_helper_diagnostic(path, call_node, name, helper_table)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
diagnostics
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def walk(node, &)
|
|
54
|
+
return unless node.is_a?(Prism::Node)
|
|
55
|
+
|
|
56
|
+
yield node if node.is_a?(Prism::CallNode) && implicit_helper_call?(node)
|
|
57
|
+
node.compact_child_nodes.each { |child| walk(child, &) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# `*_path` / `*_url` calls without an explicit
|
|
61
|
+
# receiver. Calls like `obj.users_path` or
|
|
62
|
+
# `Foo::users_path` are NOT route-helper invocations
|
|
63
|
+
# in Rails — controllers / views call helpers
|
|
64
|
+
# implicitly.
|
|
65
|
+
def implicit_helper_call?(node)
|
|
66
|
+
node.receiver.nil? && (node.name.to_s.end_with?("_path") || node.name.to_s.end_with?("_url"))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def info_diagnostic(path, call_node, entry)
|
|
70
|
+
location = call_node.location
|
|
71
|
+
method_label = entry.http_method ? entry.http_method.to_s.upcase : "*"
|
|
72
|
+
Diagnostic.new(
|
|
73
|
+
path: path,
|
|
74
|
+
line: location.start_line,
|
|
75
|
+
column: location.start_column + 1,
|
|
76
|
+
severity: :info,
|
|
77
|
+
rule: "helper",
|
|
78
|
+
message: "`#{entry.name}` → #{method_label} #{entry.path}"
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def arity_check(path, call_node, entry, helper_table)
|
|
83
|
+
actual = (call_node.arguments&.arguments || []).size
|
|
84
|
+
# Uncountable nouns (`news` / `series` / `media`) cause
|
|
85
|
+
# Rails to register two entries under the same helper
|
|
86
|
+
# name — `news_path` accepts both arity 0 (index) and
|
|
87
|
+
# arity 1 (show). The HelperTable multimap stores both;
|
|
88
|
+
# accepts_arity? checks the full set.
|
|
89
|
+
return nil if helper_table.accepts_arity?(entry.name, actual)
|
|
90
|
+
|
|
91
|
+
arities = helper_table.acceptable_arities(entry.name).sort
|
|
92
|
+
expected = arities.length == 1 ? arities.first.to_s : "#{arities.first}..#{arities.last}"
|
|
93
|
+
location = call_node.location
|
|
94
|
+
Diagnostic.new(
|
|
95
|
+
path: path,
|
|
96
|
+
line: location.start_line,
|
|
97
|
+
column: location.start_column + 1,
|
|
98
|
+
severity: :error,
|
|
99
|
+
rule: "wrong-arity",
|
|
100
|
+
message: "`#{entry.name}` expects #{expected} argument(s), got #{actual}"
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def unknown_helper_diagnostic(path, call_node, name, helper_table)
|
|
105
|
+
location = call_node.location
|
|
106
|
+
suggestion = did_you_mean(name, helper_table.names)
|
|
107
|
+
message = "no route helper `#{name}`"
|
|
108
|
+
message += " (did you mean `#{suggestion}`?)" if suggestion
|
|
109
|
+
|
|
110
|
+
Diagnostic.new(
|
|
111
|
+
path: path,
|
|
112
|
+
line: location.start_line,
|
|
113
|
+
column: location.start_column + 1,
|
|
114
|
+
severity: :error,
|
|
115
|
+
rule: "unknown-helper",
|
|
116
|
+
message: message
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Levenshtein-style nearest neighbour. Returns the
|
|
121
|
+
# closest known helper within {DID_YOU_MEAN_DISTANCE}
|
|
122
|
+
# edits, or nil.
|
|
123
|
+
def did_you_mean(name, candidates)
|
|
124
|
+
best = nil
|
|
125
|
+
best_distance = DID_YOU_MEAN_DISTANCE + 1
|
|
126
|
+
candidates.each do |candidate|
|
|
127
|
+
d = levenshtein(name, candidate)
|
|
128
|
+
if d < best_distance
|
|
129
|
+
best = candidate
|
|
130
|
+
best_distance = d
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
best
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Standard iterative Levenshtein. Lifted from
|
|
137
|
+
# rigor-routes' equivalent helper for parity.
|
|
138
|
+
def levenshtein(left, right)
|
|
139
|
+
return right.length if left.empty?
|
|
140
|
+
return left.length if right.empty?
|
|
141
|
+
|
|
142
|
+
rows = Array.new(left.length + 1) { Array.new(right.length + 1, 0) }
|
|
143
|
+
(0..left.length).each { |i| rows[i][0] = i }
|
|
144
|
+
(0..right.length).each { |j| rows[0][j] = j }
|
|
145
|
+
|
|
146
|
+
(1..left.length).each do |i|
|
|
147
|
+
(1..right.length).each do |j|
|
|
148
|
+
cost = left[i - 1] == right[j - 1] ? 0 : 1
|
|
149
|
+
rows[i][j] = [
|
|
150
|
+
rows[i - 1][j] + 1,
|
|
151
|
+
rows[i][j - 1] + 1,
|
|
152
|
+
rows[i - 1][j - 1] + cost
|
|
153
|
+
].min
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
rows[left.length][right.length]
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Plugin
|
|
5
|
+
class RailsRoutes < Rigor::Plugin::Base
|
|
6
|
+
# Frozen catalogue of route helpers parsed from
|
|
7
|
+
# `config/routes.rb`. Each entry maps a helper name
|
|
8
|
+
# (`users_path`, `edit_user_path`, …) to the metadata
|
|
9
|
+
# downstream consumers and the analyzer's per-call
|
|
10
|
+
# validation need:
|
|
11
|
+
#
|
|
12
|
+
# - `arity`: number of positional arguments the helper
|
|
13
|
+
# takes. `users_path` → 0; `user_path(:id)` → 1;
|
|
14
|
+
# `user_post_path(:user_id, :id)` → 2.
|
|
15
|
+
# - `path`: the path template Rails generates
|
|
16
|
+
# (`/users/:user_id/posts/:id`).
|
|
17
|
+
# - `http_method`: `:get` / `:post` / `:patch` / `:put` /
|
|
18
|
+
# `:delete` for the canonical action; `nil` for
|
|
19
|
+
# helpers that span multiple methods (a `resources`
|
|
20
|
+
# show helper isn't HTTP-method-specific in the
|
|
21
|
+
# helper sense — it's path-sensitive only).
|
|
22
|
+
# - `action`: `:index` / `:show` / `:new` / `:edit` /
|
|
23
|
+
# `:create` / `:update` / `:destroy` for resourceful
|
|
24
|
+
# routes; `:custom` for explicit `get`/`post`/etc.;
|
|
25
|
+
# `:root` for the root route.
|
|
26
|
+
#
|
|
27
|
+
# Both `_path` and `_url` forms share the same metadata —
|
|
28
|
+
# the table records each helper twice (once with `_path`,
|
|
29
|
+
# once with `_url`) for `O(1)` lookup at the call site.
|
|
30
|
+
class HelperTable
|
|
31
|
+
Entry = Data.define(:name, :arity, :path, :http_method, :action)
|
|
32
|
+
|
|
33
|
+
attr_reader :entries
|
|
34
|
+
|
|
35
|
+
# @param entries [Array<Entry>] freshly built; the
|
|
36
|
+
# factory below is the canonical construction path.
|
|
37
|
+
def initialize(entries)
|
|
38
|
+
@entries = entries.freeze
|
|
39
|
+
# Multimap: a single helper name can map to multiple
|
|
40
|
+
# entries when an uncountable-noun resource registers
|
|
41
|
+
# both an arity-0 index helper and an arity-1 show
|
|
42
|
+
# helper under the same `news_path` name. `find`
|
|
43
|
+
# returns the first entry (preserving the previous
|
|
44
|
+
# API); `accepts_arity?` checks against every entry.
|
|
45
|
+
@by_name = entries.group_by(&:name).transform_values(&:freeze).freeze
|
|
46
|
+
freeze
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @return [Entry, nil] First matching entry; for the
|
|
50
|
+
# uncountable-noun case this is the index helper
|
|
51
|
+
# (the show helper is also registered but starts
|
|
52
|
+
# second).
|
|
53
|
+
def find(helper_name)
|
|
54
|
+
@by_name[helper_name.to_s]&.first
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [Boolean]
|
|
58
|
+
def known?(helper_name)
|
|
59
|
+
@by_name.key?(helper_name.to_s)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [Boolean] true when any entry under this
|
|
63
|
+
# helper name accepts the given positional arity.
|
|
64
|
+
def accepts_arity?(helper_name, arity)
|
|
65
|
+
(@by_name[helper_name.to_s] || []).any? { |entry| entry.arity == arity }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @return [Array<Integer>] all accepted positional
|
|
69
|
+
# arities for a helper name. Empty when unknown.
|
|
70
|
+
def acceptable_arities(helper_name)
|
|
71
|
+
(@by_name[helper_name.to_s] || []).map(&:arity).uniq
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# All helper names — used by the "did you mean" suggester.
|
|
75
|
+
def names
|
|
76
|
+
@by_name.keys
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def empty?
|
|
80
|
+
@entries.empty?
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def size
|
|
84
|
+
@entries.size
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def to_h
|
|
88
|
+
# Plain dump for fact-store publishing (ADR-9). Each
|
|
89
|
+
# name serialises as a small Hash for the FIRST entry
|
|
90
|
+
# under that name, with `acceptable_arities` carrying
|
|
91
|
+
# the full arity set so cross-plugin consumers can
|
|
92
|
+
# honour the uncountable-noun multi-arity case.
|
|
93
|
+
@by_name.transform_values do |group|
|
|
94
|
+
entry = group.first
|
|
95
|
+
{ name: entry.name, arity: entry.arity, path: entry.path,
|
|
96
|
+
http_method: entry.http_method, action: entry.action,
|
|
97
|
+
acceptable_arities: group.map(&:arity).uniq }
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|