rigortype 0.4.0 → 0.4.2
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 +4 -3
- data/data/builtins/ruby_core/io.yml +1 -0
- data/data/builtins/ruby_core/proc.yml +4 -0
- data/data/builtins/ruby_core/random.yml +4 -0
- data/data/builtins/ruby_core/struct.yml +3 -0
- data/data/core_overlay/env.rbs +34 -0
- data/data/core_overlay/gc.rbs +50 -0
- data/data/core_overlay/hash.rbs +45 -0
- data/data/core_overlay/integer.rbs +31 -0
- data/data/core_overlay/io_buffer.rbs +27 -0
- data/data/core_overlay/kernel.rbs +40 -0
- data/data/core_overlay/match_data.rbs +28 -0
- data/data/core_overlay/module.rbs +51 -0
- data/data/core_overlay/object_space.rbs +29 -0
- data/data/core_overlay/proc.rbs +45 -0
- data/data/core_overlay/range.rbs +40 -0
- data/data/core_overlay/source_range.rbs +78 -0
- data/data/core_overlay/string.rbs +86 -0
- data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
- data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
- data/docs/handbook/01-getting-started.md +3 -3
- data/docs/handbook/06-classes.md +3 -1
- data/docs/handbook/08-understanding-errors.md +3 -2
- data/docs/handbook/11-sig-gen.md +6 -0
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/install.md +87 -15
- data/docs/manual/02-cli-reference.md +2 -2
- data/docs/manual/03-configuration.md +6 -1
- data/docs/manual/04-diagnostics.md +2 -1
- data/docs/manual/06-baseline.md +2 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/08-skills.md +4 -3
- data/docs/manual/12-caching.md +43 -4
- data/docs/manual/14-rails-quickstart.md +19 -11
- data/docs/manual/18-removing-dead-code.md +32 -2
- data/docs/manual/plugins/README.md +6 -1
- data/docs/manual/plugins/rigor-activerecord.md +21 -0
- data/docs/manual/plugins/rigor-alba.md +60 -0
- data/docs/manual/plugins/rigor-minitest.md +22 -4
- data/docs/manual/plugins/rigor-typelizer.md +71 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
- data/lib/rigor/analysis/check_rules/source_arity.rb +184 -61
- data/lib/rigor/analysis/check_rules.rb +407 -144
- data/lib/rigor/analysis/dependency_recorder.rb +215 -18
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
- data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
- data/lib/rigor/analysis/erb_template_detector.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +29 -9
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +291 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +173 -0
- data/lib/rigor/analysis/incremental_session.rb +287 -50
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
- data/lib/rigor/analysis/reachability/graph.rb +123 -23
- data/lib/rigor/analysis/reachability/scan.rb +290 -36
- data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +43 -9
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +88 -6
- data/lib/rigor/analysis/runner/pool_coordinator.rb +52 -13
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +371 -49
- data/lib/rigor/analysis/shadow_harness.rb +27 -0
- data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
- data/lib/rigor/analysis/template_unit_paths.rb +6 -4
- data/lib/rigor/analysis/template_units.rb +15 -5
- data/lib/rigor/analysis/worker_session.rb +11 -7
- data/lib/rigor/cache/descriptor.rb +40 -1
- data/lib/rigor/cache/file_digest.rb +28 -0
- data/lib/rigor/cache/incremental_snapshot.rb +92 -8
- data/lib/rigor/cache/store.rb +63 -3
- data/lib/rigor/cache/value_digest.rb +325 -0
- data/lib/rigor/cli/annotate_command.rb +5 -0
- data/lib/rigor/cli/baseline_command.rb +7 -18
- data/lib/rigor/cli/check_command.rb +85 -14
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/effects_report.rb +2 -2
- data/lib/rigor/cli/trace_command.rb +5 -0
- data/lib/rigor/cli/type_of_command.rb +5 -3
- data/lib/rigor/cli/unused_command.rb +16 -10
- data/lib/rigor/cli.rb +11 -3
- data/lib/rigor/config_audit.rb +3 -2
- data/lib/rigor/config_path.rb +29 -0
- data/lib/rigor/configuration/severity_profile.rb +11 -0
- data/lib/rigor/configuration.rb +52 -7
- data/lib/rigor/effects/plugin_facts.rb +6 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/lockfile_resolver.rb +10 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +284 -15
- data/lib/rigor/environment/required_features.rb +206 -0
- data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
- data/lib/rigor/environment.rb +34 -73
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/block_parameter_binder.rb +29 -13
- data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- data/lib/rigor/inference/captured_locals.rb +7 -1
- data/lib/rigor/inference/declaration_walk/context.rb +195 -0
- data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
- data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
- data/lib/rigor/inference/declaration_walk.rb +7 -0
- data/lib/rigor/inference/def_node_resolver.rb +26 -3
- data/lib/rigor/inference/definer_resolution.rb +266 -0
- data/lib/rigor/inference/expression_typer.rb +351 -61
- data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
- data/lib/rigor/inference/global_write_census.rb +7 -4
- data/lib/rigor/inference/guard_rebinding.rb +54 -13
- data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
- data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
- data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
- data/lib/rigor/inference/in_effect_refinements.rb +256 -0
- data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
- data/lib/rigor/inference/key_presence_guard.rb +303 -0
- data/lib/rigor/inference/macro_block_self_type.rb +73 -14
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
- data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
- data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +305 -54
- data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
- data/lib/rigor/inference/method_dispatcher.rb +44 -11
- data/lib/rigor/inference/module_function_state.rb +214 -0
- data/lib/rigor/inference/multi_target_binder.rb +21 -16
- data/lib/rigor/inference/narrowing.rb +166 -22
- data/lib/rigor/inference/object_mixins.rb +180 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
- data/lib/rigor/inference/project_patched_methods.rb +10 -5
- data/lib/rigor/inference/project_patched_scanner.rb +71 -3
- data/lib/rigor/inference/refine_census.rb +239 -0
- data/lib/rigor/inference/refine_self.rb +317 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
- data/lib/rigor/inference/refined_dispatch.rb +272 -0
- data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
- data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
- data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
- data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +143 -0
- data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
- data/lib/rigor/inference/scope_indexer.rb +2129 -354
- data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
- data/lib/rigor/inference/statement_evaluator.rb +149 -32
- data/lib/rigor/inference/string_mutation.rb +9 -3
- data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
- data/lib/rigor/inference/version_guard.rb +19 -10
- data/lib/rigor/language_server/project_context.rb +31 -0
- data/lib/rigor/language_server/server.rb +18 -0
- data/lib/rigor/plugin/base.rb +28 -8
- data/lib/rigor/plugin/inflector.rb +1 -1
- data/lib/rigor/plugin/io_boundary.rb +124 -7
- data/lib/rigor/plugin/isolation.rb +1 -1
- data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
- data/lib/rigor/plugin/trust_policy.rb +3 -3
- data/lib/rigor/project_environment.rb +29 -6
- data/lib/rigor/protection/discovery_seed.rb +18 -9
- data/lib/rigor/protection/mutation_cache.rb +4 -4
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/reflection/constant_ancestors.rb +211 -46
- data/lib/rigor/reflection/constant_path.rb +33 -7
- data/lib/rigor/reflection.rb +41 -10
- data/lib/rigor/scope/discovery_index.rb +258 -3
- data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
- data/lib/rigor/scope/resolution_chain.rb +1032 -0
- data/lib/rigor/scope.rb +480 -328
- data/lib/rigor/sig_gen/alias_index.rb +1 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
- data/lib/rigor/sig_gen/generator.rb +77 -21
- data/lib/rigor/sig_gen/method_candidate.rb +2 -2
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
- data/lib/rigor/source/alias_names.rb +52 -0
- data/lib/rigor/source/literals.rb +3 -2
- data/lib/rigor/source/symbol_name.rb +20 -0
- data/lib/rigor/type/combinator.rb +59 -3
- data/lib/rigor/type/float_range.rb +4 -1
- data/lib/rigor/type/nominal.rb +6 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
- data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
- data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
- data/plugins/rigor-alba/sig/alba.rbs +4 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +13 -2
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
- data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
- data/sig/rigor/environment.rbs +2 -0
- data/sig/rigor/plugin/io_boundary.rbs +4 -0
- data/sig/rigor/scope.rbs +69 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +3 -1
- data/skills/rigor-project-init/SKILL.md +37 -11
- data/skills/rigor-project-init/references/02-configure.md +7 -2
- metadata +90 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require_relative "../cache/value_digest"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
class Environment
|
|
8
|
+
# ADR-32 WD5 — one plugin's `source_rbs_synthesizer` output for one source file, cached per (file content,
|
|
9
|
+
# plugin entry, engine source). Two callers read it, and sharing the function is the point:
|
|
10
|
+
#
|
|
11
|
+
# - {Environment.collect_virtual_rbs} hands each output to the loader as a `virtual:` buffer;
|
|
12
|
+
# - {.digest} reduces every loaded synthesizer's output for a file to one comparable value, which the
|
|
13
|
+
# incremental session (ADR-89 WD1, issue #1536) keeps on the file's seed bundle and compares across an
|
|
14
|
+
# edit.
|
|
15
|
+
#
|
|
16
|
+
# Because the digest reads the very function (and the very cache entries) the loader is fed from, "the
|
|
17
|
+
# digest did not move" means "the loader read the same bytes for this file", not "some model of what
|
|
18
|
+
# the plugin reads did not move". That is why it is taken from the output rather than from the file's
|
|
19
|
+
# comment lines: rbs-inline binds an annotation by adjacency, so a plain comment or a blank line can move
|
|
20
|
+
# one to another declaration, and in an annotated file every `def`, `attr`, constant and mixin gets a
|
|
21
|
+
# skeleton whether or not it carries an annotation of its own.
|
|
22
|
+
module SourceRbsSynthesis
|
|
23
|
+
PRODUCER_ID = "plugin.source_rbs_synthesizer"
|
|
24
|
+
|
|
25
|
+
# The digest of a file no loaded synthesizer contributes anything for — the answer for every file of a
|
|
26
|
+
# project without an annotation, and for every file when no synthesizer is loaded at all. Not a hex
|
|
27
|
+
# digest, so it can never collide with one.
|
|
28
|
+
NO_CONTRIBUTION = "none"
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# The cache stores the empty string `""` as the "no contribution" sentinel because `Cache::Store`
|
|
33
|
+
# treats `nil` as a cache miss. Error tuples are stored as the canonical `[:error, message_string]`
|
|
34
|
+
# Array so the same wrapper short-circuits subsequent runs against unchanged broken input.
|
|
35
|
+
def output_for(plugin, callable, path, cache_store)
|
|
36
|
+
return invoke_safely(callable, path) if cache_store.nil?
|
|
37
|
+
return invoke_safely(callable, path) unless File.file?(path)
|
|
38
|
+
|
|
39
|
+
descriptor = cache_descriptor(plugin, path)
|
|
40
|
+
return invoke_safely(callable, path) if descriptor.nil?
|
|
41
|
+
|
|
42
|
+
cache_store.fetch_or_compute(
|
|
43
|
+
producer_id: PRODUCER_ID,
|
|
44
|
+
params: {},
|
|
45
|
+
descriptor: descriptor,
|
|
46
|
+
generation_cap: generation_cap
|
|
47
|
+
) { invoke_safely(callable, path) || "" }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Issue #1536 — every synthesizer's output for `path`, reduced to one value: {NO_CONTRIBUTION} when none
|
|
51
|
+
# of them contributes anything, a SHA-256 hex digest of each plugin's id and {.contribution} otherwise,
|
|
52
|
+
# and nil when an output could not be read at all.
|
|
53
|
+
#
|
|
54
|
+
# nil means "unknown", and a caller must treat it as moved. Every failure is per file: one plugin that
|
|
55
|
+
# cannot answer for one file leaves every other file's digest intact.
|
|
56
|
+
#
|
|
57
|
+
# @param synthesizers — `Plugin::Registry#source_rbs_synthesizers` pairs.
|
|
58
|
+
def digest(synthesizers, path, cache_store)
|
|
59
|
+
sha = Digest::SHA256.new
|
|
60
|
+
contributed = false
|
|
61
|
+
synthesizers.each do |plugin, callable|
|
|
62
|
+
tag, text = contribution(output_for(plugin, callable, path, cache_store))
|
|
63
|
+
next if tag.nil?
|
|
64
|
+
|
|
65
|
+
contributed = true
|
|
66
|
+
sha << plugin.manifest.id.to_s.b << "\x00" << tag << "\x00" << text.to_s.b << "\x01"
|
|
67
|
+
end
|
|
68
|
+
contributed ? sha.hexdigest : NO_CONTRIBUTION
|
|
69
|
+
rescue StandardError
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# What one output puts into the environment, as a `[tag, text]` pair, or nil for nothing: exactly the
|
|
74
|
+
# RBS {Environment.interpret_synthesizer_outcome} hands the loader. The ADR-32 WD6 / WD12 notices are left
|
|
75
|
+
# out. They reach only the run-level `source-rbs-*` rows, which every run regenerates and the per-file
|
|
76
|
+
# cache never serves, and they quote line numbers, so a line shift in a file carrying a malformed `#:`
|
|
77
|
+
# would otherwise read as a moved contribution. A failed synthesis contributes nothing to the loader but
|
|
78
|
+
# is kept as one stable value of its own, so a file flipping between "no annotation" and "broken
|
|
79
|
+
# annotation" still reads as moved. Any shape the contract does not name is digested whole, by value: the
|
|
80
|
+
# output is computed on one run and served from its cache entry on the next, and a digest of its
|
|
81
|
+
# `Marshal.dump` bytes can differ between the two (issue #1574).
|
|
82
|
+
#
|
|
83
|
+
# The RBS text itself is digested whole, comments included. RBS reads a `# resolve-type-names: false`
|
|
84
|
+
# magic comment at the start of a buffer, rbs-inline copies a `.rb` file's first comment line there, and
|
|
85
|
+
# a string-literal type may span lines that start with `#`, so no line-based stripping is sound (#1549).
|
|
86
|
+
def contribution(output)
|
|
87
|
+
return nil if output.nil? || output == ""
|
|
88
|
+
return ["rbs", output] if output.is_a?(String)
|
|
89
|
+
return ["raw", Cache::ValueDigest.hexdigest(output)] unless output.is_a?(Array)
|
|
90
|
+
|
|
91
|
+
case output[0]
|
|
92
|
+
when :error then ["error", nil]
|
|
93
|
+
when :ok then contribution(output[1])
|
|
94
|
+
else ["raw", Cache::ValueDigest.hexdigest(output)]
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# One entry per (plugin, source file), all of them live for as long as the file is in the project — a
|
|
99
|
+
# generation count says nothing about staleness here, so this producer declares itself out of
|
|
100
|
+
# `Cache::Store#evict!`'s compaction pass and is bounded only by the size-based LRU pass. A method
|
|
101
|
+
# rather than a constant: `Cache::Store` is not loaded yet when this file is.
|
|
102
|
+
def generation_cap
|
|
103
|
+
Cache::Store::UNBOUNDED_GENERATIONS
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# The key composes the file's content SHA with the plugin's `PluginEntry` (id + version + config_hash),
|
|
107
|
+
# so a config change or a content change invalidates the entry, and with the engine's source identity
|
|
108
|
+
# (issue #1009): a plugin's manifest version does not move when a checkout edits its synthesizer, and
|
|
109
|
+
# a stale string here also keeps the `rbs.virtual_rbs` env key warm, so the new build's rules would
|
|
110
|
+
# read the old build's RBS.
|
|
111
|
+
def cache_descriptor(plugin, path)
|
|
112
|
+
Cache::Descriptor.new(
|
|
113
|
+
files: [Cache::Descriptor::FileEntry.new(
|
|
114
|
+
path: path.to_s,
|
|
115
|
+
comparator: :digest,
|
|
116
|
+
value: input_digest(path)
|
|
117
|
+
)],
|
|
118
|
+
plugins: [plugin.plugin_entry],
|
|
119
|
+
configs: Cache::EngineSource.key_config_entries
|
|
120
|
+
)
|
|
121
|
+
rescue Cache::EngineSource::Unavailable
|
|
122
|
+
# An engine that cannot be identified must not be keyed by its inputs alone (issue #1009): nil runs the
|
|
123
|
+
# synthesizer uncached rather than serving an entry another build may have written.
|
|
124
|
+
nil
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def input_digest(path)
|
|
128
|
+
Digest::SHA256.hexdigest(File.binread(path))
|
|
129
|
+
rescue ::SystemCallError
|
|
130
|
+
# Unreadable file → key on the path alone; the synthesizer's File.file?/File.read will see the same
|
|
131
|
+
# failure and return nil.
|
|
132
|
+
Digest::SHA256.hexdigest(path.to_s)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def invoke_safely(callable, path)
|
|
136
|
+
callable.call(path.to_s)
|
|
137
|
+
rescue StandardError
|
|
138
|
+
# WD6 fail-soft — a synthesizer that raises does NOT crash analysis. Unlike the `[:error, msg]`
|
|
139
|
+
# return path (which the runner surfaces as `source-rbs-synthesis-failed`), an unhandled raise is
|
|
140
|
+
# swallowed silently; the unexamined-raise channel is deliberately silent per WD6.
|
|
141
|
+
nil
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private_class_method :contribution, :generation_cap, :cache_descriptor, :input_digest, :invoke_safely
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
data/lib/rigor/environment.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "digest"
|
|
4
4
|
|
|
5
5
|
require_relative "environment/default_libraries"
|
|
6
|
+
require_relative "environment/required_features"
|
|
6
7
|
require_relative "environment/class_registry"
|
|
7
8
|
require_relative "environment/rbs_loader"
|
|
8
9
|
require_relative "environment/reflection"
|
|
@@ -15,6 +16,7 @@ require_relative "environment/bundle_sig_discovery"
|
|
|
15
16
|
require_relative "environment/lockfile_resolver"
|
|
16
17
|
require_relative "environment/installed_gem_set"
|
|
17
18
|
require_relative "environment/rbs_collection_discovery"
|
|
19
|
+
require_relative "environment/source_rbs_synthesis"
|
|
18
20
|
require_relative "plugin/isolation"
|
|
19
21
|
require_relative "cache/engine_source"
|
|
20
22
|
require_relative "environment/rbs_coverage_report"
|
|
@@ -274,8 +276,8 @@ module Rigor
|
|
|
274
276
|
bundler_lockfile: nil,
|
|
275
277
|
rbs_collection_lockfile: nil, rbs_collection_auto_detect: false,
|
|
276
278
|
synthetic_method_index: nil, project_patched_methods: nil,
|
|
277
|
-
source_files: [], locked_gems: nil)
|
|
278
|
-
resolved_paths = signature_paths
|
|
279
|
+
source_files: [], locked_gems: nil, required_features: nil)
|
|
280
|
+
resolved_paths = project_signature_roots(signature_paths, root: root)
|
|
279
281
|
# O4 MVP — append per-gem `sig/` directories discovered under the target project's bundler install
|
|
280
282
|
# root. Empty array when neither an explicit path nor auto-detection finds a bundle. Order: user
|
|
281
283
|
# `signature_paths:` win first (semantic precedence inside `RbsLoader.build_env_for`); gem-shipped
|
|
@@ -313,8 +315,10 @@ module Rigor
|
|
|
313
315
|
# and the `data/vendored_gem_sigs/` bundle (`redis`, `nokogiri`, `pg`, …). `skip_gem_names:` passes
|
|
314
316
|
# both sets so the collection copy doesn't double-declare against rigor's bundled RBS (the
|
|
315
317
|
# `RBS::DuplicatedDeclarationError` hazard).
|
|
316
|
-
|
|
317
|
-
|
|
318
|
+
# Issue #1700 — plus the gated vendored directories this run's source activates ({#feature_tokens}).
|
|
319
|
+
merged_libraries = (DEFAULT_LIBRARIES + libraries.map(&:to_s) +
|
|
320
|
+
feature_tokens(required_features, source_files, libraries)).uniq
|
|
321
|
+
skip_gem_names = collection_skip_gem_names(merged_libraries)
|
|
318
322
|
collection_paths = RbsCollectionDiscovery.discover(
|
|
319
323
|
lockfile_path: rbs_collection_lockfile,
|
|
320
324
|
project_root: root,
|
|
@@ -390,8 +394,30 @@ module Rigor
|
|
|
390
394
|
)
|
|
391
395
|
end
|
|
392
396
|
|
|
397
|
+
# The project-owned `sig/` roots a run reads: the explicit `signature_paths:` when given, else the
|
|
398
|
+
# auto-detected `<root>/sig` (or none). Public so {Cache::IncrementalSnapshot} digests exactly the
|
|
399
|
+
# directories the environment loads, including the auto-detected one a `nil` `signature_paths:` hides.
|
|
400
|
+
def project_signature_roots(signature_paths, root: Dir.pwd)
|
|
401
|
+
signature_paths || default_signature_paths(root)
|
|
402
|
+
end
|
|
403
|
+
|
|
393
404
|
private
|
|
394
405
|
|
|
406
|
+
# Issue #1700 — the `RequiredFeatures` library tokens for the gated vendored directories this run asks for:
|
|
407
|
+
# each feature its source requires and each one the configuration's `libraries:` names. `required_features`
|
|
408
|
+
# is the scan a caller already took; nil scans `source_files`. Whether a directory then loads is decided
|
|
409
|
+
# by `RbsLoader.build_env_for` against the environment it builds.
|
|
410
|
+
def feature_tokens(required_features, source_files, libraries)
|
|
411
|
+
RequiredFeatures.tokens(required_features || RequiredFeatures.scan(source_files), libraries)
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# The gem names an `rbs collection` copy must not double-declare against: the library list and every
|
|
415
|
+
# vendored directory except a `RequiredFeatures`-gated one. A gated directory stands down for any other
|
|
416
|
+
# copy of its declarations (`GatedSignatureGuard`), so the collection's copy is kept and wins.
|
|
417
|
+
def collection_skip_gem_names(library_names)
|
|
418
|
+
library_names + RbsLoader.vendored_gem_names.reject { |name| RequiredFeatures.gated_dir?(name) }
|
|
419
|
+
end
|
|
420
|
+
|
|
395
421
|
def default_signature_paths(root)
|
|
396
422
|
sig = Pathname(root) / DEFAULT_PROJECT_SIG_DIR
|
|
397
423
|
sig.directory? ? [sig] : []
|
|
@@ -496,11 +522,9 @@ module Rigor
|
|
|
496
522
|
# empty and this method short-circuits to `[]` without walking the file list.
|
|
497
523
|
#
|
|
498
524
|
# WD5 — when `cache_store` is supplied, each (file, plugin) synthesizer call is memoised through
|
|
499
|
-
# `Cache::Store
|
|
500
|
-
#
|
|
501
|
-
#
|
|
502
|
-
# checkout edits its synthesizer, and a stale string here also keeps the `rbs.virtual_rbs` env key warm,
|
|
503
|
-
# so the new build's rules would read the old build's RBS.
|
|
525
|
+
# `Cache::Store` ({SourceRbsSynthesis.output_for}, which owns the key). The incremental session's
|
|
526
|
+
# per-file digest (issue #1536) reads the same function, so the two can never disagree on what a file
|
|
527
|
+
# contributed.
|
|
504
528
|
def collect_virtual_rbs(plugin_registry, source_files, cache_store, reporter)
|
|
505
529
|
return [] if plugin_registry.nil?
|
|
506
530
|
|
|
@@ -511,7 +535,7 @@ module Rigor
|
|
|
511
535
|
result = []
|
|
512
536
|
source_files.each do |path|
|
|
513
537
|
synthesizers.each do |plugin, callable|
|
|
514
|
-
outcome =
|
|
538
|
+
outcome = SourceRbsSynthesis.output_for(plugin, callable, path, cache_store)
|
|
515
539
|
outcome = interpret_synthesizer_outcome(outcome, plugin, path, reporter)
|
|
516
540
|
next if outcome.nil? || outcome.empty?
|
|
517
541
|
|
|
@@ -522,25 +546,6 @@ module Rigor
|
|
|
522
546
|
result
|
|
523
547
|
end
|
|
524
548
|
|
|
525
|
-
# ADR-32 WD5 — cache wrapper around a single (plugin, file) invocation. The cache stores the empty
|
|
526
|
-
# string `""` as the "no contribution" sentinel because `Cache::Store` treats `nil` as a cache miss.
|
|
527
|
-
# Error tuples are stored as the canonical `[:error, message_string]` Array so the same wrapper
|
|
528
|
-
# short-circuits subsequent runs against unchanged broken input.
|
|
529
|
-
def synthesizer_output_for(plugin, callable, path, cache_store)
|
|
530
|
-
return invoke_synthesizer_safely(callable, path) if cache_store.nil?
|
|
531
|
-
return invoke_synthesizer_safely(callable, path) unless File.file?(path)
|
|
532
|
-
|
|
533
|
-
descriptor = build_synthesizer_cache_descriptor(plugin, path)
|
|
534
|
-
return invoke_synthesizer_safely(callable, path) if descriptor.nil?
|
|
535
|
-
|
|
536
|
-
cache_store.fetch_or_compute(
|
|
537
|
-
producer_id: SYNTHESIZER_CACHE_PRODUCER_ID,
|
|
538
|
-
params: {},
|
|
539
|
-
descriptor: descriptor,
|
|
540
|
-
generation_cap: synthesizer_generation_cap
|
|
541
|
-
) { invoke_synthesizer_safely(callable, path) || "" }
|
|
542
|
-
end
|
|
543
|
-
|
|
544
549
|
# ADR-32 WD6 / WD12 — route a synthesizer return value through the per-run reporter. The
|
|
545
550
|
# synthesizer's contract (declared in
|
|
546
551
|
# `plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb`) admits four return shapes:
|
|
@@ -661,50 +666,6 @@ module Rigor
|
|
|
661
666
|
prefix = "#{root}#{File::SEPARATOR}"
|
|
662
667
|
path.to_s.start_with?(prefix) ? path.to_s.delete_prefix(prefix) : path.to_s
|
|
663
668
|
end
|
|
664
|
-
|
|
665
|
-
SYNTHESIZER_CACHE_PRODUCER_ID = "plugin.source_rbs_synthesizer"
|
|
666
|
-
private_constant :SYNTHESIZER_CACHE_PRODUCER_ID
|
|
667
|
-
|
|
668
|
-
# One entry per (plugin, source file), all of them live for as long as the file is in the project — a
|
|
669
|
-
# generation count says nothing about staleness here, so this producer declares itself out of
|
|
670
|
-
# `Cache::Store#evict!`'s compaction pass and is bounded only by the size-based LRU pass. A method
|
|
671
|
-
# rather than a constant: `Cache::Store` is not loaded yet when this class body runs.
|
|
672
|
-
def synthesizer_generation_cap
|
|
673
|
-
Cache::Store::UNBOUNDED_GENERATIONS
|
|
674
|
-
end
|
|
675
|
-
|
|
676
|
-
def build_synthesizer_cache_descriptor(plugin, path)
|
|
677
|
-
Cache::Descriptor.new(
|
|
678
|
-
files: [Cache::Descriptor::FileEntry.new(
|
|
679
|
-
path: path.to_s,
|
|
680
|
-
comparator: :digest,
|
|
681
|
-
value: synthesizer_input_digest(path)
|
|
682
|
-
)],
|
|
683
|
-
plugins: [plugin.plugin_entry],
|
|
684
|
-
configs: Cache::EngineSource.key_config_entries
|
|
685
|
-
)
|
|
686
|
-
rescue Cache::EngineSource::Unavailable
|
|
687
|
-
# An engine that cannot be identified must not be keyed by its inputs alone (issue #1009): nil runs the
|
|
688
|
-
# synthesizer uncached rather than serving an entry another build may have written.
|
|
689
|
-
nil
|
|
690
|
-
end
|
|
691
|
-
|
|
692
|
-
def synthesizer_input_digest(path)
|
|
693
|
-
Digest::SHA256.hexdigest(File.binread(path))
|
|
694
|
-
rescue ::SystemCallError
|
|
695
|
-
# Unreadable file → key on the path alone; the synthesizer's File.file?/File.read will see the same
|
|
696
|
-
# failure and return nil.
|
|
697
|
-
Digest::SHA256.hexdigest(path.to_s)
|
|
698
|
-
end
|
|
699
|
-
|
|
700
|
-
def invoke_synthesizer_safely(callable, path)
|
|
701
|
-
callable.call(path.to_s)
|
|
702
|
-
rescue StandardError
|
|
703
|
-
# WD6 fail-soft — a synthesizer that raises does NOT crash analysis. Unlike the `[:error, msg]`
|
|
704
|
-
# return path (which the runner surfaces as `source-rbs-synthesis-failed`), an unhandled raise is
|
|
705
|
-
# swallowed silently; the unexamined-raise channel is deliberately silent per WD6.
|
|
706
|
-
nil
|
|
707
|
-
end
|
|
708
669
|
end
|
|
709
670
|
|
|
710
671
|
# Resolves a constant name to a Rigor::Type::Nominal (the *instance* type carrier). Consults the static
|
|
@@ -486,7 +486,17 @@ module Rigor
|
|
|
486
486
|
)
|
|
487
487
|
end
|
|
488
488
|
|
|
489
|
-
|
|
489
|
+
project_sequence_to_enumerator(self_type, other_type)
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
# `Enumerator::ArithmeticSequence[E]` carries only the element of its `Enumerator[Numeric, void]` superclass
|
|
493
|
+
# (#1794), so against a two-argument `Enumerator` it projects to `Enumerator[E, top]`, `top` being what RBS
|
|
494
|
+
# `void` translates to: `Enumerator[Integer, void]` and `[Integer, untyped]` accept it, as RBS would.
|
|
495
|
+
def project_sequence_to_enumerator(self_type, other_type)
|
|
496
|
+
return nil unless self_type.class_name == "Enumerator" && self_type.type_args.size == 2
|
|
497
|
+
return nil unless other_type.class_name == "Enumerator::ArithmeticSequence" && other_type.type_args.size == 1
|
|
498
|
+
|
|
499
|
+
Type::Combinator.nominal_of("Enumerator", type_args: [other_type.type_args.first, Type::Combinator.top])
|
|
490
500
|
end
|
|
491
501
|
|
|
492
502
|
def project_tuple_to_nominal(tuple)
|
|
@@ -963,11 +973,14 @@ module Rigor
|
|
|
963
973
|
end
|
|
964
974
|
end
|
|
965
975
|
|
|
966
|
-
# Constant[v] accepts
|
|
967
|
-
# (modulo the universal Bot/Dynamic short-circuits
|
|
976
|
+
# Constant[v] accepts Constant[v'] with structurally equal value, and the plain nominal of a class whose
|
|
977
|
+
# only instance is `v`. Any other type is rejected (modulo the universal Bot/Dynamic short-circuits
|
|
978
|
+
# already applied upstream).
|
|
968
979
|
def accepts_constant(self_type, other_type, mode)
|
|
969
980
|
if other_type.is_a?(Type::Constant) && self_type == other_type
|
|
970
981
|
Type::AcceptsResult.yes(mode: mode, reasons: "structural literal match")
|
|
982
|
+
elsif sole_instance_nominal?(self_type, other_type)
|
|
983
|
+
Type::AcceptsResult.yes(mode: mode, reasons: "#{other_type.class_name} has no instance but the literal")
|
|
971
984
|
else
|
|
972
985
|
Type::AcceptsResult.no(
|
|
973
986
|
mode: mode,
|
|
@@ -976,6 +989,18 @@ module Rigor
|
|
|
976
989
|
end
|
|
977
990
|
end
|
|
978
991
|
|
|
992
|
+
# `true`, `false` and `nil` are the only instances of their classes, so `FalseClass` holds exactly the
|
|
993
|
+
# values `false` holds. Read apart, `Array.new(n, false)`, typed `Array[FalseClass]` through the
|
|
994
|
+
# `[T] (Integer, T)` binding, failed a declared `-> Array[bool]` and fired `def.return-type-mismatch` on
|
|
995
|
+
# correct code.
|
|
996
|
+
SOLE_INSTANCE_CLASSES = { true => "TrueClass", false => "FalseClass", nil => "NilClass" }.freeze
|
|
997
|
+
private_constant :SOLE_INSTANCE_CLASSES
|
|
998
|
+
|
|
999
|
+
def sole_instance_nominal?(constant, other_type)
|
|
1000
|
+
other_type.is_a?(Type::Nominal) && other_type.type_args.empty? &&
|
|
1001
|
+
SOLE_INSTANCE_CLASSES.key?(constant.value) && SOLE_INSTANCE_CLASSES[constant.value] == other_type.class_name
|
|
1002
|
+
end
|
|
1003
|
+
|
|
979
1004
|
# Tuple[A1..An] accepts:
|
|
980
1005
|
# - Tuple[B1..Bn] when arities match and each Ai accepts Bi (covariant per-position).
|
|
981
1006
|
# - Anything else: no (we cannot prove the arity from a generic nominal alone).
|
|
@@ -54,14 +54,16 @@ module Rigor
|
|
|
54
54
|
reset_per_bind_state
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
# The names the last {#bind} bound optimistically nil-free (see the class comment).
|
|
57
|
+
# The names the last {#bind} bound optimistically nil-free (see the class comment). Read-only: it is a
|
|
58
|
+
# shared frozen empty when the bind marked nothing.
|
|
58
59
|
attr_reader :optimistic
|
|
59
60
|
|
|
60
61
|
# Binds the block's parameters into `scope`: {#bind}'s types through `Scope#with_local`, then the
|
|
61
|
-
# optimistic mark for every name in {#optimistic}.
|
|
62
|
+
# optimistic mark for every name in {#optimistic}. It applies them through
|
|
63
|
+
# {MultiTargetBinder.apply_bindings} directly rather than through a `Result`, which every block entry
|
|
64
|
+
# built only to apply at once ([#1504](https://github.com/rigortype/rigor/issues/1504)).
|
|
62
65
|
def bind_onto(block_node, scope)
|
|
63
|
-
|
|
64
|
-
MultiTargetBinder::Result.new(types: types, optimistic: @optimistic.dup.freeze).apply_to(scope)
|
|
66
|
+
MultiTargetBinder.apply_bindings(scope, bind(block_node, scope: scope), @optimistic)
|
|
65
67
|
end
|
|
66
68
|
|
|
67
69
|
# @return ordered map from parameter name to bound type. Anonymous
|
|
@@ -90,29 +92,40 @@ module Rigor
|
|
|
90
92
|
|
|
91
93
|
private
|
|
92
94
|
|
|
95
|
+
NO_POSITIONS = [].freeze
|
|
96
|
+
NO_NAMES = [].freeze
|
|
97
|
+
private_constant :NO_POSITIONS, :NO_NAMES
|
|
98
|
+
|
|
93
99
|
# {#apply_auto_splat} rewrites the positional table for the block it is binding, so every {#bind} starts
|
|
94
100
|
# from the declared types again; a binder reused across blocks must not see the previous block's splat.
|
|
101
|
+
# The two lists start as shared frozen empties, since most blocks mark nothing ({#mark_optimistic}).
|
|
95
102
|
def reset_per_bind_state
|
|
96
103
|
@expected_param_types = @declared_param_types
|
|
97
104
|
@splat_rest_type = nil
|
|
98
|
-
@optimistic_positions =
|
|
99
|
-
@optimistic =
|
|
105
|
+
@optimistic_positions = NO_POSITIONS
|
|
106
|
+
@optimistic = NO_NAMES
|
|
100
107
|
@scope = nil
|
|
101
108
|
end
|
|
102
109
|
|
|
110
|
+
# Records `name` as bound optimistically, allocating {#optimistic} on the first mark of a {#bind}.
|
|
111
|
+
def mark_optimistic(name)
|
|
112
|
+
@optimistic = [] if @optimistic.equal?(NO_NAMES)
|
|
113
|
+
@optimistic << name
|
|
114
|
+
end
|
|
115
|
+
|
|
103
116
|
# `|_1, _2|` numbered-parameter form. Prism exposes the implicit count through
|
|
104
117
|
# `NumberedParametersNode#maximum` (the highest `_N` referenced in the body); we materialise bindings
|
|
105
118
|
# for `:_1` through `:_maximum`, auto-splatting exactly as the explicit list of that many required
|
|
106
119
|
# positionals would, so the body's `LocalVariableReadNode` lookups see the same types and marks.
|
|
107
120
|
def bind_numbered_parameters(numbered_node)
|
|
108
121
|
arity = numbered_node.maximum
|
|
109
|
-
apply_auto_splat
|
|
122
|
+
apply_auto_splat { BlockAutoSplat::ParameterShape.of_arity(arity) }
|
|
110
123
|
|
|
111
124
|
bindings = {}
|
|
112
125
|
arity.times do |i|
|
|
113
126
|
name = :"_#{i + 1}"
|
|
114
127
|
bindings[name] = positional_type_at(i)
|
|
115
|
-
|
|
128
|
+
mark_optimistic(name) if @optimistic_positions.include?(i)
|
|
116
129
|
end
|
|
117
130
|
bindings
|
|
118
131
|
end
|
|
@@ -127,7 +140,7 @@ module Rigor
|
|
|
127
140
|
params_node = params_root.parameters
|
|
128
141
|
return {} if params_node.nil?
|
|
129
142
|
|
|
130
|
-
apply_auto_splat
|
|
143
|
+
apply_auto_splat { BlockAutoSplat::ParameterShape.of(params_node) }
|
|
131
144
|
|
|
132
145
|
bindings = {}
|
|
133
146
|
bind_positionals(params_node, bindings, 0)
|
|
@@ -152,9 +165,12 @@ module Rigor
|
|
|
152
165
|
# and {BlockAutoSplat} answers a table for that value and `shape`. A multi-arg yield (e.g.
|
|
153
166
|
# `each_with_index`'s `(element, index)` pair) is NOT auto-splatted — matching Ruby semantics where a
|
|
154
167
|
# multi-arg yield to a `|a, b, c|` block fills the extra slot with nil rather than splatting any
|
|
155
|
-
# element.
|
|
156
|
-
|
|
168
|
+
# element. The block answers the parameter list's shape, built only past that test
|
|
169
|
+
# ([#1504](https://github.com/rigortype/rigor/issues/1504)).
|
|
170
|
+
def apply_auto_splat
|
|
157
171
|
return unless @expected_param_types.size == 1
|
|
172
|
+
|
|
173
|
+
shape = yield
|
|
158
174
|
return unless shape.splats?
|
|
159
175
|
|
|
160
176
|
table = BlockAutoSplat.for(shape, @expected_param_types[0])
|
|
@@ -242,12 +258,12 @@ module Rigor
|
|
|
242
258
|
case param
|
|
243
259
|
when Prism::RequiredParameterNode
|
|
244
260
|
bindings[param.name] = positional_type_at(cursor)
|
|
245
|
-
|
|
261
|
+
mark_optimistic(param.name) if @optimistic_positions.include?(cursor)
|
|
246
262
|
when Prism::MultiTargetNode
|
|
247
263
|
nested = MultiTargetBinder.bind_marked(param, positional_type_at(cursor),
|
|
248
264
|
optimistic: @optimistic_positions.include?(cursor), scope: @scope)
|
|
249
265
|
bindings.merge!(nested.types)
|
|
250
|
-
|
|
266
|
+
nested.optimistic.each { |name| mark_optimistic(name) }
|
|
251
267
|
end
|
|
252
268
|
end
|
|
253
269
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require_relative "method_catalog"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Inference
|
|
8
|
+
module Builtins
|
|
9
|
+
# Issue #1740 — which instance methods CRuby itself defines on a core class or module, read from the offline
|
|
10
|
+
# catalogues under `data/builtins/ruby_core/` (`tool/extract_builtin_catalog.rb` records each `rb_define_method`
|
|
11
|
+
# and prelude `def` of an `Init_*` function on the class it names, aliases included). A registration inside an
|
|
12
|
+
# `#if 0` block CRuby keeps for RDoc carries `rdoc_only: true` and is left out: `random.c`'s registers
|
|
13
|
+
# `Random#seed` on `Random`, though it runs on `Random::Base`.
|
|
14
|
+
# `spec/rigor/inference/builtins/c_ruby_definers_spec.rb` checks every listed method the running Ruby has
|
|
15
|
+
# against its runtime owner. RBS is not that record: core RBS redeclares some inherited methods on a subclass
|
|
16
|
+
# (`Integer#quo` and `File#to_path`, whose CRuby owners are `Numeric` and `IO`), and declares a few CRuby no
|
|
17
|
+
# longer defines (`Process::Status#&`). A class no catalogue covers answers false, as does a method its
|
|
18
|
+
# catalogue does not list: the caller reads both as "not proven". The catalogue is one CRuby snapshot.
|
|
19
|
+
module CRubyDefiners
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# Does the catalogue prove CRuby defines `method_name` on `class_name` itself?
|
|
23
|
+
def defines?(class_name, method_name)
|
|
24
|
+
names = INDEX[class_name.to_s.delete_prefix("::")]
|
|
25
|
+
!names.nil? && names.include?(method_name.to_s)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def build_index
|
|
29
|
+
index = Hash.new { |hash, key| hash[key] = Set.new }
|
|
30
|
+
MethodCatalog.topic_paths.each do |path|
|
|
31
|
+
data = YAML.safe_load_file(path, permitted_classes: [Symbol])
|
|
32
|
+
classes = data.is_a?(Hash) ? data["classes"] : nil
|
|
33
|
+
next unless classes.is_a?(Hash)
|
|
34
|
+
|
|
35
|
+
classes.each do |name, klass|
|
|
36
|
+
next if klass["rdoc_only"]
|
|
37
|
+
|
|
38
|
+
names = index[name]
|
|
39
|
+
names.merge(defined_names(klass["instance_methods"]))
|
|
40
|
+
names.merge(defined_names(klass["aliases"]))
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
Ractor.make_shareable(index.to_h { |name, names| [name, names.freeze] })
|
|
44
|
+
rescue Psych::SyntaxError, SystemCallError
|
|
45
|
+
Ractor.make_shareable({})
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The names of `entries` CRuby defines at runtime: one the catalogue marks `rdoc_only` was registered only for
|
|
49
|
+
# RDoc, inside an `#if 0` block.
|
|
50
|
+
def defined_names(entries)
|
|
51
|
+
return [] unless entries.is_a?(Hash)
|
|
52
|
+
|
|
53
|
+
entries.filter_map { |method, entry| method unless entry.is_a?(Hash) && entry["rdoc_only"] }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
INDEX = build_index
|
|
57
|
+
private_constant :INDEX
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -48,6 +48,9 @@ module Rigor
|
|
|
48
48
|
new(path: File.join(DATA_ROOT, "#{topic}.yml"), mutating_selectors: mutating_selectors)
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
# Every catalogue file under {DATA_ROOT}, in a stable order.
|
|
52
|
+
def self.topic_paths = Dir[File.join(DATA_ROOT, "*.yml")]
|
|
53
|
+
|
|
51
54
|
def initialize(path:, mutating_selectors: {})
|
|
52
55
|
@path = path
|
|
53
56
|
@mutating_selectors = mutating_selectors.transform_values(&:freeze).freeze
|
|
@@ -413,10 +413,16 @@ module Rigor
|
|
|
413
413
|
NESTED_SCOPE_NODES = Set[Prism::BlockNode, Prism::LambdaNode].freeze
|
|
414
414
|
private_constant :NESTED_SCOPE_NODES
|
|
415
415
|
|
|
416
|
+
# The index arm spells {INDEX_STORE_NODES} out rather than splatting it: `when *ARRAY` copies the array
|
|
417
|
+
# on every evaluation, and this runs once per node of every scanned block body, which cost `rigor check`
|
|
418
|
+
# about 126K allocations over the v0.3.9 `lib` ([#1502](https://github.com/rigortype/rigor/issues/1502),
|
|
419
|
+
# the shape `ScopeIndexer::META_CONSTANT_WRITE_NODES` records). Keep the arm and the list in step.
|
|
416
420
|
def mutated_receiver(node)
|
|
417
421
|
case node
|
|
418
422
|
when Prism::CallNode then node.receiver if MutationWidening::SHAPE_MUTATORS.include?(node.name)
|
|
419
|
-
when
|
|
423
|
+
when Prism::IndexOrWriteNode, Prism::IndexAndWriteNode, Prism::IndexOperatorWriteNode,
|
|
424
|
+
Prism::IndexTargetNode
|
|
425
|
+
node.receiver
|
|
420
426
|
end
|
|
421
427
|
end
|
|
422
428
|
|