rigortype 0.4.4 → 0.4.6
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/docs/handbook/11-sig-gen.md +100 -18
- data/docs/manual/02-cli-reference.md +85 -16
- data/docs/manual/03-configuration.md +12 -9
- data/docs/manual/04-diagnostics.md +17 -6
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +2 -1
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +17 -2
- data/docs/manual/12-caching.md +8 -1
- data/docs/manual/18-removing-dead-code.md +18 -7
- data/docs/manual/plugins/rigor-actionmailer.md +30 -0
- data/docs/manual/plugins/rigor-actionpack.md +30 -0
- data/docs/manual/plugins/rigor-devise.md +15 -0
- data/docs/manual/plugins/rigor-rspec.md +102 -1
- data/lib/rigor/analysis/buffer_binding.rb +14 -0
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +115 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +253 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +64 -4
- data/lib/rigor/analysis/check_rules.rb +319 -21
- data/lib/rigor/analysis/dependency_recorder.rb +11 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/edit_replay.rb +67 -0
- data/lib/rigor/analysis/incremental.rb +86 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +336 -46
- data/lib/rigor/analysis/path_expansion.rb +44 -1
- data/lib/rigor/analysis/reachability/graph.rb +90 -13
- data/lib/rigor/analysis/reachability/scan.rb +279 -39
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +22 -5
- data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
- data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
- data/lib/rigor/analysis/runner/pool_coordinator.rb +82 -16
- data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
- data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
- data/lib/rigor/analysis/runner.rb +222 -111
- data/lib/rigor/analysis/severity_stamp.rb +11 -0
- data/lib/rigor/analysis/worker_session.rb +24 -1
- data/lib/rigor/cache/descriptor.rb +40 -7
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +49 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/check_command.rb +93 -4
- data/lib/rigor/cli/command.rb +3 -1
- data/lib/rigor/cli/diff_command.rb +2 -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/lens_command.rb +5 -5
- data/lib/rigor/cli/mcp_command.rb +2 -1
- data/lib/rigor/cli/options.rb +20 -3
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +7 -1
- data/lib/rigor/cli/plugins_renderer.rb +24 -2
- data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +73 -13
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/type_of_project_scope.rb +27 -0
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +72 -16
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +43 -7
- 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 +136 -7
- data/lib/rigor/environment/required_features.rb +136 -29
- data/lib/rigor/environment/ruby_library_roots.rb +122 -0
- data/lib/rigor/environment.rb +4 -2
- data/lib/rigor/inference/block_call_timing.rb +7 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
- data/lib/rigor/inference/captured_locals.rb +44 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/deep_erasure.rb +138 -0
- data/lib/rigor/inference/def_return_typer.rb +159 -9
- data/lib/rigor/inference/definer_resolution.rb +101 -20
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +97 -18
- data/lib/rigor/inference/global_write_census.rb +36 -1
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/ivar_writer_census.rb +195 -0
- data/lib/rigor/inference/loop_value_record.rb +53 -0
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +278 -42
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
- data/lib/rigor/inference/method_dispatcher.rb +113 -19
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +149 -42
- data/lib/rigor/inference/mutation_widening.rb +5 -2
- data/lib/rigor/inference/narrowing.rb +116 -11
- data/lib/rigor/inference/object_mixins.rb +13 -1
- 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 +121 -0
- data/lib/rigor/inference/project_yielder.rb +128 -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 +6 -6
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
- data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
- data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
- data/lib/rigor/inference/scope_indexer.rb +1144 -197
- data/lib/rigor/inference/self_call_ivar_writes.rb +29 -12
- 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 +1088 -213
- data/lib/rigor/inference/synthetic_method_index.rb +23 -0
- data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/language_server/buffer_table.rb +6 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
- data/lib/rigor/language_server/project_context.rb +59 -3
- data/lib/rigor/language_server/server.rb +5 -3
- data/lib/rigor/lens/contract_reader.rb +45 -2
- data/lib/rigor/lens/document.rb +2 -1
- data/lib/rigor/lens/file_reader.rb +12 -2
- data/lib/rigor/lens/members.rb +16 -2
- data/lib/rigor/lens/run.rb +34 -7
- data/lib/rigor/mcp/server.rb +11 -3
- data/lib/rigor/plugin/base.rb +29 -13
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/plugin/manifest.rb +29 -3
- data/lib/rigor/plugin/registry.rb +38 -0
- data/lib/rigor/protection/discovery_seed.rb +64 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +36 -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 +59 -7
- 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 +465 -49
- data/lib/rigor/sig_gen/layout_index.rb +5 -2
- data/lib/rigor/sig_gen/observation_collector.rb +141 -12
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +16 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- 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 +5 -3
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/combinator.rb +4 -3
- data/lib/rigor/type/constant.rb +54 -3
- data/lib/rigor/type/hash_shape.rb +14 -9
- data/lib/rigor/type/struct_instance.rb +3 -2
- 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 +61 -44
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -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-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
- 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-activejob/lib/rigor/plugin/activejob.rb +4 -2
- 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-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -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 +102 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
- 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/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +3 -2
- data/sig/rigor/effects/file_collection.rbs +5 -6
- data/sig/rigor/effects/registry.rbs +8 -6
- data/sig/rigor/effects/summary.rbs +6 -5
- data/sig/rigor/environment.rbs +5 -3
- data/sig/rigor/inference/optimistic_origin.rbs +11 -0
- data/sig/rigor/inference.rbs +6 -0
- data/sig/rigor/plugin/additional_initializer.rbs +5 -5
- data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
- data/sig/rigor/plugin/effect_attribution.rbs +25 -10
- data/sig/rigor/plugin/effect_edge.rbs +12 -5
- data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
- data/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/plugin/manifest.rbs +16 -18
- data/sig/rigor/plugin/protocol_contract.rbs +10 -9
- data/sig/rigor/plugin/registry.rbs +1 -1
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +34 -52
- data/sig/rigor/type.rbs +0 -6
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +32 -1
data/lib/rigor/cli/options.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../analysis/buffer_binding"
|
|
4
|
+
require_relative "../configuration_error"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
class CLI
|
|
@@ -10,15 +11,31 @@ module Rigor
|
|
|
10
11
|
# `--instead-of` flag pair and the buffer-binding resolution that validates it. That resolution used to be copied
|
|
11
12
|
# verbatim into both `Rigor::CLI` and `TypeOfCommand`, so a fix to one (the paired-flag check, the readability
|
|
12
13
|
# check, the error wording) could silently miss the other. Centralising it keeps the two editor-mode entry points in
|
|
13
|
-
# lockstep.
|
|
14
|
+
# lockstep. It also owns the `--config` check (#1992), so every command and the MCP tools that pass `--config`
|
|
15
|
+
# read the same rule.
|
|
14
16
|
module Options
|
|
15
17
|
module_function
|
|
16
18
|
|
|
17
19
|
# Defines the standard `--config=PATH` flag on `parser`, writing the path into `options[:config]`. Used by every
|
|
18
20
|
# subcommand that loads a `.rigor.yml`; the few whose `--config` help text is intentionally bespoke (`diff`,
|
|
19
|
-
# `mcp`, `show-bleedingedge`) keep their own `parser.on`
|
|
21
|
+
# `mcp`, `show-bleedingedge`) keep their own `parser.on` and call {.config_path} for the same check.
|
|
20
22
|
def add_config(parser, options)
|
|
21
|
-
parser.on("--config=PATH", "Path to the Rigor configuration file")
|
|
23
|
+
parser.on("--config=PATH", "Path to the Rigor configuration file") do |value|
|
|
24
|
+
options[:config] = config_path(value)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The `--config` value, checked where the flag is read (#1992). A named file that is not there is an error, not
|
|
29
|
+
# a request for the defaults: the run would otherwise analyse under a configuration the caller did not ask for,
|
|
30
|
+
# and an MCP caller would get a result for it with no sign anything was missing. Auto-discovery, with no
|
|
31
|
+
# `--config` given, is not affected.
|
|
32
|
+
def config_path(value)
|
|
33
|
+
raise ConfigurationError, "empty config path" if value.strip.empty?
|
|
34
|
+
raise ConfigurationError, "config path is a directory: #{value}" if File.directory?(value)
|
|
35
|
+
raise ConfigurationError, "config file not found: #{value}" unless File.exist?(value)
|
|
36
|
+
raise ConfigurationError, "config file not readable: #{value}" unless File.readable?(value)
|
|
37
|
+
|
|
38
|
+
value
|
|
22
39
|
end
|
|
23
40
|
|
|
24
41
|
# Defines the `--tmp-file` / `--instead-of` editor-mode flag pair on `parser`, writing into `options`.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "command"
|
|
4
|
+
require_relative "../rooted_glob"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
class CLI
|
|
@@ -204,7 +205,7 @@ module Rigor
|
|
|
204
205
|
candidate = File.join(dir, "lib", "#{plugin.fetch(:name)}.rb")
|
|
205
206
|
return candidate if File.file?(candidate)
|
|
206
207
|
|
|
207
|
-
|
|
208
|
+
RootedGlob.glob(dir, "lib/*.rb").min
|
|
208
209
|
end
|
|
209
210
|
|
|
210
211
|
def docker_note
|
|
@@ -13,6 +13,7 @@ require_relative "../reflection"
|
|
|
13
13
|
require_relative "../type/combinator"
|
|
14
14
|
require_relative "plugins_renderer"
|
|
15
15
|
require_relative "command"
|
|
16
|
+
require_relative "../rooted_glob"
|
|
16
17
|
|
|
17
18
|
module Rigor
|
|
18
19
|
class CLI
|
|
@@ -232,6 +233,8 @@ module Rigor
|
|
|
232
233
|
signature_paths: signature_path_rows(plugin),
|
|
233
234
|
open_receivers: manifest.open_receivers.dup,
|
|
234
235
|
owns_receivers: manifest.owns_receivers.dup,
|
|
236
|
+
per_invocation_classes: manifest.per_invocation_classes.dup,
|
|
237
|
+
per_invocation_non_action_macros: manifest.per_invocation_non_action_macros.map(&:to_s),
|
|
235
238
|
produces: manifest.produces.map(&:to_s),
|
|
236
239
|
consumes: manifest.consumes.map { |c| "#{c.plugin_id}/#{c.name}#{'?' if c.optional}" },
|
|
237
240
|
source_rbs_synthesizer: !manifest.source_rbs_synthesizer.nil?
|
|
@@ -264,6 +267,8 @@ module Rigor
|
|
|
264
267
|
signature_paths: [],
|
|
265
268
|
open_receivers: [],
|
|
266
269
|
owns_receivers: [],
|
|
270
|
+
per_invocation_classes: [],
|
|
271
|
+
per_invocation_non_action_macros: [],
|
|
267
272
|
produces: [],
|
|
268
273
|
consumes: [],
|
|
269
274
|
block_as_methods: 0,
|
|
@@ -297,7 +302,7 @@ module Rigor
|
|
|
297
302
|
def rbs_file_count(dir)
|
|
298
303
|
return 0 unless File.directory?(dir)
|
|
299
304
|
|
|
300
|
-
|
|
305
|
+
RootedGlob.glob(dir, "**/*.rbs").size
|
|
301
306
|
end
|
|
302
307
|
|
|
303
308
|
def entry_gem_name(entry)
|
|
@@ -344,6 +349,7 @@ module Rigor
|
|
|
344
349
|
config: {},
|
|
345
350
|
signature_paths: [],
|
|
346
351
|
open_receivers: [], owns_receivers: [], produces: [], consumes: [],
|
|
352
|
+
per_invocation_classes: [], per_invocation_non_action_macros: [],
|
|
347
353
|
block_as_methods: 0, heredoc_templates: 0, trait_registries: 0,
|
|
348
354
|
type_node_resolvers: 0,
|
|
349
355
|
hkt_registrations: 0, hkt_definitions: 0,
|
|
@@ -185,12 +185,26 @@ module Rigor
|
|
|
185
185
|
lines = []
|
|
186
186
|
lines << " open_receivers: #{row[:open_receivers].join(', ')}" if row[:open_receivers].any?
|
|
187
187
|
lines << " owns_receivers: #{row[:owns_receivers].join(', ')}" if row[:owns_receivers].any?
|
|
188
|
+
lines.concat(per_invocation_lines(row))
|
|
188
189
|
lines << " produces: #{row[:produces].join(', ')}" if row[:produces].any?
|
|
189
190
|
lines << " consumes: #{row[:consumes].join(', ')}" if row[:consumes].any?
|
|
190
191
|
lines.concat(narrow_protocol_lines(row))
|
|
191
192
|
lines
|
|
192
193
|
end
|
|
193
194
|
|
|
195
|
+
# ADR-125 — the per-invocation classes a plugin vouches for, and the macros whose arguments name a method that is
|
|
196
|
+
# not one of their actions.
|
|
197
|
+
def per_invocation_lines(row)
|
|
198
|
+
lines = []
|
|
199
|
+
if row[:per_invocation_classes].any?
|
|
200
|
+
lines << " per_invocation_classes: #{row[:per_invocation_classes].join(', ')}"
|
|
201
|
+
end
|
|
202
|
+
if row[:per_invocation_non_action_macros].any?
|
|
203
|
+
lines << " per_invocation_non_action_macros: #{row[:per_invocation_non_action_macros].join(', ')}"
|
|
204
|
+
end
|
|
205
|
+
lines
|
|
206
|
+
end
|
|
207
|
+
|
|
194
208
|
# ADR-37 narrow extension protocols (node_rule / dynamic_return / narrowing_facts). Surfaced in the full report
|
|
195
209
|
# alongside the declarative surfaces; `--capabilities` is the focused view.
|
|
196
210
|
def narrow_protocol_lines(row)
|
|
@@ -249,8 +263,6 @@ module Rigor
|
|
|
249
263
|
"signature_paths" => row[:signature_paths].map do |sp|
|
|
250
264
|
{ "path" => sp[:path], "exists" => sp[:exists], "rbs_files" => sp[:rbs_files] }
|
|
251
265
|
end,
|
|
252
|
-
"open_receivers" => row[:open_receivers],
|
|
253
|
-
"owns_receivers" => row[:owns_receivers],
|
|
254
266
|
"produces" => row[:produces],
|
|
255
267
|
"consumes" => row[:consumes],
|
|
256
268
|
"block_as_methods" => row[:block_as_methods],
|
|
@@ -265,6 +277,16 @@ module Rigor
|
|
|
265
277
|
"dynamic_return_receivers" => row[:dynamic_return_receivers],
|
|
266
278
|
"narrowing_facts_methods" => row[:narrowing_facts_methods],
|
|
267
279
|
"load_error" => row[:load_error]
|
|
280
|
+
}.merge(receiver_json(row))
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# ADR-125 — the per-invocation keys are additive; an empty array when the plugin declares none.
|
|
284
|
+
def receiver_json(row)
|
|
285
|
+
{
|
|
286
|
+
"open_receivers" => row[:open_receivers],
|
|
287
|
+
"owns_receivers" => row[:owns_receivers],
|
|
288
|
+
"per_invocation_classes" => row[:per_invocation_classes],
|
|
289
|
+
"per_invocation_non_action_macros" => row[:per_invocation_non_action_macros]
|
|
268
290
|
}
|
|
269
291
|
end
|
|
270
292
|
|
|
@@ -6,6 +6,7 @@ require "optionparser"
|
|
|
6
6
|
require_relative "../bleeding_edge"
|
|
7
7
|
require_relative "../configuration"
|
|
8
8
|
require_relative "command"
|
|
9
|
+
require_relative "options"
|
|
9
10
|
|
|
10
11
|
module Rigor
|
|
11
12
|
class CLI
|
|
@@ -44,7 +45,7 @@ module Rigor
|
|
|
44
45
|
options[:format] = fmt
|
|
45
46
|
end
|
|
46
47
|
opt.on("--config=PATH", "Path to a .rigor.yml (default: auto-discovery).") do |path|
|
|
47
|
-
options[:config] = path
|
|
48
|
+
options[:config] = Options.config_path(path)
|
|
48
49
|
end
|
|
49
50
|
end.parse!(@argv)
|
|
50
51
|
options
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "optionparser"
|
|
4
4
|
|
|
5
5
|
require_relative "../configuration"
|
|
6
|
+
require_relative "../analysis/path_expansion"
|
|
6
7
|
require_relative "../effects/config_envelopes"
|
|
7
8
|
require_relative "../effects/registry"
|
|
8
9
|
require_relative "options"
|
|
@@ -55,7 +56,11 @@ module Rigor
|
|
|
55
56
|
configuration = Configuration.load(options.fetch(:config))
|
|
56
57
|
paths = @argv.empty? ? configuration.paths : @argv
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
path_errors = path_diagnostics(paths, configuration)
|
|
60
|
+
report_path_errors(path_errors, options)
|
|
61
|
+
|
|
62
|
+
observation_roots = observation_roots(configuration, options)
|
|
63
|
+
observations = collect_observations(configuration, options, observation_roots, paths)
|
|
59
64
|
generator = SigGen::Generator.new(configuration: configuration, paths: paths,
|
|
60
65
|
observations: observations,
|
|
61
66
|
include_private: options.fetch(:include_private),
|
|
@@ -68,15 +73,20 @@ module Rigor
|
|
|
68
73
|
when :write then dispatch_write(candidates, configuration, options)
|
|
69
74
|
when :check then dispatch_check(candidates, configuration, options)
|
|
70
75
|
else
|
|
71
|
-
dispatch_print_or_diff(candidates, mode, options)
|
|
76
|
+
dispatch_print_or_diff(candidates, mode, options, observation_roots, path_errors)
|
|
72
77
|
0
|
|
73
78
|
end
|
|
79
|
+
status = 1 if path_errors.any?(&:error?)
|
|
80
|
+
report_run_notes(candidates, generator, options)
|
|
81
|
+
status
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def report_run_notes(candidates, generator, options)
|
|
74
85
|
report_skipped(candidates, options)
|
|
75
86
|
report_inline_declared(candidates, options)
|
|
76
87
|
report_withheld_annotations(candidates, options)
|
|
77
88
|
report_unrenderable(generator.unrenderable)
|
|
78
89
|
report_unresolvable_superclasses(generator.unresolvable_superclasses)
|
|
79
|
-
status
|
|
80
90
|
end
|
|
81
91
|
|
|
82
92
|
private
|
|
@@ -219,12 +229,38 @@ module Rigor
|
|
|
219
229
|
unresolvable.sort.each { |class_name, superclass| @err.puts(" #{class_name} < #{superclass}") }
|
|
220
230
|
end
|
|
221
231
|
|
|
222
|
-
|
|
232
|
+
# #1991 — the path errors `rigor check` reports for the arguments: a path that is neither a directory nor a
|
|
233
|
+
# `.rb` file. Warned about beside files that will be read, an error when nothing will be, as `check` grades it.
|
|
234
|
+
def path_diagnostics(paths, configuration)
|
|
235
|
+
bad = Analysis::PathExpansion.unusable(paths)
|
|
236
|
+
return [] if bad.empty?
|
|
237
|
+
|
|
238
|
+
any_files = !Analysis::PathExpansion.ruby_files(paths, configuration.exclude_patterns,
|
|
239
|
+
base: configuration.exclude_base).empty?
|
|
240
|
+
Analysis::PathExpansion.diagnostics(bad, any_files: any_files)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Without this a missing path was read as empty, so the run said "No candidates" and exited 0 (#1991). JSON
|
|
244
|
+
# print/diff carries the errors in its payload (see {#path_errors_in_payload?}); every other form says them on
|
|
245
|
+
# stderr, as `rigor check` does.
|
|
246
|
+
def report_path_errors(path_errors, options)
|
|
247
|
+
return if path_errors_in_payload?(options)
|
|
248
|
+
|
|
249
|
+
path_errors.each { |diagnostic| @err.puts(diagnostic.to_s) }
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def path_errors_in_payload?(options)
|
|
253
|
+
options.fetch(:format) == "json" && %w[print diff].include?(options.fetch(:mode))
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def dispatch_print_or_diff(candidates, mode, options, observation_roots, path_errors)
|
|
223
257
|
SigGen::Renderer.new(out: @out).render(
|
|
224
258
|
candidates: candidates,
|
|
225
259
|
mode: mode,
|
|
226
260
|
format: options.fetch(:format),
|
|
227
|
-
selection: options.fetch(:selection)
|
|
261
|
+
selection: options.fetch(:selection),
|
|
262
|
+
observation: observation_report(observation_roots, options),
|
|
263
|
+
diagnostics: path_errors_in_payload?(options) ? path_errors : []
|
|
228
264
|
)
|
|
229
265
|
end
|
|
230
266
|
|
|
@@ -289,28 +325,52 @@ module Rigor
|
|
|
289
325
|
# specified, observe the project's test roots: `test_paths:`, or whichever of `spec/` and `test/` exist. With no
|
|
290
326
|
# root at all the run still succeeds, but says so — otherwise every parameter stays `untyped` with nothing
|
|
291
327
|
# naming the reason.
|
|
292
|
-
|
|
328
|
+
#
|
|
329
|
+
# The collector pre-indexes the classes of the paths the run generates for as well as the configured ones, so
|
|
330
|
+
# a call site on a class named only on the command line (`sig-gen ../lib` from a subdirectory, where no
|
|
331
|
+
# config file is discovered and the default `paths:` resolve under the subdirectory) attaches (#2078).
|
|
332
|
+
def collect_observations(configuration, options, observe_paths, paths)
|
|
293
333
|
return {} if options.fetch(:params) != "observed"
|
|
294
334
|
|
|
295
|
-
observe_paths = options.fetch(:observe)
|
|
296
|
-
observe_paths = configuration.resolved_test_paths if observe_paths.empty?
|
|
297
335
|
if observe_paths.empty?
|
|
298
336
|
warn_no_test_roots(configuration)
|
|
299
337
|
else
|
|
300
338
|
warn_missing_test_roots(observe_paths)
|
|
301
339
|
end
|
|
302
|
-
SigGen::ObservationCollector.new(configuration: configuration, paths: observe_paths
|
|
340
|
+
SigGen::ObservationCollector.new(configuration: configuration, paths: observe_paths,
|
|
341
|
+
source_paths: (configuration.paths + paths).uniq).collect
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# The roots `--params=observed` reads: `--observe=PATH...` when given, else the project's test roots.
|
|
345
|
+
def observation_roots(configuration, options)
|
|
346
|
+
return [] if options.fetch(:params) != "observed"
|
|
347
|
+
|
|
348
|
+
observe_paths = options.fetch(:observe)
|
|
349
|
+
observe_paths.empty? ? configuration.resolved_test_paths : observe_paths
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# #1398 — the same fact the stderr notes carry, as data for `--format=json` (and the MCP tool, whose
|
|
353
|
+
# stderr nobody reads). Absent under `--params=untyped`, where no root is read. Paths are spelled as
|
|
354
|
+
# {#observation_roots} yields them: `--observe=PATH` as typed, `test_paths:` as the configuration
|
|
355
|
+
# resolved them, an auto-detected `spec/` / `test/` absolute.
|
|
356
|
+
def observation_report(observe_paths, options)
|
|
357
|
+
return nil if options.fetch(:params) != "observed"
|
|
358
|
+
|
|
359
|
+
present, missing = observe_paths.uniq.partition { |path| SigGen::ObservationCollector.readable_root?(path) }
|
|
360
|
+
{ paths: present, missing: missing }
|
|
303
361
|
end
|
|
304
362
|
|
|
305
|
-
# A declared root that does not exist is read as empty, so name it:
|
|
306
|
-
# the test roots, and without this the run looks exactly like one
|
|
363
|
+
# A declared root that does not exist (or is not a directory or `.rb` file) is read as empty, so name it:
|
|
364
|
+
# `sig-gen` is the only command that reads the test roots, and without this the run looks exactly like one
|
|
365
|
+
# whose tests pass no typed arguments.
|
|
307
366
|
def warn_missing_test_roots(observe_paths)
|
|
308
|
-
missing = observe_paths.reject { |path|
|
|
367
|
+
missing = observe_paths.reject { |path| SigGen::ObservationCollector.readable_root?(path) }
|
|
309
368
|
return if missing.empty?
|
|
310
369
|
|
|
311
370
|
consequence = missing.size == observe_paths.size ? "; every parameter stays untyped" : ""
|
|
312
371
|
@err.puts("rigor sig-gen: no call sites are observed from #{missing.map(&:inspect).join(', ')}, " \
|
|
313
|
-
"which does not exist#{consequence}.
|
|
372
|
+
"which does not exist or is not a directory or `.rb` file#{consequence}. " \
|
|
373
|
+
"Check `test_paths:` or --observe=PATH.")
|
|
314
374
|
end
|
|
315
375
|
|
|
316
376
|
def warn_no_test_roots(configuration)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "command"
|
|
4
|
+
require_relative "../rooted_glob"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
class CLI
|
|
@@ -227,7 +228,7 @@ module Rigor
|
|
|
227
228
|
return [] unless File.directory?(dir)
|
|
228
229
|
|
|
229
230
|
# `Dir.glob` returns lexicographically sorted paths (Ruby 3.0+), so the `NN-` prefixes already drive read order.
|
|
230
|
-
|
|
231
|
+
RootedGlob.glob(dir, "*.md")
|
|
231
232
|
end
|
|
232
233
|
|
|
233
234
|
def name_error(name)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "yaml"
|
|
4
4
|
|
|
5
5
|
require_relative "../plugin_gap_advisory"
|
|
6
|
+
require_relative "../rooted_glob"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
class CLI
|
|
@@ -78,7 +79,7 @@ module Rigor
|
|
|
78
79
|
vscode = File.join(@root, ".vscode")
|
|
79
80
|
return :none unless File.directory?(vscode)
|
|
80
81
|
|
|
81
|
-
files =
|
|
82
|
+
files = RootedGlob.glob(vscode, "*.json")
|
|
82
83
|
return :unwired if files.empty?
|
|
83
84
|
|
|
84
85
|
files.any? { |path| file_mentions_rigor?(path) } ? :wired : :unwired
|
|
@@ -103,7 +104,7 @@ module Rigor
|
|
|
103
104
|
end
|
|
104
105
|
|
|
105
106
|
def ci_config_files
|
|
106
|
-
files =
|
|
107
|
+
files = RootedGlob.glob(@root, ".github/workflows/*.{yml,yaml}")
|
|
107
108
|
gitlab = File.join(@root, ".gitlab-ci.yml")
|
|
108
109
|
files << gitlab if File.file?(gitlab)
|
|
109
110
|
files
|
|
@@ -17,6 +17,7 @@ require_relative "type_of_template_probe"
|
|
|
17
17
|
require_relative "command"
|
|
18
18
|
require_relative "options"
|
|
19
19
|
require_relative "../project_environment"
|
|
20
|
+
require_relative "type_of_project_scope"
|
|
20
21
|
|
|
21
22
|
module Rigor
|
|
22
23
|
class CLI
|
|
@@ -88,7 +89,7 @@ module Rigor
|
|
|
88
89
|
def execute(targets:, options:, buffer: nil)
|
|
89
90
|
configuration = Configuration.load(options.fetch(:config))
|
|
90
91
|
environment = project_environment(targets.map(&:file).uniq, configuration, buffer)
|
|
91
|
-
base_scope =
|
|
92
|
+
base_scope = TypeOfProjectScope.build(environment: environment, configuration: configuration, buffer: buffer)
|
|
92
93
|
|
|
93
94
|
results_by_target = Array.new(targets.length)
|
|
94
95
|
targets.each_with_index.group_by { |target, _index| target.file }.each do |file, indexed_targets|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../scope"
|
|
4
|
+
require_relative "../protection/discovery_seed"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
class CLI
|
|
8
|
+
# Issue #512 — the scope `rigor type-of` probes with: single-file, plus the cross-file tables a prior
|
|
9
|
+
# `rigor check --incremental` left in the snapshot ({Protection::DiscoverySeed.from_snapshot}) when there are
|
|
10
|
+
# any. A probe never re-parses the project for its seed, so without a reusable snapshot this is the bare
|
|
11
|
+
# `Scope.empty` it always was.
|
|
12
|
+
#
|
|
13
|
+
# The seed is the full {Inference::ProjectSeedTables} fold, so a `contested_*` sibling travels with the member
|
|
14
|
+
# it qualifies (ADR-119): this is a recorded whole-index consumer in `admission_census.yml`.
|
|
15
|
+
module TypeOfProjectScope
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def build(environment:, configuration:, buffer: nil)
|
|
19
|
+
base = Scope.empty(environment: environment)
|
|
20
|
+
seed = Protection::DiscoverySeed.from_snapshot(configuration: configuration, buffer: buffer)
|
|
21
|
+
return base if seed.nil? || seed.empty?
|
|
22
|
+
|
|
23
|
+
base.with_discovery(base.discovery.with(**seed))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -15,6 +15,7 @@ require_relative "../cache/store"
|
|
|
15
15
|
require_relative "options"
|
|
16
16
|
require_relative "command"
|
|
17
17
|
require_relative "../project_environment"
|
|
18
|
+
require_relative "../rooted_glob"
|
|
18
19
|
|
|
19
20
|
module Rigor
|
|
20
21
|
class CLI
|
|
@@ -51,7 +52,7 @@ module Rigor
|
|
|
51
52
|
paths = @argv.empty? ? configuration.paths : @argv
|
|
52
53
|
establish_root(configuration)
|
|
53
54
|
scan_cache = open_scan_cache(configuration)
|
|
54
|
-
declarations, references, dynamic_uses, shadows = scan(paths, configuration, scan_cache)
|
|
55
|
+
declarations, references, dynamic_uses, shadows, ancestries = scan(paths, configuration, scan_cache)
|
|
55
56
|
references.concat(signature_references(configuration))
|
|
56
57
|
dynamic_uses.concat(template_mentions(declarations, scan_cache))
|
|
57
58
|
|
|
@@ -60,7 +61,7 @@ module Rigor
|
|
|
60
61
|
references.concat(plugin_references(contribution.references))
|
|
61
62
|
graph = build_graph(configuration, options, contribution,
|
|
62
63
|
declarations: declarations, references: references, dynamic_uses: dynamic_uses,
|
|
63
|
-
shadows: shadows)
|
|
64
|
+
shadows: shadows, ancestries: ancestries)
|
|
64
65
|
emit(graph.report, options, supply: root_supply(contribution.roots, declarations))
|
|
65
66
|
scan_cache.save
|
|
66
67
|
0
|
|
@@ -109,9 +110,9 @@ module Rigor
|
|
|
109
110
|
cwd == @root || inside?(cwd, @root) ? [@root] : [@root, cwd]
|
|
110
111
|
end
|
|
111
112
|
|
|
112
|
-
def build_graph(configuration, options, contribution, declarations:,
|
|
113
|
+
def build_graph(configuration, options, contribution, declarations:, **scanned)
|
|
113
114
|
Analysis::Reachability::Graph.new(
|
|
114
|
-
declarations: declarations,
|
|
115
|
+
declarations: declarations, **scanned,
|
|
115
116
|
root_fqns: root_fqns(declarations, options.fetch(:entry_points)) + contribution.roots,
|
|
116
117
|
foreign: foreign_predicate(configuration)
|
|
117
118
|
)
|
|
@@ -160,16 +161,18 @@ module Rigor
|
|
|
160
161
|
# is the one the report prints; any other spelling's declarations are shadows, as a wider-corpus
|
|
161
162
|
# file's are (#1732).
|
|
162
163
|
def scan(paths, configuration, cache)
|
|
163
|
-
out = { declarations: [], references: [], dynamic_uses: [], shadows: [] }
|
|
164
|
+
out = { declarations: [], references: [], dynamic_uses: [], shadows: [], ancestries: [] }
|
|
164
165
|
file_spellings(paths, configuration).sort.each do |real, list|
|
|
165
166
|
primary, analysed = list.find { |_, declared| declared } || list.first
|
|
166
167
|
result = cache.serve(:scan, primary) { read_and_scan(primary, real, configuration) }
|
|
167
168
|
next if result.nil?
|
|
168
169
|
|
|
169
170
|
out[analysed ? :declarations : :shadows].concat(result.declarations)
|
|
171
|
+
# An ancestry edge is a fact about the program, not about the file's role, so one spelling carries it.
|
|
172
|
+
out[:ancestries].concat(result.ancestries)
|
|
170
173
|
list.map(&:first).each { |file| add_spelling(out, result, file, real, primary) }
|
|
171
174
|
end
|
|
172
|
-
out.values_at(:declarations, :references, :dynamic_uses, :shadows)
|
|
175
|
+
out.values_at(:declarations, :references, :dynamic_uses, :shadows, :ancestries)
|
|
173
176
|
end
|
|
174
177
|
|
|
175
178
|
def add_spelling(out, result, file, real, primary)
|
|
@@ -289,7 +292,7 @@ module Rigor
|
|
|
289
292
|
Array(configuration.signature_paths).flat_map do |dir|
|
|
290
293
|
next [] unless File.directory?(dir)
|
|
291
294
|
|
|
292
|
-
|
|
295
|
+
RootedGlob.glob(dir, "**/*.rbs").flat_map { |file| Analysis::Reachability::SignatureScan.call(file) }
|
|
293
296
|
end
|
|
294
297
|
end
|
|
295
298
|
|
data/lib/rigor/cli.rb
CHANGED
|
@@ -250,8 +250,9 @@ module Rigor
|
|
|
250
250
|
# ships (run `rigor explain <rule>` for what
|
|
251
251
|
# each one catches):
|
|
252
252
|
#{rule_ids_comment}
|
|
253
|
-
# A bare family token (`call`, `flow`,
|
|
254
|
-
# `assert`, `dump`, `
|
|
253
|
+
# A bare family token (`call`, `flow`, `def`,
|
|
254
|
+
# `assert`, `dump`, `global`, `suppression`,
|
|
255
|
+
# `static`, `effect`, `pre-eval`) wildcards every
|
|
255
256
|
# rule under that prefix. Legacy unprefixed
|
|
256
257
|
# names (`undefined-method`, …) still
|
|
257
258
|
# resolve. In-source
|
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,12 +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
|
-
# rule id (
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
# `
|
|
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.
|
|
35
37
|
module ConfigAudit
|
|
36
38
|
# One config-level finding. `kind` discriminates the source key (`:signature_path`,
|
|
37
39
|
# `:bundled_plugin_signature_path`, `:library`, `:disabled_rule`, `:severity_override`,
|
|
@@ -148,24 +150,65 @@ module Rigor
|
|
|
148
150
|
end
|
|
149
151
|
|
|
150
152
|
# `disable:` tokens that reach no row ({.disable_token_warning}) and `severity_overrides:` keys
|
|
151
|
-
# that
|
|
152
|
-
# rule id is never mis-flagged (see the module docstring).
|
|
153
|
+
# that reach none ({.override_key_warning}).
|
|
153
154
|
def self.rule_token_warnings(configuration)
|
|
154
155
|
plugin_ids = configured_plugin_ids(configuration.plugins)
|
|
155
156
|
disable = Array(configuration.disabled_rules).filter_map do |token|
|
|
156
157
|
disable_token_warning(token.to_s, plugin_ids)
|
|
157
158
|
end
|
|
158
|
-
|
|
159
|
-
|
|
159
|
+
keys = configuration.severity_overrides.keys.map(&:to_s)
|
|
160
|
+
overrides = keys.filter_map { |key| override_key_warning(key, plugin_ids, keys) }
|
|
161
|
+
disable + overrides
|
|
162
|
+
end
|
|
160
163
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
# Issue #1985 — the plugin loader's own rows, which a `severity_overrides:` key names by their qualified
|
|
165
|
+
# id; they also answer to the bare `load-error` / `runtime-error`.
|
|
166
|
+
PLUGIN_LOADER_ROWS = %w[plugin_loader.load-error plugin_loader.runtime-error].freeze
|
|
167
|
+
private_constant :PLUGIN_LOADER_ROWS
|
|
168
|
+
|
|
169
|
+
# A warning for a `severity_overrides:` key that can reach no row, or nil. Mirrors {.disable_token_warning}
|
|
170
|
+
# (issue #1985): a plugin's row answers only to its qualified `plugin.<id>.<rule>` or `plugin.<id>`, so a
|
|
171
|
+
# `plugin.`-qualified key naming an unconfigured plugin, and any key outside the built-in vocabulary that is
|
|
172
|
+
# not a qualified plugin id (a plugin's bare rule included), is inert. A reserved run-level row is the home of
|
|
173
|
+
# this map, so naming one is never flagged; `all` is not a family wildcard here, so it is.
|
|
174
|
+
def self.override_key_warning(key, plugin_ids, present = [])
|
|
175
|
+
reason = legacy_alias_problem(key, present) || override_key_problem(key, plugin_ids)
|
|
176
|
+
return nil if reason.nil?
|
|
177
|
+
|
|
178
|
+
Warning.new(kind: :severity_override, message: "severity_overrides: #{key.inspect} #{reason}",
|
|
179
|
+
fields: { "token" => key })
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# #2005 — `disable:` expands a legacy alias to its canonical id, but the severity resolver matches canonical ids
|
|
183
|
+
# only, so an alias key reaches no row. Name the canonical id rather than accept the key. `present` holds the
|
|
184
|
+
# keys the map already has: when the canonical id is one of them, the advice to write it is already followed.
|
|
185
|
+
def self.legacy_alias_problem(key, present)
|
|
186
|
+
canonical = Analysis::CheckRules::LEGACY_RULE_ALIASES[key]
|
|
187
|
+
return nil if canonical.nil?
|
|
188
|
+
|
|
189
|
+
reason = "is a legacy alias of `#{canonical}`; the override has no effect"
|
|
190
|
+
present.include?(canonical) ? reason : "#{reason}, so write `#{canonical}`"
|
|
191
|
+
end
|
|
192
|
+
private_class_method :legacy_alias_problem
|
|
193
|
+
|
|
194
|
+
def self.override_key_problem(key, plugin_ids)
|
|
195
|
+
return "is not a recognized rule id; the override has no effect" if key == "all"
|
|
196
|
+
return nil unless Array(Analysis::CheckRules.resolve_rule_token(key)).empty?
|
|
197
|
+
|
|
198
|
+
if key.start_with?(Analysis::CheckRules::PLUGIN_FAMILY_PREFIX)
|
|
199
|
+
return nil if plugin_ids.nil? || plausible_plugin_token?(key, plugin_ids)
|
|
200
|
+
|
|
201
|
+
"names no plugin `plugins:` configures; the override has no effect"
|
|
202
|
+
elsif key.start_with?("plugin_loader.")
|
|
203
|
+
"is not a recognized rule id; the override has no effect" unless PLUGIN_LOADER_ROWS.include?(key)
|
|
204
|
+
elsif inert_builtin_token?(key)
|
|
205
|
+
"is not a recognized rule id; the override has no effect"
|
|
206
|
+
elsif !non_check_engine_token?(key) && !severity_only_id?(key)
|
|
207
|
+
"is not a recognized rule id; the override has no effect " \
|
|
208
|
+
"(if this is a plugin's rule, write it as `plugin.<id>.<rule>`)"
|
|
166
209
|
end
|
|
167
|
-
disable + overrides
|
|
168
210
|
end
|
|
211
|
+
private_class_method :override_key_problem
|
|
169
212
|
|
|
170
213
|
# Issue #1953 — the run-level rows the diagnostic policy keeps on `severity_overrides:` and the
|
|
171
214
|
# baseline. `disable:` does not reach them by any spelling, so naming one there is inert.
|
|
@@ -224,6 +267,19 @@ module Rigor
|
|
|
224
267
|
end
|
|
225
268
|
private_class_method :reserved_run_level_token?
|
|
226
269
|
|
|
270
|
+
# Bare ids that a `severity_overrides:` key re-stamps but a marker or `disable:` cannot suppress: the `--explain`
|
|
271
|
+
# `fallback` row and the `contribution_merge` conflict reasons (`FlowContribution::CONFLICT_VALID_REASONS`,
|
|
272
|
+
# kebab-cased). They are not in `CheckRules::NON_CHECK_DIAGNOSTIC_IDS`, which would silence the warnings for a
|
|
273
|
+
# `disable:` token or marker naming them. `spec/rigor/config_audit_spec.rb` pins this list's completeness.
|
|
274
|
+
CONFLICT_IDS = FlowContribution::CONFLICT_VALID_REASONS.map { |reason| reason.to_s.tr("_", "-").freeze }.freeze
|
|
275
|
+
SEVERITY_ONLY_IDS = (%w[fallback] + CONFLICT_IDS).freeze
|
|
276
|
+
private_constant :CONFLICT_IDS, :SEVERITY_ONLY_IDS
|
|
277
|
+
|
|
278
|
+
def self.severity_only_id?(key)
|
|
279
|
+
SEVERITY_ONLY_IDS.include?(key)
|
|
280
|
+
end
|
|
281
|
+
private_class_method :severity_only_id?
|
|
282
|
+
|
|
227
283
|
def self.non_check_engine_token?(token)
|
|
228
284
|
Analysis::CheckRules::NON_CHECK_DIAGNOSTIC_IDS.include?(token) ||
|
|
229
285
|
Analysis::CheckRules::NON_CHECK_DIAGNOSTIC_FAMILIES.include?(token.split(".").first)
|
|
@@ -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.
|