rigortype 0.1.11 → 0.1.12

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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/lib/rigor/analysis/erb_template_detector.rb +38 -0
  3. data/lib/rigor/analysis/runner.rb +6 -1
  4. data/lib/rigor/analysis/worker_session.rb +6 -1
  5. data/lib/rigor/cli/plugins_command.rb +308 -0
  6. data/lib/rigor/cli/plugins_renderer.rb +173 -0
  7. data/lib/rigor/cli.rb +28 -0
  8. data/lib/rigor/inference/block_parameter_binder.rb +35 -0
  9. data/lib/rigor/inference/expression_typer.rb +69 -30
  10. data/lib/rigor/inference/indexed_narrowing.rb +187 -0
  11. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +24 -0
  12. data/lib/rigor/inference/method_dispatcher.rb +23 -0
  13. data/lib/rigor/inference/mutation_widening.rb +285 -0
  14. data/lib/rigor/inference/narrowing.rb +72 -4
  15. data/lib/rigor/inference/scope_indexer.rb +409 -12
  16. data/lib/rigor/inference/statement_evaluator.rb +256 -4
  17. data/lib/rigor/scope.rb +181 -4
  18. data/lib/rigor/version.rb +1 -1
  19. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +22 -1
  20. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +94 -6
  21. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +11 -1
  22. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +7 -1
  23. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +135 -11
  24. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +94 -43
  25. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +138 -35
  26. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +17 -3
  27. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +10 -0
  28. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +13 -3
  29. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +6 -2
  30. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +83 -7
  31. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +4 -1
  32. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +16 -1
  33. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +81 -5
  34. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +11 -3
  35. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +194 -5
  36. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +264 -0
  37. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +100 -0
  38. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +175 -0
  39. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +64 -3
  40. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1107 -59
  41. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +81 -4
  42. data/sig/rigor/scope.rbs +22 -0
  43. metadata +9 -1
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Plugin
7
+ class RailsRoutes < Rigor::Plugin::Base
8
+ # Walks `app/helpers/**/*.rb` (or any configured set of
9
+ # helper directories) and extracts every public
10
+ # module-level method name so the analyzer's
11
+ # `unknown-helper` rule does not false-fire on calls to
12
+ # project-defined `*_path` / `*_url` helpers (the
13
+ # canonical Rails pattern: a `UrlHelper` module exposing
14
+ # `full_asset_url`, `host_to_url`, `frontend_asset_url`,
15
+ # and so on).
16
+ #
17
+ # The discoverer is deliberately conservative:
18
+ #
19
+ # - Walks `def name(...)` declarations at the module /
20
+ # class top level. Nested `def` inside a method body or
21
+ # a `define_method` macro is NOT extracted (the same
22
+ # reason `rigor-actionpack` is not the canonical
23
+ # custom-helper source — metaprogrammed helpers are out
24
+ # of scope for a static scan).
25
+ # - **Visibility-agnostic.** A `private` / `protected`
26
+ # `def some_url` IS registered. The `unknown-helper`
27
+ # rule is a project-wide name-presence check, not a
28
+ # visibility check — if the user wrote a `_path` /
29
+ # `_url` method anywhere in `app/`, they very likely
30
+ # intend to call it (often Mastodon's pattern: a
31
+ # controller's `private def page_url(page)` invoked
32
+ # by a `link_to` in its own view). The core engine's
33
+ # `call.undefined-method` rule still catches the case
34
+ # where the receiver genuinely cannot see the method.
35
+ # - Ignores `def self.x` (singleton-method definitions) —
36
+ # Rails helpers are instance methods on the helper
37
+ # module; class-side `self.x` shapes are usually
38
+ # internal utilities, not view helpers.
39
+ # - Filters the resulting set to names ending in `_path`
40
+ # or `_url` (the dispatch surface the analyzer's rule
41
+ # actually cares about). A helper whose name does not
42
+ # end with either suffix is irrelevant to the rule's
43
+ # false-positive surface, so excluding it costs nothing
44
+ # and keeps the registered set focused.
45
+ #
46
+ # Out of scope (intentional):
47
+ #
48
+ # - Helpers defined via `define_method` or via macros like
49
+ # `inheritance_traversed_helpers`. These would need
50
+ # ADR-16 macro substrate or per-plugin custom recognizers.
51
+ # - Helpers inherited from gems (`Devise::Controllers::Helpers`,
52
+ # `ViteRails::TagHelpers`, etc.). Those need their own
53
+ # handling — Devise auto-routes are covered by the
54
+ # companion `DeviseRoutes` generator; other gem-injected
55
+ # helpers fall to ADR-25 plugin-contributed RBS or the
56
+ # ADR-10 `dependencies.source_inference:` path.
57
+ module HelperDiscoverer
58
+ HELPER_SUFFIXES = [/_path\z/, /_url\z/].freeze
59
+
60
+ module_function
61
+
62
+ # @param contents_per_path [Hash{String => String}]
63
+ # file path → source text. The caller is responsible
64
+ # for reading files (typically through the trusted
65
+ # `IoBoundary` so cache invalidation works).
66
+ # @return [Set<String>] method names suitable for
67
+ # inclusion in the `HelperTable`'s custom-helper set.
68
+ def discover(contents_per_path)
69
+ names = []
70
+ contents_per_path.each_value do |contents|
71
+ names.concat(extract_from_contents(contents))
72
+ rescue StandardError
73
+ # Skip any file whose AST walk explodes — discovery
74
+ # is best-effort and a parse failure in one helper
75
+ # file MUST NOT abort discovery for the rest. Other
76
+ # rules will still surface the parse failure on the
77
+ # affected file through the core pipeline.
78
+ next
79
+ end
80
+ names.to_set
81
+ end
82
+
83
+ def extract_from_contents(contents)
84
+ parse_result = Prism.parse(contents)
85
+ return [] unless parse_result.errors.empty?
86
+
87
+ walker = ModuleBodyWalker.new
88
+ walker.walk(parse_result.value)
89
+ walker.helper_names
90
+ end
91
+
92
+ # Per-file AST walker. Tracks the current visibility
93
+ # mode (`:public` by default; `private` / `protected`
94
+ # calls flip the bit) so a `def` declared after a bare
95
+ # `private` is excluded.
96
+ #
97
+ # The walker is recursive into module / class bodies so
98
+ # `module UrlHelpers; module Routing; def foo_url; end;
99
+ # end; end` registers `foo_url`. It does NOT recurse
100
+ # into method bodies (a `def inside def` would be
101
+ # `define_method`-shaped, which we skip per the
102
+ # docstring).
103
+ class ModuleBodyWalker
104
+ attr_reader :helper_names
105
+
106
+ def initialize
107
+ @helper_names = []
108
+ end
109
+
110
+ def walk(node)
111
+ visit_statements(node) if node.is_a?(Prism::ProgramNode)
112
+ visit_program_children(node)
113
+ end
114
+
115
+ private
116
+
117
+ def visit_program_children(node)
118
+ return unless node.respond_to?(:compact_child_nodes)
119
+
120
+ node.compact_child_nodes.each do |child|
121
+ case child
122
+ when Prism::ModuleNode, Prism::ClassNode
123
+ visit_module_or_class(child)
124
+ when Prism::ProgramNode, Prism::StatementsNode
125
+ visit_program_children(child)
126
+ end
127
+ end
128
+ end
129
+
130
+ def visit_module_or_class(node)
131
+ visit_statements(node.body)
132
+ end
133
+
134
+ def visit_statements(body)
135
+ return if body.nil?
136
+
137
+ # Visibility tracking is intentionally absent — see
138
+ # the "visibility-agnostic" point in the module
139
+ # docstring. A `private def page_url(page)` inside
140
+ # a controller IS registered so a caller in the
141
+ # paired view does not false-fire `unknown-helper`.
142
+ statements_of(body).each do |stmt|
143
+ case stmt
144
+ when Prism::DefNode
145
+ record_helper(stmt)
146
+ when Prism::ModuleNode, Prism::ClassNode
147
+ visit_module_or_class(stmt)
148
+ when Prism::StatementsNode
149
+ visit_statements(stmt)
150
+ end
151
+ end
152
+ end
153
+
154
+ def statements_of(node)
155
+ case node
156
+ when Prism::StatementsNode then node.body
157
+ when Prism::BeginNode then statements_of(node.statements)
158
+ else [node]
159
+ end
160
+ end
161
+
162
+ def record_helper(def_node)
163
+ # Skip singleton methods (`def self.x`).
164
+ return if def_node.receiver
165
+
166
+ name = def_node.name.to_s
167
+ return unless HELPER_SUFFIXES.any? { |suffix| name.match?(suffix) }
168
+
169
+ @helper_names << name
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "devise_routes"
4
+ require_relative "doorkeeper_routes"
5
+
3
6
  module Rigor
