rigortype 0.4.3 → 0.4.5
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 +5 -5
- data/data/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +104 -15
- data/docs/manual/02-cli-reference.md +43 -12
- data/docs/manual/03-configuration.md +20 -7
- data/docs/manual/04-diagnostics.md +56 -3
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +3 -2
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +40 -4
- data/docs/manual/18-removing-dead-code.md +23 -2
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-rspec.md +101 -1
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +60 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
- data/lib/rigor/analysis/check_rules.rb +274 -51
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +61 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +270 -28
- data/lib/rigor/analysis/path_expansion.rb +3 -1
- data/lib/rigor/analysis/reachability/graph.rb +111 -14
- data/lib/rigor/analysis/reachability/scan.rb +305 -23
- data/lib/rigor/analysis/rule_catalog.rb +22 -4
- data/lib/rigor/analysis/run_cache_key.rb +31 -7
- data/lib/rigor/analysis/run_cache_probe.rb +5 -3
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +48 -12
- data/lib/rigor/analysis/runner/pool_coordinator.rb +179 -31
- data/lib/rigor/analysis/runner/project_pre_passes.rb +29 -4
- data/lib/rigor/analysis/runner.rb +102 -72
- data/lib/rigor/analysis/severity_stamp.rb +45 -1
- data/lib/rigor/analysis/worker_session.rb +69 -2
- data/lib/rigor/cache/descriptor.rb +79 -12
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +39 -5
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/command.rb +3 -1
- data/lib/rigor/cli/docs_command.rb +3 -2
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +3 -2
- data/lib/rigor/cli/mcp_command.rb +1 -1
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +32 -11
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +163 -22
- data/lib/rigor/configuration/dependencies.rb +19 -11
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +29 -3
- data/lib/rigor/effects/signature_sources.rb +3 -1
- data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
- data/lib/rigor/environment/gated_signature_guard.rb +2 -1
- data/lib/rigor/environment/gated_signature_plan.rb +2 -1
- data/lib/rigor/environment/installed_gem_set.rb +2 -1
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
- data/lib/rigor/environment/rbs_loader.rb +67 -11
- data/lib/rigor/inference/block_call_timing.rb +12 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/captured_locals.rb +67 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/definer_resolution.rb +101 -25
- data/lib/rigor/inference/expression_typer.rb +94 -9
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher.rb +67 -3
- data/lib/rigor/inference/module_function_state.rb +213 -51
- data/lib/rigor/inference/narrowing.rb +235 -75
- data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
- data/lib/rigor/inference/project_patched_methods.rb +168 -4
- data/lib/rigor/inference/project_patched_mixins.rb +162 -0
- data/lib/rigor/inference/project_patched_scanner.rb +51 -12
- data/lib/rigor/inference/project_patched_visibility.rb +214 -0
- data/lib/rigor/inference/project_seed_tables.rb +120 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
- data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +414 -1
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer.rb +1061 -122
- data/lib/rigor/inference/self_call_ivar_writes.rb +346 -0
- data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
- data/lib/rigor/inference/statement_evaluator.rb +512 -150
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/lens/anchor.rb +3 -3
- data/lib/rigor/lens/contract_reader.rb +60 -2
- data/lib/rigor/lens/document.rb +4 -3
- data/lib/rigor/lens/file_reader.rb +452 -73
- data/lib/rigor/lens/members.rb +81 -17
- data/lib/rigor/lens/run.rb +73 -4
- data/lib/rigor/lens.rb +2 -1
- data/lib/rigor/mcp/server.rb +63 -22
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/project_environment.rb +6 -3
- data/lib/rigor/protection/discovery_seed.rb +9 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +27 -0
- data/lib/rigor/scope/discovery_index.rb +5 -0
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
- data/lib/rigor/scope/resolution_chain.rb +40 -5
- data/lib/rigor/scope.rb +56 -8
- data/lib/rigor/sig_gen/alias_index.rb +2 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +167 -22
- data/lib/rigor/sig_gen/layout_index.rb +40 -1
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/observation_collector.rb +137 -11
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- data/lib/rigor/sig_gen/renderer.rb +13 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- data/lib/rigor/sig_gen/write_reporter.rb +203 -0
- data/lib/rigor/sig_gen/write_result.rb +3 -1
- data/lib/rigor/sig_gen/writer.rb +50 -35
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/constant.rb +10 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +57 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
- data/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +4 -3
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +16 -1
data/lib/rigor/config_audit.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "did_you_mean"
|
|
4
4
|
|
|
5
5
|
require_relative "config_path"
|
|
6
|
+
require_relative "flow_contribution/conflict"
|
|
6
7
|
require_relative "signature_path_audit"
|
|
7
8
|
# ADR-87 WD4 — only the pure rule-id table (`ALL_RULES` / `RULE_FAMILIES`) is needed, so require the light
|
|
8
9
|
# `rule_ids.rb` rather than the engine-pulling `check_rules.rb`; keeps `config_audit` (loaded by every check)
|
|
@@ -26,11 +27,13 @@ module Rigor
|
|
|
26
27
|
#
|
|
27
28
|
# Every check is held to the same bar that {SignaturePathAudit} set: it mirrors the
|
|
28
29
|
# loader's own acceptance test, so a warning means the loader really did load nothing, and
|
|
29
|
-
# it never fires on a setup that works.
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
# a
|
|
30
|
+
# it never fires on a setup that works. The rule-token checks lean on the reach of `disable:`
|
|
31
|
+
# and `severity_overrides:` being enumerable (issues #1953, #1985): a plugin's row answers only
|
|
32
|
+
# to its qualified `plugin.<id>.<rule>` id (or `plugin.<id>`), so a bare plugin rule there is
|
|
33
|
+
# inert (see {.disable_token_warning}, {.override_key_warning}). A `plugin.`-qualified token is
|
|
34
|
+
# checked only against `plugins:`, and a token under a non-check engine family
|
|
35
|
+
# (`rbs_extended.*`, `rbs.*`, `dynamic.*`) is left alone, since the engine emits those ids
|
|
36
|
+
# outside the catalogue.
|
|
34
37
|
module ConfigAudit
|
|
35
38
|
# One config-level finding. `kind` discriminates the source key (`:signature_path`,
|
|
36
39
|
# `:bundled_plugin_signature_path`, `:library`, `:disabled_rule`, `:severity_override`,
|
|
@@ -146,31 +149,169 @@ module Rigor
|
|
|
146
149
|
[]
|
|
147
150
|
end
|
|
148
151
|
|
|
149
|
-
# `disable:` tokens
|
|
150
|
-
#
|
|
151
|
-
# docstring).
|
|
152
|
+
# `disable:` tokens that reach no row ({.disable_token_warning}) and `severity_overrides:` keys
|
|
153
|
+
# that reach none ({.override_key_warning}).
|
|
152
154
|
def self.rule_token_warnings(configuration)
|
|
155
|
+
plugin_ids = configured_plugin_ids(configuration.plugins)
|
|
153
156
|
disable = Array(configuration.disabled_rules).filter_map do |token|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
Warning.new(
|
|
157
|
-
kind: :disabled_rule,
|
|
158
|
-
message: "disable: #{token.to_s.inspect} is not a recognized rule id; the suppression has no effect",
|
|
159
|
-
fields: { "token" => token.to_s }
|
|
160
|
-
)
|
|
157
|
+
disable_token_warning(token.to_s, plugin_ids)
|
|
161
158
|
end
|
|
162
159
|
overrides = configuration.severity_overrides.keys.filter_map do |key|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
Warning.new(
|
|
166
|
-
kind: :severity_override,
|
|
167
|
-
message: "severity_overrides: #{key.to_s.inspect} is not a recognized rule id; the override has no effect",
|
|
168
|
-
fields: { "token" => key.to_s }
|
|
169
|
-
)
|
|
160
|
+
override_key_warning(key.to_s, plugin_ids)
|
|
170
161
|
end
|
|
171
162
|
disable + overrides
|
|
172
163
|
end
|
|
173
164
|
|
|
165
|
+
# Issue #1985 — the plugin loader's own rows, which a `severity_overrides:` key names by their qualified
|
|
166
|
+
# id; they also answer to the bare `load-error` / `runtime-error`.
|
|
167
|
+
PLUGIN_LOADER_ROWS = %w[plugin_loader.load-error plugin_loader.runtime-error].freeze
|
|
168
|
+
private_constant :PLUGIN_LOADER_ROWS
|
|
169
|
+
|
|
170
|
+
# A warning for a `severity_overrides:` key that can reach no row, or nil. Mirrors {.disable_token_warning}
|
|
171
|
+
# (issue #1985): a plugin's row answers only to its qualified `plugin.<id>.<rule>` or `plugin.<id>`, so a
|
|
172
|
+
# `plugin.`-qualified key naming an unconfigured plugin, and any key outside the built-in vocabulary that is
|
|
173
|
+
# not a qualified plugin id (a plugin's bare rule included), is inert. A reserved run-level row is the home of
|
|
174
|
+
# this map, so naming one is never flagged; `all` is not a family wildcard here, so it is.
|
|
175
|
+
def self.override_key_warning(key, plugin_ids)
|
|
176
|
+
reason = override_key_problem(key, plugin_ids)
|
|
177
|
+
return nil if reason.nil?
|
|
178
|
+
|
|
179
|
+
Warning.new(kind: :severity_override, message: "severity_overrides: #{key.inspect} #{reason}",
|
|
180
|
+
fields: { "token" => key })
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def self.override_key_problem(key, plugin_ids)
|
|
184
|
+
return "is not a recognized rule id; the override has no effect" if key == "all"
|
|
185
|
+
return nil unless Array(Analysis::CheckRules.resolve_rule_token(key)).empty?
|
|
186
|
+
|
|
187
|
+
if key.start_with?(Analysis::CheckRules::PLUGIN_FAMILY_PREFIX)
|
|
188
|
+
return nil if plugin_ids.nil? || plausible_plugin_token?(key, plugin_ids)
|
|
189
|
+
|
|
190
|
+
"names no plugin `plugins:` configures; the override has no effect"
|
|
191
|
+
elsif key.start_with?("plugin_loader.")
|
|
192
|
+
"is not a recognized rule id; the override has no effect" unless PLUGIN_LOADER_ROWS.include?(key)
|
|
193
|
+
elsif inert_builtin_token?(key)
|
|
194
|
+
"is not a recognized rule id; the override has no effect"
|
|
195
|
+
elsif !non_check_engine_token?(key) && !severity_only_id?(key)
|
|
196
|
+
"is not a recognized rule id; the override has no effect " \
|
|
197
|
+
"(if this is a plugin's rule, write it as `plugin.<id>.<rule>`)"
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
private_class_method :override_key_problem
|
|
201
|
+
|
|
202
|
+
# Issue #1953 — the run-level rows the diagnostic policy keeps on `severity_overrides:` and the
|
|
203
|
+
# baseline. `disable:` does not reach them by any spelling, so naming one there is inert.
|
|
204
|
+
RESERVED_RUN_LEVEL_FAMILIES = %w[plugin_loader plugin_trust rbs.coverage dynamic.dependency-source].freeze
|
|
205
|
+
RESERVED_RUN_LEVEL_IDS = %w[load-error runtime-error pool-degraded].freeze
|
|
206
|
+
private_constant :RESERVED_RUN_LEVEL_FAMILIES, :RESERVED_RUN_LEVEL_IDS
|
|
207
|
+
|
|
208
|
+
# A warning for a `disable:` token that can reach no row, or nil. Since issue #1953 the list's
|
|
209
|
+
# reach is exactly enumerable for three shapes, and those are what this flags:
|
|
210
|
+
#
|
|
211
|
+
# - a token naming a reserved run-level row ({RESERVED_RUN_LEVEL_FAMILIES} /
|
|
212
|
+
# {RESERVED_RUN_LEVEL_IDS}), which the list never reaches;
|
|
213
|
+
# - a `plugin.`-qualified token naming no plugin `plugins:` configures — checked only when
|
|
214
|
+
# every entry's manifest id is known without loading it ({.configured_plugin_ids});
|
|
215
|
+
# - any other token that is not `all`, a family, a legacy alias or a catalogue / `pre-eval.*`
|
|
216
|
+
# id. A bare id never reaches a plugin's row, so a plugin's bare rule is inert here too.
|
|
217
|
+
#
|
|
218
|
+
# A token under a non-check engine family (`rbs_extended.*`, `rbs.*`, `dynamic.*`) or a known
|
|
219
|
+
# non-catalogue engine id stays unflagged: under-warning is this module's FP-safe direction.
|
|
220
|
+
def self.disable_token_warning(token, plugin_ids)
|
|
221
|
+
return nil if effective_disable_token?(token)
|
|
222
|
+
|
|
223
|
+
reason = disable_token_problem(token, plugin_ids)
|
|
224
|
+
return nil if reason.nil?
|
|
225
|
+
|
|
226
|
+
Warning.new(kind: :disabled_rule, message: "disable: #{token.inspect} #{reason}",
|
|
227
|
+
fields: { "token" => token })
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def self.disable_token_problem(token, plugin_ids)
|
|
231
|
+
if reserved_run_level_token?(token)
|
|
232
|
+
"names a run-level row, which `disable:` does not reach; use `severity_overrides:` or a baseline"
|
|
233
|
+
elsif token.start_with?(Analysis::CheckRules::PLUGIN_FAMILY_PREFIX)
|
|
234
|
+
unless plugin_ids.nil? || plausible_plugin_token?(token, plugin_ids)
|
|
235
|
+
"names no plugin `plugins:` configures; the suppression has no effect"
|
|
236
|
+
end
|
|
237
|
+
elsif inert_builtin_token?(token)
|
|
238
|
+
"is not a recognized rule id; the suppression has no effect"
|
|
239
|
+
elsif !non_check_engine_token?(token)
|
|
240
|
+
"is not a recognized rule id; the suppression has no effect " \
|
|
241
|
+
"(if this is a plugin's rule, write it as `plugin.<id>.<rule>`)"
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
private_class_method :disable_token_problem
|
|
245
|
+
|
|
246
|
+
def self.effective_disable_token?(token)
|
|
247
|
+
return true if token == "all"
|
|
248
|
+
|
|
249
|
+
!Array(Analysis::CheckRules.resolve_rule_token(token)).empty?
|
|
250
|
+
end
|
|
251
|
+
private_class_method :effective_disable_token?
|
|
252
|
+
|
|
253
|
+
def self.reserved_run_level_token?(token)
|
|
254
|
+
RESERVED_RUN_LEVEL_IDS.include?(token) ||
|
|
255
|
+
RESERVED_RUN_LEVEL_FAMILIES.any? { |family| token == family || token.start_with?("#{family}.") }
|
|
256
|
+
end
|
|
257
|
+
private_class_method :reserved_run_level_token?
|
|
258
|
+
|
|
259
|
+
# Bare ids that a `severity_overrides:` key re-stamps but a marker or `disable:` cannot suppress: the `--explain`
|
|
260
|
+
# `fallback` row and the `contribution_merge` conflict reasons (`FlowContribution::CONFLICT_VALID_REASONS`,
|
|
261
|
+
# kebab-cased). They are not in `CheckRules::NON_CHECK_DIAGNOSTIC_IDS`, which would silence the warnings for a
|
|
262
|
+
# `disable:` token or marker naming them. `spec/rigor/config_audit_spec.rb` pins this list's completeness.
|
|
263
|
+
CONFLICT_IDS = FlowContribution::CONFLICT_VALID_REASONS.map { |reason| reason.to_s.tr("_", "-").freeze }.freeze
|
|
264
|
+
SEVERITY_ONLY_IDS = (%w[fallback] + CONFLICT_IDS).freeze
|
|
265
|
+
private_constant :CONFLICT_IDS, :SEVERITY_ONLY_IDS
|
|
266
|
+
|
|
267
|
+
def self.severity_only_id?(key)
|
|
268
|
+
SEVERITY_ONLY_IDS.include?(key)
|
|
269
|
+
end
|
|
270
|
+
private_class_method :severity_only_id?
|
|
271
|
+
|
|
272
|
+
def self.non_check_engine_token?(token)
|
|
273
|
+
Analysis::CheckRules::NON_CHECK_DIAGNOSTIC_IDS.include?(token) ||
|
|
274
|
+
Analysis::CheckRules::NON_CHECK_DIAGNOSTIC_FAMILIES.include?(token.split(".").first)
|
|
275
|
+
end
|
|
276
|
+
private_class_method :non_check_engine_token?
|
|
277
|
+
|
|
278
|
+
# `plugin.<id>` or `plugin.<id>.<rule>` for a configured id. A manifest id may itself hold a
|
|
279
|
+
# dot, so the token is matched against each id rather than split.
|
|
280
|
+
def self.plausible_plugin_token?(token, plugin_ids)
|
|
281
|
+
rest = token.delete_prefix(Analysis::CheckRules::PLUGIN_FAMILY_PREFIX)
|
|
282
|
+
plugin_ids.any? { |id| rest == id || rest.start_with?("#{id}.") }
|
|
283
|
+
end
|
|
284
|
+
private_class_method :plausible_plugin_token?
|
|
285
|
+
|
|
286
|
+
# The manifest id of every `plugins:` entry, or nil when one cannot be known without loading
|
|
287
|
+
# the plugin. An explicit `id:` is the id; a bundled plugin's id is its gem name minus the
|
|
288
|
+
# `rigor-` prefix ({Plugin::FirstParty}); a third-party gem named without `id:` may register
|
|
289
|
+
# any id, so its presence turns the plugin-token check off. An `enabled: false` entry counts:
|
|
290
|
+
# the user has found the plugin, and under-warning is the FP-safe direction.
|
|
291
|
+
def self.configured_plugin_ids(plugins)
|
|
292
|
+
Array(plugins).each_with_object(Set.new) do |raw, ids|
|
|
293
|
+
entry = raw.is_a?(Hash) ? raw.to_h { |key, value| [key.to_s, value] } : { "gem" => raw.to_s }
|
|
294
|
+
if entry["id"]
|
|
295
|
+
ids << entry["id"].to_s
|
|
296
|
+
elsif bundled_plugin_gem?(entry["gem"].to_s)
|
|
297
|
+
ids << entry["gem"].to_s.delete_prefix(BUNDLED_GEM_PREFIX)
|
|
298
|
+
else
|
|
299
|
+
return nil
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
private_class_method :configured_plugin_ids
|
|
304
|
+
|
|
305
|
+
BUNDLED_GEM_PREFIX = "rigor-"
|
|
306
|
+
private_constant :BUNDLED_GEM_PREFIX
|
|
307
|
+
|
|
308
|
+
def self.bundled_plugin_gem?(gem)
|
|
309
|
+
return false unless gem.start_with?(BUNDLED_GEM_PREFIX)
|
|
310
|
+
|
|
311
|
+
File.file?(File.join(SignaturePathAudit::BUNDLED_PLUGINS_ROOT, gem, "lib", "#{gem}.rb"))
|
|
312
|
+
end
|
|
313
|
+
private_class_method :bundled_plugin_gem?
|
|
314
|
+
|
|
174
315
|
# True when `token` looks like a built-in rule id but matches none — its first segment
|
|
175
316
|
# is a built-in family yet it is neither a bare family wildcard nor a known canonical id.
|
|
176
317
|
# A token whose family is not built-in (a plugin / `rbs_extended.*` rule, or a bare
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../configuration_error"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
class Configuration
|
|
5
7
|
# Parsed `dependencies:` section of `.rigor.yml`. Per
|
|
@@ -56,7 +58,7 @@ module Rigor
|
|
|
56
58
|
# Hash with `source_inference:` and / or `budget_per_gem:` / `budget_overrun_strategy:` present.
|
|
57
59
|
def self.from_h(data)
|
|
58
60
|
return new([]) if data.nil?
|
|
59
|
-
raise
|
|
61
|
+
raise ConfigurationError, "dependencies: must be a Hash, got #{data.inspect}" unless data.is_a?(Hash)
|
|
60
62
|
|
|
61
63
|
raw_entries = coerce_source_inference(data["source_inference"])
|
|
62
64
|
entries, warnings = dedupe_entries(raw_entries)
|
|
@@ -139,7 +141,7 @@ module Rigor
|
|
|
139
141
|
return [] if value.nil? || value == false
|
|
140
142
|
|
|
141
143
|
unless value.is_a?(Array)
|
|
142
|
-
raise
|
|
144
|
+
raise ConfigurationError,
|
|
143
145
|
"dependencies.source_inference: must be a list of entries " \
|
|
144
146
|
"(or false / omitted to disable), got #{value.inspect}"
|
|
145
147
|
end
|
|
@@ -149,7 +151,7 @@ module Rigor
|
|
|
149
151
|
|
|
150
152
|
def coerce_entry(raw)
|
|
151
153
|
unless raw.is_a?(Hash)
|
|
152
|
-
raise
|
|
154
|
+
raise ConfigurationError,
|
|
153
155
|
"dependencies.source_inference[] entry must be a Hash, got #{raw.inspect}"
|
|
154
156
|
end
|
|
155
157
|
|
|
@@ -162,7 +164,7 @@ module Rigor
|
|
|
162
164
|
|
|
163
165
|
def coerce_gem(value)
|
|
164
166
|
unless value.is_a?(String) && !value.empty?
|
|
165
|
-
raise
|
|
167
|
+
raise ConfigurationError,
|
|
166
168
|
"dependencies.source_inference[].gem must be a non-empty String, got #{value.inspect}"
|
|
167
169
|
end
|
|
168
170
|
|
|
@@ -170,28 +172,34 @@ module Rigor
|
|
|
170
172
|
end
|
|
171
173
|
|
|
172
174
|
def coerce_mode(value)
|
|
173
|
-
mode = (value || "when_missing")
|
|
175
|
+
mode = config_symbol(value || "when_missing")
|
|
174
176
|
return mode if VALID_MODES.include?(mode)
|
|
175
177
|
|
|
176
|
-
raise
|
|
178
|
+
raise ConfigurationError,
|
|
177
179
|
"dependencies.source_inference[].mode must be one of " \
|
|
178
180
|
"#{VALID_MODES.inspect}, got #{value.inspect}"
|
|
179
181
|
end
|
|
180
182
|
|
|
183
|
+
# The Symbol a String / Symbol value names, or nil for any other YAML shape (a number, a list, a mapping), which
|
|
184
|
+
# the caller then reports as not one of its valid values rather than crashing on `#to_sym`.
|
|
185
|
+
def config_symbol(value)
|
|
186
|
+
value.to_sym if value.is_a?(String) || value.is_a?(Symbol)
|
|
187
|
+
end
|
|
188
|
+
|
|
181
189
|
def coerce_roots(raw)
|
|
182
190
|
roots = Array(raw.fetch("roots", DEFAULT_ROOTS)).map(&:to_s).freeze
|
|
183
191
|
return roots unless roots.empty?
|
|
184
192
|
|
|
185
|
-
raise
|
|
193
|
+
raise ConfigurationError,
|
|
186
194
|
"dependencies.source_inference[].roots must not be empty when supplied " \
|
|
187
195
|
"(omit the key to fall back to the default #{DEFAULT_ROOTS.inspect})"
|
|
188
196
|
end
|
|
189
197
|
|
|
190
198
|
def coerce_budget_overrun_strategy(value)
|
|
191
|
-
symbol = value
|
|
199
|
+
symbol = config_symbol(value)
|
|
192
200
|
return symbol if VALID_BUDGET_OVERRUN_STRATEGIES.include?(symbol)
|
|
193
201
|
|
|
194
|
-
raise
|
|
202
|
+
raise ConfigurationError,
|
|
195
203
|
"dependencies.budget_overrun_strategy must be one of " \
|
|
196
204
|
"#{VALID_BUDGET_OVERRUN_STRATEGIES.inspect}, got #{value.inspect}"
|
|
197
205
|
end
|
|
@@ -201,13 +209,13 @@ module Rigor
|
|
|
201
209
|
# MIN_BUDGET_PER_GEM .. MAX_BUDGET_PER_GEM (i.e. 0.25× – 4× of the default).
|
|
202
210
|
def coerce_budget_per_gem(value)
|
|
203
211
|
unless value.is_a?(Integer)
|
|
204
|
-
raise
|
|
212
|
+
raise ConfigurationError,
|
|
205
213
|
"dependencies.budget_per_gem must be an Integer, " \
|
|
206
214
|
"got #{value.inspect}"
|
|
207
215
|
end
|
|
208
216
|
|
|
209
217
|
unless value.between?(MIN_BUDGET_PER_GEM, MAX_BUDGET_PER_GEM)
|
|
210
|
-
raise
|
|
218
|
+
raise ConfigurationError,
|
|
211
219
|
"dependencies.budget_per_gem must be in the range " \
|
|
212
220
|
"#{MIN_BUDGET_PER_GEM}..#{MAX_BUDGET_PER_GEM}, " \
|
|
213
221
|
"got #{value.inspect}"
|
|
@@ -202,11 +202,17 @@ module Rigor
|
|
|
202
202
|
# always wins) and *above* the profile table. Exact rule ids only — the overlay never carries family
|
|
203
203
|
# wildcards. Empty while the overlay is unpopulated, so the default leaves resolution bit-for-bit
|
|
204
204
|
# unchanged.
|
|
205
|
+
# @param qualified_rule — the row's `<source_family>.<rule>` id (`plugin_loader.load-error`); equal to `rule`
|
|
206
|
+
# for a built-in row. Issue #1985 — an override key may spell it.
|
|
207
|
+
# @param plugin_family — `plugin.<id>` for a row a plugin emitted, nil for every other row. Such a row answers
|
|
208
|
+
# to an override only by its `qualified_rule` or this whole-plugin key, never by its bare `rule` or a family
|
|
209
|
+
# wildcard: plugins share bare rules (`load-error`), and a bare key would re-stamp them all.
|
|
205
210
|
# @return the resolved severity. Returns `:off` to mean "drop the diagnostic entirely".
|
|
206
|
-
def resolve(rule:, authored_severity:, profile: DEFAULT_PROFILE, overrides: {}, bleeding_edge_overrides: {}
|
|
211
|
+
def resolve(rule:, authored_severity:, profile: DEFAULT_PROFILE, overrides: {}, bleeding_edge_overrides: {},
|
|
212
|
+
qualified_rule: rule, plugin_family: nil)
|
|
207
213
|
return authored_severity if rule.nil?
|
|
208
214
|
|
|
209
|
-
override =
|
|
215
|
+
override = row_override(rule, qualified_rule, plugin_family, overrides)
|
|
210
216
|
return override.to_sym if override
|
|
211
217
|
|
|
212
218
|
bleeding = bleeding_edge_overrides[rule]
|
|
@@ -216,6 +222,18 @@ module Rigor
|
|
|
216
222
|
profile_table.fetch(rule, authored_severity)
|
|
217
223
|
end
|
|
218
224
|
|
|
225
|
+
# Issue #1985 — the `severity_overrides:` entry a row answers to, or nil. A plugin's row: its qualified id, then
|
|
226
|
+
# its `plugin.<id>`. Any other row: its qualified id (the plugin loader's
|
|
227
|
+
# `plugin_loader.load-error`), then its bare rule, then a family wildcard. The qualified key is the more specific
|
|
228
|
+
# one, so it wins over the bare key, as it does for a plugin's row.
|
|
229
|
+
def row_override(rule, qualified_rule, plugin_family, overrides)
|
|
230
|
+
return overrides[qualified_rule] || overrides[plugin_family] unless plugin_family.nil?
|
|
231
|
+
|
|
232
|
+
overrides[qualified_rule] || overrides[rule] || family_override(rule, overrides)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
private_class_method :row_override
|
|
236
|
+
|
|
219
237
|
# A family name has no `.` in it, so only a dot-free key can match one. `resolve` runs for every
|
|
220
238
|
# diagnostic in a run's stream, and most configurations carry no such key (usually no overrides at all),
|
|
221
239
|
# so the split below is skipped for them rather than allocating a name that nothing can match.
|
data/lib/rigor/configuration.rb
CHANGED
|
@@ -8,6 +8,7 @@ require_relative "configuration/dependencies"
|
|
|
8
8
|
require_relative "configuration/severity_profile"
|
|
9
9
|
require_relative "config_path"
|
|
10
10
|
require_relative "configuration_error"
|
|
11
|
+
require_relative "rooted_glob"
|
|
11
12
|
require_relative "effects/entry_points"
|
|
12
13
|
require_relative "effects/label"
|
|
13
14
|
require_relative "effects/method_key"
|
|
@@ -995,9 +996,34 @@ module Rigor
|
|
|
995
996
|
# this entry" so a templated `**` pattern in a fresh project doesn't generate an error per match-less
|
|
996
997
|
# pattern.
|
|
997
998
|
def expand_pre_eval_entries(entries)
|
|
998
|
-
entries.flat_map
|
|
999
|
-
|
|
1000
|
-
|
|
999
|
+
entries.flat_map { |entry| expand_pre_eval_entry(entry) }.uniq.freeze
|
|
1000
|
+
end
|
|
1001
|
+
|
|
1002
|
+
# #1956 — a relative entry arrives joined onto the config file's directory, so the metacharacters the
|
|
1003
|
+
# check above sees may be that directory's own (`ds [x]/`). The leading run of segments that name existing
|
|
1004
|
+
# directories is read literally and only the rest is globbed under it: a literal entry under such a
|
|
1005
|
+
# directory stays a literal path, and a pattern matches what it matches under a plain directory.
|
|
1006
|
+
def expand_pre_eval_entry(entry)
|
|
1007
|
+
return [entry] unless glob_pattern?(entry)
|
|
1008
|
+
|
|
1009
|
+
base, pattern = literal_directory_prefix(entry)
|
|
1010
|
+
return [entry] unless glob_pattern?(pattern)
|
|
1011
|
+
|
|
1012
|
+
base ? RootedGlob.glob(base, pattern) : Dir.glob(entry, sort: true)
|
|
1013
|
+
end
|
|
1014
|
+
|
|
1015
|
+
def literal_directory_prefix(entry)
|
|
1016
|
+
segments = entry.split("/")
|
|
1017
|
+
count = (1...segments.size).take_while { |n| File.directory?(join_segments(segments.take(n))) }.size
|
|
1018
|
+
return [nil, entry] if count.zero?
|
|
1019
|
+
|
|
1020
|
+
[join_segments(segments.take(count)), segments.drop(count).join("/")]
|
|
1021
|
+
end
|
|
1022
|
+
|
|
1023
|
+
# `"/a".split("/")` leads with an empty segment, which spells the filesystem root.
|
|
1024
|
+
def join_segments(segments)
|
|
1025
|
+
path = segments.join("/")
|
|
1026
|
+
path.empty? ? "/" : path
|
|
1001
1027
|
end
|
|
1002
1028
|
|
|
1003
1029
|
def glob_pattern?(path)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../rooted_glob"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
module Effects
|
|
5
7
|
# Everything an effect envelope may be written in, as `[buffer name, RBS source]` pairs
|
|
@@ -55,7 +57,7 @@ module Rigor
|
|
|
55
57
|
# @param signature_paths — the configured roots; empty / nil takes the default.
|
|
56
58
|
# @param virtual_rbs — the loader's virtual entries.
|
|
57
59
|
def collect(signature_paths:, virtual_rbs: nil)
|
|
58
|
-
files = roots(signature_paths).flat_map { |root|
|
|
60
|
+
files = roots(signature_paths).flat_map { |root| RootedGlob.glob(root, "**/*.rbs").sort }
|
|
59
61
|
sources = files.filter_map do |path|
|
|
60
62
|
[path, File.read(path)]
|
|
61
63
|
rescue StandardError
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "yaml"
|
|
4
4
|
|
|
5
5
|
require_relative "../config_path"
|
|
6
|
+
require_relative "../rooted_glob"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
class Environment
|
|
@@ -91,8 +92,8 @@ module Rigor
|
|
|
91
92
|
# all (`Bundler::Source::Path#path` is the user's own `path:` directory, used in place), so there is
|
|
92
93
|
# no bundle-relative location to walk. A `path:`-sourced gem's `sig/` is only reachable by adding it
|
|
93
94
|
# directly to the project's `signature_paths:` — which already works today and needs no discovery.
|
|
94
|
-
all = (
|
|
95
|
-
|
|
95
|
+
all = (RootedGlob.glob(resolved, "ruby/*/gems/*/sig") +
|
|
96
|
+
RootedGlob.glob(resolved, "ruby/*/bundler/gems/*/sig")).map { |d| Pathname.new(d) }
|
|
96
97
|
filtered = all.reject { |sig_dir| skip_gems.include?(gem_name_from_sig_path(sig_dir)) }
|
|
97
98
|
return filtered if locked_gems.nil? || locked_gems.empty?
|
|
98
99
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "rbs"
|
|
4
|
+
require_relative "../rooted_glob"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
class Environment
|
|
@@ -15,7 +16,7 @@ module Rigor
|
|
|
15
16
|
# @param dir — the vendored directory (Pathname).
|
|
16
17
|
# @return the absolute-less names (`"Prime::PseudoPrimeGenerator"`) of every class and module it declares.
|
|
17
18
|
def type_names(dir)
|
|
18
|
-
|
|
19
|
+
RootedGlob.glob(dir, "*.rbs").flat_map do |file|
|
|
19
20
|
_, _, decls = RBS::Parser.parse_signature(File.read(file))
|
|
20
21
|
names(decls, nil)
|
|
21
22
|
rescue RBS::BaseError, SystemCallError
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "rbs"
|
|
4
|
+
require_relative "../rooted_glob"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
class Environment
|
|
@@ -132,7 +133,7 @@ module Rigor
|
|
|
132
133
|
end
|
|
133
134
|
|
|
134
135
|
def signature_files(roots)
|
|
135
|
-
roots.flat_map { |root|
|
|
136
|
+
roots.flat_map { |root| RootedGlob.glob(root, "*.rbs").sort }
|
|
136
137
|
end
|
|
137
138
|
|
|
138
139
|
def shell_buffer
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "lockfile_resolver"
|
|
4
|
+
require_relative "../rooted_glob"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
class Environment
|
|
@@ -47,7 +48,7 @@ module Rigor
|
|
|
47
48
|
base = Pathname.new(bundle_path)
|
|
48
49
|
return {} unless base.directory?
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
RootedGlob.glob(base, "ruby/*/gems/*").each_with_object({}) do |dir, acc|
|
|
51
52
|
name, version, platform = split_install_dirname(File.basename(dir))
|
|
52
53
|
next unless name
|
|
53
54
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
|
+
require_relative "../rooted_glob"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
class Environment
|
|
@@ -71,7 +72,7 @@ module Rigor
|
|
|
71
72
|
base = Pathname.new(bundle_path)
|
|
72
73
|
return {} unless base.directory?
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
RootedGlob.glob(base, "ruby/*/gems/*").each_with_object({}) do |dir, acc|
|
|
75
76
|
acc[File.basename(dir)] ||= dir
|
|
76
77
|
end
|
|
77
78
|
end
|
|
@@ -114,7 +115,7 @@ module Rigor
|
|
|
114
115
|
base = Pathname.new(bundle_path)
|
|
115
116
|
return {} unless base.directory?
|
|
116
117
|
|
|
117
|
-
|
|
118
|
+
RootedGlob.glob(base, "ruby/*/bundler/gems/*").each_with_object({}) do |dir, acc|
|
|
118
119
|
name = gemspec_gem_name(dir)
|
|
119
120
|
acc[name] ||= dir if name
|
|
120
121
|
end
|
|
@@ -124,7 +125,7 @@ module Rigor
|
|
|
124
125
|
# exactly one. Bundler requires a git-sourced gem to ship one, so "not exactly one" means this is not
|
|
125
126
|
# the shape we think it is — and a guess from the directory basename is what this exists to avoid.
|
|
126
127
|
def gemspec_gem_name(dir)
|
|
127
|
-
specs =
|
|
128
|
+
specs = RootedGlob.glob(dir, "*.gemspec")
|
|
128
129
|
return nil unless specs.size == 1
|
|
129
130
|
|
|
130
131
|
File.basename(specs.first, ".gemspec")
|
|
@@ -10,6 +10,7 @@ require_relative "member_consistency"
|
|
|
10
10
|
require_relative "required_features"
|
|
11
11
|
require_relative "gated_signature_guard"
|
|
12
12
|
require_relative "gated_signature_plan"
|
|
13
|
+
require_relative "../rooted_glob"
|
|
13
14
|
|
|
14
15
|
module Rigor
|
|
15
16
|
class Environment
|
|
@@ -66,6 +67,15 @@ module Rigor
|
|
|
66
67
|
QUARANTINE_WARN_LIMIT = 10
|
|
67
68
|
|
|
68
69
|
class << self
|
|
70
|
+
# Issue #1902 — the fixed total order that picks ONE detail tuple for a class out of several: its two
|
|
71
|
+
# sides in {#report_definition_build_failure}, or a fresh and a replayed record in
|
|
72
|
+
# {Analysis::Runner::PoolCoordinator}. Content-only (the tuple carries no side), so both places choose
|
|
73
|
+
# the same tuple from the same candidates, whatever order those candidates arrived in.
|
|
74
|
+
def definition_build_detail_order(detail)
|
|
75
|
+
_, error_class, member, buffers, unresolved = detail
|
|
76
|
+
[error_class.to_s, member.to_s, Array(buffers).map(&:to_s), Array(unresolved).map(&:to_s)]
|
|
77
|
+
end
|
|
78
|
+
|
|
69
79
|
def default
|
|
70
80
|
@default ||= new.freeze
|
|
71
81
|
end
|
|
@@ -540,7 +550,7 @@ module Rigor
|
|
|
540
550
|
path = Pathname(path) unless path.is_a?(Pathname)
|
|
541
551
|
next [] unless path.directory?
|
|
542
552
|
|
|
543
|
-
|
|
553
|
+
RootedGlob.glob(path, "**/*.rbs").map { |p| File.expand_path(p) }
|
|
544
554
|
end.to_set
|
|
545
555
|
end
|
|
546
556
|
|
|
@@ -1907,6 +1917,25 @@ module Rigor
|
|
|
1907
1917
|
(@state[:definition_build_failures] || []).dup.freeze
|
|
1908
1918
|
end
|
|
1909
1919
|
|
|
1920
|
+
# Issue #1929 — re-ask for `class_name`'s definition on behalf of a PREVIOUS run's analysis, which
|
|
1921
|
+
# demanded it and found it failing. An `--incremental` recheck replays that run's failures, and one
|
|
1922
|
+
# whose conflicting buffer is a file the recheck re-synthesized cannot be replayed as it stands (the
|
|
1923
|
+
# edit may have fixed it), while the file that demanded the class may sit outside the recheck's
|
|
1924
|
+
# closure and never ask again. Re-asking here re-derives the answer over the current signatures: a
|
|
1925
|
+
# class that still fails is reported with its current tuple, one that now builds is not.
|
|
1926
|
+
#
|
|
1927
|
+
# A real demand, not {#during_internal_demand}: it stands in for the analysis's own, which is what
|
|
1928
|
+
# #696 binds the reported set to. Both sides are asked, since the replayed tuple does not record which
|
|
1929
|
+
# side the analysis demanded ({#report_definition_build_failure} builds both anyway).
|
|
1930
|
+
#
|
|
1931
|
+
# @return the class's current tuple, or nil when it now builds.
|
|
1932
|
+
def redemand_definition(class_name)
|
|
1933
|
+
key = class_name.to_s.delete_prefix("::")
|
|
1934
|
+
instance_definition(key)
|
|
1935
|
+
singleton_definition(key)
|
|
1936
|
+
definition_build_failures.find { |failure| failure.first == key }
|
|
1937
|
+
end
|
|
1938
|
+
|
|
1910
1939
|
# Virtual (inline-synthesized) contributions dropped by the collision quarantine
|
|
1911
1940
|
# ({.resolve_quarantining_virtual_collisions}): buffer names absent from the built env even though the
|
|
1912
1941
|
# entry's content is non-empty and parses (a parse failure is the synthesizer's own WD6 skip, reported
|
|
@@ -2471,6 +2500,18 @@ module Rigor
|
|
|
2471
2500
|
# fail-soft: a broken RBS environment yields no entries.
|
|
2472
2501
|
end
|
|
2473
2502
|
|
|
2503
|
+
# ADR-113 — the RBS declaration of the constant `name` (qualified) as `[type, buffer name]`, or nil when no
|
|
2504
|
+
# signature declares it. `rigor lens` reports the declared type with the provenance the buffer names.
|
|
2505
|
+
def constant_declaration(name)
|
|
2506
|
+
rbs_name = parse_type_name(name)
|
|
2507
|
+
entry = rbs_name && env&.constant_decls&.[](rbs_name)
|
|
2508
|
+
return nil unless entry
|
|
2509
|
+
|
|
2510
|
+
[entry.decl.type.to_s, entry.decl.location&.buffer&.name.to_s]
|
|
2511
|
+
rescue ::RBS::BaseError
|
|
2512
|
+
nil
|
|
2513
|
+
end
|
|
2514
|
+
|
|
2474
2515
|
# Slice A constant-value lookup. Returns the translated `Rigor::Type` for a non-class constant
|
|
2475
2516
|
# declaration (`BUCKETS: Array[Symbol]`, `DEFAULT_PATH: String`, ...) or `nil` when no constant entry
|
|
2476
2517
|
# exists for `name` in the loaded RBS environment. Callers MUST treat the return value as authoritative
|
|
@@ -2904,15 +2945,16 @@ module Rigor
|
|
|
2904
2945
|
#
|
|
2905
2946
|
# So the rescue only remembers, keyed by the `::`-stripped name (the eager walk spells it `::Acme`,
|
|
2906
2947
|
# a dispatch site spells it `Acme`, and they are one class — the {#synthesized_namespaces}
|
|
2907
|
-
# convention), and {#report_definition_build_failure} decides.
|
|
2908
|
-
#
|
|
2948
|
+
# convention), and {#report_definition_build_failure} decides. Every side's detail is kept, not the
|
|
2949
|
+
# first one written (issue #1902): the instance and singleton sides can fail for DIFFERENT reasons
|
|
2950
|
+
# (a duplicated `def self.s` and a duplicated `def i` on one class), and which side a run demands first
|
|
2951
|
+
# depends on which files it analysed, so first-writer-wins made the reported tuple — and with it the
|
|
2952
|
+
# message's "First failure", advice and file list — differ between a cold run and a warm recheck.
|
|
2909
2953
|
def store_definition_build_detail(class_name, error)
|
|
2910
2954
|
key = class_name.to_s.delete_prefix("::")
|
|
2911
|
-
details = (@state[:definition_build_details] ||= {})
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
details[key] = [key, error.class.name.to_s, definition_build_member(error),
|
|
2915
|
-
definition_build_conflict_buffers(error), unresolved_type_name_detail(error)].freeze
|
|
2955
|
+
details = ((@state[:definition_build_details] ||= {})[key] ||= [])
|
|
2956
|
+
details << [key, error.class.name.to_s, definition_build_member(error),
|
|
2957
|
+
definition_build_conflict_buffers(error), unresolved_type_name_detail(error)].freeze
|
|
2916
2958
|
end
|
|
2917
2959
|
|
|
2918
2960
|
# Issue #997 — the `RBS::NoTypeFoundError` twin of {#definition_build_member}: `nil` for every other
|
|
@@ -2987,14 +3029,28 @@ module Rigor
|
|
|
2987
3029
|
return if details.nil?
|
|
2988
3030
|
|
|
2989
3031
|
key = class_name.to_s.delete_prefix("::")
|
|
2990
|
-
|
|
2991
|
-
return if detail.nil?
|
|
3032
|
+
return if details[key].nil?
|
|
2992
3033
|
|
|
2993
3034
|
reported = (@state[:definition_build_reported] ||= {})
|
|
2994
3035
|
return if reported[key]
|
|
2995
3036
|
|
|
2996
3037
|
reported[key] = true
|
|
2997
|
-
(
|
|
3038
|
+
attempt_both_definition_sides(class_name)
|
|
3039
|
+
canonical = details[key].min_by { |candidate| self.class.definition_build_detail_order(candidate) }
|
|
3040
|
+
(@state[:definition_build_failures] ||= []) << canonical
|
|
3041
|
+
end
|
|
3042
|
+
|
|
3043
|
+
# Issue #1902 — a reported class's tuple is chosen from BOTH sides' failures, so the side this demand
|
|
3044
|
+
# did not ask for is built here too. Without it, the candidate set would be whatever sides the run
|
|
3045
|
+
# happened to demand, and a recheck whose closure touches only `Berry.s` would report the singleton
|
|
3046
|
+
# side's reason where the full run, which also demanded `Berry.new.i`, reported the instance side's.
|
|
3047
|
+
# An internal demand: it decides WHICH reason is shown for a class the analysis already demanded, never
|
|
3048
|
+
# WHETHER a class is reported, so the set #696 binds to the analysis's own demand is unchanged.
|
|
3049
|
+
def attempt_both_definition_sides(class_name)
|
|
3050
|
+
during_internal_demand do
|
|
3051
|
+
instance_definition(class_name)
|
|
3052
|
+
singleton_definition(class_name)
|
|
3053
|
+
end
|
|
2998
3054
|
end
|
|
2999
3055
|
|
|
3000
3056
|
# Marks a definition demand that is RIGOR'S OWN, not the analysed program's, so
|