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
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../diagnostic"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Analysis
|
|
7
|
+
class Runner
|
|
8
|
+
# Builds and orders every project-level diagnostic stream the
|
|
9
|
+
# {Runner} surfaces — the pre-file streams (plugin load / prepare,
|
|
10
|
+
# ADR-10 dependency-source, pre-eval, RBS coverage, path errors),
|
|
11
|
+
# the post-analysis streams (synthesized namespaces, conforms-to,
|
|
12
|
+
# the three reporter drains), and the final severity stamp.
|
|
13
|
+
#
|
|
14
|
+
# Constraint: the relative order of every stream below is the
|
|
15
|
+
# diagnostic output contract — callers MUST NOT reorder the
|
|
16
|
+
# concatenation in `pre_file_diagnostics` or the post-analysis
|
|
17
|
+
# streams the {Runner} drains after `analyze_files`.
|
|
18
|
+
#
|
|
19
|
+
# The collaborator holds the immutable per-run inputs (the
|
|
20
|
+
# configuration and the three mutable reporter accumulators, which
|
|
21
|
+
# are shared instances the dispatcher records into). The per-run
|
|
22
|
+
# varying state produced by other passes (the plugin registry, the
|
|
23
|
+
# dependency-source index, and the four end-of-pass snapshots) is
|
|
24
|
+
# read through injected reader procs so this collaborator never
|
|
25
|
+
# calls back into the {Runner} and the read happens at the exact
|
|
26
|
+
# point in the run the original inline read did.
|
|
27
|
+
class DiagnosticAggregator # rubocop:disable Metrics/ClassLength
|
|
28
|
+
# @param configuration [Rigor::Configuration]
|
|
29
|
+
# @param rbs_extended_reporter [RbsExtended::Reporter]
|
|
30
|
+
# @param boundary_cross_reporter
|
|
31
|
+
# [DependencySourceInference::BoundaryCrossReporter]
|
|
32
|
+
# @param source_rbs_synthesis_reporter
|
|
33
|
+
# [Plugin::SourceRbsSynthesisReporter]
|
|
34
|
+
# @param plugin_registry [#call] reader returning the current
|
|
35
|
+
# {Plugin::Registry} (varies per run).
|
|
36
|
+
# @param dependency_source_index [#call] reader returning the
|
|
37
|
+
# current {DependencySourceInference::Index}.
|
|
38
|
+
# @param pool_mode [#call] reader returning the pool-mode flag.
|
|
39
|
+
# @param cached_plugin_prepare_diagnostics [#call] reader
|
|
40
|
+
# returning the prepare-diagnostic snapshot.
|
|
41
|
+
# @param pre_eval_diagnostics_from_scanner [#call] reader
|
|
42
|
+
# returning the pre-eval scanner diagnostics.
|
|
43
|
+
# @param synthesized_namespaces_snapshot [#call] reader.
|
|
44
|
+
# @param conformance_results_snapshot [#call] reader.
|
|
45
|
+
def initialize(configuration:, rbs_extended_reporter:, boundary_cross_reporter:, # rubocop:disable Metrics/ParameterLists
|
|
46
|
+
source_rbs_synthesis_reporter:, plugin_registry:, dependency_source_index:,
|
|
47
|
+
pool_mode:, cached_plugin_prepare_diagnostics:,
|
|
48
|
+
pre_eval_diagnostics_from_scanner:, synthesized_namespaces_snapshot:,
|
|
49
|
+
conformance_results_snapshot:)
|
|
50
|
+
@configuration = configuration
|
|
51
|
+
@rbs_extended_reporter = rbs_extended_reporter
|
|
52
|
+
@boundary_cross_reporter = boundary_cross_reporter
|
|
53
|
+
@source_rbs_synthesis_reporter = source_rbs_synthesis_reporter
|
|
54
|
+
@plugin_registry_reader = plugin_registry
|
|
55
|
+
@dependency_source_index_reader = dependency_source_index
|
|
56
|
+
@pool_mode_reader = pool_mode
|
|
57
|
+
@cached_plugin_prepare_diagnostics_reader = cached_plugin_prepare_diagnostics
|
|
58
|
+
@pre_eval_diagnostics_from_scanner_reader = pre_eval_diagnostics_from_scanner
|
|
59
|
+
@synthesized_namespaces_snapshot_reader = synthesized_namespaces_snapshot
|
|
60
|
+
@conformance_results_snapshot_reader = conformance_results_snapshot
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Pre-file diagnostic streams that fire once per run rather
|
|
64
|
+
# than per analyzed file: plugin load / prepare envelopes,
|
|
65
|
+
# the ADR-10 dependency-source resolution surface, and the
|
|
66
|
+
# `expand_paths` errors for `paths:` entries that don't
|
|
67
|
+
# exist or aren't `.rb`. Aggregated here so `#run` stays
|
|
68
|
+
# under the ABC budget.
|
|
69
|
+
#
|
|
70
|
+
# ADR-15 Phase 4b — `plugin_prepare_diagnostics` runs on
|
|
71
|
+
# the coordinator's plugin registry under sequential mode;
|
|
72
|
+
# under pool mode each worker re-runs `prepare` against
|
|
73
|
+
# its own plugin instances, so the pool path drains the
|
|
74
|
+
# first worker's prepare-diagnostic snapshot into the
|
|
75
|
+
# aggregated diagnostic stream instead (see
|
|
76
|
+
# {#analyze_files_in_pool}). Skipping the coordinator
|
|
77
|
+
# prepare in pool mode avoids double-running `#prepare`
|
|
78
|
+
# against the coordinator-side plugin instances (which
|
|
79
|
+
# the pool path never consults for per-file analysis).
|
|
80
|
+
def pre_file_diagnostics(expansion)
|
|
81
|
+
# ADR-18 slice 3 — prepare diagnostics are captured
|
|
82
|
+
# earlier in #run (before the synthetic-method scanner)
|
|
83
|
+
# so cross-plugin facts are available to the scanner.
|
|
84
|
+
# We re-surface the captured diagnostics here so the
|
|
85
|
+
# existing pre_file_diagnostics ordering is preserved.
|
|
86
|
+
prepare = pool_mode? ? [] : cached_plugin_prepare_diagnostics
|
|
87
|
+
plugin_load_diagnostics +
|
|
88
|
+
prepare +
|
|
89
|
+
pre_eval_diagnostics +
|
|
90
|
+
dependency_source_diagnostics +
|
|
91
|
+
dependency_source_budget_diagnostics +
|
|
92
|
+
dependency_source_config_conflict_diagnostics +
|
|
93
|
+
rbs_coverage_diagnostics +
|
|
94
|
+
expansion.fetch(:errors)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# ADR-17 slice 1 — surface a `:error` diagnostic for each
|
|
98
|
+
# `pre_eval:` entry whose resolved path doesn't exist on
|
|
99
|
+
# disk. Loud failure mode (`:error`, not `:warning`):
|
|
100
|
+
# a missing pre_eval path is a configuration mistake the
|
|
101
|
+
# user must fix before analysis is meaningful.
|
|
102
|
+
#
|
|
103
|
+
# Slice 2 adds the `:warning` `pre-eval.parse-error`
|
|
104
|
+
# stream from the pre-pass scanner — accumulated as
|
|
105
|
+
# `@pre_eval_diagnostics_from_scanner` during {#run} and
|
|
106
|
+
# merged here so both diagnostics flow through the same
|
|
107
|
+
# severity / ordering pipeline.
|
|
108
|
+
def pre_eval_diagnostics
|
|
109
|
+
not_found = @configuration.pre_eval.filter_map do |path|
|
|
110
|
+
next if File.file?(path)
|
|
111
|
+
|
|
112
|
+
Diagnostic.new(
|
|
113
|
+
path: ".rigor.yml", line: 1, column: 1,
|
|
114
|
+
message: "pre_eval entry not found: #{path.inspect}. " \
|
|
115
|
+
"Pre-evaluation requires the file to exist on disk; remove the entry " \
|
|
116
|
+
"or create the file before re-running analysis.",
|
|
117
|
+
severity: :error,
|
|
118
|
+
rule: "pre-eval.file-not-found",
|
|
119
|
+
source_family: :builtin
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
not_found + Array(pre_eval_diagnostics_from_scanner).map { |hash| diagnostic_from_hash(hash) }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def diagnostic_from_hash(hash)
|
|
126
|
+
Diagnostic.new(
|
|
127
|
+
path: hash.fetch(:path), line: hash.fetch(:line), column: hash.fetch(:column),
|
|
128
|
+
message: hash.fetch(:message), severity: hash.fetch(:severity),
|
|
129
|
+
rule: hash.fetch(:rule), source_family: :builtin
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def plugin_load_diagnostics
|
|
134
|
+
plugin_registry.load_errors.map do |error|
|
|
135
|
+
Diagnostic.new(
|
|
136
|
+
path: ".rigor.yml",
|
|
137
|
+
line: 1,
|
|
138
|
+
column: 1,
|
|
139
|
+
message: error.message,
|
|
140
|
+
severity: :error,
|
|
141
|
+
rule: "load-error",
|
|
142
|
+
source_family: :plugin_loader
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# ADR-10 § "Diagnostic prefix family" — surfaces gems
|
|
148
|
+
# listed in `dependencies.source_inference` that RubyGems
|
|
149
|
+
# could not resolve. The run continues; the gem simply
|
|
150
|
+
# contributes nothing this session, mirroring the
|
|
151
|
+
# plugin-load error envelope. Authored `:warning` because
|
|
152
|
+
# an unresolvable gem usually means a typo or a missing
|
|
153
|
+
# `bundle install` rather than a project-blocking problem;
|
|
154
|
+
# the severity profile still re-stamps it.
|
|
155
|
+
def dependency_source_diagnostics
|
|
156
|
+
dependency_source_index.unresolvable.map do |entry|
|
|
157
|
+
Diagnostic.new(
|
|
158
|
+
path: ".rigor.yml",
|
|
159
|
+
line: 1,
|
|
160
|
+
column: 1,
|
|
161
|
+
message: "dependencies.source_inference[].gem #{entry.gem_name.inspect} could not be " \
|
|
162
|
+
"resolved (#{entry.reason}); skipping",
|
|
163
|
+
severity: :warning,
|
|
164
|
+
rule: "dynamic.dependency-source.gem-not-found",
|
|
165
|
+
source_family: :builtin
|
|
166
|
+
)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# ADR-10 § "Budget interaction" / slice 4 — emits one
|
|
171
|
+
# `:warning` per gem whose Walker run hit the
|
|
172
|
+
# `dependencies.budget_per_gem` cap. The cap is a Walker-
|
|
173
|
+
# side guard rail (slice 4 picks the (α) semantics from
|
|
174
|
+
# ADR-10 WD4: harvesting stops, the dispatcher behaves
|
|
175
|
+
# exactly as before for unrecorded methods). The
|
|
176
|
+
# diagnostic names the gem and points the user at the
|
|
177
|
+
# three remediations: ship RBS, reduce `mode:` from
|
|
178
|
+
# `full` to `when_missing`, or de-list the gem.
|
|
179
|
+
# ADR-10 § "config-conflict diagnostic" / 5d — surfaces
|
|
180
|
+
# `Configuration::Dependencies` warnings accumulated
|
|
181
|
+
# during `from_h` deduplication of the `includes:`-chain
|
|
182
|
+
# source_inference array. Each warning describes a
|
|
183
|
+
# per-gem mode conflict that the merge resolved
|
|
184
|
+
# right-wins; the user sees one diagnostic per conflict.
|
|
185
|
+
# `:warning` matches the user's "warn but don't block"
|
|
186
|
+
# preference per the design discussion.
|
|
187
|
+
def dependency_source_config_conflict_diagnostics
|
|
188
|
+
@configuration.dependencies.warnings.map do |message|
|
|
189
|
+
Diagnostic.new(
|
|
190
|
+
path: ".rigor.yml",
|
|
191
|
+
line: 1,
|
|
192
|
+
column: 1,
|
|
193
|
+
message: message,
|
|
194
|
+
severity: :warning,
|
|
195
|
+
rule: "dynamic.dependency-source.config-conflict",
|
|
196
|
+
source_family: :builtin
|
|
197
|
+
)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def dependency_source_budget_diagnostics
|
|
202
|
+
budget = @configuration.dependencies.budget_per_gem
|
|
203
|
+
dependency_source_index.budget_exceeded.map do |gem_name|
|
|
204
|
+
Diagnostic.new(
|
|
205
|
+
path: ".rigor.yml",
|
|
206
|
+
line: 1,
|
|
207
|
+
column: 1,
|
|
208
|
+
message: "dependencies.source_inference[].gem #{gem_name.inspect} exceeded the per-gem " \
|
|
209
|
+
"catalog cap (#{budget} method definitions); the remaining methods fall back " \
|
|
210
|
+
"to the existing RBS-or-Dynamic[top] boundary. Ship RBS for the gem, set " \
|
|
211
|
+
"`mode: when_missing` instead of `full`, or de-list the gem.",
|
|
212
|
+
severity: :warning,
|
|
213
|
+
rule: "dynamic.dependency-source.budget-exceeded",
|
|
214
|
+
source_family: :builtin
|
|
215
|
+
)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# O4 Layer 3 slice 3 — graceful-degradation coverage
|
|
220
|
+
# report. When the project has a `Gemfile.lock` (slice 1)
|
|
221
|
+
# and one or more locked gems are not covered by ANY of
|
|
222
|
+
# the four RBS resolution paths (`DEFAULT_LIBRARIES`,
|
|
223
|
+
# `data/vendored_gem_sigs/`, slice-1 bundle-shipped
|
|
224
|
+
# `sig/`, slice-2 `rbs_collection.lock.yaml`), emit a
|
|
225
|
+
# single `:info` diagnostic summarising the uncovered set
|
|
226
|
+
# so the user can act on it (run `rbs collection install`,
|
|
227
|
+
# opt the gem into `dependencies.source_inference:`, or
|
|
228
|
+
# accept the `Dynamic[T]` fallback).
|
|
229
|
+
#
|
|
230
|
+
# Suppressed when the lockfile is empty, when every gem
|
|
231
|
+
# is covered, or when slice 1's `bundler.lockfile`
|
|
232
|
+
# discovery returned nothing (no lockfile to read).
|
|
233
|
+
def rbs_coverage_diagnostics
|
|
234
|
+
locked = Environment::LockfileResolver.locked_gems(
|
|
235
|
+
lockfile_path: @configuration.bundler_lockfile,
|
|
236
|
+
project_root: Dir.pwd,
|
|
237
|
+
auto_detect: @configuration.bundler_auto_detect
|
|
238
|
+
)
|
|
239
|
+
return [] if locked.empty?
|
|
240
|
+
|
|
241
|
+
bundle_sig_paths = Environment::BundleSigDiscovery.discover(
|
|
242
|
+
bundle_path: @configuration.bundler_bundle_path,
|
|
243
|
+
project_root: Dir.pwd,
|
|
244
|
+
auto_detect: @configuration.bundler_auto_detect,
|
|
245
|
+
locked_gems: locked
|
|
246
|
+
)
|
|
247
|
+
collection_paths = Environment::RbsCollectionDiscovery.discover(
|
|
248
|
+
lockfile_path: @configuration.rbs_collection_lockfile,
|
|
249
|
+
project_root: Dir.pwd,
|
|
250
|
+
auto_detect: @configuration.rbs_collection_auto_detect
|
|
251
|
+
)
|
|
252
|
+
rows = Environment::RbsCoverageReport.classify(
|
|
253
|
+
locked_gems: locked,
|
|
254
|
+
default_libraries: Environment::DEFAULT_LIBRARIES,
|
|
255
|
+
bundle_sig_paths: bundle_sig_paths,
|
|
256
|
+
rbs_collection_paths: collection_paths
|
|
257
|
+
)
|
|
258
|
+
missing = Environment::RbsCoverageReport.missing(rows)
|
|
259
|
+
return [] if missing.empty?
|
|
260
|
+
|
|
261
|
+
[build_rbs_coverage_missing_diagnostic(missing)]
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Robustness uplift companion (ADR-5) — when the project's
|
|
265
|
+
# `signature_paths:` RBS declared qualified names without their
|
|
266
|
+
# enclosing namespace, `RbsLoader` synthesizes the missing
|
|
267
|
+
# `module`s so the otherwise-inert signatures resolve. Surface a
|
|
268
|
+
# single `:info` diagnostic naming them so the user knows their
|
|
269
|
+
# sig set is malformed (`rbs validate` rejects it) and can fix it
|
|
270
|
+
# at the source. Authored `:info`: the analysis already succeeded;
|
|
271
|
+
# this is advisory, never a gate. Empty for a well-formed sig set.
|
|
272
|
+
def rbs_synthesized_namespace_diagnostics
|
|
273
|
+
synthesized = synthesized_namespaces_snapshot
|
|
274
|
+
return [] if synthesized.nil? || synthesized.empty?
|
|
275
|
+
|
|
276
|
+
[build_rbs_synthesized_namespace_diagnostic(synthesized)]
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Maps the per-run `rigor:v1:conforms-to` scan results into
|
|
280
|
+
# diagnostics (spec: `rbs-extended.md` § "Explicit conformance
|
|
281
|
+
# directive"). A class that declares `conforms-to _Interface`
|
|
282
|
+
# but is missing a required interface method surfaces as
|
|
283
|
+
# `rbs_extended.unsatisfied-conformance`; an unresolvable
|
|
284
|
+
# interface name surfaces as `dynamic.rbs-extended.unresolved`
|
|
285
|
+
# `:info` (the same fail-soft channel the other directive
|
|
286
|
+
# parsers use). Empty for a project with no directive, a
|
|
287
|
+
# well-formed conformance, or a non-sequential pool run (the
|
|
288
|
+
# snapshot mirrors `synthesized_namespaces`).
|
|
289
|
+
def conforms_to_diagnostics
|
|
290
|
+
results = conformance_results_snapshot
|
|
291
|
+
return [] if results.nil? || results.empty?
|
|
292
|
+
|
|
293
|
+
results.map { |record| build_conformance_diagnostic(record) }
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def build_conformance_diagnostic(record)
|
|
297
|
+
case record
|
|
298
|
+
when RbsExtended::ConformanceChecker::Unsatisfied
|
|
299
|
+
build_unsatisfied_conformance_diagnostic(record)
|
|
300
|
+
when RbsExtended::ConformanceChecker::IncompatibleSignature
|
|
301
|
+
build_incompatible_signature_diagnostic(record)
|
|
302
|
+
else # UnresolvedInterface
|
|
303
|
+
build_reporter_diagnostic(
|
|
304
|
+
record.location,
|
|
305
|
+
rule: "dynamic.rbs-extended.unresolved",
|
|
306
|
+
message: "`#{record.class_name}` declares `conforms-to #{record.interface_name}` but " \
|
|
307
|
+
"interface `#{record.interface_name}` is not loaded. Check for a typo or add " \
|
|
308
|
+
"the `sig`/library that declares it to the RBS load path."
|
|
309
|
+
)
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def build_unsatisfied_conformance_diagnostic(record)
|
|
314
|
+
path, line, column = location_fields(record.location)
|
|
315
|
+
Diagnostic.new(
|
|
316
|
+
path: path, line: line, column: column,
|
|
317
|
+
message: "`#{record.class_name}` declares `conforms-to #{record.interface_name}` " \
|
|
318
|
+
"but does not provide #{pluralize_methods(record.missing_methods)}: " \
|
|
319
|
+
"#{record.missing_methods.map { |m| "`##{m}`" }.join(', ')}. Implement the " \
|
|
320
|
+
"missing method(s) or remove the directive.",
|
|
321
|
+
severity: :warning,
|
|
322
|
+
rule: "rbs_extended.unsatisfied-conformance",
|
|
323
|
+
source_family: :builtin
|
|
324
|
+
)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def build_incompatible_signature_diagnostic(record)
|
|
328
|
+
path, line, column = location_fields(record.location)
|
|
329
|
+
Diagnostic.new(
|
|
330
|
+
path: path, line: line, column: column,
|
|
331
|
+
message: "`#{record.class_name}##{record.method_name}` does not satisfy " \
|
|
332
|
+
"`conforms-to #{record.interface_name}`: #{record.detail}. Adjust the " \
|
|
333
|
+
"signature to a subtype of the interface contract.",
|
|
334
|
+
severity: :warning,
|
|
335
|
+
rule: "rbs_extended.unsatisfied-conformance",
|
|
336
|
+
source_family: :builtin,
|
|
337
|
+
method_name: record.method_name
|
|
338
|
+
)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def pluralize_methods(methods)
|
|
342
|
+
methods.size == 1 ? "required method" : "#{methods.size} required methods"
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def build_rbs_synthesized_namespace_diagnostic(synthesized)
|
|
346
|
+
sample_size = 5
|
|
347
|
+
sample = synthesized.first(sample_size)
|
|
348
|
+
suffix = synthesized.size > sample_size ? ", and #{synthesized.size - sample_size} more" : ""
|
|
349
|
+
Diagnostic.new(
|
|
350
|
+
path: ".rigor.yml",
|
|
351
|
+
line: 1,
|
|
352
|
+
column: 1,
|
|
353
|
+
message: "#{synthesized.size} RBS namespace(s) under `signature_paths:` are " \
|
|
354
|
+
"referenced by qualified declarations (e.g. `class Foo::Bar`) but never " \
|
|
355
|
+
"declared: #{sample.join(', ')}#{suffix}. `rbs validate` rejects this; " \
|
|
356
|
+
"Rigor synthesized the missing `module`(s) so the signatures still " \
|
|
357
|
+
"resolve. Declare each (`module <name>` / `class <name>`) in your RBS to " \
|
|
358
|
+
"make the sig set valid upstream.",
|
|
359
|
+
severity: :info,
|
|
360
|
+
rule: "rbs.coverage.synthesized-namespace",
|
|
361
|
+
source_family: :builtin
|
|
362
|
+
)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def build_rbs_coverage_missing_diagnostic(missing)
|
|
366
|
+
sample_size = 5
|
|
367
|
+
sample = missing.first(sample_size).map(&:gem_name)
|
|
368
|
+
suffix = missing.size > sample_size ? ", and #{missing.size - sample_size} more" : ""
|
|
369
|
+
Diagnostic.new(
|
|
370
|
+
path: ".rigor.yml",
|
|
371
|
+
line: 1,
|
|
372
|
+
column: 1,
|
|
373
|
+
message: "#{missing.size} gem(s) in Gemfile.lock have no RBS available: " \
|
|
374
|
+
"#{sample.join(', ')}#{suffix}. " \
|
|
375
|
+
"Consider `rbs collection install` to fetch community RBS from " \
|
|
376
|
+
"`ruby/gem_rbs_collection`, ship `sig/` in the gem itself, or " \
|
|
377
|
+
"opt the gem into `dependencies.source_inference:` in `.rigor.yml`.",
|
|
378
|
+
severity: :info,
|
|
379
|
+
rule: "rbs.coverage.missing-gem",
|
|
380
|
+
source_family: :builtin
|
|
381
|
+
)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# ADR-13 slice 3b — drains the per-run
|
|
385
|
+
# {RbsExtended::Reporter} into one diagnostic per accumulated
|
|
386
|
+
# event:
|
|
387
|
+
#
|
|
388
|
+
# - `dynamic.rbs-extended.unresolved` for every annotation
|
|
389
|
+
# payload the parser could not turn into a {Rigor::Type}.
|
|
390
|
+
# Surfaces typos and references to plugin-supplied names
|
|
391
|
+
# the project did not enable.
|
|
392
|
+
# - `dynamic.shape.lossy-projection` for every shape-projection
|
|
393
|
+
# type function (`pick_of`, …) applied to a carrier that
|
|
394
|
+
# loses precision (anything other than `HashShape` / `Tuple`).
|
|
395
|
+
#
|
|
396
|
+
# Both are authored `:info`; the severity profile re-stamps
|
|
397
|
+
# them per project taste. Path / line / column come from the
|
|
398
|
+
# annotation's `RBS::Location` when available, falling back
|
|
399
|
+
# to `.rigor.yml`-style file-level attribution otherwise.
|
|
400
|
+
def rbs_extended_reporter_diagnostics
|
|
401
|
+
return [] if @rbs_extended_reporter.empty?
|
|
402
|
+
|
|
403
|
+
unresolved = @rbs_extended_reporter.unresolved_payloads.map do |entry|
|
|
404
|
+
build_reporter_diagnostic(
|
|
405
|
+
entry.source_location,
|
|
406
|
+
rule: "dynamic.rbs-extended.unresolved",
|
|
407
|
+
message: "`RBS::Extended` directive payload could not be resolved: " \
|
|
408
|
+
"#{entry.payload.inspect}. Check for typos or enable a plugin " \
|
|
409
|
+
"that contributes the referenced type vocabulary."
|
|
410
|
+
)
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
lossy = @rbs_extended_reporter.lossy_projections.map do |entry|
|
|
414
|
+
build_reporter_diagnostic(
|
|
415
|
+
entry.source_location,
|
|
416
|
+
rule: "dynamic.shape.lossy-projection",
|
|
417
|
+
message: "Shape projection `#{entry.head}` applied to a carrier without a " \
|
|
418
|
+
"literal shape; the projection degrades to the input type. Author " \
|
|
419
|
+
"a `HashShape` / `Tuple` carrier or accept the unchanged result."
|
|
420
|
+
)
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
unresolved + lossy
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# ADR-10 slice 5c — drains the per-run
|
|
427
|
+
# {DependencySourceInference::BoundaryCrossReporter} into
|
|
428
|
+
# `dynamic.dependency-source.boundary-cross` `:info`
|
|
429
|
+
# diagnostics. Each event flags a call site where RBS
|
|
430
|
+
# dispatch produced a concrete answer AND a `mode: :full`
|
|
431
|
+
# opt-in gem's source catalog ALSO contains an entry for
|
|
432
|
+
# the same `(class_name, method_name)` — i.e., both
|
|
433
|
+
# contracts have an opinion. RBS still wins on the
|
|
434
|
+
# dispatch result; the diagnostic is purely advisory so
|
|
435
|
+
# the user can verify the two contracts haven't drifted.
|
|
436
|
+
#
|
|
437
|
+
# Severity profile re-stamps the rule per project taste.
|
|
438
|
+
# The diagnostic carries no `path` / `line` / `column`
|
|
439
|
+
# because the crossing is per-method-per-gem, not
|
|
440
|
+
# per-call-site — the diagnostic anchors at `.rigor.yml`
|
|
441
|
+
# like the other `dependency-source.*` diagnostics that
|
|
442
|
+
# report on opt-in configuration.
|
|
443
|
+
# ADR-32 WD6 — drains the per-run
|
|
444
|
+
# {Plugin::SourceRbsSynthesisReporter} into
|
|
445
|
+
# `source-rbs-synthesis-failed` `:info` diagnostics. Each
|
|
446
|
+
# entry names the plugin that owns the synthesizer, the
|
|
447
|
+
# source file the rbs-inline parser couldn't process, and
|
|
448
|
+
# the upstream error message. The synthesizer-emitting
|
|
449
|
+
# plugin (currently only `rigor-rbs-inline`) treats a
|
|
450
|
+
# parse failure as a no-contribution event so analysis
|
|
451
|
+
# continues; this stream surfaces the failure so the user
|
|
452
|
+
# can see which files contributed nothing and why.
|
|
453
|
+
#
|
|
454
|
+
# Severity profile re-stamps the rule per project taste.
|
|
455
|
+
def source_rbs_synthesis_diagnostics
|
|
456
|
+
return [] if @source_rbs_synthesis_reporter.empty?
|
|
457
|
+
|
|
458
|
+
@source_rbs_synthesis_reporter.entries.map do |entry|
|
|
459
|
+
Diagnostic.new(
|
|
460
|
+
path: entry.path, line: 1, column: 1,
|
|
461
|
+
message: "plugin `#{entry.plugin_id}` failed to synthesise RBS from this file: " \
|
|
462
|
+
"#{entry.message}. The file's analysis falls back to no inline-RBS " \
|
|
463
|
+
"contribution. Fix the inline-RBS comment grammar or remove the " \
|
|
464
|
+
"annotation to silence this diagnostic.",
|
|
465
|
+
severity: :info,
|
|
466
|
+
rule: "source-rbs-synthesis-failed",
|
|
467
|
+
source_family: :builtin
|
|
468
|
+
)
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def boundary_cross_diagnostics
|
|
473
|
+
return [] if @boundary_cross_reporter.empty?
|
|
474
|
+
|
|
475
|
+
@boundary_cross_reporter.entries.map do |entry|
|
|
476
|
+
Diagnostic.new(
|
|
477
|
+
path: ".rigor.yml", line: 1, column: 1,
|
|
478
|
+
message: "`#{entry.class_name}##{entry.method_name}` is contributed by both " \
|
|
479
|
+
"RBS (#{entry.rbs_display}) and the `mode: :full` opt-in gem " \
|
|
480
|
+
"`#{entry.gem_name}`. RBS wins on dispatch; verify the gem source " \
|
|
481
|
+
"has not drifted from its RBS contract.",
|
|
482
|
+
severity: :info,
|
|
483
|
+
rule: "dynamic.dependency-source.boundary-cross",
|
|
484
|
+
source_family: :builtin
|
|
485
|
+
)
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def build_reporter_diagnostic(source_location, rule:, message:)
|
|
490
|
+
path, line, column = location_fields(source_location)
|
|
491
|
+
Diagnostic.new(
|
|
492
|
+
path: path, line: line, column: column,
|
|
493
|
+
message: message, severity: :info, rule: rule, source_family: :builtin
|
|
494
|
+
)
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def location_fields(source_location)
|
|
498
|
+
return [".rigor.yml", 1, 1] if source_location.nil?
|
|
499
|
+
|
|
500
|
+
path = location_path(source_location)
|
|
501
|
+
line = source_location.respond_to?(:start_line) ? source_location.start_line : 1
|
|
502
|
+
column = source_location.respond_to?(:start_column) ? source_location.start_column + 1 : 1
|
|
503
|
+
[path, line, column]
|
|
504
|
+
rescue StandardError
|
|
505
|
+
[".rigor.yml", 1, 1]
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def location_path(source_location)
|
|
509
|
+
buffer = source_location.respond_to?(:buffer) ? source_location.buffer : nil
|
|
510
|
+
return ".rigor.yml" if buffer.nil? || !buffer.respond_to?(:name)
|
|
511
|
+
|
|
512
|
+
name = buffer.name.to_s
|
|
513
|
+
name.empty? ? ".rigor.yml" : name
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
# ADR-8 § "Severity profile" — re-stamps each diagnostic's
|
|
517
|
+
# severity from the configured profile + per-rule
|
|
518
|
+
# overrides. Rules emit with their authored severity; the
|
|
519
|
+
# profile is the final filter. Diagnostics whose resolved
|
|
520
|
+
# severity is `:off` are dropped from the run result.
|
|
521
|
+
def apply_severity_profile(diagnostics)
|
|
522
|
+
diagnostics.filter_map { |diagnostic| stamp_severity(diagnostic) }
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def stamp_severity(diagnostic)
|
|
526
|
+
return diagnostic if diagnostic.rule.nil?
|
|
527
|
+
|
|
528
|
+
resolved = Configuration::SeverityProfile.resolve(
|
|
529
|
+
rule: diagnostic.rule,
|
|
530
|
+
authored_severity: diagnostic.severity,
|
|
531
|
+
profile: @configuration.severity_profile,
|
|
532
|
+
overrides: @configuration.severity_overrides,
|
|
533
|
+
bleeding_edge_overrides: @configuration.bleeding_edge_severity_overrides
|
|
534
|
+
)
|
|
535
|
+
return nil if resolved == :off
|
|
536
|
+
return diagnostic if resolved == diagnostic.severity
|
|
537
|
+
|
|
538
|
+
Diagnostic.new(
|
|
539
|
+
path: diagnostic.path,
|
|
540
|
+
line: diagnostic.line,
|
|
541
|
+
column: diagnostic.column,
|
|
542
|
+
message: diagnostic.message,
|
|
543
|
+
severity: resolved,
|
|
544
|
+
rule: diagnostic.rule,
|
|
545
|
+
source_family: diagnostic.source_family
|
|
546
|
+
)
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
private
|
|
550
|
+
|
|
551
|
+
def plugin_registry
|
|
552
|
+
@plugin_registry_reader.call
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
def dependency_source_index
|
|
556
|
+
@dependency_source_index_reader.call
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
def pool_mode?
|
|
560
|
+
@pool_mode_reader.call
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
def cached_plugin_prepare_diagnostics
|
|
564
|
+
@cached_plugin_prepare_diagnostics_reader.call
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def pre_eval_diagnostics_from_scanner
|
|
568
|
+
@pre_eval_diagnostics_from_scanner_reader.call
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
def synthesized_namespaces_snapshot
|
|
572
|
+
@synthesized_namespaces_snapshot_reader.call
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
def conformance_results_snapshot
|
|
576
|
+
@conformance_results_snapshot_reader.call
|
|
577
|
+
end
|
|
578
|
+
end
|
|
579
|
+
end
|
|
580
|
+
end
|
|
581
|
+
end
|