rigortype 0.1.17 → 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 -222
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +24 -1
- 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 +213 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +24 -1
- data/lib/rigor/analysis/check_rules.rb +275 -44
- data/lib/rigor/analysis/diagnostic.rb +8 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +581 -0
- data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +321 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
- data/lib/rigor/analysis/runner.rb +207 -1200
- data/lib/rigor/analysis/worker_session.rb +60 -11
- data/lib/rigor/bleeding_edge.rb +123 -0
- data/lib/rigor/cache/descriptor.rb +86 -8
- data/lib/rigor/cache/incremental_snapshot.rb +10 -4
- data/lib/rigor/cache/rbs_cache_producer.rb +5 -1
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +46 -13
- data/lib/rigor/cli/annotate_command.rb +100 -15
- data/lib/rigor/cli/check_command.rb +708 -0
- data/lib/rigor/cli/ci_detector.rb +94 -0
- data/lib/rigor/cli/diagnostic_formats.rb +345 -0
- data/lib/rigor/cli/plugins_command.rb +2 -4
- data/lib/rigor/cli/plugins_renderer.rb +0 -2
- data/lib/rigor/cli/prism_colorizer.rb +10 -3
- data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
- data/lib/rigor/cli/trace_command.rb +143 -0
- data/lib/rigor/cli/trace_renderer.rb +310 -0
- data/lib/rigor/cli/triage_command.rb +6 -3
- data/lib/rigor/cli/triage_renderer.rb +15 -1
- data/lib/rigor/cli.rb +21 -612
- data/lib/rigor/configuration/severity_profile.rb +13 -1
- data/lib/rigor/configuration.rb +66 -7
- data/lib/rigor/environment/rbs_loader.rb +78 -68
- data/lib/rigor/environment.rb +1 -1
- data/lib/rigor/inference/acceptance.rb +10 -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 +1080 -105
- data/lib/rigor/inference/flow_tracer.rb +180 -0
- data/lib/rigor/inference/macro_block_self_type.rb +11 -12
- 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/overload_selector.rb +33 -1
- 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 +187 -55
- 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 +330 -37
- data/lib/rigor/inference/scope_indexer.rb +770 -39
- data/lib/rigor/inference/statement_evaluator.rb +998 -68
- data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
- data/lib/rigor/plugin/additional_initializer.rb +61 -38
- data/lib/rigor/plugin/base.rb +517 -120
- 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 +192 -0
- data/lib/rigor/plugin/registry.rb +264 -35
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/conformance_checker.rb +86 -1
- data/lib/rigor/scope/discovery_index.rb +60 -0
- data/lib/rigor/scope.rb +199 -204
- data/lib/rigor/sig_gen/generator.rb +8 -0
- data/lib/rigor/sig_gen/observation_collector.rb +6 -6
- data/lib/rigor/source/literals.rb +14 -0
- data/lib/rigor/triage/catalogue.rb +4 -19
- data/lib/rigor/triage.rb +69 -1
- data/lib/rigor/type/combinator.rb +34 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +0 -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/analyzer.rb +1 -2
- 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/model_discoverer.rb +2 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +90 -51
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -29
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +11 -40
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
- 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/let_scope_index.rb +12 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +37 -31
- 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/absurd_recognizer.rb +8 -29
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +108 -36
- data/sig/rigor/analysis/fact_store.rbs +3 -0
- data/sig/rigor/environment.rbs +0 -2
- data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
- data/sig/rigor/inference.rbs +5 -0
- data/sig/rigor/plugin/base.rbs +6 -4
- data/sig/rigor/plugin/manifest.rbs +1 -2
- data/sig/rigor/scope.rbs +50 -29
- data/sig/rigor/source.rbs +1 -0
- 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-ci-setup/SKILL.md +319 -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 +21 -3
- data/lib/rigor/cache/rbs_instance_definitions.rb +0 -66
- data/lib/rigor/plugin/macro/external_file.rb +0 -143
|
@@ -48,12 +48,12 @@ module Rigor
|
|
|
48
48
|
}
|
|
49
49
|
)
|
|
50
50
|
|
|
51
|
-
# Cached: discovered job index.
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
# `
|
|
55
|
-
#
|
|
56
|
-
producer :job_index do |_params|
|
|
51
|
+
# Cached: discovered job index. `watch:` (ADR-60 WD3) covers
|
|
52
|
+
# every `.rb` under `job_search_paths` so the cache invalidates
|
|
53
|
+
# when a job is added, removed, or edited; the discoverer's
|
|
54
|
+
# in-block `IoBoundary` reads are captured into the record-and-
|
|
55
|
+
# validate dependency descriptor after the block runs.
|
|
56
|
+
producer :job_index, watch: -> { [[@job_search_paths, "**/*.rb"]] } do |_params|
|
|
57
57
|
JobDiscoverer.new(
|
|
58
58
|
io_boundary: io_boundary,
|
|
59
59
|
search_paths: @job_search_paths,
|
|
@@ -64,50 +64,33 @@ module Rigor
|
|
|
64
64
|
def init(_services)
|
|
65
65
|
@job_search_paths = Array(config.fetch("job_search_paths")).map(&:to_s)
|
|
66
66
|
@job_base_classes = Array(config.fetch("job_base_classes")).map(&:to_s)
|
|
67
|
-
@job_index = nil
|
|
68
|
-
@load_error = nil
|
|
69
67
|
end
|
|
70
68
|
|
|
71
69
|
# File-level only: the load-error emission. Per-call arity
|
|
72
70
|
# validation runs over the engine-owned walk via the node_rule
|
|
73
71
|
# below (ADR-37). The job index is lazily loaded + memoised by
|
|
74
|
-
#
|
|
72
|
+
# `producer_value`, shared by both surfaces.
|
|
75
73
|
def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
|
|
76
|
-
index =
|
|
77
|
-
return [load_error_diagnostic(path)] if index.nil? &&
|
|
74
|
+
index = producer_value(:job_index)
|
|
75
|
+
return [load_error_diagnostic(path)] if index.nil? && producer_error(:job_index)
|
|
78
76
|
|
|
79
77
|
[]
|
|
80
78
|
end
|
|
81
79
|
|
|
82
80
|
node_rule Prism::CallNode do |node, _scope, path|
|
|
83
|
-
index =
|
|
81
|
+
index = producer_value(:job_index)
|
|
84
82
|
next [] if index.nil? || index.empty?
|
|
85
83
|
|
|
86
|
-
Analyzer.violations_for(call_node: node, job_index: index)
|
|
87
|
-
diagnostic(node, path: path, message: violation.message, severity: violation.severity, rule: violation.rule)
|
|
88
|
-
end
|
|
84
|
+
diagnostics_for(Analyzer.violations_for(call_node: node, job_index: index), path: path, node: node)
|
|
89
85
|
end
|
|
90
86
|
|
|
91
87
|
private
|
|
92
88
|
|
|
93
|
-
def job_index_or_nil
|
|
94
|
-
return @job_index if @job_index
|
|
95
|
-
|
|
96
|
-
# Read-then-cache pattern: the discoverer's
|
|
97
|
-
# IoBoundary reads happen INSIDE `discover`, which is
|
|
98
|
-
# invoked through `cache_for`'s producer block. The
|
|
99
|
-
# boundary's accumulated FileEntry digests get
|
|
100
|
-
# captured into the descriptor at cache_for time.
|
|
101
|
-
@job_index = cache_for(:job_index, params: {}).call
|
|
102
|
-
rescue StandardError => e
|
|
103
|
-
@load_error = "rigor-activejob: failed to discover jobs: #{e.class}: #{e.message}"
|
|
104
|
-
nil
|
|
105
|
-
end
|
|
106
|
-
|
|
107
89
|
def load_error_diagnostic(path)
|
|
90
|
+
error = producer_error(:job_index)
|
|
108
91
|
Rigor::Analysis::Diagnostic.new(
|
|
109
92
|
path: path, line: 1, column: 1,
|
|
110
|
-
message:
|
|
93
|
+
message: "rigor-activejob: failed to discover jobs: #{error.class}: #{error.message}",
|
|
111
94
|
severity: :warning,
|
|
112
95
|
rule: "load-error"
|
|
113
96
|
)
|
|
@@ -365,8 +365,7 @@ module Rigor
|
|
|
365
365
|
next unless arg.is_a?(Prism::KeywordHashNode)
|
|
366
366
|
|
|
367
367
|
arg.elements.each do |pair|
|
|
368
|
-
next unless pair.is_a?(Prism::AssocNode) && pair.key
|
|
369
|
-
next unless pair.key.unescaped == key
|
|
368
|
+
next unless pair.is_a?(Prism::AssocNode) && Source::Literals.symbol_named?(pair.key, key)
|
|
370
369
|
|
|
371
370
|
return true if pair.value.is_a?(Prism::TrueNode)
|
|
372
371
|
return false if pair.value.is_a?(Prism::FalseNode)
|
|
@@ -380,8 +379,7 @@ module Rigor
|
|
|
380
379
|
next unless arg.is_a?(Prism::KeywordHashNode)
|
|
381
380
|
|
|
382
381
|
arg.elements.each do |pair|
|
|
383
|
-
next unless pair.is_a?(Prism::AssocNode) && pair.key
|
|
384
|
-
next unless pair.key.unescaped == "class_name"
|
|
382
|
+
next unless pair.is_a?(Prism::AssocNode) && Source::Literals.symbol_named?(pair.key, "class_name")
|
|
385
383
|
next unless pair.value.is_a?(Prism::StringNode)
|
|
386
384
|
|
|
387
385
|
return pair.value.unescaped
|
|
@@ -27,10 +27,12 @@ module Rigor
|
|
|
27
27
|
# whose direct superclass is in `model_base_classes`, and
|
|
28
28
|
# composes them with the schema table into a {ModelIndex}.
|
|
29
29
|
#
|
|
30
|
-
# Both producers ride `Plugin::Base#cache_for
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
30
|
+
# Both producers ride `Plugin::Base#cache_for` (ADR-60 WD3
|
|
31
|
+
# record-and-validate): each producer's in-block boundary reads
|
|
32
|
+
# are captured into its dependency descriptor after the block
|
|
33
|
+
# runs, and `model_index`'s `watch:` covers model-file additions,
|
|
34
|
+
# so editing `db/schema.rb`, editing any model, or adding a new
|
|
35
|
+
# model file invalidates exactly the right cache entry.
|
|
34
36
|
#
|
|
35
37
|
# The per-file `#diagnostics_for_file` hook delegates to
|
|
36
38
|
# {Analyzer}, which walks Prism and emits diagnostics for
|
|
@@ -101,8 +103,11 @@ module Rigor
|
|
|
101
103
|
end
|
|
102
104
|
|
|
103
105
|
# Cached: model index. Walks every model file, then composes
|
|
104
|
-
# the rows with the cached schema table.
|
|
105
|
-
|
|
106
|
+
# the rows with the cached schema table. `watch:` (ADR-60 WD3)
|
|
107
|
+
# covers model-file additions; the discoverer's in-block reads
|
|
108
|
+
# are captured into the record-and-validate dependency
|
|
109
|
+
# descriptor after the block runs.
|
|
110
|
+
producer :model_index, watch: -> { [[@model_search_paths, "**/*.rb"]] } do |_params|
|
|
106
111
|
rows = ModelDiscoverer.new(
|
|
107
112
|
io_boundary: io_boundary,
|
|
108
113
|
search_paths: @model_search_paths,
|
|
@@ -197,45 +202,83 @@ module Rigor
|
|
|
197
202
|
MIGRATION_PATH_PATTERNS.any? { |pattern| path_s.match?(pattern) }
|
|
198
203
|
end
|
|
199
204
|
|
|
200
|
-
#
|
|
201
|
-
#
|
|
202
|
-
#
|
|
203
|
-
# the
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
#
|
|
208
|
-
#
|
|
209
|
-
#
|
|
210
|
-
|
|
205
|
+
# The class-side finder / relation entry-point names
|
|
206
|
+
# `finder_return_type` recognises. Static half of the
|
|
207
|
+
# `dynamic_return` name gate; the run-time half comes from
|
|
208
|
+
# the model index (scopes, associations, columns).
|
|
209
|
+
FINDER_METHOD_NAMES = %i[find find_by! find_by where all order limit none select].freeze
|
|
210
|
+
private_constant :FINDER_METHOD_NAMES
|
|
211
|
+
|
|
212
|
+
# v0.1.2 — return-type contribution; ADR-52 slice 5b —
|
|
213
|
+
# migrated off `flow_contribution_for` onto the run-time
|
|
214
|
+
# `methods:` name gate. `Model.find(id)` narrows the call
|
|
215
|
+
# site's return type to `Nominal[Model]`, so chained calls
|
|
216
|
+
# (`User.find(1).name`) resolve through the analyzer's
|
|
217
|
+
# normal dispatch instead of the RBS-level untyped
|
|
218
|
+
# fall-back; scopes, association accessors, and column
|
|
219
|
+
# readers narrow per the paths below.
|
|
220
|
+
#
|
|
221
|
+
# WHY a method-name gate and not `receivers:` — the ADR-52
|
|
222
|
+
# "rigor-activerecord blocker": a project model not in RBS
|
|
223
|
+
# types its constant as `Dynamic[top]`, so a receiver-type
|
|
224
|
+
# gate declines exactly the calls this plugin exists for. A
|
|
225
|
+
# *name* gate never reads the receiver type; the block keeps
|
|
226
|
+
# the plugin's own AST-constant / `self_type` / `type_of`
|
|
227
|
+
# resolution, so the Dynamic-constant case still reaches it
|
|
228
|
+
# (the same shape as rigor-sorbet's catalog path). The set —
|
|
229
|
+
# the static finder names ∪ every scope, association, and
|
|
230
|
+
# column name (plus `column?` predicate forms) the model
|
|
231
|
+
# index discovered — is exactly the union of names the four
|
|
232
|
+
# resolution paths below can return a type for, so gating on
|
|
233
|
+
# it is byte-identical to the old ungated hook. It is broad
|
|
234
|
+
# (`name`, `id`, …), but membership is one Set probe and the
|
|
235
|
+
# expensive block runs only on candidate hits.
|
|
236
|
+
dynamic_return methods: -> { recognised_method_names } do |call_node, scope|
|
|
237
|
+
contribution_return_type(call_node, scope)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
private
|
|
241
|
+
|
|
242
|
+
# The run-time name gate: finders ∪ scopes ∪ associations ∪
|
|
243
|
+
# column readers (+ `?` predicates). Resolved lazily on first
|
|
244
|
+
# dispatch (after `#prepare` built the index), memoised by the
|
|
245
|
+
# engine. Returns [] when discovery found nothing — the gate
|
|
246
|
+
# then declines every call, matching the old hook's
|
|
247
|
+
# `index.nil? || index.empty?` early return.
|
|
248
|
+
def recognised_method_names
|
|
249
|
+
index = model_index
|
|
250
|
+
return [] if index.nil? || index.empty?
|
|
251
|
+
|
|
252
|
+
names = FINDER_METHOD_NAMES.dup
|
|
253
|
+
index.entries.each_value do |entry|
|
|
254
|
+
names.concat(entry.scopes.map(&:to_sym))
|
|
255
|
+
names.concat(entry.association_names.map(&:to_sym))
|
|
256
|
+
entry.column_names.each do |column|
|
|
257
|
+
names << column.to_sym
|
|
258
|
+
names << :"#{column}?"
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
names
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# The migrated body of the legacy `flow_contribution_for` —
|
|
265
|
+
# same resolution order, returning the bare type the
|
|
266
|
+
# `dynamic_return` contract expects.
|
|
267
|
+
def contribution_return_type(call_node, scope)
|
|
211
268
|
return nil unless call_node.is_a?(Prism::CallNode)
|
|
212
269
|
|
|
213
270
|
index = model_index
|
|
214
271
|
return nil if index.nil? || index.empty?
|
|
215
272
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
end
|
|
224
|
-
return nil if return_type.nil?
|
|
225
|
-
|
|
226
|
-
Rigor::FlowContribution.new(
|
|
227
|
-
return_type: return_type,
|
|
228
|
-
provenance: Rigor::FlowContribution::Provenance.new(
|
|
229
|
-
source_family: "plugin.#{manifest.id}",
|
|
230
|
-
plugin_id: manifest.id,
|
|
231
|
-
node: call_node,
|
|
232
|
-
descriptor: nil
|
|
233
|
-
)
|
|
234
|
-
)
|
|
273
|
+
if call_node.receiver
|
|
274
|
+
class_call_return_type(call_node, index) ||
|
|
275
|
+
relation_call_return_type(call_node, scope, index) ||
|
|
276
|
+
instance_call_return_type(call_node, scope, index)
|
|
277
|
+
else
|
|
278
|
+
implicit_self_class_call_return_type(call_node, scope, index)
|
|
279
|
+
end
|
|
235
280
|
end
|
|
236
281
|
|
|
237
|
-
private
|
|
238
|
-
|
|
239
282
|
def class_call_return_type(call_node, index)
|
|
240
283
|
model_name = constant_receiver_name(call_node.receiver)
|
|
241
284
|
return nil if model_name.nil?
|
|
@@ -533,16 +576,11 @@ module Rigor
|
|
|
533
576
|
table = schema_table_or_nil
|
|
534
577
|
return nil if table.nil?
|
|
535
578
|
|
|
536
|
-
#
|
|
537
|
-
#
|
|
538
|
-
# descriptor
|
|
539
|
-
#
|
|
540
|
-
|
|
541
|
-
io_boundary: io_boundary,
|
|
542
|
-
search_paths: @model_search_paths,
|
|
543
|
-
base_classes: @model_base_classes
|
|
544
|
-
).discover
|
|
545
|
-
|
|
579
|
+
# ADR-60 WD3 record-and-validate: the producer's own in-block
|
|
580
|
+
# `ModelDiscoverer` reads are captured into the dependency
|
|
581
|
+
# descriptor after the block runs, and the producer's `watch:`
|
|
582
|
+
# covers model-file additions — so no priming walk is needed
|
|
583
|
+
# (it used to run the discover twice).
|
|
546
584
|
@model_index = cache_for(:model_index, params: {}).call
|
|
547
585
|
rescue StandardError => e
|
|
548
586
|
@load_errors << "model index build failed: #{e.class}: #{e.message}"
|
|
@@ -561,9 +599,10 @@ module Rigor
|
|
|
561
599
|
return nil if @schema_load_attempted
|
|
562
600
|
|
|
563
601
|
@schema_load_attempted = true
|
|
564
|
-
#
|
|
565
|
-
#
|
|
566
|
-
|
|
602
|
+
# ADR-60 WD3 record-and-validate: the producer reads
|
|
603
|
+
# `@schema_file` in-block, and that read is captured into the
|
|
604
|
+
# dependency descriptor after the block runs — so no priming
|
|
605
|
+
# read is needed here.
|
|
567
606
|
@schema_table = cache_for(:schema_table, params: {}).call
|
|
568
607
|
rescue Plugin::AccessDeniedError => e
|
|
569
608
|
@load_errors << "rigor-activerecord: #{e.message}"
|
|
@@ -12,7 +12,7 @@ module Rigor
|
|
|
12
12
|
# attachment mapping the plugin sees.
|
|
13
13
|
#
|
|
14
14
|
# No `:error` diagnostics in this slice — the
|
|
15
|
-
# `
|
|
15
|
+
# `dynamic_return` return-type narrowing carries
|
|
16
16
|
# the type-checking value; surfacing unknown attachment
|
|
17
17
|
# names as errors requires a coupled receiver-class
|
|
18
18
|
# narrowing pass that the integration spec doesn't yet
|
|
@@ -50,8 +50,8 @@ module Rigor
|
|
|
50
50
|
return if attachments.nil?
|
|
51
51
|
|
|
52
52
|
# Only flag when the method matches a known
|
|
53
|
-
# attachment name (the `
|
|
54
|
-
#
|
|
53
|
+
# attachment name (the `dynamic_return` rule
|
|
54
|
+
# provides the narrowing; the diagnostic just
|
|
55
55
|
# confirms the recognition).
|
|
56
56
|
attachment = attachments.find { |a| a[:name] == node.name.to_s }
|
|
57
57
|
return if attachment.nil?
|
|
@@ -54,8 +54,11 @@ module Rigor
|
|
|
54
54
|
)
|
|
55
55
|
|
|
56
56
|
# Cached: attachment index. Walks every `.rb` file under
|
|
57
|
-
# `model_search_paths` for `has_*_attached` macros.
|
|
58
|
-
|
|
57
|
+
# `model_search_paths` for `has_*_attached` macros. `watch:`
|
|
58
|
+
# (ADR-60 WD3) covers model-file additions; the discoverer's
|
|
59
|
+
# in-block reads are captured into the record-and-validate
|
|
60
|
+
# dependency descriptor after the block runs.
|
|
61
|
+
producer :attachment_index, watch: -> { [[@model_search_paths, "**/*.rb"]] } do |_params|
|
|
59
62
|
rows = AttachmentDiscoverer.new(
|
|
60
63
|
io_boundary: io_boundary,
|
|
61
64
|
search_paths: @model_search_paths
|
|
@@ -79,47 +82,41 @@ module Rigor
|
|
|
79
82
|
|
|
80
83
|
# Return-type contribution: when the receiver is
|
|
81
84
|
# `Nominal[Model]` and the method matches a discovered
|
|
82
|
-
# attachment,
|
|
85
|
+
# attachment, narrows to
|
|
83
86
|
# `Nominal[ActiveStorage::Attached::One]` (singular) or
|
|
84
|
-
# `Nominal[ActiveStorage::Attached::Many]` (collection)
|
|
87
|
+
# `Nominal[ActiveStorage::Attached::Many]` (collection)
|
|
88
|
+
# via a `dynamic_return` rule keyed on the live set of
|
|
89
|
+
# model class names from the attachment index.
|
|
85
90
|
# The chained call (`.attached?`, `.purge`, `.url`)
|
|
86
91
|
# then resolves through ActiveStorage's RBS surface.
|
|
87
92
|
# Attachment setters (`user.avatar=`) decline — they
|
|
88
93
|
# take side-effecting argument types that the RBS
|
|
89
94
|
# surface already covers.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
dynamic_return receivers: -> { attachment_index&.class_names || [] } do |call_node, scope|
|
|
96
|
+
next nil unless call_node.is_a?(Prism::CallNode)
|
|
97
|
+
next nil if call_node.receiver.nil?
|
|
98
|
+
next nil unless call_node.arguments.nil?
|
|
94
99
|
|
|
95
100
|
index = attachment_index
|
|
96
|
-
|
|
101
|
+
next nil if index.nil? || index.empty?
|
|
97
102
|
|
|
98
103
|
receiver_type = scope.type_of(call_node.receiver)
|
|
99
|
-
|
|
104
|
+
next nil unless receiver_type.is_a?(Rigor::Type::Nominal)
|
|
100
105
|
|
|
101
106
|
attachments = index.attachments_for(receiver_type.class_name) ||
|
|
102
107
|
index.attachments_for("::#{receiver_type.class_name}")
|
|
103
|
-
|
|
108
|
+
next nil if attachments.nil?
|
|
104
109
|
|
|
105
110
|
attachment = attachments.find { |a| a[:name] == call_node.name.to_s }
|
|
106
|
-
|
|
111
|
+
next nil if attachment.nil?
|
|
107
112
|
|
|
108
113
|
target = case attachment[:kind]
|
|
109
114
|
when :singular then "ActiveStorage::Attached::One"
|
|
110
115
|
when :collection then "ActiveStorage::Attached::Many"
|
|
111
116
|
end
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
Rigor::
|
|
115
|
-
return_type: Rigor::Type::Combinator.nominal_of(target),
|
|
116
|
-
provenance: Rigor::FlowContribution::Provenance.new(
|
|
117
|
-
source_family: "plugin.#{manifest.id}",
|
|
118
|
-
plugin_id: manifest.id,
|
|
119
|
-
node: call_node,
|
|
120
|
-
descriptor: nil
|
|
121
|
-
)
|
|
122
|
-
)
|
|
117
|
+
next nil if target.nil?
|
|
118
|
+
|
|
119
|
+
Rigor::Type::Combinator.nominal_of(target)
|
|
123
120
|
end
|
|
124
121
|
|
|
125
122
|
# @!visibility private
|
|
@@ -132,12 +129,11 @@ module Rigor
|
|
|
132
129
|
def attachment_index
|
|
133
130
|
return @attachment_index if @attachment_index
|
|
134
131
|
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
).discover
|
|
132
|
+
# ADR-60 WD3 record-and-validate: the producer's in-block
|
|
133
|
+
# `AttachmentDiscoverer` reads are captured into the
|
|
134
|
+
# dependency descriptor after the block runs, and the
|
|
135
|
+
# producer's `watch:` covers model-file additions — so no
|
|
136
|
+
# priming walk is needed (it used to run the discover twice).
|
|
141
137
|
@attachment_index = cache_for(:attachment_index, params: {}).call
|
|
142
138
|
rescue Plugin::AccessDeniedError => e
|
|
143
139
|
@load_errors << "rigor-activestorage: #{e.message}"
|
|
@@ -5,7 +5,7 @@ require "rigor/plugin"
|
|
|
5
5
|
module Rigor
|
|
6
6
|
module Plugin
|
|
7
7
|
# ADR-25 — a pure RBS-bundle plugin. It ships NO analyzer code:
|
|
8
|
-
# no `diagnostics_for_file`, no `
|
|
8
|
+
# no `diagnostics_for_file`, no `dynamic_return`. Its
|
|
9
9
|
# whole contribution is the manifest's `signature_paths: ["sig"]`,
|
|
10
10
|
# which declares the bundled ActiveSupport `core_ext` RBS
|
|
11
11
|
# directory. `Plugin::Loader` resolves that directory against
|
|
@@ -146,8 +146,7 @@ module Rigor
|
|
|
146
146
|
|
|
147
147
|
class_pair = kwargs.elements.find do |elem|
|
|
148
148
|
elem.is_a?(Prism::AssocNode) &&
|
|
149
|
-
elem.key
|
|
150
|
-
elem.key.value == "class"
|
|
149
|
+
Source::Literals.symbol_named?(elem.key, "class")
|
|
151
150
|
end
|
|
152
151
|
return nil if class_pair.nil?
|
|
153
152
|
|
|
@@ -87,19 +87,15 @@ module Rigor
|
|
|
87
87
|
]
|
|
88
88
|
)
|
|
89
89
|
|
|
90
|
-
producer :factory_index do |_params|
|
|
90
|
+
producer :factory_index, watch: -> { [[@factory_search_paths, "**/*.rb"]] } do |_params|
|
|
91
91
|
FactoryDiscoverer.new(
|
|
92
92
|
io_boundary: io_boundary,
|
|
93
93
|
search_paths: @factory_search_paths
|
|
94
94
|
).discover
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
-
def init(
|
|
98
|
-
@services = services
|
|
97
|
+
def init(_services)
|
|
99
98
|
@factory_search_paths = Array(config.fetch("factory_search_paths")).map(&:to_s)
|
|
100
|
-
@factory_index = nil
|
|
101
|
-
@model_index = nil
|
|
102
|
-
@model_index_resolved = false
|
|
103
99
|
end
|
|
104
100
|
|
|
105
101
|
# ADR-37 — per-call factory/attribute validation over the
|
|
@@ -108,42 +104,17 @@ module Rigor
|
|
|
108
104
|
# is positioned via `diagnostic(node, location:)`. No file-level
|
|
109
105
|
# diagnostic remains, so there is no `diagnostics_for_file`.
|
|
110
106
|
node_rule Prism::CallNode do |node, _scope, path|
|
|
111
|
-
index =
|
|
107
|
+
index = producer_value(:factory_index)
|
|
112
108
|
next [] if index.nil? || index.empty?
|
|
113
109
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
private
|
|
125
|
-
|
|
126
|
-
# Phase 1 (c) — lazily resolves the :model_index fact
|
|
127
|
-
# from rigor-activerecord. Returns nil when
|
|
128
|
-
# rigor-activerecord isn't loaded or hasn't published
|
|
129
|
-
# an index; the analyzer treats nil as "no cross-check"
|
|
130
|
-
# and falls back to Phase 1 (a) behaviour (factory
|
|
131
|
-
# attributes only).
|
|
132
|
-
def model_index_or_nil
|
|
133
|
-
return @model_index if @model_index_resolved
|
|
134
|
-
|
|
135
|
-
@model_index = @services.fact_store.read(plugin_id: "activerecord", name: :model_index)
|
|
136
|
-
@model_index_resolved = true
|
|
137
|
-
@model_index
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def factory_index_or_nil
|
|
141
|
-
return @factory_index if @factory_index
|
|
142
|
-
|
|
143
|
-
descriptor = glob_descriptor(@factory_search_paths, "**/*.rb")
|
|
144
|
-
@factory_index = cache_for(:factory_index, params: {}, descriptor: descriptor).call
|
|
145
|
-
rescue StandardError
|
|
146
|
-
nil
|
|
110
|
+
# `:model_index` is rigor-activerecord's published fact (ADR-9);
|
|
111
|
+
# nil when that plugin isn't loaded, in which case the analyzer
|
|
112
|
+
# falls back to factory-attributes-only checking.
|
|
113
|
+
violations = Analyzer.violations_for(
|
|
114
|
+
call_node: node, factory_index: index,
|
|
115
|
+
model_index: read_fact(plugin_id: "activerecord", name: :model_index)
|
|
116
|
+
)
|
|
117
|
+
diagnostics_for(violations, path: path, node: node)
|
|
147
118
|
end
|
|
148
119
|
end
|
|
149
120
|
|
|
@@ -281,7 +281,7 @@ module Rigor
|
|
|
281
281
|
return false unless kwargs.is_a?(Prism::KeywordHashNode)
|
|
282
282
|
|
|
283
283
|
pair = kwargs.elements.find do |el|
|
|
284
|
-
el.is_a?(Prism::AssocNode) &&
|
|
284
|
+
el.is_a?(Prism::AssocNode) && Source::Literals.symbol_named?(el.key, "required")
|
|
285
285
|
end
|
|
286
286
|
return false if pair.nil?
|
|
287
287
|
|
|
@@ -364,7 +364,7 @@ module Rigor
|
|
|
364
364
|
return true unless kwargs.is_a?(Prism::KeywordHashNode)
|
|
365
365
|
|
|
366
366
|
null_pair = kwargs.elements.find do |el|
|
|
367
|
-
el.is_a?(Prism::AssocNode) &&
|
|
367
|
+
el.is_a?(Prism::AssocNode) && Source::Literals.symbol_named?(el.key, "null")
|
|
368
368
|
end
|
|
369
369
|
return true if null_pair.nil?
|
|
370
370
|
|
|
@@ -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
|
)
|