rigortype 0.1.10 → 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.
- checksums.yaml +4 -4
- data/lib/rigor/analysis/baseline.rb +51 -15
- data/lib/rigor/analysis/erb_template_detector.rb +38 -0
- data/lib/rigor/analysis/runner.rb +6 -1
- data/lib/rigor/analysis/worker_session.rb +6 -1
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/plugins_command.rb +308 -0
- data/lib/rigor/cli/plugins_renderer.rb +173 -0
- data/lib/rigor/cli.rb +44 -3
- data/lib/rigor/inference/block_parameter_binder.rb +35 -0
- data/lib/rigor/inference/expression_typer.rb +69 -30
- data/lib/rigor/inference/indexed_narrowing.rb +187 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +24 -0
- data/lib/rigor/inference/method_dispatcher.rb +23 -0
- data/lib/rigor/inference/mutation_widening.rb +285 -0
- data/lib/rigor/inference/narrowing.rb +72 -4
- data/lib/rigor/inference/scope_indexer.rb +409 -12
- data/lib/rigor/inference/statement_evaluator.rb +256 -4
- data/lib/rigor/scope.rb +181 -4
- 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 +199 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +398 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +86 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +183 -0
- data/plugins/rigor-actionmailer/lib/rigor-actionmailer.rb +3 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +713 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +201 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +226 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +261 -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 +283 -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 +250 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +98 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +590 -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 +37 -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 +478 -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 +353 -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 +175 -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 +350 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +264 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +100 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +175 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +164 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1538 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +235 -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/scope.rbs +22 -0
- metadata +157 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "worker_index"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Plugin
|
|
9
|
+
class Sidekiq < Rigor::Plugin::Base
|
|
10
|
+
# Walks the configured worker-search paths via the
|
|
11
|
+
# plugin's `IoBoundary`, parses each `.rb` file with
|
|
12
|
+
# Prism, and collects classes that `include
|
|
13
|
+
# Sidekiq::Job` (or one of the configured marker
|
|
14
|
+
# modules). For each discovered class, the discoverer
|
|
15
|
+
# also reads the `#perform` method's parameter list
|
|
16
|
+
# and computes the arity envelope.
|
|
17
|
+
#
|
|
18
|
+
# Limitations (intentional for v0.1.0):
|
|
19
|
+
#
|
|
20
|
+
# - Only direct `include` matches against the
|
|
21
|
+
# configured marker modules. `class MyWorker;
|
|
22
|
+
# include Concerns::Sidekiqable; end` where
|
|
23
|
+
# `Concerns::Sidekiqable` re-includes `Sidekiq::Job`
|
|
24
|
+
# is NOT discovered. Add the intermediate module to
|
|
25
|
+
# `worker_marker_modules` if needed.
|
|
26
|
+
# - The qualified class name is the lexical path
|
|
27
|
+
# (`Admin::WelcomeWorker` for a class declared
|
|
28
|
+
# inside `module Admin`).
|
|
29
|
+
# - `#perform` arity is read from the syntactic
|
|
30
|
+
# parameter list. Methods built via
|
|
31
|
+
# `define_method` are out of scope.
|
|
32
|
+
class WorkerDiscoverer
|
|
33
|
+
def initialize(io_boundary:, search_paths:, marker_modules:)
|
|
34
|
+
@io_boundary = io_boundary
|
|
35
|
+
@search_paths = search_paths
|
|
36
|
+
@marker_modules = marker_modules.to_set
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [WorkerIndex]
|
|
40
|
+
def discover
|
|
41
|
+
entries = []
|
|
42
|
+
ruby_files_under(@search_paths).each do |path|
|
|
43
|
+
contents = read_safely(path)
|
|
44
|
+
next if contents.nil?
|
|
45
|
+
|
|
46
|
+
tree = Prism.parse(contents).value
|
|
47
|
+
walk_for_workers(tree, []) do |class_name, perform_def|
|
|
48
|
+
entries << build_entry(class_name, perform_def)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
WorkerIndex.new(entries)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def read_safely(path)
|
|
57
|
+
@io_boundary.read_file(path)
|
|
58
|
+
rescue Plugin::AccessDeniedError, Errno::ENOENT
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def ruby_files_under(roots)
|
|
63
|
+
roots.flat_map do |root|
|
|
64
|
+
absolute = File.expand_path(root)
|
|
65
|
+
next [] unless File.directory?(absolute)
|
|
66
|
+
|
|
67
|
+
Dir.glob(File.join(absolute, "**", "*.rb"))
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def walk_for_workers(node, lexical_path, &)
|
|
72
|
+
return if node.nil?
|
|
73
|
+
|
|
74
|
+
case node
|
|
75
|
+
when Prism::ClassNode then visit_class(node, lexical_path, &)
|
|
76
|
+
when Prism::ModuleNode then visit_module(node, lexical_path, &)
|
|
77
|
+
else
|
|
78
|
+
node.compact_child_nodes.each { |child| walk_for_workers(child, lexical_path, &) }
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def visit_class(node, lexical_path, &)
|
|
83
|
+
class_local_name = constant_path_name(node.constant_path)
|
|
84
|
+
return if class_local_name.nil?
|
|
85
|
+
|
|
86
|
+
full_name = (lexical_path + [class_local_name]).join("::")
|
|
87
|
+
if includes_marker_module?(node.body)
|
|
88
|
+
perform_def = lookup_perform_def(node.body)
|
|
89
|
+
yield full_name, perform_def
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
inner_path = lexical_path + [class_local_name]
|
|
93
|
+
walk_for_workers(node.body, inner_path, &) if node.body
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def visit_module(node, lexical_path, &)
|
|
97
|
+
module_local_name = constant_path_name(node.constant_path)
|
|
98
|
+
return if module_local_name.nil?
|
|
99
|
+
|
|
100
|
+
inner_path = lexical_path + [module_local_name]
|
|
101
|
+
walk_for_workers(node.body, inner_path, &) if node.body
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Returns true if the class body contains a top-level
|
|
105
|
+
# `include <Module>` call where `<Module>` matches
|
|
106
|
+
# one of the configured marker modules.
|
|
107
|
+
def includes_marker_module?(body)
|
|
108
|
+
return false if body.nil?
|
|
109
|
+
|
|
110
|
+
body.compact_child_nodes.any? do |node|
|
|
111
|
+
next false unless node.is_a?(Prism::CallNode)
|
|
112
|
+
next false unless node.name == :include
|
|
113
|
+
next false unless node.receiver.nil?
|
|
114
|
+
|
|
115
|
+
arg = node.arguments&.arguments&.first
|
|
116
|
+
module_name = constant_path_name(arg)
|
|
117
|
+
module_name && @marker_modules.include?(module_name)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def constant_path_name(node)
|
|
122
|
+
return nil if node.nil?
|
|
123
|
+
|
|
124
|
+
case node
|
|
125
|
+
when Prism::ConstantReadNode then node.name.to_s
|
|
126
|
+
when Prism::ConstantPathNode
|
|
127
|
+
parts = []
|
|
128
|
+
current = node
|
|
129
|
+
while current.is_a?(Prism::ConstantPathNode)
|
|
130
|
+
parts.unshift(current.name.to_s)
|
|
131
|
+
current = current.parent
|
|
132
|
+
end
|
|
133
|
+
case current
|
|
134
|
+
when nil then "::#{parts.join('::')}"
|
|
135
|
+
when Prism::ConstantReadNode then "#{current.name}::#{parts.join('::')}"
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Returns the instance-side `def perform(...)` node
|
|
141
|
+
# from a class body, or `nil` when the class doesn't
|
|
142
|
+
# override `#perform`.
|
|
143
|
+
def lookup_perform_def(body)
|
|
144
|
+
return nil if body.nil?
|
|
145
|
+
|
|
146
|
+
body.compact_child_nodes.each do |node|
|
|
147
|
+
next unless node.is_a?(Prism::DefNode) && node.name == :perform
|
|
148
|
+
next if node.receiver.is_a?(Prism::SelfNode)
|
|
149
|
+
|
|
150
|
+
return node
|
|
151
|
+
end
|
|
152
|
+
nil
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Builds a `WorkerIndex::Entry` from the discovered
|
|
156
|
+
# class's `#perform` def. When the class doesn't
|
|
157
|
+
# override `#perform`, we record an "any-arity"
|
|
158
|
+
# entry — Sidekiq itself doesn't supply a default
|
|
159
|
+
# `#perform`, so calling `perform_async` on a
|
|
160
|
+
# worker without one is the user's bug, not the
|
|
161
|
+
# plugin's call to flag without runtime context.
|
|
162
|
+
def build_entry(class_name, perform_def)
|
|
163
|
+
if perform_def.nil?
|
|
164
|
+
return WorkerIndex::Entry.new(
|
|
165
|
+
class_name: class_name, min_arity: 0,
|
|
166
|
+
max_arity: Float::INFINITY
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
parameters = perform_def.parameters
|
|
171
|
+
if parameters.nil?
|
|
172
|
+
return WorkerIndex::Entry.new(
|
|
173
|
+
class_name: class_name, min_arity: 0, max_arity: 0
|
|
174
|
+
)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
required_count = (parameters.requireds || []).size
|
|
178
|
+
optional_count = (parameters.optionals || []).size
|
|
179
|
+
rest_present = !parameters.rest.nil?
|
|
180
|
+
|
|
181
|
+
WorkerIndex::Entry.new(
|
|
182
|
+
class_name: class_name,
|
|
183
|
+
min_arity: required_count,
|
|
184
|
+
max_arity: rest_present ? Float::INFINITY : required_count + optional_count
|
|
185
|
+
)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Plugin
|
|
5
|
+
class Sidekiq < Rigor::Plugin::Base
|
|
6
|
+
# Frozen catalogue of discovered Sidekiq worker
|
|
7
|
+
# classes keyed by qualified class name. Each entry
|
|
8
|
+
# holds the `#perform` method's arity envelope so the
|
|
9
|
+
# analyzer can validate `Worker.perform_async(...)`
|
|
10
|
+
# call sites.
|
|
11
|
+
#
|
|
12
|
+
# Same envelope shape as `rigor-activejob`'s
|
|
13
|
+
# `JobIndex::Entry`: `min_arity` / `max_arity` form a
|
|
14
|
+
# closed range (`Float::INFINITY` for the upper bound
|
|
15
|
+
# when `*args` is present).
|
|
16
|
+
class WorkerIndex
|
|
17
|
+
Entry = Data.define(:class_name, :min_arity, :max_arity) do
|
|
18
|
+
def arity_label
|
|
19
|
+
return "#{min_arity}+" if max_arity == Float::INFINITY
|
|
20
|
+
return min_arity.to_s if min_arity == max_arity
|
|
21
|
+
|
|
22
|
+
"#{min_arity}..#{max_arity}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def accepts?(actual)
|
|
26
|
+
actual.between?(min_arity, max_arity)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
attr_reader :entries
|
|
31
|
+
|
|
32
|
+
def initialize(entries)
|
|
33
|
+
@entries = entries.freeze
|
|
34
|
+
@by_name = entries.to_h { |entry| [entry.class_name, entry] }.freeze
|
|
35
|
+
freeze
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [Entry, nil]
|
|
39
|
+
def find(class_name)
|
|
40
|
+
@by_name[class_name.to_s]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def known?(class_name)
|
|
44
|
+
@by_name.key?(class_name.to_s)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def empty?
|
|
48
|
+
@entries.empty?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def size
|
|
52
|
+
@entries.size
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def names
|
|
56
|
+
@by_name.keys
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rigor/plugin"
|
|
4
|
+
|
|
5
|
+
require_relative "sidekiq/worker_index"
|
|
6
|
+
require_relative "sidekiq/worker_discoverer"
|
|
7
|
+
require_relative "sidekiq/analyzer"
|
|
8
|
+
|
|
9
|
+
module Rigor
|
|
10
|
+
module Plugin
|
|
11
|
+
# rigor-sidekiq — validates `Worker.perform_async(...)`
|
|
12
|
+
# / `.perform_in(...)` / `.perform_at(...)` /
|
|
13
|
+
# `.perform_inline(...)` argument arity against the
|
|
14
|
+
# discovered `#perform` definitions.
|
|
15
|
+
#
|
|
16
|
+
# Tier 3C of the [Rails plugins roadmap](../../../../docs/design/20260508-rails-plugins-roadmap.md).
|
|
17
|
+
# Statically discovers Sidekiq workers by walking
|
|
18
|
+
# `worker_search_paths` and parsing each file with
|
|
19
|
+
# Prism — no `sidekiq` runtime dependency.
|
|
20
|
+
#
|
|
21
|
+
# ## Configuration
|
|
22
|
+
#
|
|
23
|
+
# plugins:
|
|
24
|
+
# - gem: rigor-sidekiq
|
|
25
|
+
# config:
|
|
26
|
+
# worker_search_paths: ["app/workers", "app/sidekiq"] # default; optional
|
|
27
|
+
# worker_marker_modules: ["Sidekiq::Job", "Sidekiq::Worker"] # default; optional
|
|
28
|
+
#
|
|
29
|
+
# ## What it checks
|
|
30
|
+
#
|
|
31
|
+
# 1. **Argument arity** — `perform_async(args)` /
|
|
32
|
+
# `perform_inline(args)` forward every argument to
|
|
33
|
+
# `#perform`; `perform_in(t, args)` /
|
|
34
|
+
# `perform_at(t, args)` consume the first argument
|
|
35
|
+
# as the schedule and forward the rest. Mismatches
|
|
36
|
+
# emit `wrong-arity`.
|
|
37
|
+
# 2. **Missing schedule** — `perform_in()` /
|
|
38
|
+
# `perform_at()` with zero arguments emit
|
|
39
|
+
# `missing-schedule`.
|
|
40
|
+
#
|
|
41
|
+
# ## Limitations (v0.1.0)
|
|
42
|
+
#
|
|
43
|
+
# - Direct `include` matches only against the
|
|
44
|
+
# configured marker modules. Indirect includes via a
|
|
45
|
+
# custom concern are out of scope.
|
|
46
|
+
# - `#perform` arity is read from the syntactic
|
|
47
|
+
# parameter list. `define_method` actions are out of
|
|
48
|
+
# scope.
|
|
49
|
+
# - Required keyword arguments are not validated at
|
|
50
|
+
# the call site (positional-only for v0.1.0). Sidekiq
|
|
51
|
+
# serialises arguments to JSON, so keyword args are
|
|
52
|
+
# uncommon in practice.
|
|
53
|
+
# - The schedule argument's type isn't validated (no
|
|
54
|
+
# "is this a Time?" check); we just consume it.
|
|
55
|
+
class Sidekiq < Rigor::Plugin::Base
|
|
56
|
+
manifest(
|
|
57
|
+
id: "sidekiq",
|
|
58
|
+
version: "0.1.0",
|
|
59
|
+
description: "Validates Sidekiq `Worker.perform_async` argument arity.",
|
|
60
|
+
config_schema: {
|
|
61
|
+
"worker_search_paths" => :array,
|
|
62
|
+
"worker_marker_modules" => :array
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
DEFAULT_WORKER_SEARCH_PATHS = ["app/workers", "app/sidekiq"].freeze
|
|
67
|
+
DEFAULT_WORKER_MARKER_MODULES = %w[Sidekiq::Job Sidekiq::Worker].freeze
|
|
68
|
+
|
|
69
|
+
producer :worker_index do |_params|
|
|
70
|
+
WorkerDiscoverer.new(
|
|
71
|
+
io_boundary: io_boundary,
|
|
72
|
+
search_paths: @worker_search_paths,
|
|
73
|
+
marker_modules: @worker_marker_modules
|
|
74
|
+
).discover
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def init(_services)
|
|
78
|
+
@worker_search_paths = Array(config.fetch("worker_search_paths", DEFAULT_WORKER_SEARCH_PATHS)).map(&:to_s)
|
|
79
|
+
@worker_marker_modules = Array(
|
|
80
|
+
config.fetch("worker_marker_modules", DEFAULT_WORKER_MARKER_MODULES)
|
|
81
|
+
).map(&:to_s)
|
|
82
|
+
@worker_index = nil
|
|
83
|
+
@load_error = nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
|
|
87
|
+
index = worker_index_or_nil
|
|
88
|
+
return [load_error_diagnostic(path)] if index.nil? && @load_error
|
|
89
|
+
return [] if index.nil? || index.empty?
|
|
90
|
+
|
|
91
|
+
Analyzer.diagnose(path: path, root: root, worker_index: index).map { |diag| build_diagnostic(diag) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def worker_index_or_nil
|
|
97
|
+
return @worker_index if @worker_index
|
|
98
|
+
|
|
99
|
+
@worker_index = cache_for(:worker_index, params: {}).call
|
|
100
|
+
rescue StandardError => e
|
|
101
|
+
@load_error = "rigor-sidekiq: failed to discover workers: #{e.class}: #{e.message}"
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def load_error_diagnostic(path)
|
|
106
|
+
Rigor::Analysis::Diagnostic.new(
|
|
107
|
+
path: path, line: 1, column: 1,
|
|
108
|
+
message: @load_error,
|
|
109
|
+
severity: :warning,
|
|
110
|
+
rule: "load-error"
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def build_diagnostic(diag)
|
|
115
|
+
Rigor::Analysis::Diagnostic.new(
|
|
116
|
+
path: diag.path, line: diag.line, column: diag.column,
|
|
117
|
+
message: diag.message, severity: diag.severity, rule: diag.rule
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
Rigor::Plugin.register(Sidekiq)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rigor/plugin"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Plugin
|
|
7
|
+
# ADR-16 Tier A worked plugin: recognises Sinatra's class-
|
|
8
|
+
# level route DSL.
|
|
9
|
+
#
|
|
10
|
+
# Sinatra's modular style:
|
|
11
|
+
#
|
|
12
|
+
# class MyApp < Sinatra::Base
|
|
13
|
+
# get "/hello" do
|
|
14
|
+
# "Hello #{params['name']}"
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# post "/bye" do
|
|
18
|
+
# halt 403 if params["forbidden"]
|
|
19
|
+
# redirect "/landing"
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# At runtime `Sinatra::Base#generate_method`
|
|
24
|
+
# (`lib/sinatra/base.rb:1788-1793`) does `define_method(name,
|
|
25
|
+
# &block); remove_method`, turning each block into a real
|
|
26
|
+
# instance method of the user's app class. The substrate's
|
|
27
|
+
# Tier A hook (`Rigor::Inference::MacroBlockSelfType`)
|
|
28
|
+
# replays the same contract statically: the block runs with
|
|
29
|
+
# `self : Nominal[MyApp]`, so bare identifiers (`params`,
|
|
30
|
+
# `redirect`, `halt`, `session`, `headers`, `content_type`,
|
|
31
|
+
# `body`, `status`, `erb`, …) resolve through
|
|
32
|
+
# `Sinatra::Base`'s RBS via rigor's normal inference path.
|
|
33
|
+
#
|
|
34
|
+
# ## Reach
|
|
35
|
+
#
|
|
36
|
+
# All nine class-level HTTP verb methods Sinatra exposes:
|
|
37
|
+
# `get`, `post`, `put`, `delete`, `head`, `options`, `patch`,
|
|
38
|
+
# `link`, `unlink` (`lib/sinatra/base.rb:1531-1553`). Both
|
|
39
|
+
# modular-style subclasses of `Sinatra::Base` and `Sinatra::Application`
|
|
40
|
+
# (classic top-level style, when used via `class App <
|
|
41
|
+
# Sinatra::Application`) match because the receiver
|
|
42
|
+
# constraint accepts every subclass.
|
|
43
|
+
#
|
|
44
|
+
# ## What the plugin does NOT do (yet)
|
|
45
|
+
#
|
|
46
|
+
# - **Routing diagnostics.** Path uniqueness, conflict
|
|
47
|
+
# detection, named-route reverse lookup — none of these
|
|
48
|
+
# are in slice 1c's scope.
|
|
49
|
+
# - **Custom helpers.** `helpers do ... end` blocks that
|
|
50
|
+
# inject module methods into the app's instance namespace
|
|
51
|
+
# are Tier C / Tier B work, not Tier A.
|
|
52
|
+
# - **Configure / settings.** `configure do ... end` and
|
|
53
|
+
# `set :session_secret, "..."` are settings DSL, not
|
|
54
|
+
# route DSL — handled by separate substrate entries when
|
|
55
|
+
# demand surfaces.
|
|
56
|
+
# - **Classic-style top-level routes.** A bare
|
|
57
|
+
# `get '/path' do ... end` at the top of a script (no
|
|
58
|
+
# enclosing `class < Sinatra::Base`) is the classic-mode
|
|
59
|
+
# pattern (`lib/sinatra/main.rb`). Tier A as currently
|
|
60
|
+
# wired requires the receiver's class to be visible at
|
|
61
|
+
# the call site; a top-level call's receiver is the
|
|
62
|
+
# classic-mode `Sinatra::Application`, which the
|
|
63
|
+
# `Sinatra::Delegator` mixin forwards from `main`. The
|
|
64
|
+
# classic style is deferred until the demand justifies
|
|
65
|
+
# the extra match shape.
|
|
66
|
+
#
|
|
67
|
+
# See `plugins/rigor-sinatra/README.md` for usage and the
|
|
68
|
+
# demo script under `plugins/rigor-sinatra/demo/`.
|
|
69
|
+
class Sinatra < Rigor::Plugin::Base
|
|
70
|
+
manifest(
|
|
71
|
+
id: "sinatra",
|
|
72
|
+
version: "0.1.0",
|
|
73
|
+
description: "Recognises Sinatra's class-level route DSL via ADR-16 Tier A.",
|
|
74
|
+
block_as_methods: [
|
|
75
|
+
Rigor::Plugin::Macro::BlockAsMethod.new(
|
|
76
|
+
receiver_constraint: "Sinatra::Base",
|
|
77
|
+
verbs: %i[get post put delete head options patch link unlink]
|
|
78
|
+
)
|
|
79
|
+
]
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
Rigor::Plugin.register(Sinatra)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Gem entry point. Required by Rigor's plugin loader when
|
|
4
|
+
# `.rigor.yml` lists `rigor-sinatra` under `plugins:`. The
|
|
5
|
+
# loader expects this `require` to side-effect a call to
|
|
6
|
+
# `Rigor::Plugin.register`, which the body of
|
|
7
|
+
# `lib/rigor/plugin/sinatra.rb` performs at load time.
|
|
8
|
+
require_relative "rigor/plugin/sinatra"
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "type_translator"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Plugin
|
|
9
|
+
class Sorbet < Rigor::Plugin::Base
|
|
10
|
+
# Slice 6 of ADR-11 — recognises `T.absurd(x)` calls and
|
|
11
|
+
# composes them with the engine's flow-sensitive
|
|
12
|
+
# narrowing. `T.absurd` asserts that a code branch is
|
|
13
|
+
# statically unreachable; it's the standard Sorbet idiom
|
|
14
|
+
# for case/when exhaustiveness:
|
|
15
|
+
#
|
|
16
|
+
# case x
|
|
17
|
+
# when A then ...
|
|
18
|
+
# when B then ...
|
|
19
|
+
# else
|
|
20
|
+
# T.absurd(x)
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# If every case has been handled, `x` at the `else` branch
|
|
24
|
+
# has been narrowed to `T.noreturn` (Rigor's `Type::Bot`)
|
|
25
|
+
# and the assertion holds. If the user forgot a case, `x`
|
|
26
|
+
# narrows to whatever's left and the assertion is wrong —
|
|
27
|
+
# we surface that mistake as `plugin.sorbet.absurd-reachable`.
|
|
28
|
+
#
|
|
29
|
+
# ## Two-phase mechanism
|
|
30
|
+
#
|
|
31
|
+
# The recogniser is invoked from `flow_contribution_for`
|
|
32
|
+
# where the per-node `scope:` carries the proper narrowing
|
|
33
|
+
# context. It returns:
|
|
34
|
+
#
|
|
35
|
+
# - A `FlowContribution` with `return_type: bot` and
|
|
36
|
+
# `exceptional: :raises` regardless of reachability
|
|
37
|
+
# (faithful to `T.absurd`'s runtime behaviour: it always
|
|
38
|
+
# raises). This lets the engine's existing flow analysis
|
|
39
|
+
# treat code after `T.absurd` as unreachable, matching
|
|
40
|
+
# what users of Sorbet expect.
|
|
41
|
+
# - When the branch is REACHABLE (the discriminant's type
|
|
42
|
+
# isn't `bot`), the recogniser also records the call
|
|
43
|
+
# node in a per-plugin set. The plugin's
|
|
44
|
+
# `diagnostics_for_file` later walks the AST for
|
|
45
|
+
# `T.absurd` calls and emits a
|
|
46
|
+
# `plugin.sorbet.absurd-reachable` warning at every
|
|
47
|
+
# call_node whose object identity matches the recorded
|
|
48
|
+
# set. We rely on the runner only parsing each file
|
|
49
|
+
# once per run, so the same Prism node object is seen
|
|
50
|
+
# in both `flow_contribution_for` and
|
|
51
|
+
# `diagnostics_for_file`.
|
|
52
|
+
module AbsurdRecognizer
|
|
53
|
+
# @param call_node [Prism::CallNode]
|
|
54
|
+
# @return [Boolean] true when `call_node` is `T.absurd(x)`.
|
|
55
|
+
def self.absurd_call?(call_node)
|
|
56
|
+
return false unless call_node.is_a?(Prism::CallNode)
|
|
57
|
+
return false unless call_node.name == :absurd
|
|
58
|
+
return false unless TypeTranslator.sorbet_t_namespaced?(call_node.receiver)
|
|
59
|
+
|
|
60
|
+
# Slice 6 only handles single-argument `T.absurd(x)`;
|
|
61
|
+
# no-arg / multi-arg shapes are syntax errors at
|
|
62
|
+
# Sorbet's level too.
|
|
63
|
+
arguments = call_node.arguments&.arguments
|
|
64
|
+
arguments&.size == 1
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @param call_node [Prism::CallNode]
|
|
68
|
+
# @param scope [Rigor::Scope, nil]
|
|
69
|
+
# @return [Boolean] true when the discriminant has been
|
|
70
|
+
# narrowed to `bot` (the branch is unreachable, so
|
|
71
|
+
# `T.absurd` is correct). The caller suppresses the
|
|
72
|
+
# `absurd-reachable` diagnostic in this case.
|
|
73
|
+
def self.exhaustive?(call_node, scope)
|
|
74
|
+
return false if scope.nil?
|
|
75
|
+
|
|
76
|
+
arg = call_node.arguments.arguments.first
|
|
77
|
+
arg_type = scope.type_of(arg)
|
|
78
|
+
arg_type.equal?(Rigor::Type::Bot.instance) || arg_type.is_a?(Rigor::Type::Bot)
|
|
79
|
+
rescue StandardError
|
|
80
|
+
# On synthetic / unrecognised nodes the typer may
|
|
81
|
+
# raise; treat as "can't prove unreachable" so the
|
|
82
|
+
# diagnostic fires conservatively.
|
|
83
|
+
false
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# The contribution every `T.absurd` call gets,
|
|
87
|
+
# regardless of static reachability — `T.absurd` raises
|
|
88
|
+
# at runtime, so its return type is `bot` and the call
|
|
89
|
+
# is exceptional. This lets the engine's flow analysis
|
|
90
|
+
# treat code after the call as unreachable (no
|
|
91
|
+
# `flow.unreachable-branch` from us; that's an engine
|
|
92
|
+
# rule that consults the same effect lattice).
|
|
93
|
+
def self.contribution(call_node, plugin_id)
|
|
94
|
+
Rigor::FlowContribution.new(
|
|
95
|
+
return_type: Rigor::Type::Combinator.bot,
|
|
96
|
+
exceptional: :raises,
|
|
97
|
+
provenance: Rigor::FlowContribution::Provenance.new(
|
|
98
|
+
source_family: "plugin.#{plugin_id}",
|
|
99
|
+
plugin_id: plugin_id,
|
|
100
|
+
node: call_node,
|
|
101
|
+
descriptor: nil
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|