4
7
  module Plugin
5
8
  class RailsRoutes < Rigor::Plugin::Base
@@ -30,11 +33,27 @@ module Rigor
30
33
  class HelperTable
31
34
  Entry = Data.define(:name, :arity, :path, :http_method, :action)
32
35
 
33
- attr_reader :entries
36
+ attr_reader :entries, :custom_helpers, :devise_resources
34
37
 
35
38
  # @param entries [Array<Entry>] freshly built; the
36
39
  # factory below is the canonical construction path.
37
- def initialize(entries)
40
+ # @param custom_helpers [Enumerable<String>] names of
41
+ # project-defined helper methods (typically pulled
42
+ # from `app/helpers/**/*.rb` by
43
+ # {HelperDiscoverer}) that the analyzer should treat
44
+ # as known — they do NOT correspond to a route but
45
+ # their presence MUST NOT fire `unknown-helper`. No
46
+ # arity / path metadata is recorded; the analyzer
47
+ # skips the route-side checks for these names.
48
+ # @param devise_resources [Enumerable<String>] resource
49
+ # names declared via `devise_for :resource` (already
50
+ # singularised, e.g. `"user"`). Used to recognise the
51
+ # dynamic OmniAuth helper family
52
+ # (`<resource>_<provider>_omniauth_(authorize|callback)_(path|url)`)
53
+ # whose provider segment is supplied at runtime — no
54
+ # per-name entry is registered for them but they MUST
55
+ # NOT fire `unknown-helper` either.
56
+ def initialize(entries, custom_helpers: [], devise_resources: [])
38
57
  @entries = entries.freeze
