rigortype 0.3.5 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +5 -5
- data/data/builtins/ruby_core/date.yml +6 -3
- data/data/builtins/ruby_core/enumerable.yml +1 -0
- data/data/builtins/ruby_core/exception.yml +2 -1
- data/data/builtins/ruby_core/file.yml +28 -14
- data/data/builtins/ruby_core/hash.yml +5 -2
- data/data/builtins/ruby_core/io.yml +33 -16
- data/data/builtins/ruby_core/random.yml +2 -1
- data/data/builtins/ruby_core/re.yml +6 -3
- data/data/builtins/ruby_core/struct.yml +2 -0
- data/data/builtins/ruby_core/time.yml +2 -1
- data/data/gem_overlay/activesupport/core_ext.rbs +442 -11
- data/docs/handbook/02-everyday-types.md +1 -1
- data/docs/handbook/04-tuples-and-shapes.md +1 -1
- data/docs/handbook/08-understanding-errors.md +1 -1
- data/docs/handbook/09-plugins.md +2 -2
- data/docs/handbook/10-sorbet.md +1 -1
- data/docs/handbook/README.md +2 -2
- data/docs/handbook/appendix-go.md +1 -1
- data/docs/handbook/appendix-java-csharp.md +2 -2
- data/docs/handbook/appendix-mypy.md +1 -1
- data/docs/handbook/appendix-protocols-and-structural-typing.md +2 -2
- data/docs/handbook/appendix-rust.md +1 -1
- data/docs/handbook/appendix-type-theory.md +2 -2
- data/docs/handbook/appendix-typescript.md +4 -4
- data/docs/manual/02-cli-reference.md +29 -16
- data/docs/manual/03-configuration.md +1 -1
- data/docs/manual/04-diagnostics.md +12 -0
- data/docs/manual/05-inspecting-types.md +20 -8
- data/docs/manual/07-plugins.md +4 -0
- data/docs/manual/08-skills.md +1 -1
- data/docs/manual/11-ci.md +1 -1
- data/docs/manual/12-caching.md +1 -1
- data/docs/manual/15-type-protection-coverage.md +1 -1
- data/docs/manual/18-removing-dead-code.md +13 -4
- data/docs/manual/19-effect-labels.md +19 -12
- data/docs/manual/README.md +2 -2
- data/docs/manual/plugins/rigor-actionmailer.md +4 -4
- data/docs/manual/plugins/rigor-actionpack.md +61 -0
- data/docs/manual/plugins/rigor-activejob.md +3 -3
- data/docs/manual/plugins/rigor-activerecord.md +78 -5
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
- data/docs/manual/plugins/rigor-rails-i18n.md +5 -5
- data/docs/manual/plugins/rigor-rspec.md +6 -2
- data/docs/manual/plugins/rigor-sidekiq.md +22 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +12 -0
- data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +98 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -3
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +199 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +1 -2
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -2
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +6 -12
- data/lib/rigor/analysis/check_rules.rb +383 -48
- data/lib/rigor/analysis/crash_signature.rb +116 -0
- data/lib/rigor/analysis/dependency_recorder.rb +18 -3
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/diagnostic.rb +17 -6
- data/lib/rigor/analysis/effects_cache_probe.rb +132 -0
- data/lib/rigor/analysis/incremental.rb +28 -0
- data/lib/rigor/analysis/incremental_session.rb +44 -9
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +1 -2
- data/lib/rigor/analysis/reachability/graph.rb +44 -16
- data/lib/rigor/analysis/reachability/plugin_roots.rb +11 -8
- data/lib/rigor/analysis/reachability/scan.rb +27 -7
- data/lib/rigor/analysis/reachability/scan_cache.rb +130 -0
- data/lib/rigor/analysis/result.rb +32 -0
- data/lib/rigor/analysis/run_cache_key.rb +53 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +93 -1
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +14 -8
- data/lib/rigor/analysis/runner/pool_coordinator.rb +34 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
- data/lib/rigor/analysis/runner/run_snapshots.rb +7 -2
- data/lib/rigor/analysis/runner.rb +298 -38
- data/lib/rigor/analysis/worker_session.rb +10 -1
- data/lib/rigor/bleeding_edge.rb +7 -6
- data/lib/rigor/builtins/hkt_builtins.rb +45 -6
- data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
- data/lib/rigor/builtins/static_return_refinements.rb +20 -1
- data/lib/rigor/cache/descriptor.rb +42 -2
- data/lib/rigor/cache/file_digest.rb +20 -2
- data/lib/rigor/cache/incremental_snapshot.rb +40 -3
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +31 -8
- data/lib/rigor/cache/store.rb +10 -1
- data/lib/rigor/cli/check_command.rb +67 -48
- data/lib/rigor/cli/coverage_command.rb +28 -24
- data/lib/rigor/cli/coverage_mutation.rb +13 -17
- data/lib/rigor/cli/coverage_scan.rb +47 -6
- data/lib/rigor/cli/doc_links.rb +100 -0
- data/lib/rigor/cli/docs_command.rb +32 -2
- data/lib/rigor/cli/effects_command.rb +27 -2
- data/lib/rigor/cli/effects_diff_renderer.rb +82 -12
- data/lib/rigor/cli/effects_explain_renderer.rb +6 -3
- data/lib/rigor/cli/effects_snapshot_command.rb +52 -7
- data/lib/rigor/cli/fused_protection_renderer.rb +19 -2
- data/lib/rigor/cli/fused_protection_report.rb +23 -2
- data/lib/rigor/cli/measurement_integrity_warning.rb +57 -0
- data/lib/rigor/cli/mutation_protection_renderer.rb +22 -2
- data/lib/rigor/cli/mutation_protection_report.rb +31 -2
- data/lib/rigor/cli/sig_gen_command.rb +18 -0
- data/lib/rigor/cli/type_of_command.rb +188 -41
- data/lib/rigor/cli/type_of_renderer.rb +72 -3
- data/lib/rigor/cli/type_scan_command.rb +10 -1
- data/lib/rigor/cli/unused_command.rb +65 -16
- data/lib/rigor/cli.rb +22 -9
- data/lib/rigor/configuration.rb +7 -4
- data/lib/rigor/effects/definition_lines.rb +100 -0
- data/lib/rigor/effects/envelope_check.rb +18 -1
- data/lib/rigor/effects/liskov_check.rb +17 -8
- data/lib/rigor/effects/plugin_facts.rb +1 -1
- data/lib/rigor/effects/scanner.rb +2 -3
- data/lib/rigor/effects/signature_sources.rb +13 -2
- data/lib/rigor/effects/snapshot.rb +53 -21
- data/lib/rigor/effects/snapshot_diff.rb +26 -3
- data/lib/rigor/environment/bundle_sig_discovery.rb +64 -13
- data/lib/rigor/environment/lockfile_resolver.rb +15 -5
- data/lib/rigor/environment/missing_gem_constant_index.rb +45 -4
- data/lib/rigor/environment/rbs_hierarchy.rb +16 -22
- data/lib/rigor/environment/rbs_loader.rb +525 -46
- data/lib/rigor/environment.rb +38 -15
- data/lib/rigor/inference/acceptance.rb +172 -20
- data/lib/rigor/inference/captured_locals.rb +62 -0
- data/lib/rigor/inference/content_join.rb +347 -0
- data/lib/rigor/inference/def_handle.rb +15 -3
- data/lib/rigor/inference/def_node_resolver.rb +62 -3
- data/lib/rigor/inference/expression_typer.rb +1020 -141
- data/lib/rigor/inference/fork_map.rb +6 -1
- data/lib/rigor/inference/hkt_reducer.rb +12 -1
- data/lib/rigor/inference/hkt_registry.rb +46 -0
- data/lib/rigor/inference/hkt_sugar_translator.rb +93 -0
- data/lib/rigor/inference/index_write_widening.rb +48 -0
- data/lib/rigor/inference/indexed_narrowing.rb +14 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +7 -5
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +10 -0
- data/lib/rigor/inference/method_dispatcher/json_folding.rb +58 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +81 -38
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +54 -17
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -2
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +63 -10
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +104 -0
- data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +81 -0
- data/lib/rigor/inference/method_dispatcher.rb +155 -14
- data/lib/rigor/inference/method_parameter_binder.rb +3 -3
- data/lib/rigor/inference/mutation_widening.rb +244 -174
- data/lib/rigor/inference/narrowing.rb +331 -27
- data/lib/rigor/inference/parameter_inference_collector.rb +1 -0
- data/lib/rigor/inference/pre_eval_constants.rb +15 -0
- data/lib/rigor/inference/precision_scanner.rb +5 -2
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +89 -39
- data/lib/rigor/inference/scope_indexer.rb +1493 -140
- data/lib/rigor/inference/statement_evaluator.rb +276 -91
- data/lib/rigor/inference/struct_fold_safety.rb +186 -27
- data/lib/rigor/inference/synthetic_method_scanner.rb +7 -0
- data/lib/rigor/inference/version_guard.rb +229 -0
- data/lib/rigor/language_server/selection_range_provider.rb +1 -1
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/inflector.rb +14 -5
- data/lib/rigor/plugin/io_boundary.rb +107 -4
- data/lib/rigor/plugin/loader.rb +14 -0
- data/lib/rigor/protection/analysis_guard.rb +60 -0
- data/lib/rigor/protection/closure_kill_oracle.rb +43 -38
- data/lib/rigor/protection/diagnostic_oracle.rb +12 -4
- data/lib/rigor/protection/discovery_seed.rb +1 -1
- data/lib/rigor/protection/measurement_integrity.rb +36 -0
- data/lib/rigor/protection/mutation_scanner.rb +61 -6
- data/lib/rigor/protection/mutator.rb +121 -18
- data/lib/rigor/rbs_extended/envelope_scanner.rb +9 -0
- data/lib/rigor/reflection.rb +182 -30
- data/lib/rigor/scope/discovery_index.rb +39 -1
- data/lib/rigor/scope.rb +385 -4
- data/lib/rigor/sig_gen/classification.rb +12 -1
- data/lib/rigor/sig_gen/generator.rb +254 -5
- data/lib/rigor/sig_gen/renderer.rb +1 -6
- data/lib/rigor/sig_gen/writer.rb +3 -0
- data/lib/rigor/source/constant_path.rb +79 -2
- data/lib/rigor/triage/catalogue.rb +1 -1
- data/lib/rigor/type/combinator.rb +10 -0
- data/lib/rigor/type/maybe.rb +47 -0
- data/lib/rigor/type/refined.rb +1 -2
- data/lib/rigor/type/result.rb +53 -0
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +3 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +58 -8
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +15 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +4 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +3 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +39 -14
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +36 -3
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +21 -5
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +47 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +13 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +242 -4
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +3 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +7 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +62 -11
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +15 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +3 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +5 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +798 -22
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +156 -13
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +140 -40
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +20 -6
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +14 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -3
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +23 -6
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +264 -9
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +876 -23
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +99 -0
- data/plugins/rigor-dry-monads/lib/rigor-dry-monads.rb +5 -0
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +34 -0
- data/plugins/rigor-ethon/lib/rigor-ethon.rb +3 -0
- data/plugins/rigor-ethon/sig/ethon.rbs +27 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +198 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/binding_recognizer.rb +75 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/catalog.rb +64 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +173 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +46 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +161 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +121 -0
- data/plugins/rigor-ffi/lib/rigor-ffi.rb +3 -0
- data/plugins/rigor-ffi/sig/ffi.rbs +67 -0
- data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +19 -0
- data/plugins/rigor-ffi-rzmq/lib/rigor-ffi-rzmq.rb +3 -0
- data/plugins/rigor-ffi-rzmq/sig/ffi_rzmq.rbs +29 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +2 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +2 -1
- data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +156 -11
- data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +30 -0
- data/plugins/rigor-rbnacl/lib/rigor-rbnacl.rb +3 -0
- data/plugins/rigor-rbnacl/sig/rbnacl.rbs +22 -0
- data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +55 -0
- data/plugins/rigor-sassc/lib/rigor-sassc.rb +3 -0
- data/plugins/rigor-sassc/sig/sassc.rbs +24 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +32 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +3 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +63 -10
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +15 -2
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +61 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +4 -2
- data/sig/rigor/analysis/baseline.rbs +1 -1
- data/sig/rigor/environment.rbs +3 -0
- data/sig/rigor/inference.rbs +4 -1
- data/sig/rigor/plugin/io_boundary.rbs +2 -0
- data/sig/rigor/reflection.rbs +3 -1
- data/sig/rigor/scope.rbs +25 -1
- data/sig/rigor/type.rbs +95 -1
- data/sig/rigor.rbs +16 -0
- data/skills/rigor-ci-setup/SKILL.md +2 -2
- data/skills/rigor-editor-setup/SKILL.md +2 -2
- data/skills/rigor-mcp-setup/SKILL.md +2 -2
- data/skills/rigor-monkeypatch-resolve/SKILL.md +1 -1
- data/skills/rigor-plugin-review/SKILL.md +3 -3
- metadata +73 -3
|
@@ -28,15 +28,34 @@ module Rigor
|
|
|
28
28
|
|
|
29
29
|
# One constant reference. `from` is the fully-qualified name of the innermost enclosing declaration, or
|
|
30
30
|
# nil for a reference written at file level — that is what makes the graph a reachability graph rather
|
|
31
|
-
# than a reference count (ADR-102 WD2). `role` is the referring FILE's role (WD8).
|
|
32
|
-
|
|
31
|
+
# than a reference count (ADR-102 WD2). `role` is the referring FILE's role (WD8). `rooted` is true
|
|
32
|
+
# when the constant was written with a leading `::` (#625).
|
|
33
|
+
Reference = Data.define(:as_written, :nesting, :from, :role, :path, :line, :rooted) do
|
|
34
|
+
def initialize(rooted: false, **) = super
|
|
35
|
+
end
|
|
33
36
|
|
|
34
37
|
# ADR-102 WD4 — a site where a constant is reached by a mechanism the static reading cannot follow.
|
|
35
38
|
# `name` is the exact constant when the argument is a literal (`"Foo".constantize`), in which case this
|
|
36
|
-
# is as good as a reference. `prefix` is
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
|
|
39
|
+
# is as good as a reference. `prefix` is what a dynamic construction can reach, and `scope` says how
|
|
40
|
+
# far — the two kinds of weak evidence are not the same shape:
|
|
41
|
+
#
|
|
42
|
+
# - `:namespace` — `"Foo::#{k}".constantize` can construct ANY name under `Foo`, so it taints `Foo`
|
|
43
|
+
# and every declaration beneath it. This is the default and the original meaning of `prefix`.
|
|
44
|
+
# - `:exact` — a data-file or template match names ONE declaration and says nothing about its
|
|
45
|
+
# children (#370). `config/recurring.yml` mentioning `Admin` is not evidence about
|
|
46
|
+
# `Admin::CollectionPolicy`; treating it as such demoted 18 unrelated Mastodon rows off one
|
|
47
|
+
# Afrikaans word ("Administrasie" contains "Admin").
|
|
48
|
+
DynamicUse = Data.define(:name, :prefix, :reason, :site, :path, :line, :scope) do
|
|
49
|
+
def initialize(scope: :namespace, **) = super
|
|
50
|
+
|
|
51
|
+
# Whether this site's evidence reaches `fqn`.
|
|
52
|
+
def taints?(fqn)
|
|
53
|
+
return false if prefix.nil?
|
|
54
|
+
return fqn == prefix if scope == :exact
|
|
55
|
+
|
|
56
|
+
fqn == prefix || fqn.start_with?("#{prefix}::")
|
|
57
|
+
end
|
|
58
|
+
end
|
|
40
59
|
|
|
41
60
|
Result = Data.define(:declarations, :references, :dynamic_uses)
|
|
42
61
|
|
|
@@ -232,7 +251,8 @@ module Rigor
|
|
|
232
251
|
|
|
233
252
|
@references << Reference.new(as_written: as_written, nesting: nesting.dup.freeze,
|
|
234
253
|
from: nesting.empty? ? nil : nesting.join("::"),
|
|
235
|
-
role: @role, path: @path, line: node.location.start_line
|
|
254
|
+
role: @role, path: @path, line: node.location.start_line,
|
|
255
|
+
rooted: Source::ConstantPath.rooted?(node))
|
|
236
256
|
end
|
|
237
257
|
end
|
|
238
258
|
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "zlib"
|
|
6
|
+
|
|
7
|
+
require_relative "../../version"
|
|
8
|
+
|
|
9
|
+
module Rigor
|
|
10
|
+
module Analysis
|
|
11
|
+
module Reachability
|
|
12
|
+
# ADR-102 — the warm cache for `rigor unused`'s two per-file passes: the Prism reachability
|
|
13
|
+
# scan over `.rb` / `.rake` files, and the capital-run extraction over template files. Both
|
|
14
|
+
# passes are pure functions of one file's bytes, and both were measured re-running in full on
|
|
15
|
+
# every invocation (2.0 s of a 2.6 s warm run on Mastodon; the Marshal restore is 25× cheaper
|
|
16
|
+
# than the rescan, `docs/notes/20260825-feature-warm-cold-corpus-perf.md`).
|
|
17
|
+
#
|
|
18
|
+
# One self-validating zlib-Marshal blob under the cache root, the ADR-46 IncrementalSnapshot
|
|
19
|
+
# shape rather than an ADR-54 `Cache::Store` producer: the sound unit of reuse is the FILE
|
|
20
|
+
# (any subset of files may change between runs), so the payload carries a stat signature per
|
|
21
|
+
# entry and validates each on read, where a store entry validates all-or-nothing and a miss
|
|
22
|
+
# would throw away every unchanged file's work. The whole-project answer stays complete —
|
|
23
|
+
# every file is still consulted every run; an unchanged one contributes its cached product.
|
|
24
|
+
#
|
|
25
|
+
# Fail-soft everywhere: a missing, torn, or stale-schema blob is an empty cache, and a file
|
|
26
|
+
# whose signature cannot be taken is computed and never recorded. Nothing here can change
|
|
27
|
+
# what the report says — only whether a per-file product was recomputed to say it.
|
|
28
|
+
class ScanCache
|
|
29
|
+
SCHEMA = 1
|
|
30
|
+
FILE_NAME = "reachability-scan.bundle"
|
|
31
|
+
|
|
32
|
+
# The sources whose behaviour the cached products embody. Digested into the header so a
|
|
33
|
+
# checkout that edits the scan invalidates its survey targets' bundles without anyone
|
|
34
|
+
# remembering to bump {SCHEMA} — the `Cache::EngineSource` idea, scoped to this feature.
|
|
35
|
+
SOURCE_FILES = [
|
|
36
|
+
File.expand_path("scan.rb", __dir__),
|
|
37
|
+
File.expand_path(__FILE__)
|
|
38
|
+
].freeze
|
|
39
|
+
|
|
40
|
+
# A file modified within this window of the recording instant is computed but never
|
|
41
|
+
# recorded — the FileDigest racy-write guard, one tier simpler because a refused signature
|
|
42
|
+
# costs one rescan on the next run, never a wrong answer.
|
|
43
|
+
RACY_WINDOW_NS = 2_000_000_000
|
|
44
|
+
private_constant :RACY_WINDOW_NS
|
|
45
|
+
|
|
46
|
+
# @param cache_path [String, nil] the configuration's cache root; nil or empty is an inert
|
|
47
|
+
# cache (every fetch computes, nothing persists).
|
|
48
|
+
# @param target_ruby [String, nil] parse-affecting configuration, part of the identity.
|
|
49
|
+
def self.open(cache_path, target_ruby: nil)
|
|
50
|
+
return new(path: nil, header: nil) if cache_path.nil? || cache_path.to_s.empty?
|
|
51
|
+
|
|
52
|
+
new(path: File.join(cache_path.to_s, FILE_NAME),
|
|
53
|
+
header: "#{SCHEMA}:#{Rigor::VERSION}:#{target_ruby}:#{source_digest}")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.source_digest
|
|
57
|
+
Digest::SHA256.hexdigest(SOURCE_FILES.map { |path| File.read(path) }.join)
|
|
58
|
+
rescue StandardError
|
|
59
|
+
"unreadable"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def initialize(path:, header:)
|
|
63
|
+
@path = path
|
|
64
|
+
@header = header
|
|
65
|
+
@entries = load_entries
|
|
66
|
+
@live = {}
|
|
67
|
+
@dirty = false
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Serves the cached product for `(kind, path)` while the file's stat signature holds;
|
|
71
|
+
# otherwise computes via the block and records the fresh product.
|
|
72
|
+
def serve(kind, path)
|
|
73
|
+
key = [kind, path]
|
|
74
|
+
sig = signature(path)
|
|
75
|
+
cached = @entries[key]
|
|
76
|
+
if cached && sig && cached[0] == sig
|
|
77
|
+
@live[key] = cached
|
|
78
|
+
return cached[1]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
value = yield
|
|
82
|
+
@live[key] = [sig, value] if sig
|
|
83
|
+
@dirty = true
|
|
84
|
+
value
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Best-effort persist, only when something changed. Carries forward entries this run never
|
|
88
|
+
# consulted (a path-argument run must not shrink the full-project bundle) but drops the ones
|
|
89
|
+
# whose file no longer exists; tmp-then-rename so a torn write is never read back.
|
|
90
|
+
def save
|
|
91
|
+
return if @path.nil? || !@dirty
|
|
92
|
+
|
|
93
|
+
keep = @entries.merge(@live).select { |(_, path), _| File.exist?(path) }
|
|
94
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
95
|
+
tmp = "#{@path}.#{Process.pid}.tmp"
|
|
96
|
+
File.binwrite(tmp, Zlib::Deflate.deflate(Marshal.dump([@header, keep])))
|
|
97
|
+
File.rename(tmp, @path)
|
|
98
|
+
nil
|
|
99
|
+
rescue StandardError
|
|
100
|
+
FileUtils.rm_f(tmp) if tmp
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def load_entries
|
|
107
|
+
return {} if @path.nil?
|
|
108
|
+
|
|
109
|
+
# Same trust model as the ADR-45 store and the ADR-46 snapshot: the blob lives in the
|
|
110
|
+
# project's own cache directory and a corrupt one degrades to an empty cache.
|
|
111
|
+
header, entries = Marshal.load(Zlib::Inflate.inflate(File.binread(@path))) # rubocop:disable Security/MarshalLoad
|
|
112
|
+
header == @header && entries.is_a?(Hash) ? entries : {}
|
|
113
|
+
rescue StandardError
|
|
114
|
+
{}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def signature(path)
|
|
118
|
+
st = File.stat(path)
|
|
119
|
+
mtime_ns = (st.mtime.to_i * 1_000_000_000) + st.mtime.nsec
|
|
120
|
+
now_ns = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
|
|
121
|
+
return nil if mtime_ns >= now_ns - RACY_WINDOW_NS
|
|
122
|
+
|
|
123
|
+
[st.size, mtime_ns, (st.ctime.to_i * 1_000_000_000) + st.ctime.nsec, st.ino]
|
|
124
|
+
rescue SystemCallError
|
|
125
|
+
nil
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "crash_signature"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
module Analysis
|
|
5
7
|
class Result
|
|
@@ -17,6 +19,36 @@ module Rigor
|
|
|
17
19
|
diagnostics.none?(&:error?)
|
|
18
20
|
end
|
|
19
21
|
|
|
22
|
+
# Issue #686 — true when a check rule (or a plugin's node-rule contribution) raised, so
|
|
23
|
+
# `Runner#analyze_file_body`'s rescue REPLACED the affected file's whole diagnostic list with one
|
|
24
|
+
# synthetic row. What comes back is then not a weaker account of the code; it is no account of it.
|
|
25
|
+
#
|
|
26
|
+
# Any consumer that compares two results, or reads an EMPTY result as "clean", has to ask this first.
|
|
27
|
+
# A crashed baseline and a crashed mutant carry the same synthetic row, so `==` reports agreement; an
|
|
28
|
+
# absence assertion holds on the one-diagnostic list the rescue leaves behind. That is how a kill
|
|
29
|
+
# oracle scored survivors it never measured (#686) and how 1,177 spec examples passed with every check
|
|
30
|
+
# rule crashing (#674).
|
|
31
|
+
#
|
|
32
|
+
# Narrower than "the run reported a failure", deliberately. A raising PLUGIN loses only that plugin's
|
|
33
|
+
# contribution and a failed RBS definition build loses only a type — in both cases every builtin rule
|
|
34
|
+
# still ran and its diagnostics are still here, so a consumer that refused those would be discarding a
|
|
35
|
+
# real measurement. {CrashSignature} carries the full table and the reasoning.
|
|
36
|
+
#
|
|
37
|
+
# `success?` is deliberately NOT this question: a crash makes `success?` false (the row is `:error`),
|
|
38
|
+
# which is why the CLI is loud and CI goes red. It is the callers that read the diagnostic LIST rather
|
|
39
|
+
# than the exit code which need the predicate.
|
|
40
|
+
def crashed?
|
|
41
|
+
diagnostics.any? { |diagnostic| CrashSignature.discards_file_analysis?(diagnostic) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The diagnostics {#crashed?} answers true for, so a caller can name what it saw rather than only that
|
|
45
|
+
# it saw something.
|
|
46
|
+
#
|
|
47
|
+
# @return [Array<Rigor::Analysis::Diagnostic>]
|
|
48
|
+
def crash_diagnostics
|
|
49
|
+
diagnostics.select { |diagnostic| CrashSignature.discards_file_analysis?(diagnostic) }
|
|
50
|
+
end
|
|
51
|
+
|
|
20
52
|
def error_count
|
|
21
53
|
diagnostics.count(&:error?)
|
|
22
54
|
end
|
|
@@ -14,8 +14,13 @@ end
|
|
|
14
14
|
require_relative "../version"
|
|
15
15
|
require_relative "../cache/descriptor"
|
|
16
16
|
require_relative "../cache/engine_source"
|
|
17
|
+
require_relative "../cache/file_digest"
|
|
17
18
|
require_relative "../cache/rbs_descriptor"
|
|
18
19
|
require_relative "../environment/default_libraries"
|
|
20
|
+
# Both resolvers are leaf files (no requires beyond `yaml`), so the boot-slimming probe reaches the two
|
|
21
|
+
# lockfile paths without loading `rigor/environment` or the RBS machinery.
|
|
22
|
+
require_relative "../environment/lockfile_resolver"
|
|
23
|
+
require_relative "../environment/rbs_collection_discovery"
|
|
19
24
|
|
|
20
25
|
module Rigor
|
|
21
26
|
module Analysis
|
|
@@ -48,6 +53,18 @@ module Rigor
|
|
|
48
53
|
# collecting run wrote elsewhere.
|
|
49
54
|
RUN_EFFECTS_PRODUCER_ID = "analysis.run-effects"
|
|
50
55
|
|
|
56
|
+
# #482 — the **serving** half of the sidecar, under the same effects identity: the propagated table,
|
|
57
|
+
# the unit sources and the merged as-written superclass table. Everything a warm run reads, and
|
|
58
|
+
# nothing else.
|
|
59
|
+
#
|
|
60
|
+
# It is a separate entry rather than a section of {RUN_EFFECTS_PRODUCER_ID} because the collections
|
|
61
|
+
# blob is large in exactly the projects where warm latency matters — 6.9 MB and 0.71 s of `Marshal`
|
|
62
|
+
# on gitlab `app lib`, against ~40 KB here — and a warm run consumes none of it. The collections
|
|
63
|
+
# entry stays, read lazily by the paths that genuinely need per-file form (an ADR-46 recheck, the
|
|
64
|
+
# fail-soft re-propagation), so a consumer this split did not anticipate loads the blob rather than
|
|
65
|
+
# seeing an empty table.
|
|
66
|
+
RUN_EFFECTS_TABLE_PRODUCER_ID = "analysis.run-effects-table"
|
|
67
|
+
|
|
51
68
|
# The run-result producer's declared compaction budget (`Cache::Store#evict!` pass 2). Whole-project,
|
|
52
69
|
# but unlike the `rbs.*` producers several generations can be live at once: the `paths` key slot means
|
|
53
70
|
# one entry per analyzed-path SET, so `rigor check` over the whole project, over `lib`, and over a
|
|
@@ -68,7 +85,7 @@ module Rigor
|
|
|
68
85
|
def descriptor(configuration:, files:, explain:, rbs_config_entries:)
|
|
69
86
|
Cache::Descriptor.new(
|
|
70
87
|
gems: [Cache::RbsDescriptor.rbs_gem_entry],
|
|
71
|
-
configs: rbs_config_entries + engine_source_entries + [
|
|
88
|
+
configs: rbs_config_entries + engine_source_entries + lockfile_entries(configuration) + [
|
|
72
89
|
config_entry("configuration", Marshal.dump(configuration.to_h)),
|
|
73
90
|
config_entry("engine",
|
|
74
91
|
"#{Rigor::VERSION}:#{Cache::Descriptor::SCHEMA_VERSION}:#{explain}"),
|
|
@@ -94,6 +111,41 @@ module Rigor
|
|
|
94
111
|
[config_entry("engine-source", identity)]
|
|
95
112
|
end
|
|
96
113
|
|
|
114
|
+
# Issue #564 — the two dependency lockfiles, BY CONTENT. Both are inputs to the run's diagnostics and
|
|
115
|
+
# neither was represented in the key: the locked gem set decides which bundle-shipped `sig/` dirs and
|
|
116
|
+
# which `rbs_collection` dirs load, which ADR-72 gem overlays apply, what the ADR-82 WD9 missing-gem
|
|
117
|
+
# constant index owns, and whether `rbs.coverage.missing-gem` fires at all. Editing a lockfile touches
|
|
118
|
+
# no analyzed source and no `.rbs` file, so the `paths` slot, the `configuration` slot (which carries
|
|
119
|
+
# the lockfile PATH, never its bytes) and the recorded dependency descriptor were all unchanged by a
|
|
120
|
+
# `bundle add` — and a warm run replayed the pre-edit diagnostics in BOTH directions: an
|
|
121
|
+
# `undefined-method` on `3.minutes` surviving the `bundle add activesupport` that licenses the overlay,
|
|
122
|
+
# and the same call staying silent after the `bundle remove` that revokes it.
|
|
123
|
+
#
|
|
124
|
+
# A KEY slot rather than a dependency entry, because a dependency descriptor can only say "a file I
|
|
125
|
+
# recorded changed": it cannot express a lockfile that did not exist when the entry was written and
|
|
126
|
+
# does now, which is the `bundle init` / first-`bundle install` case.
|
|
127
|
+
#
|
|
128
|
+
# Content-only — the resolved PATH is deliberately not in the payload, so a checkout that moves (a CI
|
|
129
|
+
# runner's workspace, a renamed directory) still hits with an identical lockfile.
|
|
130
|
+
def lockfile_entries(configuration)
|
|
131
|
+
bundler = Environment::LockfileResolver.resolve_lockfile_path(
|
|
132
|
+
lockfile_path: configuration.bundler_lockfile, auto_detect: configuration.bundler_auto_detect
|
|
133
|
+
)
|
|
134
|
+
collection = Environment::RbsCollectionDiscovery.resolve_lockfile_path(
|
|
135
|
+
lockfile_path: configuration.rbs_collection_lockfile,
|
|
136
|
+
auto_detect: configuration.rbs_collection_auto_detect
|
|
137
|
+
)
|
|
138
|
+
[lockfile_entry("bundler.lockfile", bundler), lockfile_entry("rbs_collection.lockfile", collection)]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# `nil` — no lockfile resolves — is itself part of the identity, so it carries its own sentinel rather
|
|
142
|
+
# than dropping the slot: "absent" and "present but empty" must not key the same.
|
|
143
|
+
ABSENT_LOCKFILE = "\0absent"
|
|
144
|
+
|
|
145
|
+
def lockfile_entry(key, path)
|
|
146
|
+
config_entry(key, path.nil? ? ABSENT_LOCKFILE : Cache::FileDigest.hexdigest(path.to_s))
|
|
147
|
+
end
|
|
148
|
+
|
|
97
149
|
def config_entry(key, payload)
|
|
98
150
|
Cache::Descriptor::ConfigEntry.new(key: key, value_hash: Digest::SHA256.hexdigest(payload))
|
|
99
151
|
end
|
|
@@ -36,13 +36,16 @@ module Rigor
|
|
|
36
36
|
# because they do not parse (`[path, first_error_line]` pairs).
|
|
37
37
|
# @param env_build_failure_snapshot [#call] reader returning the total RBS env-build failure tuple
|
|
38
38
|
# (`[error_class, first_error_line, conflicting_buffer_names]`) or nil when the env built.
|
|
39
|
+
# @param definition_build_failures_snapshot [#call] issue #696 — reader returning the per-class
|
|
40
|
+
# `RBS::DefinitionBuilder` failures the run observed, as `[class_name, error_class, member,
|
|
41
|
+
# conflicting_buffer_names]` tuples. Empty for a healthy sig set.
|
|
39
42
|
# @param conformance_results_snapshot [#call] reader.
|
|
40
43
|
def initialize(configuration:, rbs_extended_reporter:, boundary_cross_reporter:, # rubocop:disable Metrics/ParameterLists
|
|
41
44
|
source_rbs_synthesis_reporter:, plugin_registry:, dependency_source_index:,
|
|
42
45
|
pool_mode:, cached_plugin_prepare_diagnostics:,
|
|
43
46
|
pre_eval_diagnostics_from_scanner:, synthesized_namespaces_snapshot:,
|
|
44
47
|
quarantined_signatures_snapshot:, env_build_failure_snapshot:,
|
|
45
|
-
conformance_results_snapshot:)
|
|
48
|
+
definition_build_failures_snapshot:, conformance_results_snapshot:)
|
|
46
49
|
@configuration = configuration
|
|
47
50
|
@rbs_extended_reporter = rbs_extended_reporter
|
|
48
51
|
@boundary_cross_reporter = boundary_cross_reporter
|
|
@@ -55,6 +58,7 @@ module Rigor
|
|
|
55
58
|
@synthesized_namespaces_snapshot_reader = synthesized_namespaces_snapshot
|
|
56
59
|
@quarantined_signatures_snapshot_reader = quarantined_signatures_snapshot
|
|
57
60
|
@env_build_failure_snapshot_reader = env_build_failure_snapshot
|
|
61
|
+
@definition_build_failures_snapshot_reader = definition_build_failures_snapshot
|
|
58
62
|
@conformance_results_snapshot_reader = conformance_results_snapshot
|
|
59
63
|
end
|
|
60
64
|
|
|
@@ -351,6 +355,32 @@ module Rigor
|
|
|
351
355
|
[build_rbs_environment_build_failed_diagnostic(failure)]
|
|
352
356
|
end
|
|
353
357
|
|
|
358
|
+
# Issue #696 — the third rung of the same ladder, between its two neighbours by consequence: a
|
|
359
|
+
# QUARANTINED file removes what one FILE declared, this removes what one CLASS declared, an
|
|
360
|
+
# ENV-BUILD failure removes everything. `RBS::DefinitionBuilder` raising for a class (typically
|
|
361
|
+
# `DuplicatedMethodDefinitionError`: two signature sources declaring the same method, e.g. a project
|
|
362
|
+
# `sig/` carrying a vendored COPY of a bundled plugin's `.rbs`) leaves the class KNOWN but with no
|
|
363
|
+
# method surface, so every call on it — real methods and typos alike — reads `Dynamic[top]`. When
|
|
364
|
+
# the collision is on a class others inherit, the whole bundled type universe goes with it: the
|
|
365
|
+
# observed shape is thousands of stderr warnings, ZERO diagnostics, and exit 0.
|
|
366
|
+
#
|
|
367
|
+
# Not "drop the class to genuinely-unknown" instead. That also removes diagnostics, so it is not
|
|
368
|
+
# louder; it changes `Dynamic[top]` semantics for every rule that consults `class_known?`; and
|
|
369
|
+
# `class_known?` reads {Environment::RbsLoader#known_class_names_set}, a different table from the
|
|
370
|
+
# definition builder, so it would need a new "known but unbuildable" state regardless. The failure
|
|
371
|
+
# is made REPORTABLE; the class is not made invisible.
|
|
372
|
+
#
|
|
373
|
+
# Authored `:warning`, not `:error`, for the same reason as both neighbours: the collision is
|
|
374
|
+
# typically between the user's `sig/` and Rigor's OWN bundled RBS, so an `:error` default would let
|
|
375
|
+
# a Rigor release turn a green build red with no user change (ADR-5 / AGENTS.md § FP discipline).
|
|
376
|
+
# The `reject-unparseable-signatures` bleeding-edge feature promotes it to `:error`.
|
|
377
|
+
def rbs_definition_build_failed_diagnostics
|
|
378
|
+
failures = definition_build_failures_snapshot
|
|
379
|
+
return [] if failures.nil? || failures.empty?
|
|
380
|
+
|
|
381
|
+
[build_rbs_definition_build_failed_diagnostic(failures)]
|
|
382
|
+
end
|
|
383
|
+
|
|
354
384
|
def rbs_synthesized_namespace_diagnostics
|
|
355
385
|
synthesized = synthesized_namespaces_snapshot
|
|
356
386
|
return [] if synthesized.nil? || synthesized.empty?
|
|
@@ -462,6 +492,64 @@ module Rigor
|
|
|
462
492
|
)
|
|
463
493
|
end
|
|
464
494
|
|
|
495
|
+
# Issue #696. One diagnostic per RUN, not per class: a collision on a widely-inherited class fails
|
|
496
|
+
# every descendant, and 2,709 rows saying the same thing about one `.rbs` line is noise, not
|
|
497
|
+
# visibility.
|
|
498
|
+
#
|
|
499
|
+
# The failed-class list alone is not actionable, and that is what the "First failure" clause is for.
|
|
500
|
+
# A duplicate on `::Object#blank?` fails `String`, `Integer`, `Array` and 1,333 more — every name in
|
|
501
|
+
# that list is a DESCENDANT, and none of them is where the fix goes. The member comes off the error
|
|
502
|
+
# object rather than out of its message ({RbsLoader#definition_build_member}), so it names the
|
|
503
|
+
# culprit and reads the same on a cache hit, where every `RBS::Location` has been dumped to a
|
|
504
|
+
# sentinel. The error class is attributed to that first failure explicitly rather than to all of
|
|
505
|
+
# them: the per-class memo keeps only the first reason, and one run can mix error classes.
|
|
506
|
+
def build_rbs_definition_build_failed_diagnostic(failures)
|
|
507
|
+
sample_size = 5
|
|
508
|
+
files = failures.flat_map { |failure| Array(failure[3]) }.uniq.map { |name| relative_signature_path(name) }
|
|
509
|
+
Diagnostic.new(
|
|
510
|
+
path: ".rigor.yml",
|
|
511
|
+
line: 1,
|
|
512
|
+
column: 1,
|
|
513
|
+
message: "#{failures.size} RBS class definition(s) failed to build: " \
|
|
514
|
+
"#{sampled(failures.map(&:first), sample_size)}." \
|
|
515
|
+
"#{first_failure_clause(failures.first)}#{conflicting_files_clause(files, sample_size)} " \
|
|
516
|
+
"Rigor still treats each class as KNOWN, so calls into it — real methods and typos " \
|
|
517
|
+
"alike — silently read `Dynamic[top]` instead of resolving, and this run is quieter " \
|
|
518
|
+
"than it should be rather than cleaner. Two signature sources declare the same " \
|
|
519
|
+
"member; remove the duplicate declaration (`rbs validate`) to restore type coverage.",
|
|
520
|
+
severity: :warning,
|
|
521
|
+
rule: "rbs.coverage.definition-build-failed",
|
|
522
|
+
source_family: :builtin
|
|
523
|
+
)
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
# `[class_name, error_class, member, buffers]`. The member is nil for the error classes that carry no
|
|
527
|
+
# name at all (`RecursiveAncestorError`), and the clause then names the error class alone rather than
|
|
528
|
+
# inventing a member.
|
|
529
|
+
def first_failure_clause(failure)
|
|
530
|
+
_, error_class, member, = failure
|
|
531
|
+
return " First failure: #{error_class}." if member.nil? || member.empty?
|
|
532
|
+
|
|
533
|
+
" First failure: #{error_class} on `#{member}`."
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# Identical on a cache HIT: the ADR-54 env cache drops location POSITIONS but keeps each buffer's
|
|
537
|
+
# NAME, so a warm run names the same files a cold one does. Where a name is absent anyway, the
|
|
538
|
+
# reconstructed {RbsLoader::CACHED_LOCATION_BUFFER_NAME} sentinel is filtered out and the clause is
|
|
539
|
+
# omitted, rather than naming `<cached>` as if it were a path.
|
|
540
|
+
def conflicting_files_clause(files, sample_size)
|
|
541
|
+
return "" if files.empty?
|
|
542
|
+
|
|
543
|
+
" Conflicting signature file(s): #{sampled(files, sample_size)}."
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# "a, b, c, and N more" — the sampling shape every `rbs.coverage.*` message above uses.
|
|
547
|
+
def sampled(names, sample_size)
|
|
548
|
+
sample = names.first(sample_size)
|
|
549
|
+
suffix = names.size > sample_size ? ", and #{names.size - sample_size} more" : ""
|
|
550
|
+
"#{sample.join(', ')}#{suffix}"
|
|
551
|
+
end
|
|
552
|
+
|
|
465
553
|
# The absolute path is what the loader records; the user thinks in project-relative terms.
|
|
466
554
|
def relative_signature_path(path)
|
|
467
555
|
root = "#{Dir.pwd}#{File::SEPARATOR}"
|
|
@@ -684,6 +772,10 @@ module Rigor
|
|
|
684
772
|
@env_build_failure_snapshot_reader.call
|
|
685
773
|
end
|
|
686
774
|
|
|
775
|
+
def definition_build_failures_snapshot
|
|
776
|
+
@definition_build_failures_snapshot_reader.call
|
|
777
|
+
end
|
|
778
|
+
|
|
687
779
|
def conformance_results_snapshot
|
|
688
780
|
@conformance_results_snapshot_reader.call
|
|
689
781
|
end
|
|
@@ -30,11 +30,13 @@ module Rigor
|
|
|
30
30
|
#
|
|
31
31
|
# 1. **Read the envelopes** off the project's own RBS ({RbsExtended::EnvelopeScanner}). A project
|
|
32
32
|
# with `effects:` but no envelope pays this walk and stops here.
|
|
33
|
-
# 2. **
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
33
|
+
# 2. **Judge** ({Effects::EnvelopeCheck}). Cross-file discovery — the diagnostic is positioned
|
|
34
|
+
# at the Ruby `def`, and the discovery tables are what map a method key to one — is forced
|
|
35
|
+
# from inside the judgment, on the first finding built, so an envelope nothing exceeds costs
|
|
36
|
+
# no discovery parse at all.
|
|
37
|
+
# 3. **Render**, then run the findings through the ordinary suppression filter so
|
|
38
|
+
# `# rigor:disable effect.envelope-exceeded` on the `def` line and `disable:` in `.rigor.yml`
|
|
39
|
+
# work exactly as they do for a per-file rule.
|
|
38
40
|
#
|
|
39
41
|
# **Why this is not part of the cached run assembly.** ADR-103 WD12 says envelope diagnostics are
|
|
40
42
|
# recomputed every run from the (possibly cached) summaries and never stored. The `effects:` block
|
|
@@ -80,7 +82,8 @@ module Rigor
|
|
|
80
82
|
# @param rbs_loader [Rigor::Environment::RbsLoader, nil] the run's loader; nil disables the pass.
|
|
81
83
|
# @param effect_table [Rigor::Effects::EffectTable] the propagated graph.
|
|
82
84
|
# @param discovery [#call] forces and returns the cross-file discovery tables as
|
|
83
|
-
# `[def_sources, singleton_def_sources, class_sources]`. Called only when
|
|
85
|
+
# `[def_sources, singleton_def_sources, class_sources]`. Called only when a finding needs a
|
|
86
|
+
# position — a judged-clean envelope never forces it.
|
|
84
87
|
# @param sources [Hash{String => String}] in-memory sources, for the buffer-backed run path.
|
|
85
88
|
# @param unit_sources [Hash{String => Array<String>}] `Runner#effect_sources` — where each effect
|
|
86
89
|
# unit is defined, which is what an `effects.envelopes[].match:` path glob selects on. Its paths
|
|
@@ -272,9 +275,12 @@ module Rigor
|
|
|
272
275
|
)
|
|
273
276
|
end
|
|
274
277
|
|
|
275
|
-
# The discovery tables both judgments position their findings from
|
|
278
|
+
# The discovery tables both judgments position their findings from — behind a deferred value,
|
|
279
|
+
# forced at most once, on the first finding either judgment builds. A clean run (an envelope
|
|
280
|
+
# nothing exceeds — the common CI case) therefore never calls `@discovery` at all, which on a
|
|
281
|
+
# warm run is the difference between a no-op and a Prism parse of every project file.
|
|
276
282
|
def positions
|
|
277
|
-
@positions ||=
|
|
283
|
+
@positions ||= Effects::EnvelopeCheck::DeferredPositions.new do
|
|
278
284
|
def_sources, singleton_def_sources, class_sources = @discovery.call
|
|
279
285
|
Effects::EnvelopeCheck::Positions.build(
|
|
280
286
|
def_sources: def_sources, singleton_def_sources: singleton_def_sources,
|
|
@@ -126,6 +126,10 @@ module Rigor
|
|
|
126
126
|
snapshot_project_signature_state(environment)
|
|
127
127
|
snapshot_effect_annotation_carrier(environment&.rbs_loader)
|
|
128
128
|
result = files.flat_map { |path| @analyze_file.call(path, environment) }
|
|
129
|
+
# Issue #696 — AFTER the loop, deliberately. Definition builds are lazy (ADR-54 WD1: per class, on
|
|
130
|
+
# first demand), so a snapshot taken beside the ones above — which run BEFORE `files.flat_map` —
|
|
131
|
+
# would read an empty list on every run, including the ones this diagnostic exists for.
|
|
132
|
+
record_definition_build_failures(environment&.rbs_loader&.definition_build_failures)
|
|
129
133
|
if @collect_stats
|
|
130
134
|
loader = environment.rbs_loader
|
|
131
135
|
@snapshots.class_decl_paths = loader&.class_decl_paths || {}.freeze
|
|
@@ -567,6 +571,11 @@ module Rigor
|
|
|
567
571
|
snapshot_effect_annotation_carrier(environment.rbs_loader)
|
|
568
572
|
diagnostics = files.flat_map { |path| @analyze_file.call(path, environment) }
|
|
569
573
|
loader = environment.rbs_loader
|
|
574
|
+
# Issue #696 — this path analyses on the coordinator, so its loader IS the one that reached the
|
|
575
|
+
# failing build, and it must snapshot the same slot the two real paths do. Reached whenever
|
|
576
|
+
# `fork` is unavailable (Windows) and on `--incremental` / effects runs without it: a run that
|
|
577
|
+
# degraded to sequential must not also report less than a sequential run would.
|
|
578
|
+
record_definition_build_failures(loader&.definition_build_failures)
|
|
570
579
|
@snapshots.class_decl_paths = loader&.class_decl_paths || {}.freeze
|
|
571
580
|
@snapshots.signature_paths = loader&.signature_paths || [].freeze
|
|
572
581
|
@snapshots.quarantined_signatures =
|
|
@@ -608,10 +617,35 @@ module Rigor
|
|
|
608
617
|
plugin_id: entry.plugin_id, path: entry.path, message: entry.message
|
|
609
618
|
)
|
|
610
619
|
end
|
|
620
|
+
# Issue #696. Fetched with a default so an older drain stays compatible, exactly as the line above.
|
|
621
|
+
record_definition_build_failures(drained[:definition_build_failures])
|
|
611
622
|
end
|
|
612
623
|
|
|
613
624
|
private
|
|
614
625
|
|
|
626
|
+
# Issue #696 — accumulate the per-class `RBS::DefinitionBuilder` failures this run observed, deduped
|
|
627
|
+
# by class name and kept in first-seen order.
|
|
628
|
+
#
|
|
629
|
+
# Accumulating rather than assigning is what makes a pooled run say what a sequential one says: each
|
|
630
|
+
# worker holds its OWN loader and its own memo (the `@state` that also gates the stderr banner, which
|
|
631
|
+
# is why the banner repeats per worker — #295), so a class that fails in two workers arrives twice
|
|
632
|
+
# and a class only one worker touched arrives once. The union over the workers is the set the RUN
|
|
633
|
+
# hit; neither worker alone is.
|
|
634
|
+
#
|
|
635
|
+
# NOT gated on `project_signature_paths?`, unlike {#snapshot_project_signature_state}. That gate
|
|
636
|
+
# exists because each condition there forces the otherwise-lazy env build, which would warm the cache
|
|
637
|
+
# on a project with no sig set of its own. Nothing is forced here — this reads what the analysis
|
|
638
|
+
# already demanded — and the failure is not a project-sig-only condition anyway: two BUNDLED sources
|
|
639
|
+
# declaring the same method collapse a class with no `signature_paths:` in sight (the
|
|
640
|
+
# `bigdecimal` / `BigMath` shape, #299). Gating it would suppress exactly the variant the user cannot
|
|
641
|
+
# fix by editing their own `sig/`.
|
|
642
|
+
def record_definition_build_failures(failures)
|
|
643
|
+
return if failures.nil? || failures.empty?
|
|
644
|
+
|
|
645
|
+
@snapshots.definition_build_failures =
|
|
646
|
+
(@snapshots.definition_build_failures + failures).uniq(&:first).freeze
|
|
647
|
+
end
|
|
648
|
+
|
|
615
649
|
# True when the project declares its own `signature_paths:` (the only place the
|
|
616
650
|
# qualified-name-without-namespace mistake lives).
|
|
617
651
|
def project_signature_paths?
|
|
@@ -53,17 +53,11 @@ module Rigor
|
|
|
53
53
|
# consumes them and so never pays the double parse (see {#discover} + `Runner#ensure_project_discovery`).
|
|
54
54
|
# The slot names mirror the discovery half of {Result} exactly.
|
|
55
55
|
Discovery = Data.define(
|
|
56
|
-
:discovered_classes,
|
|
57
|
-
:
|
|
58
|
-
:
|
|
59
|
-
:
|
|
60
|
-
:
|
|
61
|
-
:discovered_superclasses,
|
|
62
|
-
:discovered_includes,
|
|
63
|
-
:discovered_class_sources,
|
|
64
|
-
:discovered_method_visibilities,
|
|
65
|
-
:discovered_methods,
|
|
66
|
-
:data_member_layouts,
|
|
56
|
+
:discovered_classes, :discovered_def_nodes, :discovered_def_nestings,
|
|
57
|
+
:discovered_singleton_def_nodes, :discovered_def_sources, :discovered_singleton_def_sources,
|
|
58
|
+
:discovered_superclasses, :discovered_header_nestings, :discovered_includes,
|
|
59
|
+
:discovered_class_sources, :constant_values, :constant_sources, :constant_writes,
|
|
60
|
+
:discovered_method_visibilities, :discovered_methods, :data_member_layouts,
|
|
67
61
|
:struct_member_layouts
|
|
68
62
|
)
|
|
69
63
|
|
|
@@ -156,12 +150,22 @@ module Rigor
|
|
|
156
150
|
Discovery.new(
|
|
157
151
|
discovered_classes: index.fetch(:classes),
|
|
158
152
|
discovered_def_nodes: def_index.fetch(:def_nodes),
|
|
153
|
+
# Issue #681 — the recorded `Module.nesting` per declared `def`, so a callee's body re-walk
|
|
154
|
+
# resolves its constants the way the declaration that owns it does.
|
|
155
|
+
discovered_def_nestings: def_index.fetch(:def_nestings),
|
|
159
156
|
discovered_singleton_def_nodes: def_index.fetch(:singleton_def_nodes),
|
|
160
157
|
discovered_def_sources: def_index.fetch(:def_sources),
|
|
161
158
|
discovered_singleton_def_sources: def_index.fetch(:singleton_def_sources),
|
|
162
159
|
discovered_superclasses: def_index.fetch(:superclasses),
|
|
160
|
+
# Issue #682 — the nesting each declaration header is written in, so an ancestor name resolves in
|
|
161
|
+
# the cref Ruby resolves it in rather than by peeling the subclass's own qualified name.
|
|
162
|
+
discovered_header_nestings: def_index.fetch(:header_nestings),
|
|
163
163
|
discovered_includes: def_index.fetch(:includes),
|
|
164
164
|
discovered_class_sources: def_index.fetch(:class_sources),
|
|
165
|
+
# Issue #644 — the cross-file value-constant publication table and its write attribution.
|
|
166
|
+
constant_values: def_index.fetch(:constant_values),
|
|
167
|
+
constant_sources: def_index.fetch(:constant_sources),
|
|
168
|
+
constant_writes: def_index.fetch(:constant_writes),
|
|
165
169
|
discovered_method_visibilities: def_index.fetch(:method_visibilities),
|
|
166
170
|
discovered_methods: def_index.fetch(:methods),
|
|
167
171
|
data_member_layouts: def_index.fetch(:data_member_layouts),
|
|
@@ -17,12 +17,15 @@ module Rigor
|
|
|
17
17
|
class RunSnapshots
|
|
18
18
|
attr_accessor :class_decl_paths, :signature_paths,
|
|
19
19
|
:synthesized_namespaces, :quarantined_signatures, :conformance_results,
|
|
20
|
-
:env_build_failure, :effect_annotation_carrier
|
|
20
|
+
:env_build_failure, :definition_build_failures, :effect_annotation_carrier
|
|
21
21
|
|
|
22
22
|
# Constructor defaults match the {Runner} constructor: the pre-seed values `build_run_stats` /
|
|
23
23
|
# `pre_file_diagnostics` read before the first analysis path runs are frozen empties. The
|
|
24
24
|
# `env_build_failure` slot is nil (no failure) rather than an empty collection — it holds a single
|
|
25
|
-
# `[error_class, first_line, buffer_names]` tuple or nothing.
|
|
25
|
+
# `[error_class, first_line, buffer_names]` tuple or nothing. Its per-class sibling
|
|
26
|
+
# `definition_build_failures` (#696) holds a LIST, because a collapsed universe fails many classes,
|
|
27
|
+
# and is accumulated ACROSS pool workers rather than assigned once — see
|
|
28
|
+
# {PoolCoordinator#merge_worker_reporters}.
|
|
26
29
|
def initialize
|
|
27
30
|
@class_decl_paths = {}.freeze
|
|
28
31
|
@signature_paths = [].freeze
|
|
@@ -30,6 +33,7 @@ module Rigor
|
|
|
30
33
|
@quarantined_signatures = [].freeze
|
|
31
34
|
@conformance_results = [].freeze
|
|
32
35
|
@env_build_failure = nil
|
|
36
|
+
@definition_build_failures = [].freeze
|
|
33
37
|
@effect_annotation_carrier = [].freeze
|
|
34
38
|
end
|
|
35
39
|
|
|
@@ -42,6 +46,7 @@ module Rigor
|
|
|
42
46
|
@quarantined_signatures = []
|
|
43
47
|
@conformance_results = []
|
|
44
48
|
@env_build_failure = nil
|
|
49
|
+
@definition_build_failures = []
|
|
45
50
|
@effect_annotation_carrier = [].freeze
|
|
46
51
|
end
|
|
47
52
|
end
|