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
@@ -4,6 +4,7 @@ require "rigor/plugin"
4
4
 
5
5
  require_relative "rails_routes/helper_table"
6
6
  require_relative "rails_routes/routes_parser"
7
+ require_relative "rails_routes/helper_discoverer"
7
8
  require_relative "rails_routes/analyzer"
8
9
 
9
10
  module Rigor
@@ -52,32 +53,104 @@ module Rigor
52
53
  class RailsRoutes < Rigor::Plugin::Base
53
54
  manifest(
54
55
  id: "rails-routes",
55
- version: "0.1.0",
56
+ # Bumped 2026-05-28 — GitLab FOSS sweep adds: (a)
57
+ # `draw_all :name` support (action_dispatch-draw_all
58
+ # gem; single-file load semantics matching `draw :name`);
59
+ # (b) keyword-style `scope(path: ':project_id',
60
+ # as: :project)` — path read from the `:path` keyword,
61
+ # not only from the positional first arg; (c) detection
62
+ # of iterative `direct(name.sub(FROM, TO)) do ... end`
63
+ # alias-generation idiom — generates `<TO>_*` aliases
64
+ # for every registered `<FROM>_*` helper. GitLab uses
65
+ # this to shorten `namespace_project_*` → `project_*`.
66
+ version: "0.27.0",
56
67
  description: "Validates Rails route-helper calls against `config/routes.rb`.",
57
68
  config_schema: {
58
- "routes_file" => :string
69
+ "routes_file" => :string,
70
+ "helper_paths" => :array
59
71
  },
60
72
  produces: [:helper_table]
61
73
  )
62
74
 
63
75
  DEFAULT_ROUTES_FILE = "config/routes.rb"
64
76
 
77
+ # The directories `HelperDiscoverer` walks for project-
78
+ # defined `*_path` / `*_url` methods. Default to the whole
79
+ # `app/` tree — the suffix filter inside the discoverer
80
+ # keeps the registered set tight, and real-world Rails
81
+ # apps routinely keep URL builders under `app/controllers`
82
+ # (private `def page_url`, `def callback_url` shapes),
83
+ # `app/lib` (Mastodon's `TranslationService::DeepL#base_url`),
84
+ # `app/services` (`SoftwareUpdateCheckService#api_url`),
85
+ # `app/serializers`, `app/presenters`, `app/decorators`,
86
+ # not only `app/helpers/`. Walking the whole tree is the
87
+ # honest answer to "does this `_path` / `_url` name exist
88
+ # anywhere in the project?"; the cost is a one-time Prism
89
+ # parse per file at startup, which is bounded.
90
+ DEFAULT_HELPER_PATHS = ["app"].freeze
91
+
65
92
  # Cached producer — reads `config/routes.rb` through
66
93
  # the trusted `IoBoundary` and parses through
67
94
  # {RoutesParser}. The descriptor's auto-collected
68
95
  # `FileEntry` digest invalidates the cache on routes-
69
96
  # file edits.
97
+ #
98
+ # Passes a `file_reader` lambda so the parser can follow
99
+ # `draw(:admin)` → `config/routes/admin.rb` partials.
70
100
  producer :helper_table do |_params|
101
+ routes_dir = "#{File.dirname(@routes_file)}/routes"
102
+ file_reader = lambda do |name|
103
+ io_boundary.read_file("#{routes_dir}/#{name}")
104
+ rescue StandardError
105
+ nil
106
+ end
71
107
  contents = io_boundary.read_file(@routes_file)
72
- RoutesParser.parse(contents)
108
+ custom_helpers = discover_custom_helpers
109
+ RoutesParser.parse(contents, file_reader: file_reader, custom_helpers: custom_helpers)
73
110
  end
74
111
 
75
112
  def init(_services)
76
113
  @routes_file = config.fetch("routes_file", DEFAULT_ROUTES_FILE)
114
+ @helper_paths = Array(config.fetch("helper_paths", DEFAULT_HELPER_PATHS)).map(&:to_s)
77
115
  @helper_table = nil
78
116
  @load_error = nil