39
58
  # Multimap: a single helper name can map to multiple
40
59
  # entries when an uncountable-noun resource registers
@@ -43,6 +62,8 @@ module Rigor
43
62
  # returns the first entry (preserving the previous
44
63
  # API); `accepts_arity?` checks against every entry.
45
64
  @by_name = entries.group_by(&:name).transform_values(&:freeze).freeze
65
+ @custom_helpers = custom_helpers.to_set.freeze
66
+ @devise_resources = devise_resources.to_set(&:to_s).freeze
46
67
  freeze
47
68
  end
48
69
 
@@ -59,10 +80,50 @@ module Rigor
59
80
  @by_name.key?(helper_name.to_s)
60
81
  end
61
82
 
83
+ # True when `helper_name` is either a registered route
84
+ # helper, a discovered project-defined custom helper,
85
+ # OR a dynamic OmniAuth-shaped helper for one of the
86
+ # declared `devise_for` resources. The
87
+ # `unknown-helper` rule consults this predicate to
88
+ # decide whether to fire — `known?` alone misses
89
+ # custom helpers and OmniAuth providers, which would
90
+ # then false-fire on canonical Rails-app patterns.
91
+ def recognised?(helper_name)
92
+ name = helper_name.to_s
93
+ return true if @by_name.key?(name)
94
+ return true if @custom_helpers.include?(name)
95
+
96
+ omniauth_match?(name)
97
+ end
98
+
99
+ # `<resource>_<provider>_omniauth_(authorize|callback)_(path|url)` — when
100
+ # `<resource>` matches a declared `devise_for` resource
101
+ # the helper is dynamic-provider Devise OmniAuth. The
102
+ # provider segment is opaque to a static parser, so we
103
+ # accept any non-empty token between the resource and
104
+ # the omniauth suffix.
105
+ def omniauth_match?(name)
106
+ return false if @devise_resources.empty?
107
+
108
+ DeviseRoutes::OMNIAUTH_SUFFIXES.any? do |suffix|
109
+ next false unless name.end_with?(suffix)
110
+
111
+ stem = name.delete_suffix(suffix)
112
+ @devise_resources.any? do |resource|
113
+ stem.start_with?("#{resource}_") && stem.length > resource.length + 1
114
+ end
115
+ end
116
+ end
117
+
62
118
  # @return [Boolean] true when any entry under this
63
119
  # helper name accepts the given positional arity.
120
+ #
121
+ # Rails helpers have the signature `helper(*segments, options = {})`,
122
+ # so `expected + 1` is always valid — the extra argument is treated
123
+ # as a query-params/options hash (e.g. `users_path(page: 2)` or
124
+ # `user_path(@u, pagination_params(...))`).
64
125
  def accepts_arity?(helper_name, arity)
65
- (@by_name[helper_name.to_s] || []).any? { |entry| entry.arity == arity }
126
+ (@by_name[helper_name.to_s] || []).any? { |entry| entry.arity == arity || entry.arity + 1 == arity }
66
127
  end
67
128
 
68
129
  # @return [Array<Integer>] all accepted positional