rigortype 0.3.6 → 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 +1 -1
- 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 +409 -11
- data/docs/manual/02-cli-reference.md +16 -7
- data/docs/manual/05-inspecting-types.md +20 -8
- data/docs/manual/07-plugins.md +4 -0
- data/docs/manual/plugins/rigor-actionpack.md +61 -0
- data/docs/manual/plugins/rigor-activerecord.md +74 -1
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
- 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/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 +14 -5
- data/lib/rigor/analysis/reachability/scan.rb +7 -3
- data/lib/rigor/analysis/result.rb +32 -0
- data/lib/rigor/analysis/run_cache_key.rb +41 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +93 -1
- 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 +147 -10
- 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/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/coverage_command.rb +23 -18
- data/lib/rigor/cli/coverage_mutation.rb +13 -17
- data/lib/rigor/cli/coverage_scan.rb +47 -6
- 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.rb +1 -1
- data/lib/rigor/configuration.rb +7 -4
- data/lib/rigor/effects/plugin_facts.rb +1 -1
- data/lib/rigor/effects/scanner.rb +2 -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/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/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
- metadata +69 -3
|
@@ -7,7 +7,9 @@ module Rigor
|
|
|
7
7
|
# Refined types for predefined Ruby / stdlib constants whose upstream RBS signatures are
|
|
8
8
|
# broader than the constants' documented runtime invariants.
|
|
9
9
|
#
|
|
10
|
-
# Resolution is two-tiered
|
|
10
|
+
# Resolution is two-tiered, and **both tiers are closed tables authored in this file**. A name
|
|
11
|
+
# taken from the analysed program is never resolved against the analyzer's own object space —
|
|
12
|
+
# see *Why the table is closed* below.
|
|
11
13
|
#
|
|
12
14
|
# **Tier 1 — exact-value whitelist** (`FOLDED_CONSTANTS`):
|
|
13
15
|
# Constants whose value is bit-for-bit identical across every Ruby version and platform
|
|
@@ -23,24 +25,44 @@ module Rigor
|
|
|
23
25
|
# and as plain `Integer`s they fall through Tier 2 to the RBS type harmlessly.
|
|
24
26
|
# `Complex::I` is deferred (no complex-fold consumer).
|
|
25
27
|
#
|
|
26
|
-
# **Tier 2 —
|
|
27
|
-
#
|
|
28
|
-
# own
|
|
29
|
-
# always loaded into the analyzer process; project-defined constants are not (they live
|
|
30
|
-
# only in ASTs), so their `const_get` raises `NameError` and the lookup falls through to
|
|
31
|
-
# the RBS type tier.
|
|
28
|
+
# **Tier 2 — String refinements for a closed set of interpreter constants**
|
|
29
|
+
# (`RUNTIME_STRING_CONSTANTS`): each listed name is read ONCE, at load time, from the analyzer's
|
|
30
|
+
# own runtime and classified by the value found:
|
|
32
31
|
#
|
|
33
|
-
#
|
|
34
|
-
# - empty string → no refinement (fall through to RBS `String`)
|
|
32
|
+
# - not a String, or an empty one → no entry (the name falls through to the RBS type)
|
|
35
33
|
# - a Ruby numeric literal → `numeric-string`
|
|
36
34
|
# - non-empty otherwise → `non-empty-string`
|
|
37
35
|
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
36
|
+
# Admission criterion for the list: the constant's value is fixed by the same interpreter choice
|
|
37
|
+
# that already fixes the core / stdlib RBS the engine reads, and it is non-empty in every build.
|
|
38
|
+
# A third-party gem's constant does NOT qualify — the analysed project resolves its own copy
|
|
39
|
+
# through its `Gemfile.lock`, which this layer cannot see. This is the criterion
|
|
40
|
+
# `Inference::VersionGuard::VERSION_CONSTANTS` already applies to the constants it folds guards on.
|
|
41
|
+
#
|
|
42
|
+
# **Why the table is closed** ([#680](https://github.com/rigortype/rigor/issues/680)):
|
|
43
|
+
# Tier 2 used to resolve ANY name reaching it — a name read out of the analysed source — with
|
|
44
|
+
# `const_get` against the analyzer's runtime, guarded by `const_defined?(part, false)`. That
|
|
45
|
+
# guard does not answer the question it was written for: `const_defined?` is true for a
|
|
46
|
+
# **registered but not yet triggered autoload**, so `const_get` fired the autoload and the target
|
|
47
|
+
# file was *executed* inside the analyzer. Analysing CRuby's own `lib/prism` reached
|
|
48
|
+
# `prism/translation/ruby_parser.rb`, which calls `exit` at the top level when `sexp_processor`
|
|
49
|
+
# is absent; `SystemExit` is not a `StandardError`, so neither the rescue in this file nor the
|
|
50
|
+
# runner's per-file rescue saw it and the run stopped with no diagnostics and no summary. The
|
|
51
|
+
# target of an autoload is arbitrary — it may exit, raise, print, mutate global state, or take
|
|
52
|
+
# unbounded time — and `rigor check` is routinely pointed at code the user did not write.
|
|
53
|
+
#
|
|
54
|
+
# Closing the table rather than only guarding the walk is what makes that structural: no name
|
|
55
|
+
# from the analysed program reaches `const_get` at all. A census of what the open walk actually
|
|
56
|
+
# bought says the precision cost is nil — over GitLab's `app` + `lib` (11,189 files, 184,965
|
|
57
|
+
# constant references) it answered for five names and ten references (`RUBY_VERSION`,
|
|
58
|
+
# `RUBY_PLATFORM`, `RUBY_DESCRIPTION`, `File::SEPARATOR`, `File::PATH_SEPARATOR`, every one of
|
|
59
|
+
# them listed below), against 17 references that would have autoloaded `ipaddr`; over Mastodon,
|
|
60
|
+
# three names and five references against 43 such. What it answered for *beyond* the list was
|
|
61
|
+
# always a constant that happened to live in **Rigor's own** bundle — `Rigor::VERSION` and
|
|
62
|
+
# `RBS::VERSION` while Rigor checks itself, rubygems and Bundler internals while it checks
|
|
63
|
+
# CRuby's `lib/` — and that is worse than no answer: identical source would type differently
|
|
64
|
+
# depending on how the analyzer was installed. Removing an ad hoc `Object.const_get` from an
|
|
65
|
+
# analysis path is the same move ADR-4 § 330 already made for predicate narrowing.
|
|
44
66
|
#
|
|
45
67
|
# This module is consulted by `Environment#constant_for_name` BEFORE the RBS
|
|
46
68
|
# constant-type table (widest types) but AFTER in-source constant writes (the user's own
|
|
@@ -68,70 +90,97 @@ module Rigor
|
|
|
68
90
|
|
|
69
91
|
# --- tier 2 -------------------------------------------------------
|
|
70
92
|
|
|
71
|
-
#
|
|
72
|
-
# NOT be narrowed because they are documented to be potentially empty in some build or
|
|
73
|
-
# implementation.
|
|
93
|
+
# The closed set of names tier 2 may read, by the admission criterion in the module note.
|
|
74
94
|
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
|
|
79
|
-
|
|
95
|
+
# The `RUBY_*` / `Ruby::*` pairs and `Encoding::UNICODE_VERSION` are defined by the interpreter
|
|
96
|
+
# itself; `File::SEPARATOR` / `Separator` / `PATH_SEPARATOR` / `ALT_SEPARATOR` by its `File`
|
|
97
|
+
# core class (`Separator` is the capital-S alias of `SEPARATOR`, declared in core RBS and just
|
|
98
|
+
# as spellable in project code; `ALT_SEPARATOR` is `nil` off Windows and simply gets no entry
|
|
99
|
+
# there); `Gem::VERSION` by rubygems, which is not the interpreter's to version but IS loaded
|
|
100
|
+
# in every process that can run `rigor` at all. A name that resolves to something other than a
|
|
101
|
+
# non-empty String on the running interpreter is skipped, so listing one costs nothing where it
|
|
102
|
+
# does not exist.
|
|
103
|
+
RUNTIME_STRING_CONSTANTS = %w[
|
|
104
|
+
RUBY_VERSION RUBY_RELEASE_DATE RUBY_PLATFORM RUBY_DESCRIPTION RUBY_COPYRIGHT
|
|
105
|
+
RUBY_ENGINE RUBY_ENGINE_VERSION RUBY_REVISION
|
|
106
|
+
Ruby::VERSION Ruby::RELEASE_DATE Ruby::PLATFORM Ruby::DESCRIPTION Ruby::COPYRIGHT
|
|
107
|
+
Ruby::ENGINE Ruby::ENGINE_VERSION Ruby::REVISION
|
|
108
|
+
File::SEPARATOR File::Separator File::PATH_SEPARATOR File::ALT_SEPARATOR
|
|
109
|
+
Encoding::UNICODE_VERSION
|
|
110
|
+
Gem::VERSION
|
|
111
|
+
].freeze
|
|
112
|
+
private_constant :RUNTIME_STRING_CONSTANTS
|
|
80
113
|
|
|
81
114
|
NON_EMPTY_STRING = Type::Combinator.non_empty_string.freeze
|
|
82
115
|
NUMERIC_STRING = Type::Combinator.numeric_string.freeze
|
|
83
116
|
private_constant :NON_EMPTY_STRING, :NUMERIC_STRING
|
|
84
117
|
|
|
85
|
-
# --- public API ---------------------------------------------------
|
|
86
|
-
|
|
87
|
-
# @param name [String] unqualified constant name (e.g. `"Math::PI"`,
|
|
88
|
-
# `"RUBY_VERSION"`, `"Ruby::ENGINE"`)
|
|
89
|
-
# @return [Rigor::Type, nil] refined type, or nil to fall through
|
|
90
|
-
def self.lookup(name)
|
|
91
|
-
FOLDED_CONSTANTS[name] || inspect_runtime_string(name)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
118
|
# --- private ------------------------------------------------------
|
|
95
119
|
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
def self.
|
|
99
|
-
|
|
120
|
+
# @param value [String] a non-empty string
|
|
121
|
+
# @return [Rigor::Type]
|
|
122
|
+
def self.classify_string(value)
|
|
123
|
+
if Type::Refined.ruby_numeric_literal?(value)
|
|
124
|
+
NUMERIC_STRING
|
|
125
|
+
else
|
|
126
|
+
NON_EMPTY_STRING
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
private_class_method :classify_string
|
|
100
130
|
|
|
131
|
+
# Reads `name` in the analyzer's runtime, returning its value when that is a non-empty String.
|
|
132
|
+
#
|
|
133
|
+
# Called ONLY with the names in {RUNTIME_STRING_CONSTANTS} — Rigor's own source — and only
|
|
134
|
+
# while this file is being loaded. It is never handed a name from the analysed program.
|
|
135
|
+
#
|
|
136
|
+
# @param name [String] a qualified constant path without a leading "::"
|
|
137
|
+
# @return [String, nil]
|
|
138
|
+
def self.runtime_string_value(name)
|
|
101
139
|
mod = ::Object
|
|
102
140
|
name.split("::").each do |part|
|
|
103
|
-
# Resolve only constants already present — never let analysing a reference drive
|
|
104
|
-
# the analyzer's own runtime to autoload or run a `const_missing` hook. A
|
|
105
|
-
# `Digest::UUID` reference in project code otherwise makes `const_get` trigger
|
|
106
|
-
# `Digest.const_missing` → `require "digest/uuid"`, and a missing optional library
|
|
107
|
-
# raises `LoadError` (a `ScriptError`, not the `NameError` the const_get walk
|
|
108
|
-
# expects), which would abort the whole run rather than fall through to the RBS
|
|
109
|
-
# tier. `const_defined?(part, false)` answers the same "is this resolvable here"
|
|
110
|
-
# question without the side effect — a project-defined constant (the common case)
|
|
111
|
-
# is simply absent and returns nil, no exception raised.
|
|
112
141
|
return nil unless mod.is_a?(::Module) && mod.const_defined?(part, false)
|
|
142
|
+
# `const_defined?` is true for a REGISTERED-BUT-NOT-YET-TRIGGERED autoload, so on its own
|
|
143
|
+
# it does not mean "already in memory" and `const_get` would EXECUTE the target file. Only
|
|
144
|
+
# `Module#autoload?` separates the two: it returns the registered path while the autoload
|
|
145
|
+
# is pending and nil once it has run, so declining on it declines exactly the dangerous
|
|
146
|
+
# case and still resolves a constant genuinely loaded ([#680]).
|
|
147
|
+
return nil if mod.autoload?(part)
|
|
113
148
|
|
|
114
149
|
mod = mod.const_get(part, false)
|
|
115
150
|
end
|
|
116
151
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
152
|
+
mod.is_a?(::String) && !mod.empty? ? mod : nil
|
|
153
|
+
rescue ::StandardError, ::ScriptError, ::SystemExit
|
|
154
|
+
# Deliberately wider than the `NameError` / `TypeError` / `LoadError` this used to name.
|
|
155
|
+
# Refining a constant is an optimisation and nothing it can hit justifies stopping the
|
|
156
|
+
# process; `SystemExit` is the one that actually did ([#680]) and is not a `StandardError`,
|
|
157
|
+
# and `ScriptError` covers the `LoadError` family for the same reason. `Interrupt`,
|
|
158
|
+
# `SignalException` and `NoMemoryError` are deliberately NOT caught — a Ctrl-C must keep
|
|
159
|
+
# reaching the user, and an out-of-memory process is not one to keep analysing in.
|
|
121
160
|
nil
|
|
122
161
|
end
|
|
123
|
-
private_class_method :
|
|
162
|
+
private_class_method :runtime_string_value
|
|
163
|
+
|
|
164
|
+
# @return [Hash{String => Rigor::Type}] frozen, built once at load time.
|
|
165
|
+
def self.build_runtime_string_refinements
|
|
166
|
+
RUNTIME_STRING_CONSTANTS.each_with_object({}) do |name, table|
|
|
167
|
+
value = runtime_string_value(name)
|
|
168
|
+
table[name] = classify_string(value) if value
|
|
169
|
+
end.freeze
|
|
170
|
+
end
|
|
171
|
+
private_class_method :build_runtime_string_refinements
|
|
124
172
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
173
|
+
RUNTIME_STRING_REFINEMENTS = build_runtime_string_refinements
|
|
174
|
+
private_constant :RUNTIME_STRING_REFINEMENTS
|
|
175
|
+
|
|
176
|
+
# --- public API ---------------------------------------------------
|
|
177
|
+
|
|
178
|
+
# @param name [String] unqualified constant name (e.g. `"Math::PI"`,
|
|
179
|
+
# `"RUBY_VERSION"`, `"Ruby::ENGINE"`)
|
|
180
|
+
# @return [Rigor::Type, nil] refined type, or nil to fall through
|
|
181
|
+
def self.lookup(name)
|
|
182
|
+
FOLDED_CONSTANTS[name] || RUNTIME_STRING_REFINEMENTS[name]
|
|
133
183
|
end
|
|
134
|
-
private_class_method :classify_string
|
|
135
184
|
end
|
|
136
185
|
end
|
|
137
186
|
end
|
|
@@ -66,13 +66,32 @@ module Rigor
|
|
|
66
66
|
FILE_NON_EMPTY = ->(_arg_types) { NON_EMPTY_STRING }
|
|
67
67
|
private_constant :FILE_NON_EMPTY
|
|
68
68
|
|
|
69
|
+
STRING_NOMINAL = Type::Combinator.nominal_of("String").freeze
|
|
70
|
+
private_constant :STRING_NOMINAL
|
|
71
|
+
|
|
72
|
+
# `IO.read(name)` / `File.read(name)` return nil ONLY when a `length` argument is given
|
|
73
|
+
# and the stream is at EOF; the no-length form always returns a String (it raises on
|
|
74
|
+
# error). Upstream RBS declares one row with an optional length — `-> String?` for every
|
|
75
|
+
# call shape — so the whole-file idiom `File.read(path, mode: 'rb')` read as nilable and
|
|
76
|
+
# fired `possible-nil-receiver` on working code (redmine's Import#read_file_head, three
|
|
77
|
+
# sites). The refinement drops the nil exactly when no positional length is present; a
|
|
78
|
+
# keyword-only trailing hash (`mode:`, `encoding:`) is not a length. Same family as the
|
|
79
|
+
# staged upstream signature fixes (#159).
|
|
80
|
+
IO_READ_NO_LENGTH = lambda do |arg_types|
|
|
81
|
+
positional = arg_types.grep_v(Type::HashShape)
|
|
82
|
+
positional.size <= 1 ? STRING_NOMINAL : nil
|
|
83
|
+
end
|
|
84
|
+
private_constant :IO_READ_NO_LENGTH
|
|
85
|
+
|
|
69
86
|
# Frozen ((owner_class_name, method_name, kind) => handler) table. The kind tag is
|
|
70
87
|
# `:both`, `:singleton`, or `:instance`. New entries SHOULD prefer `:both` unless the
|
|
71
88
|
# singleton- and instance-side shapes genuinely differ.
|
|
72
89
|
OVERRIDES = {
|
|
73
90
|
["Kernel", :__dir__, :both] => KERNEL_DIR,
|
|
74
91
|
["File", :expand_path, :singleton] => FILE_NON_EMPTY,
|
|
75
|
-
["File", :dirname, :singleton] => FILE_NON_EMPTY
|
|
92
|
+
["File", :dirname, :singleton] => FILE_NON_EMPTY,
|
|
93
|
+
["IO", :read, :singleton] => IO_READ_NO_LENGTH,
|
|
94
|
+
["File", :read, :singleton] => IO_READ_NO_LENGTH
|
|
76
95
|
}.freeze
|
|
77
96
|
private_constant :OVERRIDES
|
|
78
97
|
|
|
@@ -38,7 +38,17 @@ module Rigor
|
|
|
38
38
|
# whose RBS dangles an interface or type-alias reference gets stubs where an older Rigor discarded the
|
|
39
39
|
# whole batch and cached an env in which those signatures are inert. Same reasoning as v3: the
|
|
40
40
|
# marshalled env is the cached value, so it MUST be rebuilt for the fix to take effect.
|
|
41
|
-
|
|
41
|
+
# v7: #526 — the def-index seed bundles gain the `:extends` table (`extend M` / `extend self` /
|
|
42
|
+
# bare `module_function`), which `finalize_def_index` folds into the singleton-side method tables. A
|
|
43
|
+
# pre-7 bundle would silently contribute no extends for an unchanged file, so cached bundles must
|
|
44
|
+
# read as misses once and rebuild carrying the slot.
|
|
45
|
+
# v8: #577 / ADR-45 WD1 — a plugin {Plugin::IoBoundary} read that finds its path MISSING now records
|
|
46
|
+
# an absence row ({FileEntry.absent}: `:exists` / `"false"`) into the run-result and plugin-producer
|
|
47
|
+
# dependency descriptors, so a file that appears after a run which probed for it (a `db/schema.rb`
|
|
48
|
+
# added to a schema-less project) invalidates the warm entry. A pre-8 entry carries no absence rows
|
|
49
|
+
# and would validate fresh across exactly that edit, so cached entries must read as misses once and
|
|
50
|
+
# rebuild carrying the rows.
|
|
51
|
+
SCHEMA_VERSION = 8
|
|
42
52
|
|
|
43
53
|
# Per-slot entry value objects. Constructors validate enums / required fields and freeze the resulting
|
|
44
54
|
# struct so no caller can mutate after the entry is in a Descriptor.
|
|
@@ -54,6 +64,11 @@ module Rigor
|
|
|
54
64
|
# `:digest`, while the {Runner} run-dependency descriptor and plugin {IoBoundary} reads use `:stat`.
|
|
55
65
|
VALID_COMPARATORS = %i[digest stat mtime exists].freeze
|
|
56
66
|
|
|
67
|
+
# The two `:exists` values — `File.exist?`'s answer rendered with `to_s`, which is what
|
|
68
|
+
# {Descriptor#file_entry_fresh?} compares the live answer against.
|
|
69
|
+
PRESENT = "true"
|
|
70
|
+
ABSENT = "false"
|
|
71
|
+
|
|
57
72
|
attr_reader :path, :comparator, :value
|
|
58
73
|
|
|
59
74
|
value_fields :path, :comparator, :value
|
|
@@ -80,6 +95,30 @@ module Rigor
|
|
|
80
95
|
new(path: path, comparator: :stat, value: packed)
|
|
81
96
|
end
|
|
82
97
|
|
|
98
|
+
# ADR-45 WD1 (#577) — the absence row: fresh while `path` does not exist, stale the moment anything
|
|
99
|
+
# (a file, a directory, a symlink) comes into existence there. {Plugin::IoBoundary#read_file}
|
|
100
|
+
# records one when a read fails because the path is missing, so a value computed on "X is absent"
|
|
101
|
+
# invalidates when X appears. Validation is a single `File.exist?` — no stat tuple, no digest,
|
|
102
|
+
# nothing that can move on an unchanged tree — so an absence row never costs a warm run its hit.
|
|
103
|
+
def self.absent(path:)
|
|
104
|
+
new(path: path, comparator: :exists, value: ABSENT)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# ADR-45 WD1b (#613) — {.absent}'s positive twin: fresh while `path` exists, stale once it is gone.
|
|
108
|
+
# {Plugin::IoBoundary#file?} / `#directory?` record one when an existence probe found what it asked
|
|
109
|
+
# for and the plugin never read the bytes (a discovery root it globbed, a config file whose mere
|
|
110
|
+
# presence switched a mode). It is the weakest file row — a rewrite in place leaves it fresh — so it
|
|
111
|
+
# is only ever the row for a dependency that really is on existence alone; a read records the `:stat`
|
|
112
|
+
# row over it.
|
|
113
|
+
def self.present(path:)
|
|
114
|
+
new(path: path, comparator: :exists, value: PRESENT)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# @return [Boolean] whether this row records the path as absent (see {.absent}).
|
|
118
|
+
def absent?
|
|
119
|
+
comparator == :exists && value == ABSENT
|
|
120
|
+
end
|
|
121
|
+
|
|
83
122
|
def to_h
|
|
84
123
|
{ "path" => path, "comparator" => comparator.to_s, "value" => value }
|
|
85
124
|
end
|
|
@@ -280,7 +319,8 @@ module Rigor
|
|
|
280
319
|
# version) belong in the cache *key*, not the validated dependency set — so a descriptor carrying any
|
|
281
320
|
# non-file slot is never considered fresh (it was built wrong for this use). ADR-60 WD3 adds `globs`
|
|
282
321
|
# alongside `files` as a re-validatable slot: a {GlobEntry} is fresh when re-globbing + re-digesting
|
|
283
|
-
# reproduces its recorded value.
|
|
322
|
+
# reproduces its recorded value. ADR-45 WD1 (#577): an absence row ({FileEntry.absent}) is fresh
|
|
323
|
+
# while its path is still missing — the negative half of the dependency set.
|
|
284
324
|
def fresh?
|
|
285
325
|
return false unless gems.empty? && plugins.empty? && configs.empty? && dependencies.empty?
|
|
286
326
|
|
|
@@ -54,8 +54,37 @@ module Rigor
|
|
|
54
54
|
# loads as nil (a clean cold rebuild — no migration). 12: ADR-103 WD13 / issue #382 adds the effects
|
|
55
55
|
# sidecar — `effect_collections` (the per-file {Rigor::Effects::FileCollection}s a collecting run
|
|
56
56
|
# produced) and the `effects_identity` they were produced under; a pre-12 blob mismatches the SCHEMA
|
|
57
|
-
# gate and loads as nil (a clean cold rebuild — no migration).
|
|
58
|
-
|
|
57
|
+
# gate and loads as nil (a clean cold rebuild — no migration). 13: issue #644 adds `constant_decls`
|
|
58
|
+
# (the per-file constant PUBLICATION CENSUS — `{qualified name => [literal] | :unpublishable}` — whose
|
|
59
|
+
# diff drives the new `constant:` edge's producer) and grows each seed bundle's row grammar by its own
|
|
60
|
+
# `constant_writes` census; a pre-13 blob mismatches the SCHEMA gate and loads as nil (a clean cold
|
|
61
|
+
# rebuild — no migration). 14: issue #707 widens each seed-bundle def row from `[node_id, name,
|
|
62
|
+
# fingerprint]` to `[node_id, name, fingerprint, nesting]`, carrying issue #681's recorded
|
|
63
|
+
# `Module.nesting` so a bundle-served callee resolves its constants under the declaration that owns it
|
|
64
|
+
# instead of a peel of the receiver's name; a pre-14 blob mismatches the SCHEMA gate and loads as nil (a
|
|
65
|
+
# clean cold rebuild — no migration). The bump is what keeps a pre-14 blob REJECTED rather than MISREAD:
|
|
66
|
+
# a three-element row destructures with `nesting` nil, which is indistinguishable from a top-level def,
|
|
67
|
+
# so every unchanged file would silently keep the pre-fix peel. 15: issue #682 gives each seed bundle a
|
|
68
|
+
# `header_nestings` table — the `Module.nesting` each class / module DECLARATION HEADER is written in —
|
|
69
|
+
# so a bundle-served class resolves its superclass and include names in the cref Ruby resolves them in
|
|
70
|
+
# rather than by peeling its own qualified name; a pre-15 blob mismatches the SCHEMA gate and loads as
|
|
71
|
+
# nil (a clean cold rebuild — no migration). Absence here is gradual rather than misread (the resolver
|
|
72
|
+
# falls back to the peel), but a warm run that peels where a cold run does not is exactly the
|
|
73
|
+
# `--verify-incremental` divergence 14 was bumped for.
|
|
74
|
+
# 16: issue #722 residue 1 gives each seed bundle's `superclasses` values a ROOTED MARKER — a leading
|
|
75
|
+
# `::` on an ancestor name the site wrote rooted (`class X < ::Base`, `Class.new(::Parent)`) — so the
|
|
76
|
+
# resolvers anchor it at the top level instead of walking the enclosing nesting. The value stays a
|
|
77
|
+
# String, so a pre-16 blob deserialises cleanly and is exactly why the gate has to reject it: an
|
|
78
|
+
# un-rooted value is indistinguishable from "not rooted", and every unchanged file would keep the
|
|
79
|
+
# pre-fix answer on a warm run while a cold run gave the right one.
|
|
80
|
+
# 17: issue #716 changes what a seed-bundle def row's `nesting` MEANS without changing its shape. A
|
|
81
|
+
# top-level def now records the EMPTY chain (Ruby's `Module.nesting` there is `[]`), so the resolver
|
|
82
|
+
# anchors its constants at the top level instead of peeling the caller's namespace; `nil` narrows to
|
|
83
|
+
# "no chain recorded at all". A pre-17 blob stored `nil` for exactly the top-level defs that now store
|
|
84
|
+
# `[]`, deserialises cleanly, and would send every unchanged file's top-level helper back to the peel
|
|
85
|
+
# on a warm run while a cold run resolved at the top level — the `--verify-incremental` divergence 14
|
|
86
|
+
# was bumped for, in the same table.
|
|
87
|
+
SCHEMA = 17
|
|
59
88
|
|
|
60
89
|
# The persisted per-file state.
|
|
61
90
|
# `cache` maps an analyzed file to its diagnostics.
|
|
@@ -69,6 +98,12 @@ module Rigor
|
|
|
69
98
|
# ADR-46 slice 3:
|
|
70
99
|
# `missing` maps a consumer to Set<"kind:name"> it looked up and missed.
|
|
71
100
|
# `class_decls` maps a path to Set<qualified class name> it declares.
|
|
101
|
+
# Issue #644:
|
|
102
|
+
# `constant_decls` maps a path to that file's constant PUBLICATION CENSUS,
|
|
103
|
+
# `{qualified name => [literal] | :unpublishable}`. The descriptor is what the recheck diffs: a name's
|
|
104
|
+
# published answer is a function of the whole project's write set for it, so a value edit, a write
|
|
105
|
+
# becoming unpublishable, or a second declarer appearing / going away all move the answer without
|
|
106
|
+
# moving the name set.
|
|
72
107
|
# ADR-85 WD2:
|
|
73
108
|
# `seed_bundles` maps an analyzed path to its per-file discovery contribution (plain-data tables +
|
|
74
109
|
# `(node_id, name, fingerprint)` def-node handles + content digest), so a warm recheck rebuilds the
|
|
@@ -103,7 +138,7 @@ module Rigor
|
|
|
103
138
|
# and a vocabulary / catalogue / `effects:` change must invalidate the summaries alone.
|
|
104
139
|
Payload = Data.define(:cache, :sources, :digests, :analyzed,
|
|
105
140
|
:symbol_sources, :ancestry_sources, :symbol_fingerprints,
|
|
106
|
-
:missing, :class_decls, :seed_bundles, :plugin_fact_digest,
|
|
141
|
+
:missing, :class_decls, :constant_decls, :seed_bundles, :plugin_fact_digest,
|
|
107
142
|
:return_summaries, :param_table,
|
|
108
143
|
:effect_collections, :effects_identity)
|
|
109
144
|
|
|
@@ -218,6 +253,7 @@ module Rigor
|
|
|
218
253
|
symbol_fingerprints: data[:symbol_fingerprints] || {},
|
|
219
254
|
missing: data[:missing] || {},
|
|
220
255
|
class_decls: data[:class_decls] || {},
|
|
256
|
+
constant_decls: data[:constant_decls] || {},
|
|
221
257
|
seed_bundles: data[:seed_bundles] || {},
|
|
222
258
|
plugin_fact_digest: data[:plugin_fact_digest],
|
|
223
259
|
return_summaries: data[:return_summaries] || {},
|
|
@@ -241,6 +277,7 @@ module Rigor
|
|
|
241
277
|
symbol_fingerprints: payload.symbol_fingerprints,
|
|
242
278
|
missing: payload.missing,
|
|
243
279
|
class_decls: payload.class_decls,
|
|
280
|
+
constant_decls: payload.constant_decls,
|
|
244
281
|
seed_bundles: payload.seed_bundles,
|
|
245
282
|
plugin_fact_digest: payload.plugin_fact_digest,
|
|
246
283
|
return_summaries: payload.return_summaries,
|
|
@@ -9,11 +9,28 @@ require "rbs"
|
|
|
9
9
|
#
|
|
10
10
|
# Patch policy (purely additive):
|
|
11
11
|
#
|
|
12
|
-
# - `_dump` returns
|
|
13
|
-
# consult
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
12
|
+
# - `_dump` returns the buffer's NAME and nothing else. Per-node source POSITIONS are still dropped (Rigor
|
|
13
|
+
# does not consult them from any analysis path — every diagnostic uses Prism's own location), but the
|
|
14
|
+
# file a declaration came from is cheap to keep and is not inert: `rbs.coverage.definition-build-failed`
|
|
15
|
+
# names the conflicting signature files, and dropping the name made a warm run omit that clause while a
|
|
16
|
+
# cold run printed it, so the same project said different things by cache state (issue #696 review,
|
|
17
|
+
# second pass). It costs cache SIZE: a `_dump` payload is raw bytes, not a linkable object graph, so the
|
|
18
|
+
# path is written once per Location rather than once per buffer. Memoising the coerced name per buffer was
|
|
19
|
+
# tried and changes nothing for exactly that reason. Measured +6.6% on a realistic project (Rigor's own
|
|
20
|
+
# 349-file `lib`: 4,332K to 4,616K) and +12.3% on a one-file project (1,672K to 1,876K), both stable
|
|
21
|
+
# across reps — the env blob is fixed overhead, so the ratio falls as a project's own cached data grows
|
|
22
|
+
# and the realistic figure is the lower one. That is the price of a diagnostic that reads the same warm
|
|
23
|
+
# and cold; positions, which are far more numerous and which nothing reads, stay dropped.
|
|
24
|
+
# - `_load` reconstructs a zero-range Location over that name, falling back to a `<cached>` sentinel when
|
|
25
|
+
# the dump carried none. Code paths that DID consult Location after a cache hit see a benign value rather
|
|
26
|
+
# than crashing, and one that reads `buffer.name` now sees the real path.
|
|
27
|
+
# - Both directions were exercised rather than assumed. A NEW blob read by an OLDER `_load` — which ignored
|
|
28
|
+
# its argument — loads and behaves exactly as before; that direction is clean. An OLD blob read by THIS
|
|
29
|
+
# `_load` also loads without raising, and the sentinel fallback keeps it from ever naming `<cached>` as a
|
|
30
|
+
# path — but the conflicting-files clause then goes missing, so a warm run off a pre-change blob says less
|
|
31
|
+
# than a cold run does. ADR-6's store never evicts, so that would persist. `Cache::Store::FORMAT_VERSION`
|
|
32
|
+
# is therefore bumped to 3: `PAYLOAD_ABI_VERSION` already rebuilds across a release, and the bump closes
|
|
33
|
+
# the same-version window too.
|
|
17
34
|
#
|
|
18
35
|
# Idempotent: the guard checks `method_defined?(:_dump)` so requiring this file twice (or against an upstream
|
|
19
36
|
# rbs that adds Marshal hooks itself) is a no-op.
|
|
@@ -33,13 +50,19 @@ require "rbs"
|
|
|
33
50
|
# reference. Both classes are value objects fully described by `to_s`, so the round-trip is lossless.
|
|
34
51
|
module RBS
|
|
35
52
|
class Location
|
|
53
|
+
# The name a Location gets back when the dump carried none — an old blob, or a buffer that never had
|
|
54
|
+
# one. Never a real path, so a consumer that reports file names must filter it
|
|
55
|
+
# ({Rigor::Environment::RbsLoader::CACHED_LOCATION_BUFFER_NAME}).
|
|
56
|
+
CACHED_BUFFER_NAME = "<cached>"
|
|
57
|
+
|
|
36
58
|
unless method_defined?(:_dump)
|
|
37
59
|
def _dump(_)
|
|
38
|
-
|
|
60
|
+
buffer&.name.to_s
|
|
39
61
|
end
|
|
40
62
|
|
|
41
|
-
def self._load(
|
|
42
|
-
|
|
63
|
+
def self._load(name)
|
|
64
|
+
name = CACHED_BUFFER_NAME if name.nil? || name.empty?
|
|
65
|
+
new(buffer: ::RBS::Buffer.new(name: name, content: ""), start_pos: 0, end_pos: 0)
|
|
43
66
|
end
|
|
44
67
|
end
|
|
45
68
|
end
|
data/lib/rigor/cache/store.rb
CHANGED
|
@@ -29,7 +29,16 @@ module Rigor
|
|
|
29
29
|
# fail the header check and read as silent misses; the `schema_version.txt` marker additionally carries
|
|
30
30
|
# this version, so the first writable run after a bump clears the root and reclaims the unreadable
|
|
31
31
|
# bytes.
|
|
32
|
-
|
|
32
|
+
#
|
|
33
|
+
# v3 (issue #696 review, second pass): `RBS::Location#_dump` now carries the buffer NAME rather than an
|
|
34
|
+
# empty string, so an env blob written before this change reconstructs every location behind the
|
|
35
|
+
# `<cached>` sentinel. That degrades gracefully — the sentinel is filtered, never printed as a path —
|
|
36
|
+
# but `rbs.coverage.definition-build-failed` then omits its conflicting-files clause, so a warm run off
|
|
37
|
+
# a pre-change blob says less than a cold run does, indefinitely: ADR-6's store never evicts.
|
|
38
|
+
# `PAYLOAD_ABI_VERSION` already rebuilds across a RELEASE, so the exposure is a same-version tree; this
|
|
39
|
+
# closes that window too, because "the same project reports differently depending on how you ran it" is
|
|
40
|
+
# the defect the diagnostic exists to end and a stale blob reintroduces it.
|
|
41
|
+
FORMAT_VERSION = 3
|
|
33
42
|
|
|
34
43
|
# Payload ABI version. Store values are mostly Marshal blobs of Rigor/RBS objects, so a Rigor release
|
|
35
44
|
# upgrade is an ABI boundary even when the byte layout and descriptor schema are unchanged. Folding the
|
|
@@ -217,24 +217,23 @@ module Rigor
|
|
|
217
217
|
# class. Tier 1 seeds unconditionally because it only reclassifies sites it already counted; Tier 2 adds
|
|
218
218
|
# sites to a denominator `--threshold` gates CI on, which is why only the latter is gated on a feature id.
|
|
219
219
|
def scope_with_inferred_params(paths, configuration, environment, workers)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
discovered = Inference::ScopeIndexer.discovered_classes_for_paths(paths)
|
|
224
|
-
seed[:discovered_classes] = discovered unless discovered.empty?
|
|
225
|
-
|
|
226
|
-
table = Inference::ParameterInferenceCollector.collect(
|
|
227
|
-
files: paths, environment: environment, target_ruby: configuration.target_ruby, workers: workers
|
|
220
|
+
CoverageScan.discovery_seeded_scope(
|
|
221
|
+
files: paths, configuration: configuration, environment: environment,
|
|
222
|
+
parameter_inference: true, workers: workers
|
|
228
223
|
)
|
|
229
|
-
seed[:param_inferred_types] = table unless table.empty?
|
|
230
|
-
|
|
231
|
-
return base if seed.empty?
|
|
232
|
-
|
|
233
|
-
base.with_discovery(base.discovery.with(**seed))
|
|
234
224
|
end
|
|
235
225
|
|
|
226
|
+
# Issue #686 — an unmeasured file fails the build the way a parse error already does, and for the same
|
|
227
|
+
# reason: the command could not measure part of what it was asked about, so its ratio is an answer to
|
|
228
|
+
# a smaller question than the user asked. Deliberately NOT routed through `--threshold`, which the
|
|
229
|
+
# line below applies to a ratio: a crashed file's mutants leave `killed + survived` at zero, the
|
|
230
|
+
# project ratio is then computed over the files that DID work, and a run that measured nothing at all
|
|
231
|
+
# came out at 1.0 and passed every threshold. It is also deliberately not the `harness_errors` count,
|
|
232
|
+
# which #264 keeps out of the exit code on purpose — a few rescued mutants are the transient that
|
|
233
|
+
# bucket exists to make visible, while a wholly unmeasured FILE is not a transient.
|
|
236
234
|
def determine_protection_exit(report, options)
|
|
237
235
|
return 1 unless report.parse_errors.empty?
|
|
236
|
+
return 1 if unmeasured_files_in(report).positive?
|
|
238
237
|
|
|
239
238
|
threshold = options[:threshold]
|
|
240
239
|
return 0 if threshold.nil?
|
|
@@ -242,6 +241,12 @@ module Rigor
|
|
|
242
241
|
report.ratio < threshold ? 1 : 0
|
|
243
242
|
end
|
|
244
243
|
|
|
244
|
+
# Issue #686 — the one reader of the count, shared with the stderr warning. The Tier-1 protection
|
|
245
|
+
# report has no such notion, so a report without the field reads as zero rather than erroring.
|
|
246
|
+
def unmeasured_files_in(report)
|
|
247
|
+
report.respond_to?(:unmeasured_files) ? report.unmeasured_files : 0
|
|
248
|
+
end
|
|
249
|
+
|
|
245
250
|
def usage_error
|
|
246
251
|
@err.puts("coverage: at least one path is required")
|
|
247
252
|
@err.puts(USAGE)
|
|
@@ -254,11 +259,11 @@ module Rigor
|
|
|
254
259
|
|
|
255
260
|
# The protection scan must see the same receiver types `rigor check` does — including plugin-contributed
|
|
256
261
|
# `dynamic_return` types (a controller's `params` → `ActionController::Parameters`, a `Model.where` →
|
|
257
|
-
# `ActiveRecord::Relation[Model]`).
|
|
258
|
-
#
|
|
259
|
-
#
|
|
260
|
-
#
|
|
261
|
-
#
|
|
262
|
+
# `ActiveRecord::Relation[Model]`). A bare RBS environment carries no plugin registry, so every plugin-typed
|
|
263
|
+
# receiver reads `Dynamic` and its dispatch site is miscounted as *unprotected* — a systematic undercount of
|
|
264
|
+
# what Rigor actually types on a plugin-using project. `ProjectContext` builds the plugin-aware environment
|
|
265
|
+
# (registry materialised + the per-run prepare pass that primes producers like the controller / model index)
|
|
266
|
+
# exactly as the LSP, the runner, and — since #513 — the precision path all do.
|
|
262
267
|
def plugin_aware_environment(configuration)
|
|
263
268
|
LanguageServer::ProjectContext.new(configuration: configuration).environment
|
|
264
269
|
end
|
|
@@ -9,6 +9,8 @@ require_relative "../protection/dependency_closure"
|
|
|
9
9
|
require_relative "../protection/discovery_seed"
|
|
10
10
|
require_relative "../protection/mutation_cache"
|
|
11
11
|
|
|
12
|
+
require_relative "measurement_integrity_warning"
|
|
13
|
+
|
|
12
14
|
module Rigor
|
|
13
15
|
class CLI
|
|
14
16
|
# ADR-63 Tier 2 + ADR-70 — the mutation-effectiveness and fused static∪dynamic protection paths, factored out of
|
|
@@ -43,17 +45,13 @@ module Rigor
|
|
|
43
45
|
|
|
44
46
|
private
|
|
45
47
|
|
|
46
|
-
#
|
|
48
|
+
# Both measurement-integrity warnings, and the escalation rule between them, live in
|
|
49
|
+
# {MeasurementIntegrityWarning}. Only the floor stays here: it is this command's policy, and
|
|
50
|
+
# `coverage_command_spec` reads it off this module.
|
|
51
|
+
#
|
|
52
|
+
# @param report [MutationProtectionReport, FusedProtectionReport]
|
|
47
53
|
def warn_harness_errors(report)
|
|
48
|
-
|
|
49
|
-
return if count < HARNESS_ERROR_WARN_FLOOR
|
|
50
|
-
|
|
51
|
-
@err.puts(
|
|
52
|
-
"coverage: #{count} mutants failed inside the measurement harness (\"harness_errors\", " \
|
|
53
|
-
"at/above the #{HARNESS_ERROR_WARN_FLOOR}-mutant floor) — excluded from the ratio like a " \
|
|
54
|
-
"parse-invalid mutant, but this many suggests a harness defect rather than one-off noise. " \
|
|
55
|
-
"Investigate before trusting --threshold on this run."
|
|
56
|
-
)
|
|
54
|
+
MeasurementIntegrityWarning.emit(report, err: @err, floor: HARNESS_ERROR_WARN_FLOOR)
|
|
57
55
|
end
|
|
58
56
|
|
|
59
57
|
# The cross-file knowledge Tier 2 measures with — the #253 gate, and the ONLY place in this feature that
|
|
@@ -262,13 +260,11 @@ module Rigor
|
|
|
262
260
|
# the two sets were assembled.
|
|
263
261
|
#
|
|
264
262
|
# The two cache phases each get their OWN {Cache::FileDigest.with_run} scope and the MEASUREMENT sits
|
|
265
|
-
# between them, deliberately: that scope installs a per-path digest memo
|
|
266
|
-
#
|
|
267
|
-
#
|
|
268
|
-
#
|
|
269
|
-
#
|
|
270
|
-
# the other side. The cost of two scopes is one extra SHA-256 per cached file, against a measurement
|
|
271
|
-
# that is hundreds of analyses.
|
|
263
|
+
# between them, deliberately: that scope installs a per-path digest memo. {Protection::ClosureKillOracle}
|
|
264
|
+
# now writes each mutant to a fresh block-scoped path, so a memo spanning the measurement could no
|
|
265
|
+
# longer serve one mutant another's digest — but keeping the measurement outside any memo scope stays
|
|
266
|
+
# correct by construction rather than by the oracle's path discipline, and costs only one extra
|
|
267
|
+
# SHA-256 per cached file against a measurement that is hundreds of analyses.
|
|
272
268
|
def measure_mutation_files(paths, cache:, scanner:, context:, configuration:, workers:)
|
|
273
269
|
cached = with_digest_run(configuration) { paths.to_h { |path| [path, cache.fetch(path)] }.compact }
|
|
274
270
|
pending = paths - cached.keys
|