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
|
@@ -52,6 +52,15 @@ module Rigor
|
|
|
52
52
|
Rigor::Inference::HktBodyParser.parse(CSV_PARSED_BODY, params: [:K])
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
# `csv::row[K]` — `Array[K | nil]` (CSV.parse_line's shape:
|
|
56
|
+
# an Array of optionally-nil cell values for a single row).
|
|
57
|
+
CSV_ROW_BODY = "Array[K | nil]"
|
|
58
|
+
private_constant :CSV_ROW_BODY
|
|
59
|
+
|
|
60
|
+
def csv_row_body_tree
|
|
61
|
+
Rigor::Inference::HktBodyParser.parse(CSV_ROW_BODY, params: [:K])
|
|
62
|
+
end
|
|
63
|
+
|
|
55
64
|
def json_value_registration
|
|
56
65
|
Rigor::Inference::HktRegistry::Registration.new(
|
|
57
66
|
uri: :"json::value",
|
|
@@ -90,6 +99,25 @@ module Rigor
|
|
|
90
99
|
)
|
|
91
100
|
end
|
|
92
101
|
|
|
102
|
+
def csv_row_registration
|
|
103
|
+
Rigor::Inference::HktRegistry::Registration.new(
|
|
104
|
+
uri: :"csv::row",
|
|
105
|
+
arity: 1,
|
|
106
|
+
variance: [:out],
|
|
107
|
+
bound: Rigor::Type::Combinator.untyped
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def csv_row_definition
|
|
112
|
+
Rigor::Inference::HktRegistry.definition_with_body_tree(
|
|
113
|
+
uri: :"csv::row",
|
|
114
|
+
params: [:K],
|
|
115
|
+
body_tree: csv_row_body_tree,
|
|
116
|
+
source_path: __FILE__,
|
|
117
|
+
source_line: __LINE__ - 5
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
|
|
93
121
|
# @return [Rigor::Inference::HktRegistry] frozen registry
|
|
94
122
|
# pre-seeded with all bundled HKT registrations +
|
|
95
123
|
# bodies. Allocated fresh each call rather than
|
|
@@ -104,8 +132,8 @@ module Rigor
|
|
|
104
132
|
# once ADR-15 phase 4b.x covers the dependency graph.
|
|
105
133
|
def registry
|
|
106
134
|
Rigor::Inference::HktRegistry.new(
|
|
107
|
-
registrations: [json_value_registration, csv_parsed_registration],
|
|
108
|
-
definitions: [json_value_definition, csv_parsed_definition]
|
|
135
|
+
registrations: [json_value_registration, csv_parsed_registration, csv_row_registration],
|
|
136
|
+
definitions: [json_value_definition, csv_parsed_definition, csv_row_definition]
|
|
109
137
|
)
|
|
110
138
|
end
|
|
111
139
|
|
|
@@ -160,6 +188,14 @@ module Rigor
|
|
|
160
188
|
}.freeze
|
|
161
189
|
private_constant :CSV_PARSED_SPEC
|
|
162
190
|
|
|
191
|
+
CSV_ROW_SPEC = {
|
|
192
|
+
uri: :"csv::row",
|
|
193
|
+
args: ["String"],
|
|
194
|
+
discriminator: nil,
|
|
195
|
+
post_reduce: nil
|
|
196
|
+
}.freeze
|
|
197
|
+
private_constant :CSV_ROW_SPEC
|
|
198
|
+
|
|
163
199
|
METHOD_RETURN_OVERRIDES = {
|
|
164
200
|
# JSON — stdlib's `json` library. Upstream rbs declares
|
|
165
201
|
# `(string, ?options) -> untyped`; the HKT-builtin tier
|
|
@@ -190,17 +226,20 @@ module Rigor
|
|
|
190
226
|
["YAML", :safe_load_file, :singleton] => YAML_SAFE_VALUE_SPEC,
|
|
191
227
|
["Psych", :safe_load, :singleton] => YAML_SAFE_VALUE_SPEC,
|
|
192
228
|
["Psych", :safe_load_file, :singleton] => YAML_SAFE_VALUE_SPEC,
|
|
193
|
-
# CSV.parse / CSV.read — no-headers shape only.
|
|
229
|
+
# CSV.parse / CSV.read / CSV.readlines / CSV.parse_line — no-headers shape only.
|
|
194
230
|
# Upstream rbs declares broader return shapes but
|
|
195
231
|
# the common case is `Array[Array[String?]]` which
|
|
196
|
-
# the `csv::parsed[String]` URI matches
|
|
232
|
+
# the `csv::parsed[String]` URI matches, or
|
|
233
|
+
# `Array[String?]` which `csv::row[String]` matches. The
|
|
197
234
|
# `headers: true` shape (`CSV::Table` of `CSV::Row`)
|
|
198
235
|
# is NOT covered — calls passing the option fall
|
|
199
236
|
# through to the upstream RBS type. CSV.foreach also
|
|
200
237
|
# falls through (it yields rows rather than
|
|
201
238
|
# returning a typed structure).
|
|
202
|
-
["CSV", :parse,
|
|
203
|
-
["CSV", :read,
|
|
239
|
+
["CSV", :parse, :singleton] => CSV_PARSED_SPEC,
|
|
240
|
+
["CSV", :read, :singleton] => CSV_PARSED_SPEC,
|
|
241
|
+
["CSV", :readlines, :singleton] => CSV_PARSED_SPEC,
|
|
242
|
+
["CSV", :parse_line, :singleton] => CSV_ROW_SPEC
|
|
204
243
|
}.freeze
|
|
205
244
|
|
|
206
245
|
# @return [Rigor::Type, nil] the reduced HKT type for
|
|
@@ -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
|
|
|
@@ -30,9 +30,10 @@ module Rigor
|
|
|
30
30
|
# identical to a bare `Digest::SHA256.file`.
|
|
31
31
|
module FileDigest
|
|
32
32
|
MEMO_KEY = :rigor_cache_file_digest_memo
|
|
33
|
+
STAT_KEY = :rigor_cache_file_stat_memo
|
|
33
34
|
INSTANT_KEY = :rigor_cache_recording_instant
|
|
34
35
|
STRICT_KEY = :rigor_cache_strict_validation
|
|
35
|
-
private_constant :MEMO_KEY, :INSTANT_KEY, :STRICT_KEY
|
|
36
|
+
private_constant :MEMO_KEY, :STAT_KEY, :INSTANT_KEY, :STRICT_KEY
|
|
36
37
|
|
|
37
38
|
# Set in the environment to force the strict digest-always validation path for a single run, regardless
|
|
38
39
|
# of the `cache.validation` config setting (the env wins). The escape hatch for a filesystem whose stat
|
|
@@ -47,14 +48,17 @@ module Rigor
|
|
|
47
48
|
# digest-always path for `cache.validation: digest`.
|
|
48
49
|
def self.with_run(strict: false)
|
|
49
50
|
previous_memo = Thread.current[MEMO_KEY]
|
|
51
|
+
previous_stat = Thread.current[STAT_KEY]
|
|
50
52
|
previous_instant = Thread.current[INSTANT_KEY]
|
|
51
53
|
previous_strict = Thread.current[STRICT_KEY]
|
|
52
54
|
Thread.current[MEMO_KEY] = {}
|
|
55
|
+
Thread.current[STAT_KEY] = {}
|
|
53
56
|
Thread.current[INSTANT_KEY] = now_ns
|
|
54
57
|
Thread.current[STRICT_KEY] = strict
|
|
55
58
|
yield
|
|
56
59
|
ensure
|
|
57
60
|
Thread.current[MEMO_KEY] = previous_memo
|
|
61
|
+
Thread.current[STAT_KEY] = previous_stat
|
|
58
62
|
Thread.current[INSTANT_KEY] = previous_instant
|
|
59
63
|
Thread.current[STRICT_KEY] = previous_strict
|
|
60
64
|
end
|
|
@@ -102,12 +106,26 @@ module Rigor
|
|
|
102
106
|
digest = parsed[0]
|
|
103
107
|
return hexdigest(path) == digest if strict_validation?
|
|
104
108
|
|
|
105
|
-
st =
|
|
109
|
+
st = validation_stat(path)
|
|
106
110
|
return true if !racy?(parsed) && tuple_matches?(st, parsed)
|
|
107
111
|
|
|
108
112
|
hexdigest(path) == digest
|
|
109
113
|
end
|
|
110
114
|
|
|
115
|
+
# The VALIDATION-side stat, served from the per-run table when one is installed — a collecting run
|
|
116
|
+
# validates the effects entry and the diagnostics entry against the same ~thousands-of-files
|
|
117
|
+
# dependency descriptor, and the second pass is pure repetition under the run's own stable-filesystem
|
|
118
|
+
# premise (see the module doc). The RECORDING side ({.pack_stat}) deliberately keeps its direct
|
|
119
|
+
# `File.stat`: it packs the tuple a *future* run validates, after the content was read, and must
|
|
120
|
+
# describe that moment rather than an earlier probe's. A stat failure propagates un-memoised, exactly
|
|
121
|
+
# as {.hexdigest} treats a read failure.
|
|
122
|
+
def self.validation_stat(path)
|
|
123
|
+
memo = Thread.current[STAT_KEY]
|
|
124
|
+
return File.stat(path) if memo.nil?
|
|
125
|
+
|
|
126
|
+
memo[path] ||= File.stat(path)
|
|
127
|
+
end
|
|
128
|
+
|
|
111
129
|
def self.recording_instant_ns
|
|
112
130
|
Thread.current[INSTANT_KEY] || now_ns
|
|
113
131
|
end
|
|
@@ -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
|