79
117
  end
80
118
 
119
+ # Walks every configured `helper_paths:` directory
120
+ # through the trusted `IoBoundary` and returns the set
121
+ # of project-defined `*_path` / `*_url` method names
122
+ # for {HelperDiscoverer}. Each file digest is captured
123
+ # by the boundary so editing a helper file invalidates
124
+ # the `:helper_table` cache automatically. Returns the
125
+ # empty set when nothing under `helper_paths:` exists —
126
+ # the routes table still works.
127
+ def discover_custom_helpers
128
+ contents_per_path = {}
129
+ each_helper_file do |path|
130
+ contents_per_path[path] = io_boundary.read_file(path)
131
+ rescue Plugin::AccessDeniedError, Errno::ENOENT
132
+ next
133
+ end
134
+ HelperDiscoverer.discover(contents_per_path)
135
+ end
136
+
137
+ def pre_read_helper_files
138
+ each_helper_file do |path|
139
+ io_boundary.read_file(path)
140
+ rescue Plugin::AccessDeniedError, Errno::ENOENT
141
+ next
142
+ end
143
+ end
144
+
145
+ def each_helper_file(&)
146
+ @helper_paths.each do |dir|
147
+ absolute = File.expand_path(dir)
148
+ next unless File.directory?(absolute)
149
+
150
+ Dir.glob(File.join(absolute, "**", "*.rb")).each(&)
151
+ end
152
+ end
153
+
81
154
  # Publishes the parsed table to the cross-plugin fact
82
155
  # store so future Tier 2 plugins (rigor-actionpack
83
156
  # Phase 4) can read it via `services.fact_store.read`.
@@ -122,8 +195,12 @@ module Rigor
122
195
  # Read first so the IoBoundary's FileEntry digest
123
196
  # captures into the descriptor before `cache_for`
124
197
  # snapshots it (the same pattern documented in
125
- # rigor-routes / rigor-activerecord).
198
+ # rigor-routes / rigor-activerecord). Helper files are
199
+ # pre-read for the same reason — editing a file under
200
+ # `app/helpers/` MUST invalidate the helper_table cache
201
+ # so the new custom-helper set is picked up.
126
202
  io_boundary.read_file(@routes_file)
203
+ pre_read_helper_files
127
204
  @helper_table = cache_for(:helper_table, params: {}).call
128
205
  rescue Plugin::AccessDeniedError => e
129
206
  @load_error = "rigor-rails-routes: #{e.message}"
data/sig/rigor/scope.rbs CHANGED
@@ -18,8 +18,22 @@ module Rigor
18
18
  attr_reader discovered_method_visibilities: Hash[String, Hash[Symbol, Symbol]]
19
19
  attr_reader discovered_superclasses: Hash[String, String]
20
20
  attr_reader discovered_includes: Hash[String, Array[String]]
21
+ attr_reader indexed_narrowings: Hash[IndexedKey, Type::t]
22
+ attr_reader method_chain_narrowings: Hash[ChainKey, Type::t]
21
23
  attr_reader source_path: String?
22
24
 
25
+ class IndexedKey
26
+ attr_reader receiver_kind: Symbol
27
+ attr_reader receiver_name: Symbol
28
+ attr_reader key: untyped
29
+ end
30
+
31
+ class ChainKey
32
+ attr_reader receiver_kind: Symbol
33
+ attr_reader receiver_name: Symbol
34
+ attr_reader method_name: Symbol
35
+ end
36
+
23
37
  def self.empty: (?environment: Environment, ?source_path: String?) -> Scope
24
38
 
25
39
  def initialize: (environment: Environment, locals: Hash[Symbol, Type::t], ?fact_store: Analysis::FactStore, ?self_type: Type::t?, ?declared_types: Hash[untyped, Type::t], ?ivars: Hash[Symbol, Type::t], ?cvars: Hash[Symbol, Type::t], ?globals: Hash[Symbol, Type::t], ?source_path: String?) -> void
@@ -50,6 +64,14 @@ module Rigor
50
64
  def with_discovered_superclasses: (Hash[String, String] table) -> Scope
