rigortype 0.4.5 → 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 +1 -1
- data/docs/handbook/11-sig-gen.md +11 -2
- data/docs/manual/02-cli-reference.md +46 -7
- data/docs/manual/03-configuration.md +2 -1
- data/docs/manual/04-diagnostics.md +1 -1
- data/docs/manual/10-mcp-server.md +9 -1
- data/docs/manual/12-caching.md +8 -1
- 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 +2 -1
- data/lib/rigor/analysis/buffer_binding.rb +14 -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 +4 -0
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
- 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/unreachable_clause_collector.rb +51 -0
- data/lib/rigor/analysis/check_rules.rb +93 -8
- data/lib/rigor/analysis/dependency_recorder.rb +11 -0
- data/lib/rigor/analysis/edit_replay.rb +67 -0
- data/lib/rigor/analysis/incremental.rb +37 -0
- data/lib/rigor/analysis/incremental_session.rb +175 -27
- data/lib/rigor/analysis/path_expansion.rb +41 -0
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +5 -1
- 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 +17 -4
- 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 +156 -49
- data/lib/rigor/analysis/worker_session.rb +14 -2
- data/lib/rigor/cache/incremental_snapshot.rb +30 -7
- data/lib/rigor/cli/check_command.rb +93 -4
- data/lib/rigor/cli/diff_command.rb +2 -1
- 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/plugins_command.rb +5 -0
- 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 +46 -7
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/type_of_project_scope.rb +27 -0
- data/lib/rigor/config_audit.rb +16 -5
- data/lib/rigor/configuration.rb +14 -4
- data/lib/rigor/environment/rbs_loader.rb +115 -6
- 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/callee_mutation_chain.rb +115 -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 +17 -3
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +62 -13
- data/lib/rigor/inference/global_write_census.rb +36 -1
- data/lib/rigor/inference/ivar_writer_census.rb +195 -0
- data/lib/rigor/inference/loop_value_record.rb +53 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +110 -9
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
- data/lib/rigor/inference/method_dispatcher.rb +46 -16
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +12 -0
- data/lib/rigor/inference/mutation_widening.rb +5 -2
- data/lib/rigor/inference/narrowing.rb +66 -5
- data/lib/rigor/inference/object_mixins.rb +13 -1
- data/lib/rigor/inference/project_seed_tables.rb +2 -1
- data/lib/rigor/inference/project_yielder.rb +128 -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 +480 -103
- data/lib/rigor/inference/self_call_ivar_writes.rb +18 -6
- data/lib/rigor/inference/statement_evaluator.rb +658 -90
- 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/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/mcp/server.rb +9 -1
- data/lib/rigor/plugin/base.rb +26 -11
- data/lib/rigor/plugin/manifest.rb +29 -3
- data/lib/rigor/plugin/registry.rb +38 -0
- data/lib/rigor/protection/discovery_seed.rb +57 -2
- data/lib/rigor/rooted_glob.rb +10 -1
- data/lib/rigor/scope.rb +14 -5
- data/lib/rigor/sig_gen/generator.rb +308 -37
- data/lib/rigor/sig_gen/observation_collector.rb +4 -1
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +6 -3
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- data/lib/rigor/type/combinator.rb +4 -3
- data/lib/rigor/type/constant.rb +45 -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-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +60 -43
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +6 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -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 +4 -4
- 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/scope.rbs +31 -50
- data/sig/rigor/type.rbs +0 -6
- metadata +20 -1
|
@@ -12,6 +12,8 @@ require_relative "../cache/file_digest"
|
|
|
12
12
|
require_relative "../cache/incremental_snapshot"
|
|
13
13
|
require_relative "run_cache_key"
|
|
14
14
|
require_relative "path_expansion"
|
|
15
|
+
require_relative "require_relative_closure"
|
|
16
|
+
require_relative "run_disclosure_credit"
|
|
15
17
|
require_relative "../plugin"
|
|
16
18
|
require_relative "../plugin/source_rbs_synthesis_reporter"
|
|
17
19
|
require_relative "../rbs_extended/reporter"
|
|
@@ -75,7 +77,10 @@ module Rigor
|
|
|
75
77
|
# this and serves reused files from it without re-stamping: a run-level row is regenerated
|
|
76
78
|
# every run wherever it is positioned, and slicing the run's full stream by path cached the
|
|
77
79
|
# file-positioned ones (`effect.annotations-unchecked`, `source-rbs-*`).
|
|
78
|
-
:per_file_diagnostics
|
|
80
|
+
:per_file_diagnostics,
|
|
81
|
+
# PR #2286 review M1 — the ADR-16 index this run's pre-pass built; `IncrementalSession` digests it
|
|
82
|
+
# to tell when a class-body DSL edit moved what a served file's dispatch read.
|
|
83
|
+
:synthetic_method_index
|
|
79
84
|
|
|
80
85
|
# Issues #796 / #794 — the two run-level rows a NARROWED run cannot re-derive from the files it
|
|
81
86
|
# analyses: the `definition-build-failed` set (whose producer is the analysis's own demand, and #696
|
|
@@ -109,6 +114,13 @@ module Rigor
|
|
|
109
114
|
pooled.empty? ? @analysis_reads : @analysis_reads.merge(pooled)
|
|
110
115
|
end
|
|
111
116
|
|
|
117
|
+
# Issue #2020 — `{path => [run-disclosure record]}`, the run-scoped rows each analysed file's plugin hooks
|
|
118
|
+
# registered ({RunDisclosureCredit}), sequential and pooled alike; empty unless `record_dependencies:`.
|
|
119
|
+
def analysis_disclosures
|
|
120
|
+
pooled = @pool_coordinator.collected_analysis_disclosures
|
|
121
|
+
pooled.empty? ? @analysis_disclosures : @analysis_disclosures.merge(pooled)
|
|
122
|
+
end
|
|
123
|
+
|
|
112
124
|
# ADR-103 — the run's propagated effect graph, or {Effects::EffectTable.empty} when collection did
|
|
113
125
|
# not run. Reconciled from the sequential and pooled halves exactly as {#file_dependencies} is, then
|
|
114
126
|
# closed by the post-pool fixpoint in {#assemble_run_diagnostics}.
|
|
@@ -294,10 +306,11 @@ module Rigor
|
|
|
294
306
|
# worker continues to build its own Environment.
|
|
295
307
|
# @param discovery_seed — issue #260 — opt-in cross-file discovery tables, keyed by
|
|
296
308
|
# {Scope::DiscoveryIndex} slot name, seeded onto every per-file scope through
|
|
297
|
-
# `project_scope_seed_tables`.
|
|
298
|
-
#
|
|
299
|
-
# judges anchors against,
|
|
300
|
-
#
|
|
309
|
+
# `project_scope_seed_tables`. A `prebuilt:` runner computes no discovery tables of its own, so this is
|
|
310
|
+
# how one learns the project: {Protection::DiagnosticOracle} threads the table set Tier 2's site filter
|
|
311
|
+
# already judges anchors against, and (issue #2147, ADR-124) the LSP's per-buffer publish threads
|
|
312
|
+
# `LanguageServer::ProjectContext#discovery_seed_for`, so a class another file declares is not an
|
|
313
|
+
# external ancestor there. nil (the default) seeds nothing.
|
|
301
314
|
# @param no_tolerated_effects — ADR-103 WD1 / #385 — `rigor check --no-tolerated-effects`.
|
|
302
315
|
# Judges effect envelopes as if `effects.tolerated:` were empty. A judgment-time switch only: the
|
|
303
316
|
# run, its collection and its cache identity are unchanged.
|
|
@@ -309,7 +322,7 @@ module Rigor
|
|
|
309
322
|
seed_bundles: nil, collect_seed_bundles: false, param_inferred_types: nil,
|
|
310
323
|
discovery_seed: nil, no_tolerated_effects: false,
|
|
311
324
|
restored_run_level_rows: nil, served_per_file_diagnostics: nil, served_effect_collections: nil,
|
|
312
|
-
on_file_indexed: nil)
|
|
325
|
+
served_run_disclosures: nil, on_file_indexed: nil)
|
|
313
326
|
@configuration = configuration
|
|
314
327
|
# ADR-113 WD2 — `rigor lens` reads a target's seeded index here: called with `(path, parse_result, scope,
|
|
315
328
|
# index)` for every file this process indexes, after its rules ran and inside the run window, so the reader
|
|
@@ -327,6 +340,9 @@ module Rigor
|
|
|
327
340
|
# effect graph this run closes (and the envelope pass judges) spans the whole project, not just the closure.
|
|
328
341
|
# Never part of {#effect_collections_by_path}, which stays what THIS run collected. Empty on every other run.
|
|
329
342
|
@served_effect_collections = served_effect_collections || {}
|
|
343
|
+
# Issue #2020 — the run-scoped plugin rows ({RunDisclosureCredit}) those unanalysed files registered when
|
|
344
|
+
# they were last analysed, emitted beside this run's own registrations. Empty on every other run.
|
|
345
|
+
@served_run_disclosures = served_run_disclosures || []
|
|
330
346
|
@explain = explain
|
|
331
347
|
@cache_store = enforce_read_only_cache(cache_store, buffer)
|
|
332
348
|
@plugin_requirer = plugin_requirer
|
|
@@ -382,6 +398,7 @@ module Rigor
|
|
|
382
398
|
# rows as they stood around the per-file loop; see {#incremental_slot_rows}. Recorded only by a
|
|
383
399
|
# dependency-recording (incremental) run.
|
|
384
400
|
@analysis_reads = {}
|
|
401
|
+
@analysis_disclosures = {}
|
|
385
402
|
@pre_analysis_plugin_rows = nil
|
|
386
403
|
@post_analysis_plugin_rows = nil
|
|
387
404
|
# Memoised activation decision for the `call.self-undefined-method` rule (nil = not yet computed).
|
|
@@ -521,6 +538,16 @@ module Rigor
|
|
|
521
538
|
end
|
|
522
539
|
end
|
|
523
540
|
|
|
541
|
+
# ADR-45 WD2 (#1507) / #2020 — the per-run reset of what each file's analysis is credited with: its plugin
|
|
542
|
+
# reads, its run-scoped plugin registrations, and the boundary rows around the per-file loop.
|
|
543
|
+
def reset_analysis_attribution
|
|
544
|
+
@analysis_reads = {}
|
|
545
|
+
@analysis_disclosures = {}
|
|
546
|
+
@pre_analysis_plugin_rows = nil
|
|
547
|
+
@post_analysis_plugin_rows = nil
|
|
548
|
+
end
|
|
549
|
+
private :reset_analysis_attribution
|
|
550
|
+
|
|
524
551
|
def run_analysis(paths)
|
|
525
552
|
Inference::MethodDispatcher::FileFolding.fold_platform_specific_paths =
|
|
526
553
|
@configuration.fold_platform_specific_paths
|
|
@@ -548,9 +575,7 @@ module Rigor
|
|
|
548
575
|
# #788 — the per-file reader is assigned only on the analysis (miss) path; reset it here so a run the
|
|
549
576
|
# ADR-45 result cache serves does not answer with the previous run's rows.
|
|
550
577
|
@per_file_diagnostics = [].freeze
|
|
551
|
-
|
|
552
|
-
@pre_analysis_plugin_rows = nil
|
|
553
|
-
@post_analysis_plugin_rows = nil
|
|
578
|
+
reset_analysis_attribution
|
|
554
579
|
# ADR-84 WD2 — roll the return-memo bucket: a fresh frozen token per run makes every per-file scope
|
|
555
580
|
# of THIS run share one memo bucket while entries from any earlier run in this process (stale after
|
|
556
581
|
# an edit) become unreachable.
|
|
@@ -1414,7 +1439,7 @@ module Rigor
|
|
|
1414
1439
|
end
|
|
1415
1440
|
|
|
1416
1441
|
def build_run_dependency_descriptor(expansion, rbs_descriptor)
|
|
1417
|
-
entries = analyzed_file_entries(expansion) + discovery_file_entries(expansion) +
|
|
1442
|
+
entries = analyzed_file_entries(expansion) + discovery_file_entries(expansion) + required_missing_entries +
|
|
1418
1443
|
pre_eval_file_entries + template_unit_file_entries + rbs_descriptor.files +
|
|
1419
1444
|
root_presence_entries(expansion)
|
|
1420
1445
|
# #979 — the signature ROOTS' listings, one glob row each. `rbs_descriptor.files` covers only the
|
|
@@ -1509,7 +1534,8 @@ module Rigor
|
|
|
1509
1534
|
IncrementalSlotRows.new(
|
|
1510
1535
|
observed: plugin_boundary_rows,
|
|
1511
1536
|
derived: Cache::Descriptor.new(
|
|
1512
|
-
files: template_unit_file_entries + kind_entries(Array(roots) + @configuration.pre_eval)
|
|
1537
|
+
files: template_unit_file_entries + kind_entries(Array(roots) + @configuration.pre_eval) +
|
|
1538
|
+
required_missing_entries,
|
|
1513
1539
|
globs: template_unit_glob_entries
|
|
1514
1540
|
),
|
|
1515
1541
|
by_file: @analysis_reads.transform_values do |reads|
|
|
@@ -1568,7 +1594,8 @@ module Rigor
|
|
|
1568
1594
|
BaselineRows.new(
|
|
1569
1595
|
owned: Cache::Descriptor.new(
|
|
1570
1596
|
files: own.files + kind_entries(signature_root_candidates) +
|
|
1571
|
-
discovery_file_entries({ files: files }) +
|
|
1597
|
+
discovery_file_entries({ files: files }) + required_missing_entries +
|
|
1598
|
+
unanalysed_pre_eval_entries(files),
|
|
1572
1599
|
globs: (own.globs + discovery_glob_entries).uniq
|
|
1573
1600
|
),
|
|
1574
1601
|
pinned: pinned
|
|
@@ -1808,20 +1835,19 @@ module Rigor
|
|
|
1808
1835
|
end
|
|
1809
1836
|
|
|
1810
1837
|
# Internal: builds the deferred cross-file discovery tables at most once per run and adopts them.
|
|
1811
|
-
# Memoised on `@project_discovery_done` (reset at the start of `#run`). No-op under `@prebuilt
|
|
1812
|
-
#
|
|
1813
|
-
#
|
|
1814
|
-
#
|
|
1815
|
-
#
|
|
1816
|
-
#
|
|
1817
|
-
# cache HIT (which never assembles) never pays the double parse.
|
|
1838
|
+
# Memoised on `@project_discovery_done` (reset at the start of `#run`). No-op under `@prebuilt`: the
|
|
1839
|
+
# snapshot carries no discovery tables, and a per-request re-walk of the project is what `prebuilt:` exists
|
|
1840
|
+
# to avoid. A caller that wants cross-file knowledge under `prebuilt:` supplies it through `discovery_seed:`
|
|
1841
|
+
# (issue #260), as the LSP publish does since issue #2147 (ADR-124). Called eagerly from `#run` for the
|
|
1842
|
+
# recording / subset (ADR-46) modes and lazily from `#assemble_run_diagnostics` on the analysis path, so a
|
|
1843
|
+
# warm cache HIT (which never assembles) never pays the double parse.
|
|
1818
1844
|
def ensure_project_discovery(expansion)
|
|
1819
1845
|
return if @prebuilt
|
|
1820
1846
|
return if @project_discovery_done
|
|
1821
1847
|
|
|
1822
1848
|
@project_discovery_done = true
|
|
1823
1849
|
# Issue #684 — the cross-file index spans the PROJECT even when the analysis targets a file list.
|
|
1824
|
-
expansion = @project_discovery_expansion || once_each(expansion)
|
|
1850
|
+
expansion = buffer_spelled(@project_discovery_expansion || once_each(expansion))
|
|
1825
1851
|
if @collect_seed_bundles
|
|
1826
1852
|
# ADR-85 WD2 — rebuild discovery from the prior run's bundles (re-walking only changed files) and
|
|
1827
1853
|
# capture the refreshed bundle set for the session to persist.
|
|
@@ -1853,7 +1879,13 @@ module Rigor
|
|
|
1853
1879
|
# on what else was on the command line.
|
|
1854
1880
|
#
|
|
1855
1881
|
# Excluded, each because its expansion is already the one it means: the LSP `prebuilt:` path (which
|
|
1856
|
-
# seeds discovery from a snapshot
|
|
1882
|
+
# seeds discovery from a snapshot, ADR-124) and `run_source`'s in-memory single file.
|
|
1883
|
+
#
|
|
1884
|
+
# Issue #2165 — editor `buffer:` mode (`rigor check --tmp-file T --instead-of F F`) widens like a plain run.
|
|
1885
|
+
# It was excluded on the reading that its expansion is already the one it means, but an editor passes the
|
|
1886
|
+
# buffer's own path, so discovery saw that file alone: every class another file declares read as an external
|
|
1887
|
+
# ancestor, and the run reported what `rigor check F` does not. The discovery pre-pass reads through the
|
|
1888
|
+
# binding, so the buffer's file is walked from the buffer's bytes.
|
|
1857
1889
|
#
|
|
1858
1890
|
# Issue #1583 — ADR-46's `analyze_only` subset mode widens like a full run. It was excluded on the
|
|
1859
1891
|
# reading that it "already passes the full expansion", which holds only when the incremental session
|
|
@@ -1867,15 +1899,51 @@ module Rigor
|
|
|
1867
1899
|
# the pre-pass then saw two writers for every constant a file declares and withheld its publication (#644),
|
|
1868
1900
|
# and every file was a discovered-not-analysed dependency of the run's cache entry.
|
|
1869
1901
|
#
|
|
1902
|
+
# Issue #2021 — an analysed file outside the configured paths has no project there, so the widened set also
|
|
1903
|
+
# holds what it loads with a literal `require_relative` ({#required_outside_paths}). A plugin file checked on
|
|
1904
|
+
# its own otherwise missed its sibling's nested `Effects` and resolved the project's `Rigor::Effects`.
|
|
1905
|
+
#
|
|
1870
1906
|
# The widening decision itself compares the paths as spelled. `exclude:` patterns apply to both spellings
|
|
1871
1907
|
# alike (#1576), so the configured absolute `lib` and the argument `lib` exclude the same files.
|
|
1872
1908
|
def project_discovery_expansion(paths, expansion)
|
|
1873
1909
|
return nil unless widen_discovery_to_project?(paths)
|
|
1874
1910
|
|
|
1875
1911
|
analysed = expansion.fetch(:files)
|
|
1876
|
-
|
|
1912
|
+
project = expand_paths(@configuration.paths | paths).fetch(:files)
|
|
1913
|
+
walked = required_outside_paths(analysed)
|
|
1914
|
+
files = discovery_files(project + walked.found, analysed)
|
|
1877
1915
|
# Overlapping arguments (`lib lib/user.rb`) name an analysed file twice; the widened set names it once.
|
|
1878
|
-
files.size > analysed.uniq { |path| File.absolute_path(path) }.size ?
|
|
1916
|
+
return nil unless files.size > analysed.uniq { |path| File.absolute_path(path) }.size || walked.missing.any?
|
|
1917
|
+
|
|
1918
|
+
{ files: files, errors: [], uncertain: walked.found.to_set.freeze, missing: walked.missing }
|
|
1919
|
+
end
|
|
1920
|
+
|
|
1921
|
+
# Issue #2021 — what the analysed files outside the configured `paths:` load with a literal `require_relative`,
|
|
1922
|
+
# through targets that are themselves outside them ({RequireRelativeClosure}). A file under a configured path
|
|
1923
|
+
# is discovered already, so the walk adds nothing for a run whose files all lie inside them. The comparison is
|
|
1924
|
+
# by real path, so a symlinked spelling of a configured file does not join discovery a second time (two
|
|
1925
|
+
# writers would withhold its constants, #644). `exclude:` drops a target from discovery as it drops a
|
|
1926
|
+
# configured file; the walk still follows what an excluded target requires.
|
|
1927
|
+
#
|
|
1928
|
+
# The found files are discovered as `uncertain` ({Analysis::Runner::ProjectPrePasses#uncertain_paths}): the walk
|
|
1929
|
+
# sees one entry point's load graph, and a file it does not reach may redefine what they define.
|
|
1930
|
+
def required_outside_paths(analysed)
|
|
1931
|
+
roots = @configuration.paths.map { |root| RequireRelativeClosure.real_path(root) }
|
|
1932
|
+
covered = ->(real) { roots.any? { |root| real == root || real.start_with?(File.join(root, "")) } }
|
|
1933
|
+
seeds = analysed.reject { |path| covered.call(RequireRelativeClosure.real_path(path)) }
|
|
1934
|
+
return RequireRelativeClosure::Result.new(found: [], missing: []) if seeds.empty?
|
|
1935
|
+
|
|
1936
|
+
read = ->(path) { File.read(@buffer ? @buffer.resolve(path) : path) }
|
|
1937
|
+
walked = RequireRelativeClosure.closure(seeds, covered: covered, read: read)
|
|
1938
|
+
walked.with(found: PathExpansion.reject_excluded(walked.found, @configuration.exclude_patterns,
|
|
1939
|
+
base: @configuration.exclude_base))
|
|
1940
|
+
end
|
|
1941
|
+
|
|
1942
|
+
# Issue #2021 — a kind row per `require_relative` target the walk named and found absent: a file appearing
|
|
1943
|
+
# there joins the discovery, so it moves the answer of the run-result cache and the `--incremental` slot.
|
|
1944
|
+
def required_missing_entries
|
|
1945
|
+
missing = @project_discovery_expansion&.fetch(:missing, nil)
|
|
1946
|
+
missing.nil? || missing.empty? ? [] : kind_entries(missing)
|
|
1879
1947
|
end
|
|
1880
1948
|
|
|
1881
1949
|
# The expansion with each file once, first spelling kept: overlapping arguments (`lib ./lib`) that do not
|
|
@@ -1887,6 +1955,23 @@ module Rigor
|
|
|
1887
1955
|
unique.size == files.size ? expansion : expansion.merge(files: unique)
|
|
1888
1956
|
end
|
|
1889
1957
|
|
|
1958
|
+
# Issue #2165 — the discovery set with the buffer's file under the binding's own spelling, once. The binding
|
|
1959
|
+
# substitutes bytes only for a path spelled exactly as its logical path, while the file can reach discovery as
|
|
1960
|
+
# the argument's spelling, the configured one, or through a symlink; each of those read it from disk, and a
|
|
1961
|
+
# second spelling made it a second declarer. Every member whose real path is the buffer's file is replaced by
|
|
1962
|
+
# the logical path, and the duplicates that leaves are dropped (`IncrementalSession#rebind_buffer` is option
|
|
1963
|
+
# B's counterpart).
|
|
1964
|
+
def buffer_spelled(expansion)
|
|
1965
|
+
return expansion if @buffer.nil?
|
|
1966
|
+
|
|
1967
|
+
target = Analysis::BufferBinding.real_path(@buffer.logical_path)
|
|
1968
|
+
files = expansion.fetch(:files)
|
|
1969
|
+
spelled = files.map do |path|
|
|
1970
|
+
Analysis::BufferBinding.real_path(path) == target ? @buffer.logical_path : path
|
|
1971
|
+
end.uniq
|
|
1972
|
+
spelled == files ? expansion : expansion.merge(files: spelled)
|
|
1973
|
+
end
|
|
1974
|
+
|
|
1890
1975
|
# Each file once, in project order, under the spelling the analysis uses when it is analysed.
|
|
1891
1976
|
def discovery_files(project_files, analysed_files)
|
|
1892
1977
|
spelling = analysed_files.to_h { |path| [File.absolute_path(path), path] }
|
|
@@ -1898,7 +1983,7 @@ module Rigor
|
|
|
1898
1983
|
end
|
|
1899
1984
|
|
|
1900
1985
|
def widen_discovery_to_project?(paths)
|
|
1901
|
-
@prebuilt.nil? && @
|
|
1986
|
+
@prebuilt.nil? && @in_memory_sources.nil? && paths != @configuration.paths
|
|
1902
1987
|
end
|
|
1903
1988
|
|
|
1904
1989
|
# ADR-46 — the dependency-recording and subset-analysis modes read the discovery tables OUTSIDE the
|
|
@@ -1912,7 +1997,8 @@ module Rigor
|
|
|
1912
1997
|
|
|
1913
1998
|
private :run_project_pre_passes, :adopt_prebuilt_project_scan, :apply_pre_passes_result,
|
|
1914
1999
|
:apply_discovery_result, :ensure_project_discovery, :force_eager_discovery?,
|
|
1915
|
-
:project_discovery_expansion, :discovery_files, :once_each, :widen_discovery_to_project
|
|
2000
|
+
:project_discovery_expansion, :discovery_files, :once_each, :widen_discovery_to_project?,
|
|
2001
|
+
:required_outside_paths, :required_missing_entries
|
|
1916
2002
|
|
|
1917
2003
|
# Ruby versions probed (ascending) to discover the lowest one this Prism build accepts for
|
|
1918
2004
|
# `version:`. Prism exposes no version list, so the floor is found empirically — only when a
|
|
@@ -2102,6 +2188,7 @@ module Rigor
|
|
|
2102
2188
|
hkt_scan_failure_snapshot: -> { @snapshots.hkt_scan_failure },
|
|
2103
2189
|
conformance_results_snapshot: -> { @snapshots.conformance_results },
|
|
2104
2190
|
pooled_run_disclosures: -> { @pool_coordinator.collected_run_disclosures },
|
|
2191
|
+
served_run_disclosures: -> { @served_run_disclosures },
|
|
2105
2192
|
pooled_trust_refusals: -> { @pool_coordinator.collected_trust_refusals }
|
|
2106
2193
|
)
|
|
2107
2194
|
end
|
|
@@ -2249,10 +2336,8 @@ module Rigor
|
|
|
2249
2336
|
# client editing a brand-new file.
|
|
2250
2337
|
elsif accept_as_ruby_file?(path)
|
|
2251
2338
|
files << path
|
|
2252
|
-
elsif File.exist?(path)
|
|
2253
|
-
bad << [path, "not a Ruby file (expected `.rb` or a directory)"]
|
|
2254
2339
|
else
|
|
2255
|
-
bad << [path,
|
|
2340
|
+
bad << [path, PathExpansion.unusable_message(path)]
|
|
2256
2341
|
end
|
|
2257
2342
|
end
|
|
2258
2343
|
{ files: files, roots: Array(paths).map(&:to_s), errors: path_expansion_errors(bad, any_files: files.any?) }
|
|
@@ -2262,27 +2347,24 @@ module Rigor
|
|
|
2262
2347
|
# app lib` with no `lib/` (the 20260620 field trial's strap case) analyses `app` instead of aborting
|
|
2263
2348
|
# on exit 1. A bad path that leaves NOTHING to analyse stays an error so a lone typo (`rigor check
|
|
2264
2349
|
# typo.rb`) is not silently masked (the regression the path-error check was added to catch in the
|
|
2265
|
-
# first place).
|
|
2350
|
+
# first place). The wording and grading live in {PathExpansion.diagnostics}, shared with `sig-gen` (#1991).
|
|
2266
2351
|
def path_expansion_errors(bad, any_files:)
|
|
2267
|
-
|
|
2268
|
-
suffix = any_files ? " (skipped)" : ""
|
|
2269
|
-
bad.map { |path, message| path_error(path, "#{message}#{suffix}", severity: severity) }
|
|
2352
|
+
PathExpansion.diagnostics(bad, any_files: any_files)
|
|
2270
2353
|
end
|
|
2271
2354
|
|
|
2355
|
+
# #2191 — the buffer's logical path is accepted under any spelling that names the same file (relative, absolute,
|
|
2356
|
+
# or through a symlinked root), since the discovery set respells it to the logical path (`buffer_spelled`).
|
|
2272
2357
|
def accept_as_ruby_file?(path)
|
|
2273
2358
|
(File.file?(path) && path.end_with?(".rb")) ||
|
|
2274
|
-
(
|
|
2359
|
+
buffer_path?(path) ||
|
|
2275
2360
|
@in_memory_sources&.key?(path)
|
|
2276
2361
|
end
|
|
2277
2362
|
|
|
2278
|
-
def
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
message: message,
|
|
2284
|
-
severity: severity
|
|
2285
|
-
)
|
|
2363
|
+
def buffer_path?(path)
|
|
2364
|
+
return false if @buffer.nil?
|
|
2365
|
+
|
|
2366
|
+
path == @buffer.logical_path ||
|
|
2367
|
+
Analysis::BufferBinding.real_path(path) == Analysis::BufferBinding.real_path(@buffer.logical_path)
|
|
2286
2368
|
end
|
|
2287
2369
|
|
|
2288
2370
|
# Reads + parses the source at `path`. Under editor mode (`@buffer` set) reads bytes from
|
|
@@ -2481,12 +2563,31 @@ module Rigor
|
|
|
2481
2563
|
# widener erases it to the class, and listing a file under `pre_eval:` must never LOSE precision. The
|
|
2482
2564
|
# per-file table still wins over both (`ScopeIndexer.index`'s merge) — same-file stays the most
|
|
2483
2565
|
# specific authority.
|
|
2566
|
+
#
|
|
2567
|
+
# ADR-124 — a `prebuilt:` runner computes no literal table of its own; a `discovery_seed:` (the editor
|
|
2568
|
+
# publish, ADR-124) brings the project's, which then plays the same part.
|
|
2484
2569
|
def merged_seed_constants
|
|
2485
2570
|
pre_eval = publishable_pre_eval_constants
|
|
2486
|
-
|
|
2487
|
-
return
|
|
2571
|
+
literal = literal_seed_constants
|
|
2572
|
+
return literal if pre_eval.empty?
|
|
2573
|
+
return pre_eval if literal.empty?
|
|
2574
|
+
|
|
2575
|
+
pre_eval.merge(literal).freeze
|
|
2576
|
+
end
|
|
2577
|
+
|
|
2578
|
+
# The project's literal constant table: this run's, or the one a `discovery_seed:` carries (ADR-124).
|
|
2579
|
+
def literal_seed_constants
|
|
2580
|
+
return @project_constant_values unless @project_constant_values.empty? && @discovery_seed
|
|
2581
|
+
|
|
2582
|
+
@discovery_seed.fetch(:in_source_constants, @project_constant_values)
|
|
2583
|
+
end
|
|
2584
|
+
|
|
2585
|
+
# The per-name writer paths the #663 retraction reads: this run's census, or the one a `discovery_seed:` carries
|
|
2586
|
+
# (ADR-124).
|
|
2587
|
+
def constant_writer_sources
|
|
2588
|
+
return @project_constant_sources unless @project_constant_sources.empty? && @discovery_seed
|
|
2488
2589
|
|
|
2489
|
-
|
|
2590
|
+
@discovery_seed.fetch(:constant_sources, @project_constant_sources)
|
|
2490
2591
|
end
|
|
2491
2592
|
|
|
2492
2593
|
# Issue #663 — the `pre_eval:` publication with every name an UNLISTED file also assigns retracted.
|
|
@@ -2515,8 +2616,9 @@ module Rigor
|
|
|
2515
2616
|
# name of its own for the wildcard's paths to have been folded into.
|
|
2516
2617
|
def unlisted_constant_writer?(name, listed)
|
|
2517
2618
|
segment = name.split("::").last
|
|
2518
|
-
|
|
2519
|
-
|
|
2619
|
+
sources = constant_writer_sources
|
|
2620
|
+
writers = sources[name]
|
|
2621
|
+
wildcard = sources["#{Inference::ScopeIndexer::DYNAMIC_TARGET_PREFIX}#{segment}"]
|
|
2520
2622
|
[writers, wildcard].any? do |paths|
|
|
2521
2623
|
paths&.any? { |path| !listed.include?(File.absolute_path(path)) }
|
|
2522
2624
|
end
|
|
@@ -2560,13 +2662,18 @@ module Rigor
|
|
|
2560
2662
|
@pre_analysis_plugin_rows ||= plugin_boundary_rows
|
|
2561
2663
|
reads = []
|
|
2562
2664
|
diagnostics = nil
|
|
2563
|
-
record =
|
|
2564
|
-
|
|
2565
|
-
|
|
2665
|
+
record = nil
|
|
2666
|
+
# Issue #2020 — and every run-scoped row its analysis registered, with the registering plugins' reads.
|
|
2667
|
+
disclosures = RunDisclosureCredit.collect do
|
|
2668
|
+
record = Plugin::IoBoundary.attributing(reads) do
|
|
2669
|
+
DependencyRecorder.record_for(path) do
|
|
2670
|
+
diagnostics = analyze_with_effects(path, environment)
|
|
2671
|
+
end
|
|
2566
2672
|
end
|
|
2567
2673
|
end
|
|
2568
2674
|
@file_dependencies[path] = record
|
|
2569
2675
|
@analysis_reads[path] = reads unless reads.empty?
|
|
2676
|
+
@analysis_disclosures[path] = disclosures unless disclosures.empty?
|
|
2570
2677
|
diagnostics
|
|
2571
2678
|
end
|
|
2572
2679
|
|
|
@@ -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"
|
|
@@ -171,12 +172,17 @@ module Rigor
|
|
|
171
172
|
|
|
172
173
|
diagnostics = nil
|
|
173
174
|
reads = []
|
|
174
|
-
record =
|
|
175
|
-
|
|
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
|
|
176
181
|
end
|
|
177
182
|
@file_dependencies[path] = record
|
|
178
183
|
# Issue #1553 — the plugin reads this file's analysis made, as `Runner#analyze_file` keeps them.
|
|
179
184
|
(@analysis_reads ||= {})[path] = reads unless reads.empty?
|
|
185
|
+
(@analysis_disclosures ||= {})[path] = disclosures unless disclosures.empty?
|
|
180
186
|
diagnostics
|
|
181
187
|
end
|
|
182
188
|
|
|
@@ -320,6 +326,12 @@ module Rigor
|
|
|
320
326
|
@analysis_reads || {}
|
|
321
327
|
end
|
|
322
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
|
+
|
|
323
335
|
# ADR-103 WD12 — the per-file {Effects::FileCollection}s this session collected (empty unless the
|
|
324
336
|
# configuration carries an `effects:` block). Marshal-clean by construction — frozen Hashes of
|
|
325
337
|
# Strings, `LabelSet`s of frozen Arrays, and `Data` edges — so the fork pool ships them back to the
|
|
@@ -182,7 +182,20 @@ module Rigor
|
|
|
182
182
|
# 41: issue #1583 — the payload gains `discovery_digests`, the packed digest of each file a run over path
|
|
183
183
|
# arguments discovered without analysing, which a recheck compares to re-baseline when one moved. A pre-41
|
|
184
184
|
# blob carries none; the gate rejects it and the next run is cold.
|
|
185
|
-
|
|
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
|
|
186
199
|
|
|
187
200
|
# The persisted per-file state.
|
|
188
201
|
# `cache` maps an analyzed file to its diagnostics.
|
|
@@ -244,16 +257,24 @@ module Rigor
|
|
|
244
257
|
# `effect_collections` (nil when collection was off). A plugin stamps its attributions and edges into a file's
|
|
245
258
|
# collection, and `effects_identity` is plugin-blind (it is compared before any plugin loads), so the session
|
|
246
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.
|
|
247
264
|
Payload = Data.define(:cache, :sources, :digests, :analyzed,
|
|
248
265
|
:symbol_sources, :ancestry_sources, :symbol_fingerprints,
|
|
249
266
|
:missing, :class_decls, :constant_decls, :seed_bundles, :plugin_fact_digest,
|
|
250
267
|
:return_summaries, :param_table,
|
|
251
268
|
:effect_collections, :effects_identity, :run_level_rows,
|
|
252
|
-
:effect_plugin_digest, :plugin_reads, :discovery_digests
|
|
269
|
+
:effect_plugin_digest, :plugin_reads, :discovery_digests, :plugin_disclosures,
|
|
270
|
+
:synthetic_surface_digest) do
|
|
253
271
|
# `effect_plugin_digest` defaults to nil so a payload built without it (an older caller, a spec fixture)
|
|
254
272
|
# reads as "unknown", which the session treats as moved. `plugin_reads` defaults to no reads, and
|
|
255
|
-
# `discovery_digests` to no discovery-only files.
|
|
256
|
-
|
|
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)
|
|
257
278
|
super
|
|
258
279
|
end
|
|
259
280
|
end
|
|
@@ -465,7 +486,9 @@ module Rigor
|
|
|
465
486
|
run_level_rows: data[:run_level_rows],
|
|
466
487
|
effect_plugin_digest: data[:effect_plugin_digest],
|
|
467
488
|
plugin_reads: data[:plugin_reads] || {},
|
|
468
|
-
|
|
489
|
+
plugin_disclosures: data.fetch(:plugin_disclosures, {}),
|
|
490
|
+
discovery_digests: data.fetch(:discovery_digests, {}),
|
|
491
|
+
synthetic_surface_digest: data[:synthetic_surface_digest]
|
|
469
492
|
)
|
|
470
493
|
end
|
|
471
494
|
private :payload_from
|
|
@@ -492,8 +515,8 @@ module Rigor
|
|
|
492
515
|
effects_identity: payload.effects_identity,
|
|
493
516
|
run_level_rows: payload.run_level_rows,
|
|
494
517
|
effect_plugin_digest: payload.effect_plugin_digest,
|
|
495
|
-
plugin_reads: payload.plugin_reads,
|
|
496
|
-
discovery_digests: payload.discovery_digests
|
|
518
|
+
plugin_reads: payload.plugin_reads, plugin_disclosures: payload.plugin_disclosures,
|
|
519
|
+
discovery_digests: payload.discovery_digests, synthetic_surface_digest: payload.synthetic_surface_digest
|
|
497
520
|
)
|
|
498
521
|
# #1507 — every `--incremental` run that analysed anything rewrites this whole blob, so its deflate is
|
|
499
522
|
# on the edit path; see {Store::DEFLATE_LEVEL}.
|
|
@@ -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
|