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
|
@@ -19,6 +19,9 @@ module Rigor
|
|
|
19
19
|
# `plugin.<id>`, never to a bare id or `all`. The run-level rows outside the check-rule catalogue
|
|
20
20
|
# (`rbs.coverage.*`, `dynamic.dependency-source.*`, the plugin loader, `plugin_trust.*`, `pool-degraded`) keep
|
|
21
21
|
# answering to `severity_overrides:` only, by any spelling.
|
|
22
|
+
# Issue #1985 — `severity_overrides:` follows the same rule: a plugin's row answers to its qualified id or
|
|
23
|
+
# `plugin.<id>`, never to its bare rule or a family wildcard; the plugin loader's rows answer to
|
|
24
|
+
# `plugin_loader.<rule>` first and to their bare `load-error` / `runtime-error` after it.
|
|
22
25
|
#
|
|
23
26
|
# Extracted (ADR-87 WD4) so the miss path ({Runner::DiagnosticAggregator}) and the boot-slimming hit path
|
|
24
27
|
# ({RunCacheProbe}, which never loads the aggregator) apply the IDENTICAL final filter — a warm HIT's
|
|
@@ -52,11 +55,19 @@ module Rigor
|
|
|
52
55
|
diagnostic.source_family == Diagnostic::DEFAULT_SOURCE_FAMILY && CHECK_RULES.include?(diagnostic.rule)
|
|
53
56
|
end
|
|
54
57
|
|
|
58
|
+
# `plugin.<id>` for a row a plugin emitted, nil for every other row.
|
|
59
|
+
def plugin_family(diagnostic)
|
|
60
|
+
family = diagnostic.source_family.to_s
|
|
61
|
+
family if family.start_with?(CheckRules::PLUGIN_FAMILY_PREFIX)
|
|
62
|
+
end
|
|
63
|
+
|
|
55
64
|
def stamp(diagnostic, configuration)
|
|
56
65
|
return diagnostic if diagnostic.rule.nil?
|
|
57
66
|
|
|
58
67
|
resolved = Configuration::SeverityProfile.resolve(
|
|
59
68
|
rule: diagnostic.rule,
|
|
69
|
+
qualified_rule: diagnostic.qualified_rule,
|
|
70
|
+
plugin_family: plugin_family(diagnostic),
|
|
60
71
|
authored_severity: diagnostic.severity,
|
|
61
72
|
profile: configuration.severity_profile,
|
|
62
73
|
overrides: configuration.severity_overrides,
|
|
@@ -20,6 +20,7 @@ require_relative "../inference/method_dispatcher/file_folding"
|
|
|
20
20
|
require_relative "check_rules"
|
|
21
21
|
require_relative "crash_signature"
|
|
22
22
|
require_relative "dependency_recorder"
|
|
23
|
+
require_relative "run_disclosure_credit"
|
|
23
24
|
require_relative "dependency_source_inference"
|
|
24
25
|
require_relative "diagnostic"
|
|
25
26
|
require_relative "erb_template_detector"
|
|
@@ -170,8 +171,18 @@ module Rigor
|
|
|
170
171
|
return analyze_with_effects(path) unless @record_dependencies
|
|
171
172
|
|
|
172
173
|
diagnostics = nil
|
|
173
|
-
|
|
174
|
+
reads = []
|
|
175
|
+
record = nil
|
|
176
|
+
# Issue #2020 — and the run-scoped rows it registered, as `Runner#analyze_file` keeps them.
|
|
177
|
+
disclosures = RunDisclosureCredit.collect do
|
|
178
|
+
record = Plugin::IoBoundary.attributing(reads) do
|
|
179
|
+
DependencyRecorder.record_for(path) { diagnostics = analyze_with_effects(path) }
|
|
180
|
+
end
|
|
181
|
+
end
|
|
174
182
|
@file_dependencies[path] = record
|
|
183
|
+
# Issue #1553 — the plugin reads this file's analysis made, as `Runner#analyze_file` keeps them.
|
|
184
|
+
(@analysis_reads ||= {})[path] = reads unless reads.empty?
|
|
185
|
+
(@analysis_disclosures ||= {})[path] = disclosures unless disclosures.empty?
|
|
175
186
|
diagnostics
|
|
176
187
|
end
|
|
177
188
|
|
|
@@ -309,6 +320,18 @@ module Rigor
|
|
|
309
320
|
@file_dependencies
|
|
310
321
|
end
|
|
311
322
|
|
|
323
|
+
# Issue #1553 — the plugin reads ({Plugin::IoBoundary.attributing}) each `#analyze` call's file made (empty
|
|
324
|
+
# unless `record_dependencies:` was set). The rows are value objects of frozen Strings, so they marshal back.
|
|
325
|
+
def drain_analysis_reads
|
|
326
|
+
@analysis_reads || {}
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Issue #2020 — the run-scoped rows each file's analysis registered, `{path => [record]}`; see
|
|
330
|
+
# {RunDisclosureCredit}. Marshal-clean like {#drain_run_disclosures}.
|
|
331
|
+
def drain_analysis_disclosures
|
|
332
|
+
@analysis_disclosures || {}
|
|
333
|
+
end
|
|
334
|
+
|
|
312
335
|
# ADR-103 WD12 — the per-file {Effects::FileCollection}s this session collected (empty unless the
|
|
313
336
|
# configuration carries an `effects:` block). Marshal-clean by construction — frozen Hashes of
|
|
314
337
|
# Strings, `LabelSet`s of frozen Arrays, and `Data` edges — so the fork pool ships them back to the
|
|
@@ -4,6 +4,7 @@ require "digest"
|
|
|
4
4
|
require "json"
|
|
5
5
|
require_relative "../value_semantics"
|
|
6
6
|
require_relative "file_digest"
|
|
7
|
+
require_relative "../rooted_glob"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module Cache
|
|
@@ -87,7 +88,13 @@ module Rigor
|
|
|
87
88
|
# appears, vanishes or changes kind invalidates the warm entry. A
|
|
88
89
|
# pre-19 entry carries none of them and would validate fresh across exactly those edits, so cached
|
|
89
90
|
# entries must read as misses once and rebuild carrying the rows.
|
|
90
|
-
|
|
91
|
+
# v20: #1977 — the `--incremental` run-result slot records a `:kind` row, not an existence row, per analysis
|
|
92
|
+
# root, `pre_eval:` entry and project signature root. A pre-20 slot's existence row validates fresh across a
|
|
93
|
+
# directory replaced by a file of the same name, so it must read as a miss once and rebuild.
|
|
94
|
+
# v21: #1984 — the visibility walk records each class's singleton slots (`[:singleton, name]`) in the seed
|
|
95
|
+
# bundles' `method_visibilities`. A pre-21 bundle carries none for an unchanged file, so cached bundles must read
|
|
96
|
+
# as misses once and rebuild carrying them.
|
|
97
|
+
SCHEMA_VERSION = 21
|
|
91
98
|
|
|
92
99
|
# Per-slot entry value objects. Constructors validate enums / required fields and freeze the resulting
|
|
93
100
|
# struct so no caller can mutate after the entry is in a Descriptor.
|
|
@@ -325,7 +332,12 @@ module Rigor
|
|
|
325
332
|
# tuple moves, so a `git checkout`, a `bundle install` or a CI cache restored over a fresh checkout
|
|
326
333
|
# keeps the file rows fresh while it would move every glob signature, turning every such run into a
|
|
327
334
|
# full re-analysis. `:names` also reads no stat at all, so it costs a `Dir.glob` and nothing else.
|
|
328
|
-
|
|
335
|
+
#
|
|
336
|
+
# `:kinds` (#1959) is `:names` plus each match's `File.lstat` file type: the set moves it, and so does a
|
|
337
|
+
# path that keeps its name and changes kind (a file `gen` replaced by a directory `gen/`, a symlinked
|
|
338
|
+
# directory replaced by a real one). It reads one `lstat` per match and no content, so a `touch` or an
|
|
339
|
+
# edit leaves it fresh.
|
|
340
|
+
VALID_MODES = %i[stat names kinds].freeze
|
|
329
341
|
|
|
330
342
|
attr_reader :root, :pattern, :value
|
|
331
343
|
|
|
@@ -357,13 +369,15 @@ module Rigor
|
|
|
357
369
|
# The aggregate signature the entry's `value` carries: SHA-256 over the sorted per-file rows. In the
|
|
358
370
|
# default (`:stat`) mode a row is the file's `(path, size, mtime_ns, ctime_ns, inode)` tuple — NO
|
|
359
371
|
# content read; in strict mode a row is `(path, content-sha256)`, restoring the pre-ADR-87 authority.
|
|
360
|
-
# In `:names` mode a row is the path alone, stat-free, so only the matching SET moves the signature
|
|
372
|
+
# In `:names` mode a row is the path alone, stat-free, so only the matching SET moves the signature; in
|
|
373
|
+
# `:kinds` mode it is the path and its {.lstat_kind}.
|
|
361
374
|
# Per-file stat failures (a file vanishing between the glob and the stat) drop the row — same race
|
|
362
375
|
# posture as {Descriptor#file_entry_fresh?}. `Dir.glob` returns sorted entries by default so the row
|
|
363
376
|
# order — and therefore the signature — is stable.
|
|
364
377
|
def self.signature_for(root:, pattern:, mode: :stat)
|
|
365
|
-
paths =
|
|
378
|
+
paths = RootedGlob.glob(root, pattern)
|
|
366
379
|
return Digest::SHA256.hexdigest(paths.map { |path| "#{path}#{ROW_SEPARATOR}" }.join) if mode == :names
|
|
380
|
+
return kinds_signature(paths) if mode == :kinds
|
|
367
381
|
|
|
368
382
|
strict = FileDigest.strict_validation?
|
|
369
383
|
rows = paths.filter_map do |path|
|
|
@@ -382,10 +396,29 @@ module Rigor
|
|
|
382
396
|
Digest::SHA256.hexdigest(rows.join)
|
|
383
397
|
end
|
|
384
398
|
|
|
399
|
+
# #1959 — what `path` itself is, a symlink not followed: `:file`, `:directory`, `:symlink` or `:other`, and
|
|
400
|
+
# `:absent` for a path that vanished between the glob and the `lstat`.
|
|
401
|
+
def self.lstat_kind(path)
|
|
402
|
+
case File.lstat(path).ftype
|
|
403
|
+
when "file" then :file
|
|
404
|
+
when "directory" then :directory
|
|
405
|
+
when "link" then :symlink
|
|
406
|
+
else :other
|
|
407
|
+
end
|
|
408
|
+
rescue SystemCallError
|
|
409
|
+
:absent
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def self.kinds_signature(paths)
|
|
413
|
+
rows = paths.map { |path| "#{path}#{FIELD_SEPARATOR}#{lstat_kind(path)}#{ROW_SEPARATOR}" }
|
|
414
|
+
Digest::SHA256.hexdigest(rows.join)
|
|
415
|
+
end
|
|
416
|
+
private_class_method :kinds_signature
|
|
417
|
+
|
|
385
418
|
# ADR-87 WD2 — fresh iff re-globbing + re-stat-ing reproduces the recorded signature. Zero file-content
|
|
386
|
-
# bytes are read in the default mode,
|
|
387
|
-
# (recompute), never a crash. A stored signature recorded under the opposite
|
|
388
|
-
# mismatches and recomputes.
|
|
419
|
+
# bytes are read in the default mode, no stat at all in `:names`, and one `lstat` per match in `:kinds`.
|
|
420
|
+
# Any failure reads as stale (recompute), never a crash. A stored signature recorded under the opposite
|
|
421
|
+
# validation mode simply mismatches and recomputes.
|
|
389
422
|
def self.fresh?(entry)
|
|
390
423
|
signature_for(root: entry.root, pattern: entry.pattern, mode: entry.mode) == entry.value
|
|
391
424
|
rescue StandardError
|
|
@@ -4,6 +4,7 @@ require "digest"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../version"
|
|
6
6
|
require_relative "descriptor"
|
|
7
|
+
require_relative "../rooted_glob"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module Cache
|
|
@@ -195,8 +196,8 @@ module Rigor
|
|
|
195
196
|
# Sorted, de-duplicated absolute paths of every engine `.rb` file and {DATA_GLOBS} data file, across
|
|
196
197
|
# the directories that exist. `plugins/*/sig/**/*.rbs` overlaps nothing else but is de-duplicated anyway.
|
|
197
198
|
def source_files(root)
|
|
198
|
-
sources = SOURCE_DIRECTORIES.flat_map { |relative|
|
|
199
|
-
data = DATA_GLOBS.flat_map { |pattern|
|
|
199
|
+
sources = SOURCE_DIRECTORIES.flat_map { |relative| RootedGlob.glob(root, File.join(relative, "**", "*.rb")) }
|
|
200
|
+
data = DATA_GLOBS.flat_map { |pattern| RootedGlob.glob(root, pattern) }
|
|
200
201
|
(sources + data).uniq.sort
|
|
201
202
|
end
|
|
202
203
|
end
|
|
@@ -10,6 +10,7 @@ require_relative "../analysis/shadow_harness"
|
|
|
10
10
|
require_relative "../analysis/run_cache_key"
|
|
11
11
|
require_relative "../environment"
|
|
12
12
|
require_relative "../analysis/path_expansion"
|
|
13
|
+
require_relative "../rooted_glob"
|
|
13
14
|
|
|
14
15
|
module Rigor
|
|
15
16
|
module Cache
|
|
@@ -172,7 +173,29 @@ module Rigor
|
|
|
172
173
|
# 38: issue #1898 — the payload gains `effect_plugin_digest`, the plugin effect-fact digest the collections were
|
|
173
174
|
# stamped under, which a recheck now checks before its served collections reach the envelope pass. A pre-38
|
|
174
175
|
# blob carries none, so the gate rejects it and the next run is cold.
|
|
175
|
-
|
|
176
|
+
# 39: issue #1984 — the visibility walk records the singleton slots (`[:singleton, name]`) and contests the ones
|
|
177
|
+
# two files disagree on, which a pre-39 bundle carries none of, so a warm run would answer a singleton call a
|
|
178
|
+
# cold run reports.
|
|
179
|
+
# 40: issue #1553 — the payload gains `plugin_reads`, the plugin reads each analysed file's last analysis made,
|
|
180
|
+
# which a recheck validates to put a reader of a changed input into the closure. A pre-40 blob carries none,
|
|
181
|
+
# so its readers would be served stale; the gate rejects it and the next run is cold.
|
|
182
|
+
# 41: issue #1583 — the payload gains `discovery_digests`, the packed digest of each file a run over path
|
|
183
|
+
# arguments discovered without analysing, which a recheck compares to re-baseline when one moved. A pre-41
|
|
184
|
+
# blob carries none; the gate rejects it and the next run is cold.
|
|
185
|
+
# 42: issue #2184 — a `deferred_ranges` row for a `def` inside a block now carries the method's name, and a
|
|
186
|
+
# literal-named `define_method` / `define_singleton_method` call adds a row. A pre-42 bundle carries neither,
|
|
187
|
+
# so a warm run would report a top-level block call that a cold run declines.
|
|
188
|
+
# 43: issue #2020 — the payload gains `plugin_disclosures`, the run-scoped plugin rows each analysed file's
|
|
189
|
+
# last analysis registered, which a recheck replays for the files it serves. A pre-43 blob carries none, so
|
|
190
|
+
# its rechecks would drop every such row; the gate rejects it and the next run is cold.
|
|
191
|
+
# 44: PR #2286 review M1 — the payload gains `synthetic_surface_digest`, the ADR-16 synthetic-method index's
|
|
192
|
+
# {Rigor::Inference::SyntheticMethodIndex#surface_digest} at the run the cache was built under, which a recheck
|
|
193
|
+
# compares to re-baseline when a class-body DSL edit moved it. A pre-44 blob carries none; the gate rejects it
|
|
194
|
+
# and the next run is cold.
|
|
195
|
+
# 45: issue #1154 — each seed bundle's `global_write_census` also records the ivar writers the declared-parameter
|
|
196
|
+
# credit reads (`[:ivar_set, name]`, `[:ivar_set_class, class]`, `[:own_write, class, name, path]`), which a
|
|
197
|
+
# pre-45 bundle carries none of, so a warm run would credit an ivar write a cold run declines.
|
|
198
|
+
SCHEMA = 45
|
|
176
199
|
|
|
177
200
|
# The persisted per-file state.
|
|
178
201
|
# `cache` maps an analyzed file to its diagnostics.
|
|
@@ -227,19 +250,31 @@ module Rigor
|
|
|
227
250
|
# Issues #796 / #794 — the run-level rows a narrowed run replays instead of re-deriving; see
|
|
228
251
|
# docs/internal-spec/cache.md § "Run-level rows the snapshot carries" for why exactly these two are
|
|
229
252
|
# fresh under the fingerprint and why no other `.rigor.yml`-level row may join them.
|
|
253
|
+
# Issue #1583 — `discovery_digests` maps each file a run over path arguments discovered but did not analyse
|
|
254
|
+
# (#684's widening to the configured `paths:`) to its ADR-87 packed stat-digest entry; `{}` on a run over
|
|
255
|
+
# the configured paths. A recheck that finds one moved, or the set changed, re-baselines.
|
|
230
256
|
# Issue #1898 — `effect_plugin_digest` is {Rigor::Effects::PluginFacts#digest} at the run that collected
|
|
231
257
|
# `effect_collections` (nil when collection was off). A plugin stamps its attributions and edges into a file's
|
|
232
258
|
# collection, and `effects_identity` is plugin-blind (it is compared before any plugin loads), so the session
|
|
233
259
|
# compares this one after the recheck has loaded them.
|
|
260
|
+
# Issue #2020 — `plugin_disclosures` is {Analysis::RunDisclosureLedger#to_h}: `files`, each analysed file's
|
|
261
|
+
# run-scoped plugin rows ({Plugin::Base#disclose_once} / {Plugin::Base#emit_once}) from its last analysis, which a
|
|
262
|
+
# recheck replays for the files it serves, and `reads`, the boundary rows per `[plugin id, key]` that make one
|
|
263
|
+
# stale.
|
|
234
264
|
Payload = Data.define(:cache, :sources, :digests, :analyzed,
|
|
235
265
|
:symbol_sources, :ancestry_sources, :symbol_fingerprints,
|
|
236
266
|
:missing, :class_decls, :constant_decls, :seed_bundles, :plugin_fact_digest,
|
|
237
267
|
:return_summaries, :param_table,
|
|
238
268
|
:effect_collections, :effects_identity, :run_level_rows,
|
|
239
|
-
:effect_plugin_digest
|
|
240
|
-
|
|
241
|
-
#
|
|
242
|
-
|
|
269
|
+
:effect_plugin_digest, :plugin_reads, :discovery_digests, :plugin_disclosures,
|
|
270
|
+
:synthetic_surface_digest) do
|
|
271
|
+
# `effect_plugin_digest` defaults to nil so a payload built without it (an older caller, a spec fixture)
|
|
272
|
+
# reads as "unknown", which the session treats as moved. `plugin_reads` defaults to no reads, and
|
|
273
|
+
# `discovery_digests` to no discovery-only files, and `plugin_disclosures` (#2020) to no replayed rows.
|
|
274
|
+
# `synthetic_surface_digest` defaults to nil, the empty index's digest: a run that builds a non-empty one
|
|
275
|
+
# reads it as moved.
|
|
276
|
+
def initialize(effect_plugin_digest: nil, plugin_reads: {}, discovery_digests: {}, plugin_disclosures: {},
|
|
277
|
+
synthetic_surface_digest: nil, **rest)
|
|
243
278
|
super
|
|
244
279
|
end
|
|
245
280
|
end
|
|
@@ -367,7 +402,7 @@ module Rigor
|
|
|
367
402
|
# checkouts.
|
|
368
403
|
def self.digest_signature_paths(signature_paths)
|
|
369
404
|
globbed = Array(signature_paths).flat_map do |entry|
|
|
370
|
-
File.directory?(entry) ?
|
|
405
|
+
File.directory?(entry) ? RootedGlob.glob(entry, "**/*.rbs") : [entry]
|
|
371
406
|
end
|
|
372
407
|
files = globbed.select { |path| File.file?(path) }.sort
|
|
373
408
|
digest = Digest::SHA256.new
|
|
@@ -449,7 +484,11 @@ module Rigor
|
|
|
449
484
|
effect_collections: data[:effect_collections] || {},
|
|
450
485
|
effects_identity: data[:effects_identity],
|
|
451
486
|
run_level_rows: data[:run_level_rows],
|
|
452
|
-
effect_plugin_digest: data[:effect_plugin_digest]
|
|
487
|
+
effect_plugin_digest: data[:effect_plugin_digest],
|
|
488
|
+
plugin_reads: data[:plugin_reads] || {},
|
|
489
|
+
plugin_disclosures: data.fetch(:plugin_disclosures, {}),
|
|
490
|
+
discovery_digests: data.fetch(:discovery_digests, {}),
|
|
491
|
+
synthetic_surface_digest: data[:synthetic_surface_digest]
|
|
453
492
|
)
|
|
454
493
|
end
|
|
455
494
|
private :payload_from
|
|
@@ -475,7 +514,9 @@ module Rigor
|
|
|
475
514
|
effect_collections: payload.effect_collections,
|
|
476
515
|
effects_identity: payload.effects_identity,
|
|
477
516
|
run_level_rows: payload.run_level_rows,
|
|
478
|
-
effect_plugin_digest: payload.effect_plugin_digest
|
|
517
|
+
effect_plugin_digest: payload.effect_plugin_digest,
|
|
518
|
+
plugin_reads: payload.plugin_reads, plugin_disclosures: payload.plugin_disclosures,
|
|
519
|
+
discovery_digests: payload.discovery_digests, synthetic_surface_digest: payload.synthetic_surface_digest
|
|
479
520
|
)
|
|
480
521
|
# #1507 — every `--incremental` run that analysed anything rewrites this whole blob, so its deflate is
|
|
481
522
|
# on the edit path; see {Store::DEFLATE_LEVEL}.
|
|
@@ -5,6 +5,7 @@ require "digest"
|
|
|
5
5
|
require_relative "descriptor"
|
|
6
6
|
require_relative "engine_source"
|
|
7
7
|
require_relative "file_digest"
|
|
8
|
+
require_relative "../rooted_glob"
|
|
8
9
|
|
|
9
10
|
module Rigor
|
|
10
11
|
module Cache
|
|
@@ -110,7 +111,7 @@ module Rigor
|
|
|
110
111
|
roots.flat_map do |root|
|
|
111
112
|
next [] unless root.directory?
|
|
112
113
|
|
|
113
|
-
|
|
114
|
+
RootedGlob.glob(root, "**/*.rbs").map do |path|
|
|
114
115
|
digest = FileDigest.hexdigest(path)
|
|
115
116
|
if comparator == :stat
|
|
116
117
|
Descriptor::FileEntry.stat(path: path, digest: digest)
|
data/lib/rigor/cache/store.rb
CHANGED
|
@@ -9,6 +9,7 @@ require "zlib"
|
|
|
9
9
|
|
|
10
10
|
require_relative "../version"
|
|
11
11
|
require_relative "descriptor"
|
|
12
|
+
require_relative "../rooted_glob"
|
|
12
13
|
|
|
13
14
|
module Rigor
|
|
14
15
|
module Cache
|
|
@@ -176,7 +177,7 @@ module Rigor
|
|
|
176
177
|
subdir = File.join(root, child)
|
|
177
178
|
next nil unless File.directory?(subdir)
|
|
178
179
|
|
|
179
|
-
entries =
|
|
180
|
+
entries = RootedGlob.glob(subdir, "**/*.entry")
|
|
180
181
|
next nil if entries.empty?
|
|
181
182
|
|
|
182
183
|
{ id: child, entries: entries.size, bytes: entries.sum { |e| File.size(e) } }
|
|
@@ -709,7 +710,7 @@ module Rigor
|
|
|
709
710
|
|
|
710
711
|
def cleanup_stale_temp_files
|
|
711
712
|
cutoff = Time.now - STALE_TEMP_FILE_AGE_SECONDS
|
|
712
|
-
|
|
713
|
+
RootedGlob.glob(@root, "**/*.tmp.*").each do |path|
|
|
713
714
|
next unless File.file?(path)
|
|
714
715
|
next if File.mtime(path) > cutoff
|
|
715
716
|
|
|
@@ -771,7 +772,7 @@ module Rigor
|
|
|
771
772
|
# Returns an array of `{ path:, producer:, mtime:, bytes: }` hashes for every `.entry` file under the
|
|
772
773
|
# cache root, skipping unreadable entries.
|
|
773
774
|
def collect_entry_stats
|
|
774
|
-
|
|
775
|
+
RootedGlob.glob(@root, "**/*.entry").filter_map do |path|
|
|
775
776
|
stat = File.stat(path)
|
|
776
777
|
producer = producer_id_for_entry(path)
|
|
777
778
|
next nil if producer.nil?
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "json"
|
|
5
5
|
require "optionparser"
|
|
6
|
+
require "tempfile"
|
|
6
7
|
|
|
7
8
|
require_relative "../configuration"
|
|
8
9
|
require_relative "../config_audit"
|
|
@@ -11,6 +12,7 @@ require_relative "../analysis/rule_catalog"
|
|
|
11
12
|
# The baseline filter runs on EVERY exit path — including the ADR-87 WD4 cache-HIT fast path, which skips
|
|
12
13
|
# `load_check_dependencies` — so it must be a load-time require. It pulls only YAML, never the engine.
|
|
13
14
|
require_relative "../analysis/baseline"
|
|
15
|
+
require_relative "../analysis/edit_replay"
|
|
14
16
|
require_relative "command"
|
|
15
17
|
require_relative "options"
|
|
16
18
|
require_relative "diagnostic_formats"
|
|
@@ -272,8 +274,83 @@ module Rigor
|
|
|
272
274
|
incremental = session.reanalyze_subset(subset).map(&:to_h)
|
|
273
275
|
full = verify_full_diagnostics(configuration, paths, collect_stats: options.fetch(:stats)).map(&:to_h)
|
|
274
276
|
|
|
275
|
-
report_verify_incremental(incremental, full, subset_size: subset.size, total: analyzed.size,
|
|
276
|
-
|
|
277
|
+
status = report_verify_incremental(incremental, full, subset_size: subset.size, total: analyzed.size,
|
|
278
|
+
format: options.fetch(:format))
|
|
279
|
+
return status unless status.zero? && options.fetch(:verify_incremental_edits).positive?
|
|
280
|
+
|
|
281
|
+
verify_edit_replays(session, configuration, paths, options)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Issue #1967 — the partition check above re-analyses an unchanged tree, so every served row is still right and a
|
|
285
|
+
# missing invalidation edge goes unseen. Here each of `--verify-incremental-edits` sampled files is edited through
|
|
286
|
+
# an editor buffer (never on disk) by each of {Analysis::EditReplay.edits} — emptied, then reduced to its
|
|
287
|
+
# `class` / `module` skeleton — and the warm recheck of the baseline ({IncrementalSession#replay_edit}) must equal
|
|
288
|
+
# a full analysis of the edited tree: a dependent the recheck failed to re-analyse is served from the baseline and
|
|
289
|
+
# differs. A run in which every replay declined compared nothing, so it fails rather than report OK.
|
|
290
|
+
def verify_edit_replays(session, configuration, paths, options)
|
|
291
|
+
sample = edit_replay_sample(session.analyzed_files, options.fetch(:verify_incremental_edits))
|
|
292
|
+
replayed = 0
|
|
293
|
+
declined = 0
|
|
294
|
+
sample.each do |path|
|
|
295
|
+
Analysis::EditReplay.edits(File.read(path)).each do |edit|
|
|
296
|
+
incremental, full = replay_edit(session, configuration, paths, path, edit)
|
|
297
|
+
next declined += 1 if incremental.nil?
|
|
298
|
+
|
|
299
|
+
replayed += 1
|
|
300
|
+
next if incremental == full
|
|
301
|
+
|
|
302
|
+
return report_edit_replay_mismatch(path, edit, incremental, full)
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
report_edit_replays(replayed, declined, options.fetch(:format))
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def report_edit_replay_mismatch(path, edit, incremental, full)
|
|
309
|
+
@err.puts("rigor: --verify-incremental FAILED — the recheck after #{edit.label} #{path} " \
|
|
310
|
+
"differs from a full run.")
|
|
311
|
+
return report_verify_order_mismatch(incremental, full) if incremental.tally == full.tally
|
|
312
|
+
|
|
313
|
+
report_verify_set_mismatch(incremental, full, header: false)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# `count` files spread evenly over the analysed list, so a small count still reaches both ends of it.
|
|
317
|
+
def edit_replay_sample(analyzed, count)
|
|
318
|
+
return analyzed if count >= analyzed.size
|
|
319
|
+
|
|
320
|
+
Array.new(count) { |index| analyzed[index * analyzed.size / count] }
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# The warm recheck's rows and a full run's, both with `path` replaced by `edit`; `[nil, nil]` when the recheck
|
|
324
|
+
# declined.
|
|
325
|
+
def replay_edit(session, configuration, paths, path, edit)
|
|
326
|
+
Tempfile.create(["rigor-verify-edit-", ".rb"]) do |file|
|
|
327
|
+
file.write(edit.source)
|
|
328
|
+
file.close
|
|
329
|
+
buffer = Analysis::BufferBinding.new(logical_path: path, physical_path: file.path)
|
|
330
|
+
recheck = session.replay_edit(buffer)
|
|
331
|
+
return [nil, nil] if recheck.nil?
|
|
332
|
+
|
|
333
|
+
# `analyze_only:` with the buffer selects editor mode option B, a whole-project run reading the buffer at
|
|
334
|
+
# its logical path; the buffer alone would analyse that one file.
|
|
335
|
+
runner = Analysis::Runner.new(configuration: configuration, cache_store: nil, collect_stats: false,
|
|
336
|
+
buffer: buffer, analyze_only: session.analyzed_files.to_set)
|
|
337
|
+
full = paths ? runner.run(paths) : runner.run
|
|
338
|
+
[recheck.diagnostics.map(&:to_h), full.diagnostics.map(&:to_h)]
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def report_edit_replays(replayed, declined, format)
|
|
343
|
+
if replayed.zero? && declined.positive?
|
|
344
|
+
@err.puts("rigor: --verify-incremental FAILED — every edit replay (#{declined}) declined, " \
|
|
345
|
+
"since the recheck would have been a full analysis, so no edit was compared.")
|
|
346
|
+
return 1
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
line = "rigor: --verify-incremental OK — #{replayed} edits replayed (files emptied and reduced to their " \
|
|
350
|
+
"class/module skeleton), every recheck matches full"
|
|
351
|
+
line = "#{line}; #{declined} declined (the recheck would have been a full analysis)" if declined.positive?
|
|
352
|
+
side_output(format).puts(line)
|
|
353
|
+
0
|
|
277
354
|
end
|
|
278
355
|
|
|
279
356
|
# ADR-46 — cross-process incremental analysis (`--incremental`). Derives the global fingerprint cheaply (no RBS
|
|
@@ -490,10 +567,10 @@ module Rigor
|
|
|
490
567
|
end
|
|
491
568
|
end
|
|
492
569
|
|
|
493
|
-
def report_verify_set_mismatch(incremental, full)
|
|
570
|
+
def report_verify_set_mismatch(incremental, full, header: true)
|
|
494
571
|
only_incremental = multiset_difference(incremental, full)
|
|
495
572
|
only_full = multiset_difference(full, incremental)
|
|
496
|
-
@err.puts("rigor: --verify-incremental FAILED — incremental and full diagnostics differ.")
|
|
573
|
+
@err.puts("rigor: --verify-incremental FAILED — incremental and full diagnostics differ.") if header
|
|
497
574
|
@err.puts(" incremental-only: #{only_incremental.size}, full-only: #{only_full.size}")
|
|
498
575
|
(only_incremental + only_full).first(10).each do |hash|
|
|
499
576
|
@err.puts(" #{diagnostic_location(hash)}")
|
|
@@ -643,6 +720,9 @@ module Rigor
|
|
|
643
720
|
# the rest from the per-file cache, and asserts the merged diagnostics are byte-identical to a full
|
|
644
721
|
# `--no-cache` run. Exits non-zero on any mismatch. Off by default.
|
|
645
722
|
verify_incremental: false,
|
|
723
|
+
# Issue #1967 — the number of files `--verify-incremental` replays an edit on (each emptied through an editor
|
|
724
|
+
# buffer) after the partition check. 0 replays none.
|
|
725
|
+
verify_incremental_edits: 0,
|
|
646
726
|
# ADR-46 — cross-process incremental analysis. With a disk snapshot of the prior run's per-file diagnostics +
|
|
647
727
|
# dependency graph, re-analyzes only the changed closure and serves the rest from the snapshot. Off by
|
|
648
728
|
# default.
|
|
@@ -720,6 +800,15 @@ module Rigor
|
|
|
720
800
|
"assert incremental analysis matches a full run, then exit") do
|
|
721
801
|
options[:verify_incremental] = true
|
|
722
802
|
end
|
|
803
|
+
opts.on("--verify-incremental-edits=N", Integer,
|
|
804
|
+
"with --verify-incremental, also replay 2 edits (empty, class/module skeleton) on N files") do |value|
|
|
805
|
+
if value.negative?
|
|
806
|
+
raise OptionParser::InvalidArgument, "--verify-incremental-edits=#{value} (must be 0 or more)"
|
|
807
|
+
end
|
|
808
|
+
|
|
809
|
+
options[:verify_incremental] = true
|
|
810
|
+
options[:verify_incremental_edits] = value
|
|
811
|
+
end
|
|
723
812
|
opts.on("--incremental",
|
|
724
813
|
"re-analyze only files changed since the last run (cross-process cache)") do
|
|
725
814
|
options[:incremental] = true
|
data/lib/rigor/cli/command.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../rooted_glob"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
class CLI
|
|
5
7
|
# Base class for the `rigor <subcommand>` command objects.
|
|
@@ -26,7 +28,7 @@ module Rigor
|
|
|
26
28
|
paths = []
|
|
27
29
|
args.each do |arg|
|
|
28
30
|
if File.directory?(arg)
|
|
29
|
-
paths.concat(
|
|
31
|
+
paths.concat(RootedGlob.glob(arg, "**/*.rb"))
|
|
30
32
|
elsif File.file?(arg)
|
|
31
33
|
paths << arg
|
|
32
34
|
else
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "command"
|
|
4
|
+
require_relative "options"
|
|
4
5
|
|
|
5
6
|
require "json"
|
|
6
7
|
require "optionparser"
|
|
@@ -63,7 +64,7 @@ module Rigor
|
|
|
63
64
|
options[:current_path] = path
|
|
64
65
|
end
|
|
65
66
|
opt.on("--config=PATH", "Path to .rigor.yml. Forwarded to the implicit `rigor check` run.") do |path|
|
|
66
|
-
options[:config] = path
|
|
67
|
+
options[:config] = Options.config_path(path)
|
|
67
68
|
end
|
|
68
69
|
end.parse!(@argv)
|
|
69
70
|
options
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "command"
|
|
4
4
|
require_relative "doc_links"
|
|
5
|
+
require_relative "../rooted_glob"
|
|
5
6
|
|
|
6
7
|
module Rigor
|
|
7
8
|
class CLI
|
|
@@ -186,8 +187,8 @@ module Rigor
|
|
|
186
187
|
# `install.md` sits at the docs root (category `guide`); the rest are manual / handbook chapters.
|
|
187
188
|
def discover_docs
|
|
188
189
|
paths = [File.join(DOCS_ROOT, "install.md")]
|
|
189
|
-
paths +=
|
|
190
|
-
paths +=
|
|
190
|
+
paths += RootedGlob.glob(MANUAL_ROOT, "**/*.md") # Dir.glob is already sorted
|
|
191
|
+
paths += RootedGlob.glob(HANDBOOK_ROOT, "**/*.md")
|
|
191
192
|
paths.select { |path| File.file?(path) }.map { |path| doc_entry(path) }
|
|
192
193
|
end
|
|
193
194
|
|
|
@@ -364,7 +364,7 @@ module Rigor
|
|
|
364
364
|
status: :warn,
|
|
365
365
|
message: "#{gap.locked_gems.join(', ')} #{gap.locked_gems.one? ? 'is a dependency' : 'are dependencies'} " \
|
|
366
366
|
"of this project and #{gap.plugin_gem} models it, but it is not enabled",
|
|
367
|
-
hint: "Add `#{gap.plugin_gem}` to `.rigor.yml` `plugins:`, or
|
|
367
|
+
hint: "Add `#{gap.plugin_gem}` to `.rigor.yml` `plugins:`, or decline it with an `enabled: false` entry."
|
|
368
368
|
}
|
|
369
369
|
end
|
|
370
370
|
|
|
@@ -10,7 +10,7 @@ require_relative "command"
|
|
|
10
10
|
module Rigor
|
|
11
11
|
class CLI
|
|
12
12
|
# Executes `rigor explain <rule>`. Prints the catalog entry for one canonical rule id, a legacy alias, or a family
|
|
13
|
-
# wildcard (`call`, `flow`, `assert`, `dump`, `
|
|
13
|
+
# wildcard (`call`, `flow`, `def`, `assert`, `dump`, `global`, `suppression`, `static`, `effect`).
|
|
14
14
|
#
|
|
15
15
|
# Without arguments lists every rule's id and one-line summary.
|
|
16
16
|
#
|
|
@@ -74,7 +74,8 @@ module Rigor
|
|
|
74
74
|
end
|
|
75
75
|
@out.puts("")
|
|
76
76
|
@out.puts("Run `rigor explain <rule>` for the full description.")
|
|
77
|
-
@out.puts("Family wildcards (
|
|
77
|
+
@out.puts("Family wildcards (#{Analysis::CheckRules::RULE_FAMILIES.map { |f| "`#{f}`" }.join(', ')}) " \
|
|
78
|
+
"print every rule under that prefix.")
|
|
78
79
|
@out.puts("")
|
|
79
80
|
@out.puts("`rigor sig-gen` skip reasons:")
|
|
80
81
|
@out.puts("")
|
|
@@ -22,11 +22,6 @@ module Rigor
|
|
|
22
22
|
return CLI::EXIT_USAGE if options.nil?
|
|
23
23
|
|
|
24
24
|
config = options.fetch(:config)
|
|
25
|
-
if config && !File.file?(config)
|
|
26
|
-
usage_error("no configuration file at #{config}")
|
|
27
|
-
return CLI::EXIT_USAGE
|
|
28
|
-
end
|
|
29
|
-
|
|
30
25
|
configuration = Configuration.load(config)
|
|
31
26
|
config_path = options.fetch(:config) || Configuration.discover
|
|
32
27
|
# The working directory: Rigor auto-detects sig/ and the lockfiles there (lens.md § Serialization).
|
|
@@ -56,6 +51,11 @@ module Rigor
|
|
|
56
51
|
options
|
|
57
52
|
rescue OptionParser::ParseError => e
|
|
58
53
|
usage_error(e.message)
|
|
54
|
+
rescue Rigor::ConfigurationError => e
|
|
55
|
+
# The shared --config check (CLI::Options.config_path). Mapped here the way Rigor::CLI#run maps it, so a
|
|
56
|
+
# direct LensCommand run exits with the usage code too.
|
|
57
|
+
@err.puts("rigor: #{e.message}")
|
|
58
|
+
nil
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def usage_error(message)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "command"
|
|
4
|
+
require_relative "options"
|
|
4
5
|
require_relative "../runtime/jit"
|
|
5
6
|
|
|
6
7
|
require "optionparser"
|
|
@@ -54,7 +55,7 @@ module Rigor
|
|
|
54
55
|
end
|
|
55
56
|
opts.on("--config=PATH",
|
|
56
57
|
"Session-level default config path (individual tool calls may override)") do |value|
|
|
57
|
-
options[:config] = value
|
|
58
|
+
options[:config] = Options.config_path(value)
|
|
58
59
|
end
|
|
59
60
|
end
|
|
60
61
|
parser.parse!(@argv)
|