51
65
  def includes_of: (String | Symbol class_name) -> Array[String]
52
66
  def with_discovered_includes: (Hash[String, Array[String]] table) -> Scope
67
+ def indexed_narrowing: (Symbol receiver_kind, String | Symbol receiver_name, untyped key) -> Type::t?
68
+ def with_indexed_narrowing: (Symbol receiver_kind, String | Symbol receiver_name, untyped key, Type::t type) -> Scope
69
+ def without_indexed_narrowing: (Symbol receiver_kind, String | Symbol receiver_name, untyped key) -> Scope
70
+ def without_indexed_narrowings_for: (Symbol receiver_kind, String | Symbol receiver_name) -> Scope
71
+ def method_chain_narrowing: (Symbol receiver_kind, String | Symbol receiver_name, String | Symbol method_name) -> Type::t?
72
+ def with_method_chain_narrowing: (Symbol receiver_kind, String | Symbol receiver_name, String | Symbol method_name, Type::t type) -> Scope
73
+ def without_method_chain_narrowing: (Symbol receiver_kind, String | Symbol receiver_name, String | Symbol method_name) -> Scope
74
+ def without_method_chain_narrowings_for: (Symbol receiver_kind, String | Symbol receiver_name) -> Scope
53
75
  def with_fact: (Analysis::FactStore::Fact fact) -> Scope
54
76
  def with_self_type: (Type::t? type) -> Scope
55
77
  def with_declared_types: (Hash[untyped, Type::t] table) -> Scope
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rigortype
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.11
4
+ version: 0.1.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rigor contributors
@@ -257,6 +257,7 @@ files:
257
257
  - lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb
258
258
  - lib/rigor/analysis/dependency_source_inference/walker.rb
259
259
  - lib/rigor/analysis/diagnostic.rb
260
+ - lib/rigor/analysis/erb_template_detector.rb
260
261
  - lib/rigor/analysis/fact_store.rb
261
262
  - lib/rigor/analysis/project_scan.rb
262
263
  - lib/rigor/analysis/result.rb
@@ -290,6 +291,8 @@ files:
290
291
  - lib/rigor/cli/explain_command.rb
291
292
  - lib/rigor/cli/lsp_command.rb
292
293
  - lib/rigor/cli/mcp_command.rb
294
+ - lib/rigor/cli/plugins_command.rb
295
+ - lib/rigor/cli/plugins_renderer.rb
293
296
  - lib/rigor/cli/prism_colorizer.rb
294
297
  - lib/rigor/cli/sig_gen_command.rb
295
298
  - lib/rigor/cli/triage_command.rb
@@ -351,6 +354,7 @@ files:
351
354
  - lib/rigor/inference/hkt_body_parser.rb
352
355
  - lib/rigor/inference/hkt_reducer.rb
353
356
  - lib/rigor/inference/hkt_registry.rb
357
+ - lib/rigor/inference/indexed_narrowing.rb
354
358
  - lib/rigor/inference/macro_block_self_type.rb
355
359
  - lib/rigor/inference/method_dispatcher.rb
356
360
  - lib/rigor/inference/method_dispatcher/block_folding.rb
@@ -373,6 +377,7 @@ files:
373
377
  - lib/rigor/inference/method_dispatcher/uri_folding.rb
374
378
  - lib/rigor/inference/method_parameter_binder.rb
375
379
  - lib/rigor/inference/multi_target_binder.rb
380
+ - lib/rigor/inference/mutation_widening.rb
376
381
  - lib/rigor/inference/narrowing.rb
377
382
  - lib/rigor/inference/precision_scanner.rb
378
383
  - lib/rigor/inference/project_patched_methods.rb
@@ -555,6 +560,9 @@ files:
555
560
  - plugins/rigor-rails-routes/lib/rigor-rails-routes.rb
556
561
  - plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb
557
562
  - plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb
563
+ - plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb
564
+ - plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb
565
+ - plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb
558
566
  - plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb
559
567
  - plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb
560
568
  - plugins/rigor-rails/lib/rigor-rails.rb