rigortype 0.1.18 → 0.1.19
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 +159 -224
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -3
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +29 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +169 -23
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +9 -3
- data/lib/rigor/analysis/check_rules.rb +266 -63
- data/lib/rigor/analysis/diagnostic.rb +8 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +2 -1
- data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -1
- data/lib/rigor/analysis/runner.rb +58 -21
- data/lib/rigor/analysis/worker_session.rb +21 -11
- data/lib/rigor/bleeding_edge.rb +123 -0
- data/lib/rigor/cache/descriptor.rb +86 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cli/annotate_command.rb +100 -15
- data/lib/rigor/cli/check_command.rb +3 -0
- data/lib/rigor/cli/plugins_command.rb +2 -4
- data/lib/rigor/cli/plugins_renderer.rb +0 -2
- data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
- data/lib/rigor/cli/triage_command.rb +6 -3
- data/lib/rigor/cli/triage_renderer.rb +15 -1
- data/lib/rigor/cli.rb +9 -1
- data/lib/rigor/configuration/severity_profile.rb +13 -1
- data/lib/rigor/configuration.rb +57 -1
- data/lib/rigor/environment/rbs_loader.rb +25 -0
- data/lib/rigor/inference/body_fixpoint.rb +89 -0
- data/lib/rigor/inference/budget_trace.rb +29 -2
- data/lib/rigor/inference/expression_typer.rb +1052 -43
- data/lib/rigor/inference/macro_block_self_type.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +54 -14
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +148 -10
- data/lib/rigor/inference/method_dispatcher.rb +72 -1
- data/lib/rigor/inference/method_parameter_binder.rb +56 -2
- data/lib/rigor/inference/multi_target_binder.rb +46 -3
- data/lib/rigor/inference/mutation_widening.rb +142 -0
- data/lib/rigor/inference/narrowing.rb +270 -37
- data/lib/rigor/inference/scope_indexer.rb +696 -25
- data/lib/rigor/inference/statement_evaluator.rb +963 -16
- data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
- data/lib/rigor/plugin/base.rb +235 -79
- data/lib/rigor/plugin/macro/block_as_method.rb +22 -21
- data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
- data/lib/rigor/plugin/macro.rb +2 -3
- data/lib/rigor/plugin/manifest.rb +4 -24
- data/lib/rigor/plugin/node_rule_walk.rb +59 -14
- data/lib/rigor/plugin/registry.rb +12 -11
- data/lib/rigor/scope/discovery_index.rb +2 -0
- data/lib/rigor/scope.rb +132 -6
- data/lib/rigor/sig_gen/generator.rb +8 -0
- data/lib/rigor/triage/catalogue.rb +4 -19
- data/lib/rigor/triage.rb +69 -1
- data/lib/rigor/type/combinator.rb +29 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +13 -29
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +27 -90
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +20 -19
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +10 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +11 -40
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +21 -34
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +11 -18
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -13
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +25 -0
- data/sig/rigor/analysis/fact_store.rbs +3 -0
- data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
- data/sig/rigor/plugin/base.rbs +5 -2
- data/sig/rigor/plugin/manifest.rbs +1 -2
- data/sig/rigor/scope.rbs +10 -1
- data/sig/rigor/type.rbs +1 -0
- data/sig/rigor.rbs +1 -1
- data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
- data/skills/rigor-plugin-author/SKILL.md +6 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
- metadata +7 -2
- data/lib/rigor/plugin/macro/external_file.rb +0 -143
|
@@ -66,7 +66,7 @@ module Rigor
|
|
|
66
66
|
}
|
|
67
67
|
)
|
|
68
68
|
|
|
69
|
-
producer :policy_index do |_params|
|
|
69
|
+
producer :policy_index, watch: -> { [[@policy_search_paths, "**/*.rb"]] } do |_params|
|
|
70
70
|
PolicyDiscoverer.new(
|
|
71
71
|
io_boundary: io_boundary,
|
|
72
72
|
search_paths: @policy_search_paths,
|
|
@@ -77,46 +77,35 @@ module Rigor
|
|
|
77
77
|
def init(_services)
|
|
78
78
|
@policy_search_paths = Array(config.fetch("policy_search_paths")).map(&:to_s)
|
|
79
79
|
@policy_base_classes = Array(config.fetch("policy_base_classes")).map(&:to_s)
|
|
80
|
-
@policy_index = nil
|
|
81
|
-
@load_error = nil
|
|
82
80
|
end
|
|
83
81
|
|
|
84
82
|
# File-level only: the load-error emission. The per-call policy
|
|
85
83
|
# validation runs over the engine-owned walk via the node_rule
|
|
86
84
|
# below (ADR-37). The index is lazily loaded + memoised by
|
|
87
|
-
#
|
|
85
|
+
# `producer_value`, so both surfaces share one load.
|
|
88
86
|
def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
|
|
89
|
-
index =
|
|
90
|
-
return [load_error_diagnostic(path)] if index.nil? &&
|
|
87
|
+
index = producer_value(:policy_index)
|
|
88
|
+
return [load_error_diagnostic(path)] if index.nil? && producer_error(:policy_index)
|
|
91
89
|
|
|
92
90
|
[]
|
|
93
91
|
end
|
|
94
92
|
|
|
95
93
|
node_rule Prism::CallNode do |node, scope, path|
|
|
96
|
-
index =
|
|
94
|
+
index = producer_value(:policy_index)
|
|
97
95
|
next [] if index.nil? || index.empty?
|
|
98
96
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
diagnostics_for(
|
|
98
|
+
Analyzer.violations_for(call_node: node, policy_index: index, scope: scope), path: path, node: node
|
|
99
|
+
)
|
|
102
100
|
end
|
|
103
101
|
|
|
104
102
|
private
|
|
105
103
|
|
|
106
|
-
def policy_index_or_nil
|
|
107
|
-
return @policy_index if @policy_index
|
|
108
|
-
|
|
109
|
-
descriptor = glob_descriptor(@policy_search_paths, "**/*.rb")
|
|
110
|
-
@policy_index = cache_for(:policy_index, params: {}, descriptor: descriptor).call
|
|
111
|
-
rescue StandardError => e
|
|
112
|
-
@load_error = "rigor-pundit: failed to discover policies: #{e.class}: #{e.message}"
|
|
113
|
-
nil
|
|
114
|
-
end
|
|
115
|
-
|
|
116
104
|
def load_error_diagnostic(path)
|
|
105
|
+
error = producer_error(:policy_index)
|
|
117
106
|
Rigor::Analysis::Diagnostic.new(
|
|
118
107
|
path: path, line: 1, column: 1,
|
|
119
|
-
message:
|
|
108
|
+
message: "rigor-pundit: failed to discover policies: #{error.class}: #{error.message}",
|
|
120
109
|
severity: :warning,
|
|
121
110
|
rule: "load-error"
|
|
122
111
|
)
|
|
@@ -72,7 +72,13 @@ module Rigor
|
|
|
72
72
|
}
|
|
73
73
|
)
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
# `watch:` covers every `.yml` / `.yaml` file under the locale
|
|
76
|
+
# search paths so the cache invalidates when locale files are
|
|
77
|
+
# added, removed, or edited (ADR-60 WD3). `@load_errors` is a
|
|
78
|
+
# producer-side capture: it is populated only when the block
|
|
79
|
+
# runs (a cache miss / a watched file changed), which is exactly
|
|
80
|
+
# when a malformed YAML must re-surface.
|
|
81
|
+
producer :locale_index, watch: -> { [[@locale_search_paths, "**/*.yml", "**/*.yaml"]] } do |_params|
|
|
76
82
|
loader = LocaleLoader.new(
|
|
77
83
|
io_boundary: io_boundary,
|
|
78
84
|
search_paths: @locale_search_paths
|
|
@@ -85,21 +91,19 @@ module Rigor
|
|
|
85
91
|
def init(_services)
|
|
86
92
|
@locale_search_paths = Array(config.fetch("locale_search_paths")).map(&:to_s)
|
|
87
93
|
@configured_locales = Array(config.fetch("configured_locales")).map(&:to_s)
|
|
88
|
-
@locale_index = nil
|
|
89
94
|
@load_errors = []
|
|
90
95
|
@load_errors_emitted = false
|
|
91
|
-
@runtime_error = nil
|
|
92
96
|
end
|
|
93
97
|
|
|
94
98
|
# File-level only: the once-per-run YAML load errors + the
|
|
95
99
|
# runtime (cache-load) error. Per-call `t('key')` validation runs
|
|
96
100
|
# over the engine-owned walk via the node_rule below (ADR-37). The
|
|
97
|
-
# locale index is lazily loaded + memoised by
|
|
101
|
+
# locale index is lazily loaded + memoised by `producer_value`.
|
|
98
102
|
def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
|
|
99
|
-
index =
|
|
103
|
+
index = producer_value(:locale_index)
|
|
100
104
|
diagnostics = []
|
|
101
105
|
diagnostics.concat(consume_load_error_diagnostics(path)) unless @load_errors.empty?
|
|
102
|
-
diagnostics << runtime_error_diagnostic(path) if index.nil? &&
|
|
106
|
+
diagnostics << runtime_error_diagnostic(path) if index.nil? && producer_error(:locale_index)
|
|
103
107
|
diagnostics
|
|
104
108
|
end
|
|
105
109
|
|
|
@@ -107,39 +111,21 @@ module Rigor
|
|
|
107
111
|
# (the controller action), supplied by the node-rule NodeContext;
|
|
108
112
|
# the controller scope comes from the file path.
|
|
109
113
|
node_rule Prism::CallNode do |node, _scope, path, _fc, context|
|
|
110
|
-
index =
|
|
114
|
+
index = producer_value(:locale_index)
|
|
111
115
|
next [] if index.nil? || index.empty?
|
|
112
116
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
diagnostics_for(
|
|
118
|
+
Analyzer.violations_for(
|
|
119
|
+
call_node: node, locale_index: index, configured_locales: @configured_locales,
|
|
120
|
+
controller_scope: Analyzer.controller_scope_from_path(path),
|
|
121
|
+
action: context.enclosing_def&.name
|
|
122
|
+
),
|
|
123
|
+
path: path, node: node
|
|
124
|
+
)
|
|
120
125
|
end
|
|
121
126
|
|
|
122
127
|
private
|
|
123
128
|
|
|
124
|
-
def locale_index_or_nil
|
|
125
|
-
return @locale_index if @locale_index
|
|
126
|
-
|
|
127
|
-
# Pass an explicit descriptor covering every `.yml` / `.yaml`
|
|
128
|
-
# file under the configured locale search paths so the cache
|
|
129
|
-
# invalidates when locale files are added, removed, or edited.
|
|
130
|
-
# Without it the auto-built descriptor depends on the
|
|
131
|
-
# `IoBoundary`'s in-process read history — empty on the
|
|
132
|
-
# first call of a fresh process — so warm cache hits would
|
|
133
|
-
# serve stale `LocaleIndex` data and hide per-call load
|
|
134
|
-
# errors (a malformed YAML in one run would not surface
|
|
135
|
-
# when a healthy cache entry from an earlier run exists).
|
|
136
|
-
descriptor = glob_descriptor(@locale_search_paths, "**/*.yml", "**/*.yaml")
|
|
137
|
-
@locale_index = cache_for(:locale_index, params: {}, descriptor: descriptor).call
|
|
138
|
-
rescue StandardError => e
|
|
139
|
-
@runtime_error = "rigor-rails-i18n: failed to load locales: #{e.class}: #{e.message}"
|
|
140
|
-
nil
|
|
141
|
-
end
|
|
142
|
-
|
|
143
129
|
# The runner only invokes `diagnostics_for_file` for
|
|
144
130
|
# Ruby files (`paths:` is filtered to `.rb`). YAML
|
|
145
131
|
# parse errors therefore can't be anchored on the
|
|
@@ -161,9 +147,10 @@ module Rigor
|
|
|
161
147
|
end
|
|
162
148
|
|
|
163
149
|
def runtime_error_diagnostic(path)
|
|
150
|
+
error = producer_error(:locale_index)
|
|
164
151
|
Rigor::Analysis::Diagnostic.new(
|
|
165
152
|
path: path, line: 1, column: 1,
|
|
166
|
-
message:
|
|
153
|
+
message: "rigor-rails-i18n: failed to load locales: #{error.class}: #{error.message}",
|
|
167
154
|
severity: :warning,
|
|
168
155
|
rule: "load-error"
|
|
169
156
|
)
|
|
@@ -94,7 +94,12 @@ module Rigor
|
|
|
94
94
|
#
|
|
95
95
|
# Passes a `file_reader` lambda so the parser can follow
|
|
96
96
|
# `draw(:admin)` → `config/routes/admin.rb` partials.
|
|
97
|
-
|
|
97
|
+
# `watch:` (ADR-60 WD3) covers every `.rb` under `helper_paths:`
|
|
98
|
+
# so ADDING a helper file invalidates the table — the producer's
|
|
99
|
+
# own in-block reads (the routes file + the partials it follows
|
|
100
|
+
# via `draw`) are captured post-compute, but a brand-new helper
|
|
101
|
+
# file the prior run never read would otherwise read as fresh.
|
|
102
|
+
producer :helper_table, watch: -> { @helper_paths.map { |dir| [dir, "**/*.rb"] } } do |_params|
|
|
98
103
|
routes_dir = "#{File.dirname(@routes_file)}/routes"
|
|
99
104
|
file_reader = lambda do |name|
|
|
100
105
|
io_boundary.read_file("#{routes_dir}/#{name}")
|
|
@@ -132,14 +137,6 @@ module Rigor
|
|
|
132
137
|
HelperDiscoverer.discover(contents_per_path)
|
|
133
138
|
end
|
|
134
139
|
|
|
135
|
-
def pre_read_helper_files
|
|
136
|
-
each_helper_file do |path|
|
|
137
|
-
io_boundary.read_file(path)
|
|
138
|
-
rescue Plugin::AccessDeniedError, Errno::ENOENT
|
|
139
|
-
next
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
|
|
143
140
|
def each_helper_file(&)
|
|
144
141
|
@helper_paths.each do |dir|
|
|
145
142
|
absolute = File.expand_path(dir)
|
|
@@ -214,15 +211,11 @@ module Rigor
|
|
|
214
211
|
return @helper_table if @helper_table_built
|
|
215
212
|
|
|
216
213
|
@helper_table_built = true
|
|
217
|
-
#
|
|
218
|
-
#
|
|
219
|
-
#
|
|
220
|
-
#
|
|
221
|
-
#
|
|
222
|
-
# `app/helpers/` MUST invalidate the helper_table cache
|
|
223
|
-
# so the new custom-helper set is picked up.
|
|
224
|
-
io_boundary.read_file(@routes_file)
|
|
225
|
-
pre_read_helper_files
|
|
214
|
+
# ADR-60 WD3 record-and-validate: the producer's in-block reads
|
|
215
|
+
# (the routes file + the partials it follows) are captured into
|
|
216
|
+
# the dependency descriptor AFTER the block runs, and the
|
|
217
|
+
# producer's `watch:` covers helper-file additions — so no
|
|
218
|
+
# priming read is needed here.
|
|
226
219
|
@helper_table = cache_for(:helper_table, params: {}).call
|
|
227
220
|
rescue Plugin::AccessDeniedError => e
|
|
228
221
|
@load_error = "rigor-rails-routes: #{e.message}"
|
|
@@ -147,7 +147,6 @@ module Rigor
|
|
|
147
147
|
block_as_methods: base.block_as_methods,
|
|
148
148
|
heredoc_templates: base.heredoc_templates,
|
|
149
149
|
trait_registries: base.trait_registries,
|
|
150
|
-
external_files: base.external_files,
|
|
151
150
|
hkt_registrations: base.hkt_registrations,
|
|
152
151
|
hkt_definitions: base.hkt_definitions,
|
|
153
152
|
signature_paths: base.signature_paths,
|
|
@@ -86,10 +86,7 @@ module Rigor
|
|
|
86
86
|
]
|
|
87
87
|
)
|
|
88
88
|
|
|
89
|
-
def init(
|
|
90
|
-
@services = services
|
|
91
|
-
@factory_index_resolved = false
|
|
92
|
-
@factory_index = nil
|
|
89
|
+
def init(_services)
|
|
93
90
|
# Per-path `LetScopeIndex` cache. The let-binding
|
|
94
91
|
# `dynamic_return` rule (and its `file_methods:` gate)
|
|
95
92
|
# consult the index per call node; building it once
|
|
@@ -152,7 +149,7 @@ module Rigor
|
|
|
152
149
|
LetTypeResolver.resolve(
|
|
153
150
|
block_node,
|
|
154
151
|
describe_const: describe_const,
|
|
155
|
-
factory_index:
|
|
152
|
+
factory_index: read_fact(plugin_id: "factorybot", name: :factory_index),
|
|
156
153
|
environment: scope.environment
|
|
157
154
|
)
|
|
158
155
|
end
|
|
@@ -185,14 +182,6 @@ module Rigor
|
|
|
185
182
|
nil
|
|
186
183
|
end
|
|
187
184
|
|
|
188
|
-
def factory_index_or_nil
|
|
189
|
-
return @factory_index if @factory_index_resolved
|
|
190
|
-
|
|
191
|
-
@factory_index = @services&.fact_store&.read(plugin_id: "factorybot", name: :factory_index)
|
|
192
|
-
@factory_index_resolved = true
|
|
193
|
-
@factory_index
|
|
194
|
-
end
|
|
195
|
-
|
|
196
185
|
def build_diagnostic(diag)
|
|
197
186
|
Rigor::Analysis::Diagnostic.new(
|
|
198
187
|
path: diag.path, line: diag.line, column: diag.column,
|
|
@@ -70,19 +70,14 @@ module Rigor
|
|
|
70
70
|
]
|
|
71
71
|
)
|
|
72
72
|
|
|
73
|
-
def init(services)
|
|
74
|
-
@services = services
|
|
75
|
-
@model_index = nil
|
|
76
|
-
@model_index_resolved = false
|
|
77
|
-
end
|
|
78
|
-
|
|
79
73
|
# ADR-37 — per-matcher validation over the engine-owned walk. The
|
|
80
74
|
# model anchor (the enclosing `describe <Model>` const) comes from
|
|
81
75
|
# the node-rule NodeContext ancestors; the diagnostic points at the
|
|
82
76
|
# matcher name (message_loc). The :model_index fact (from
|
|
83
|
-
# rigor-activerecord) is read lazily
|
|
77
|
+
# rigor-activerecord) is read lazily via `read_fact`; without it
|
|
78
|
+
# the rule is silent.
|
|
84
79
|
node_rule Prism::CallNode do |node, _scope, path, _fc, context|
|
|
85
|
-
index =
|
|
80
|
+
index = read_fact(plugin_id: "activerecord", name: :model_index)
|
|
86
81
|
next [] if index.nil?
|
|
87
82
|
|
|
88
83
|
Analyzer.violations_for(matcher_call: node, ancestors: context.ancestors, model_index: index).map do |violation|
|
|
@@ -90,21 +85,6 @@ module Rigor
|
|
|
90
85
|
message: violation.message, severity: :warning, rule: violation.rule)
|
|
91
86
|
end
|
|
92
87
|
end
|
|
93
|
-
|
|
94
|
-
private
|
|
95
|
-
|
|
96
|
-
# Lazily resolves `:model_index` from
|
|
97
|
-
# `rigor-activerecord`. Returns nil when the plugin
|
|
98
|
-
# isn't loaded or no index has been published; the
|
|
99
|
-
# analyzer treats nil as "no cross-check available" and
|
|
100
|
-
# falls silent.
|
|
101
|
-
def model_index_or_nil
|
|
102
|
-
return @model_index if @model_index_resolved
|
|
103
|
-
|
|
104
|
-
@model_index = @services.fact_store.read(plugin_id: "activerecord", name: :model_index)
|
|
105
|
-
@model_index_resolved = true
|
|
106
|
-
@model_index
|
|
107
|
-
end
|
|
108
88
|
end
|
|
109
89
|
|
|
110
90
|
Rigor::Plugin.register(ShouldaMatchers)
|
|
@@ -63,7 +63,7 @@ module Rigor
|
|
|
63
63
|
}
|
|
64
64
|
)
|
|
65
65
|
|
|
66
|
-
producer :worker_index do |_params|
|
|
66
|
+
producer :worker_index, watch: -> { [[@worker_search_paths, "**/*.rb"]] } do |_params|
|
|
67
67
|
WorkerDiscoverer.new(
|
|
68
68
|
io_boundary: io_boundary,
|
|
69
69
|
search_paths: @worker_search_paths,
|
|
@@ -74,46 +74,33 @@ module Rigor
|
|
|
74
74
|
def init(_services)
|
|
75
75
|
@worker_search_paths = Array(config.fetch("worker_search_paths")).map(&:to_s)
|
|
76
76
|
@worker_marker_modules = Array(config.fetch("worker_marker_modules")).map(&:to_s)
|
|
77
|
-
@worker_index = nil
|
|
78
|
-
@load_error = nil
|
|
79
77
|
end
|
|
80
78
|
|
|
81
79
|
# File-level only: the load-error emission. The per-call arity
|
|
82
80
|
# validation runs over the engine-owned walk via the node_rule
|
|
83
81
|
# below (ADR-37). The worker index is lazily loaded + memoised by
|
|
84
|
-
#
|
|
82
|
+
# `producer_value`, shared by both surfaces.
|
|
85
83
|
def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
|
|
86
|
-
index =
|
|
87
|
-
return [load_error_diagnostic(path)] if index.nil? &&
|
|
84
|
+
index = producer_value(:worker_index)
|
|
85
|
+
return [load_error_diagnostic(path)] if index.nil? && producer_error(:worker_index)
|
|
88
86
|
|
|
89
87
|
[]
|
|
90
88
|
end
|
|
91
89
|
|
|
92
90
|
node_rule Prism::CallNode do |node, _scope, path|
|
|
93
|
-
index =
|
|
91
|
+
index = producer_value(:worker_index)
|
|
94
92
|
next [] if index.nil? || index.empty?
|
|
95
93
|
|
|
96
|
-
Analyzer.violations_for(call_node: node, worker_index: index)
|
|
97
|
-
diagnostic(node, path: path, message: violation.message, severity: violation.severity, rule: violation.rule)
|
|
98
|
-
end
|
|
94
|
+
diagnostics_for(Analyzer.violations_for(call_node: node, worker_index: index), path: path, node: node)
|
|
99
95
|
end
|
|
100
96
|
|
|
101
97
|
private
|
|
102
98
|
|
|
103
|
-
def worker_index_or_nil
|
|
104
|
-
return @worker_index if @worker_index
|
|
105
|
-
|
|
106
|
-
descriptor = glob_descriptor(@worker_search_paths, "**/*.rb")
|
|
107
|
-
@worker_index = cache_for(:worker_index, params: {}, descriptor: descriptor).call
|
|
108
|
-
rescue StandardError => e
|
|
109
|
-
@load_error = "rigor-sidekiq: failed to discover workers: #{e.class}: #{e.message}"
|
|
110
|
-
nil
|
|
111
|
-
end
|
|
112
|
-
|
|
113
99
|
def load_error_diagnostic(path)
|
|
100
|
+
error = producer_error(:worker_index)
|
|
114
101
|
Rigor::Analysis::Diagnostic.new(
|
|
115
102
|
path: path, line: 1, column: 1,
|
|
116
|
-
message:
|
|
103
|
+
message: "rigor-sidekiq: failed to discover workers: #{error.class}: #{error.message}",
|
|
117
104
|
severity: :warning,
|
|
118
105
|
rule: "load-error"
|
|
119
106
|
)
|
|
@@ -74,7 +74,7 @@ module Rigor
|
|
|
74
74
|
block_as_methods: [
|
|
75
75
|
Rigor::Plugin::Macro::BlockAsMethod.new(
|
|
76
76
|
receiver_constraint: "Sinatra::Base",
|
|
77
|
-
|
|
77
|
+
method_names: %i[get post put delete head options patch link unlink]
|
|
78
78
|
)
|
|
79
79
|
]
|
|
80
80
|
)
|
|
@@ -329,7 +329,32 @@ module Rigor
|
|
|
329
329
|
chain_lookup(singleton_target, method_name, anchor_kind: :singleton, mixin_kind: :extend)
|
|
330
330
|
elsif receiver
|
|
331
331
|
instance_chain_lookup(receiver, method_name, scope)
|
|
332
|
+
else
|
|
333
|
+
implicit_self_lookup(method_name, scope)
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# ADR-11 slice 2 (deferred from slice 1) — implicit-self calls.
|
|
338
|
+
# A receiver-less call inside a method body resolves against the
|
|
339
|
+
# engine's own `scope.self_type`: `Nominal[Foo]` inside an
|
|
340
|
+
# instance method (instance-side lookup), `Singleton[Foo]` inside
|
|
341
|
+
# a `def self.x` body (singleton-side lookup, `extend` mixins).
|
|
342
|
+
# Without this, an enforced sig on a sibling method was invisible
|
|
343
|
+
# to in-class calls — the engine's body-inference tiers then
|
|
344
|
+
# re-typed the sibling's body, overriding an explicit
|
|
345
|
+
# `T.untyped` opt-out (the dispatcher's plugin tier had already
|
|
346
|
+
# run and declined). Anything else (toplevel / Dynamic / DSL
|
|
347
|
+
# self) contributes nothing and the dispatcher continues.
|
|
348
|
+
def implicit_self_lookup(method_name, scope)
|
|
349
|
+
self_type = scope&.self_type
|
|
350
|
+
case self_type
|
|
351
|
+
when Rigor::Type::Singleton
|
|
352
|
+
chain_lookup(self_type.class_name, method_name, anchor_kind: :singleton, mixin_kind: :extend)
|
|
353
|
+
when Rigor::Type::Nominal
|
|
354
|
+
chain_lookup(self_type.class_name, method_name, anchor_kind: :instance, mixin_kind: :include)
|
|
332
355
|
end
|
|
356
|
+
rescue StandardError
|
|
357
|
+
nil
|
|
333
358
|
end
|
|
334
359
|
|
|
335
360
|
def instance_chain_lookup(receiver_node, method_name, scope)
|
|
@@ -2,6 +2,9 @@ module Rigor
|
|
|
2
2
|
module Analysis
|
|
3
3
|
module CheckRules
|
|
4
4
|
def self?.diagnose: (path: String, root: untyped, scope_index: Hash[untyped, Scope]) -> Array[Diagnostic]
|
|
5
|
+
def self?.build_node_collectors: (String path, untyped scope_index) -> Hash[Symbol, untyped]
|
|
6
|
+
def self?.node_collector_driver: (Hash[Symbol, untyped] collectors) -> untyped
|
|
7
|
+
def self?.shadow_verify_converged_collectors: (String path, untyped root, untyped scope_index, Hash[Symbol, untyped]? collectors) -> void
|
|
5
8
|
end
|
|
6
9
|
|
|
7
10
|
class FactStore
|
data/sig/rigor/plugin/base.rbs
CHANGED
|
@@ -14,7 +14,7 @@ class Rigor::Plugin::Base
|
|
|
14
14
|
# the no-arg `manifest` reads the cached value back.
|
|
15
15
|
def self.manifest: (**untyped fields) -> Rigor::Plugin::Manifest
|
|
16
16
|
|
|
17
|
-
def self.producer: (untyped id, ?serialize: untyped, ?deserialize: untyped) { (untyped params) -> untyped } -> Symbol
|
|
17
|
+
def self.producer: (untyped id, ?watch: untyped, ?serialize: untyped, ?deserialize: untyped) { (untyped params) -> untyped } -> Symbol
|
|
18
18
|
def self.producers: () -> Hash[Symbol, untyped]
|
|
19
19
|
|
|
20
20
|
def self.node_rule: (untyped node_type) { (*untyped) -> untyped } -> untyped
|
|
@@ -50,11 +50,14 @@ class Rigor::Plugin::Base
|
|
|
50
50
|
|
|
51
51
|
# Authoring helpers.
|
|
52
52
|
def diagnostic: (untyped node, path: untyped, message: untyped, ?severity: untyped, ?rule: untyped, ?location: untyped) -> untyped
|
|
53
|
+
def diagnostics_for: (untyped violations, path: untyped, ?node: untyped) -> Array[untyped]
|
|
54
|
+
def read_fact: (plugin_id: untyped, name: untyped) -> untyped
|
|
55
|
+
def producer_value: (untyped id, ?params: untyped) -> untyped
|
|
56
|
+
def producer_error: (untyped id) -> untyped
|
|
53
57
|
def manifest: () -> Rigor::Plugin::Manifest
|
|
54
58
|
def signature_paths: () -> Array[String]
|
|
55
59
|
def protocol_contracts: () -> untyped
|
|
56
60
|
def io_boundary: () -> Rigor::Plugin::IoBoundary
|
|
57
61
|
def cache_for: (untyped producer_id, ?params: untyped, ?descriptor: untyped) -> untyped
|
|
58
|
-
def glob_descriptor: (untyped roots, *untyped patterns) -> untyped
|
|
59
62
|
def plugin_entry: () -> untyped
|
|
60
63
|
end
|
|
@@ -3,7 +3,7 @@ class Rigor::Plugin::Manifest::Consumption
|
|
|
3
3
|
end
|
|
4
4
|
|
|
5
5
|
class Rigor::Plugin::Manifest
|
|
6
|
-
def initialize: (id: untyped, version: untyped, ?description: untyped, ?config_schema: untyped, ?produces: untyped, ?consumes: untyped, ?owns_receivers: untyped, ?open_receivers: untyped, ?type_node_resolvers: untyped, ?block_as_methods: untyped, ?heredoc_templates: untyped, ?nested_class_templates: untyped, ?trait_registries: untyped, ?
|
|
6
|
+
def initialize: (id: untyped, version: untyped, ?description: untyped, ?config_schema: untyped, ?produces: untyped, ?consumes: untyped, ?owns_receivers: untyped, ?open_receivers: untyped, ?type_node_resolvers: untyped, ?block_as_methods: untyped, ?heredoc_templates: untyped, ?nested_class_templates: untyped, ?trait_registries: untyped, ?hkt_registrations: untyped, ?hkt_definitions: untyped, ?signature_paths: untyped, ?protocol_contracts: untyped, ?source_rbs_synthesizer: untyped, ?additional_initializers: untyped) -> void
|
|
7
7
|
|
|
8
8
|
# Public attribute readers (the full `attr_reader` surface). Plugins
|
|
9
9
|
# read `manifest.id` / `manifest.protocol_contracts` etc.; declaring
|
|
@@ -25,7 +25,6 @@ class Rigor::Plugin::Manifest
|
|
|
25
25
|
def heredoc_templates: () -> untyped
|
|
26
26
|
def nested_class_templates: () -> untyped
|
|
27
27
|
def trait_registries: () -> untyped
|
|
28
|
-
def external_files: () -> untyped
|
|
29
28
|
def hkt_registrations: () -> untyped
|
|
30
29
|
def hkt_definitions: () -> untyped
|
|
31
30
|
def signature_paths: () -> untyped
|
data/sig/rigor/scope.rbs
CHANGED
|
@@ -22,6 +22,7 @@ module Rigor
|
|
|
22
22
|
def in_source_constants: () -> Hash[String, Type::t]
|
|
23
23
|
def discovered_methods: () -> Hash[String, Hash[Symbol, Symbol]]
|
|
24
24
|
def discovered_def_nodes: () -> Hash[String, Hash[Symbol, untyped]]
|
|
25
|
+
def discovered_singleton_def_nodes: () -> Hash[String, Hash[Symbol, untyped]]
|
|
25
26
|
def discovered_def_sources: () -> Hash[String, Hash[Symbol, String]]
|
|
26
27
|
def discovered_method_visibilities: () -> Hash[String, Hash[Symbol, Symbol]]
|
|
27
28
|
def discovered_superclasses: () -> Hash[String, String]
|
|
@@ -38,6 +39,7 @@ module Rigor
|
|
|
38
39
|
attr_reader in_source_constants: Hash[String, Type::t]
|
|
39
40
|
attr_reader discovered_methods: Hash[String, Hash[Symbol, Symbol]]
|
|
40
41
|
attr_reader discovered_def_nodes: Hash[String, Hash[Symbol, untyped]]
|
|
42
|
+
attr_reader discovered_singleton_def_nodes: Hash[String, Hash[Symbol, untyped]]
|
|
41
43
|
attr_reader discovered_def_sources: Hash[String, Hash[Symbol, String]]
|
|
42
44
|
attr_reader discovered_method_visibilities: Hash[String, Hash[Symbol, Symbol]]
|
|
43
45
|
attr_reader discovered_superclasses: Hash[String, String]
|
|
@@ -47,7 +49,7 @@ module Rigor
|
|
|
47
49
|
|
|
48
50
|
EMPTY: DiscoveryIndex
|
|
49
51
|
|
|
50
|
-
def with: (?declared_types: Hash[untyped, Type::t], ?class_ivars: Hash[String, Hash[Symbol, Type::t]], ?class_cvars: Hash[String, Hash[Symbol, Type::t]], ?program_globals: Hash[Symbol, Type::t], ?discovered_classes: Hash[String, Type::Singleton], ?in_source_constants: Hash[String, Type::t], ?discovered_methods: Hash[String, Hash[Symbol, Symbol]], ?discovered_def_nodes: Hash[String, Hash[Symbol, untyped]], ?discovered_def_sources: Hash[String, Hash[Symbol, String]], ?discovered_method_visibilities: Hash[String, Hash[Symbol, Symbol]], ?discovered_superclasses: Hash[String, String], ?discovered_includes: Hash[String, Array[String]], ?discovered_class_sources: Hash[String, Set[String]], ?data_member_layouts: Hash[String, Array[Symbol]]) -> DiscoveryIndex
|
|
52
|
+
def with: (?declared_types: Hash[untyped, Type::t], ?class_ivars: Hash[String, Hash[Symbol, Type::t]], ?class_cvars: Hash[String, Hash[Symbol, Type::t]], ?program_globals: Hash[Symbol, Type::t], ?discovered_classes: Hash[String, Type::Singleton], ?in_source_constants: Hash[String, Type::t], ?discovered_methods: Hash[String, Hash[Symbol, Symbol]], ?discovered_def_nodes: Hash[String, Hash[Symbol, untyped]], ?discovered_singleton_def_nodes: Hash[String, Hash[Symbol, untyped]], ?discovered_def_sources: Hash[String, Hash[Symbol, String]], ?discovered_method_visibilities: Hash[String, Hash[Symbol, Symbol]], ?discovered_superclasses: Hash[String, String], ?discovered_includes: Hash[String, Array[String]], ?discovered_class_sources: Hash[String, Set[String]], ?data_member_layouts: Hash[String, Array[Symbol]]) -> DiscoveryIndex
|
|
51
53
|
end
|
|
52
54
|
|
|
53
55
|
class IndexedKey
|
|
@@ -75,10 +77,17 @@ module Rigor
|
|
|
75
77
|
def with_ivar: (String | Symbol name, Type::t type) -> Scope
|
|
76
78
|
def with_cvar: (String | Symbol name, Type::t type) -> Scope
|
|
77
79
|
def with_global: (String | Symbol name, Type::t type) -> Scope
|
|
80
|
+
def declaration_sourced: () -> Set[[Symbol, Symbol]]
|
|
81
|
+
def seed_declaration_sourced_ivar: (String | Symbol name, Type::t type) -> Scope
|
|
82
|
+
def with_declaration_sourced_local: (String | Symbol name, Type::t type) -> Scope
|
|
83
|
+
def with_local_declaration_mark: (String | Symbol name) -> Scope
|
|
84
|
+
def declaration_sourced?: (Symbol kind, String | Symbol name) -> bool
|
|
85
|
+
def forget_match_globals: () -> Scope
|
|
78
86
|
def class_ivars_for: (String | Symbol? class_name) -> Hash[Symbol, Type::t]
|
|
79
87
|
def class_cvars_for: (String | Symbol? class_name) -> Hash[Symbol, Type::t]
|
|
80
88
|
def discovered_method?: (String | Symbol class_name, String | Symbol method_name, Symbol kind) -> bool
|
|
81
89
|
def user_def_for: (String | Symbol class_name, String | Symbol method_name) -> untyped?
|
|
90
|
+
def singleton_def_for: (String | Symbol class_name, String | Symbol method_name) -> untyped?
|
|
82
91
|
def user_def_site_for: (String | Symbol class_name, String | Symbol method_name) -> String?
|
|
83
92
|
def top_level_def_for: (String | Symbol method_name) -> untyped?
|
|
84
93
|
def toplevel?: () -> bool
|
data/sig/rigor/type.rbs
CHANGED
|
@@ -342,6 +342,7 @@ module Rigor
|
|
|
342
342
|
def self?.union: (*Type::t types) -> Type::t
|
|
343
343
|
def self?.key_of: (Type::t type) -> Type::t
|
|
344
344
|
def self?.value_of: (Type::t type) -> Type::t
|
|
345
|
+
def self?.widen_value_pinned: (Type::t type) -> Type::t
|
|
345
346
|
def self?.int_mask: (Array[Integer] flags) -> Type::t?
|
|
346
347
|
def self?.int_mask_of: (Type::t type) -> Type::t?
|
|
347
348
|
def self?.indexed_access: (Type::t type, Type::t key) -> Type::t
|
data/sig/rigor.rbs
CHANGED
|
@@ -11,7 +11,7 @@ module Rigor
|
|
|
11
11
|
attr_reader cache_path: String
|
|
12
12
|
attr_reader baseline_path: String?
|
|
13
13
|
|
|
14
|
-
def self.load: (?String path) -> Configuration
|
|
14
|
+
def self.load: (?String? path) -> Configuration
|
|
15
15
|
def self.discover: () -> String?
|
|
16
16
|
def self.load_with_includes: (String path, ?visited: Set[String]) -> Hash[String, untyped]
|
|
17
17
|
def initialize: (?Hash[String, untyped] data) -> void
|
|
@@ -28,6 +28,33 @@ signal — each hint has an `id`:
|
|
|
28
28
|
| `project-monkey-patch` | A DSL / monkey-patch Rigor can't see. | Escalate — a `pre_eval:` entry or a plugin clears the whole cluster. |
|
|
29
29
|
| `activerecord-relation-misinference` | Likely an engine gap. | Treat sites as candidate false positives (Phase 2). |
|
|
30
30
|
|
|
31
|
+
### `rigor triage --format json` `.selectors` — the by-(class, method) axis
|
|
32
|
+
|
|
33
|
+
Beside `hints`, the triage JSON carries a `selectors` array: one row
|
|
34
|
+
per dispatch target the diagnostics cluster on, built from the
|
|
35
|
+
structured `receiver_type` / `method_name` fields (never message
|
|
36
|
+
parsing). Each row is `{receiver, method, count, files, rules}`. Use
|
|
37
|
+
it to pick *which sites within a rule* to sample first — and to tell a
|
|
38
|
+
systemic cause from a scatter of real bugs — with `jq`, not eyeballing
|
|
39
|
+
the stream:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
# the dispatch targets responsible for the most diagnostics
|
|
43
|
+
rigor triage --format json | jq -r '.selectors[:15][] | "\(.count)\t\(.files)f\t\(.receiver)#\(.method)"'
|
|
44
|
+
# one method, one receiver, spread across many files → systemic
|
|
45
|
+
# (a plugin / pre_eval clears it) rather than N independent bugs
|
|
46
|
+
rigor triage --format json | jq '.selectors[] | select(.files >= 4)'
|
|
47
|
+
# narrow to a rule you are about to work, ranked by concentration
|
|
48
|
+
rigor triage --format json \
|
|
49
|
+
| jq '[.selectors[] | select(.rules["call.possible-nil-receiver"])] | sort_by(-.count)'
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Read it as: **high `count` × high `files` = a systemic selector**
|
|
53
|
+
(escalate as a decision — one fix clears the cluster); **low `count` =
|
|
54
|
+
a candidate genuine bug** to sample directly in Phase 2. The `receiver`
|
|
55
|
+
is a normalised class (`"hi".squish` and `name.squish` both bucket
|
|
56
|
+
under `String#squish`), so a single idiom does not scatter across rows.
|
|
57
|
+
|
|
31
58
|
### `rigor baseline dump --format json` — the bucket list
|
|
32
59
|
|
|
33
60
|
```sh
|
|
@@ -98,9 +98,11 @@ AST walk per file — hands every matching node to the block along with a
|
|
|
98
98
|
`Rigor::Analysis::Diagnostic` (built via the `diagnostic` helper).
|
|
99
99
|
Optionally the plugin also declares `dynamic_return(receivers:)` /
|
|
100
100
|
`type_specifier(methods:)` to *supply* a return type or narrowing facts
|
|
101
|
-
for call sites the core analyzer types as `Dynamic`.
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
for call sites the core analyzer types as `Dynamic`. `#diagnostics_for_file`
|
|
102
|
+
is the file-rule surface for whole-file diagnostics a per-node walk can't
|
|
103
|
+
express. (`flow_contribution_for` was removed pre-1.0 in ADR-52 WD3 —
|
|
104
|
+
defining it now raises `ArgumentError`; use `dynamic_return` /
|
|
105
|
+
`type_specifier`. See Phase 2.)
|
|
104
106
|
|
|
105
107
|
## Phase outline
|
|
106
108
|
|
|
@@ -115,5 +117,5 @@ deprecated escape valves — see Phase 2.)
|
|
|
115
117
|
| Module | Read | Covers |
|
|
116
118
|
| --- | --- | --- |
|
|
117
119
|
| 1 | [`references/01-plan-and-scaffold.md`](references/01-plan-and-scaffold.md) | **Phase 1.** The gem vs project-private packaging split, directory trees for both, gemspec template, project-private path-gem / `RUBYLIB` activation, the `Rigor::Plugin::Base` skeleton, `.rigor.yml` `plugins:` wiring. |
|
|
118
|
-
| 2 | [`references/02-walker-and-types.md`](references/02-walker-and-types.md) | **Phase 2.** The `node_rule` engine-owned AST walk over Prism nodes, the `Base#diagnostic` helper, asking the analyzer for inferred types via `scope.type_of`, two-pass / lexical context (`node_file_context` / `NodeContext`), the optional `dynamic_return` / `type_specifier` return-type hooks (
|
|
120
|
+
| 2 | [`references/02-walker-and-types.md`](references/02-walker-and-types.md) | **Phase 2.** The `node_rule` engine-owned AST walk over Prism nodes, the `Base#diagnostic` helper, asking the analyzer for inferred types via `scope.type_of`, two-pass / lexical context (`node_file_context` / `NodeContext`), the optional `dynamic_return` / `type_specifier` return-type hooks (`flow_contribution_for` was removed pre-1.0 in ADR-52 WD3), calling the target library's pure methods directly rather than reimplementing them (ADR-39: `Plugin::Inflector` over the real `ActiveSupport::Inflector`; `Base.suggest` for did-you-mean), and shipping `sig/*.rbs` so the DSL's types are visible. |
|
|
119
121
|
| 3 | [`references/03-test-and-ship.md`](references/03-test-and-ship.md) | **Phase 3.** Testing a plugin from outside the monorepo — fixture projects driven through `rigor check --format json`, plus pure unit tests of dispatch tables — with RSpec or Minitest. Version pinning against the pre-1.0 contract. README. Publishing to RubyGems or keeping the plugin private. |
|