rigortype 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/data/builtins/ruby_core/date.yml +6 -3
- data/data/builtins/ruby_core/enumerable.yml +1 -0
- data/data/builtins/ruby_core/exception.yml +2 -1
- data/data/builtins/ruby_core/file.yml +28 -14
- data/data/builtins/ruby_core/hash.yml +5 -2
- data/data/builtins/ruby_core/io.yml +33 -16
- data/data/builtins/ruby_core/random.yml +2 -1
- data/data/builtins/ruby_core/re.yml +6 -3
- data/data/builtins/ruby_core/struct.yml +2 -0
- data/data/builtins/ruby_core/time.yml +2 -1
- data/data/gem_overlay/activesupport/core_ext.rbs +409 -11
- data/docs/handbook/11-sig-gen.md +24 -14
- data/docs/manual/02-cli-reference.md +23 -7
- data/docs/manual/04-diagnostics.md +1 -1
- data/docs/manual/05-inspecting-types.md +20 -8
- data/docs/manual/07-plugins.md +4 -0
- data/docs/manual/plugins/rigor-actionpack.md +61 -0
- data/docs/manual/plugins/rigor-activerecord.md +74 -1
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
- data/docs/manual/plugins/rigor-sidekiq.md +22 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +12 -0
- data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +98 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -3
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +199 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +1 -2
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -2
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +6 -12
- data/lib/rigor/analysis/check_rules.rb +383 -48
- data/lib/rigor/analysis/crash_signature.rb +188 -0
- data/lib/rigor/analysis/dependency_recorder.rb +18 -3
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +28 -0
- data/lib/rigor/analysis/incremental_session.rb +63 -16
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +1 -2
- data/lib/rigor/analysis/reachability/graph.rb +14 -5
- data/lib/rigor/analysis/reachability/scan.rb +7 -3
- data/lib/rigor/analysis/result.rb +32 -0
- data/lib/rigor/analysis/run_cache_key.rb +41 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +233 -16
- data/lib/rigor/analysis/runner/pool_coordinator.rb +266 -47
- data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
- data/lib/rigor/analysis/runner/run_snapshots.rb +12 -2
- data/lib/rigor/analysis/runner.rb +212 -16
- data/lib/rigor/analysis/worker_session.rb +37 -3
- data/lib/rigor/bleeding_edge.rb +7 -6
- data/lib/rigor/builtins/hkt_builtins.rb +45 -6
- data/lib/rigor/builtins/imported_refinements.rb +11 -9
- data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
- data/lib/rigor/builtins/static_return_refinements.rb +20 -1
- data/lib/rigor/cache/annotation_location.rb +72 -0
- data/lib/rigor/cache/descriptor.rb +42 -2
- data/lib/rigor/cache/incremental_snapshot.rb +40 -3
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +70 -8
- data/lib/rigor/cache/store.rb +17 -1
- data/lib/rigor/cli/coverage_command.rb +23 -18
- data/lib/rigor/cli/coverage_mutation.rb +13 -17
- data/lib/rigor/cli/coverage_scan.rb +47 -6
- data/lib/rigor/cli/fused_protection_renderer.rb +19 -2
- data/lib/rigor/cli/fused_protection_report.rb +23 -2
- data/lib/rigor/cli/measurement_integrity_warning.rb +57 -0
- data/lib/rigor/cli/mutation_protection_renderer.rb +22 -2
- data/lib/rigor/cli/mutation_protection_report.rb +31 -2
- data/lib/rigor/cli/sig_gen_command.rb +47 -0
- data/lib/rigor/cli/type_of_command.rb +188 -41
- data/lib/rigor/cli/type_of_renderer.rb +72 -3
- data/lib/rigor/cli/type_scan_command.rb +10 -1
- data/lib/rigor/cli.rb +1 -1
- data/lib/rigor/configuration.rb +7 -4
- data/lib/rigor/effects/plugin_facts.rb +1 -1
- data/lib/rigor/effects/scanner.rb +2 -3
- data/lib/rigor/environment/bundle_sig_discovery.rb +64 -13
- data/lib/rigor/environment/failure_slot.rb +28 -0
- 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 +743 -93
- data/lib/rigor/environment.rb +125 -25
- 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 +61 -3
- data/lib/rigor/inference/hkt_sugar_translator.rb +87 -0
- data/lib/rigor/inference/index_write_widening.rb +48 -0
- data/lib/rigor/inference/indexed_narrowing.rb +14 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +7 -5
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +10 -0
- data/lib/rigor/inference/method_dispatcher/json_folding.rb +58 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +81 -38
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +54 -17
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -2
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +63 -10
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +104 -0
- data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +81 -0
- data/lib/rigor/inference/method_dispatcher.rb +155 -14
- data/lib/rigor/inference/method_parameter_binder.rb +3 -3
- data/lib/rigor/inference/mutation_widening.rb +244 -174
- data/lib/rigor/inference/narrowing.rb +331 -27
- data/lib/rigor/inference/parameter_inference_collector.rb +1 -0
- data/lib/rigor/inference/pre_eval_constants.rb +15 -0
- data/lib/rigor/inference/precision_scanner.rb +5 -2
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +89 -39
- data/lib/rigor/inference/scope_indexer.rb +1493 -140
- data/lib/rigor/inference/statement_evaluator.rb +276 -91
- data/lib/rigor/inference/struct_fold_safety.rb +186 -27
- data/lib/rigor/inference/version_guard.rb +229 -0
- data/lib/rigor/language_server/selection_range_provider.rb +1 -1
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/inflector.rb +14 -5
- data/lib/rigor/plugin/io_boundary.rb +107 -4
- data/lib/rigor/plugin/loader.rb +14 -0
- data/lib/rigor/plugin/registry.rb +25 -2
- 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 +8 -7
- data/lib/rigor/rbs_extended/hkt_directives.rb +16 -1
- data/lib/rigor/rbs_extended/reporter.rb +93 -13
- data/lib/rigor/rbs_extended.rb +6 -1
- 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 +320 -85
- data/lib/rigor/sig_gen/renderer.rb +14 -12
- 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 +9 -0
- data/sig/rigor/inference.rbs +4 -1
- data/sig/rigor/plugin/io_boundary.rbs +2 -0
- data/sig/rigor/reflection.rbs +3 -1
- data/sig/rigor/scope.rbs +25 -1
- data/sig/rigor/type.rbs +95 -1
- data/sig/rigor.rbs +16 -0
- metadata +71 -3
data/lib/rigor/environment.rb
CHANGED
|
@@ -8,6 +8,7 @@ require_relative "environment/rbs_loader"
|
|
|
8
8
|
require_relative "environment/reflection"
|
|
9
9
|
require_relative "environment/reporters"
|
|
10
10
|
require_relative "environment/hkt_registry_holder"
|
|
11
|
+
require_relative "environment/failure_slot"
|
|
11
12
|
require_relative "environment/constant_type_cache_holder"
|
|
12
13
|
require_relative "environment/missing_gem_constant_index"
|
|
13
14
|
require_relative "environment/bundle_sig_discovery"
|
|
@@ -38,9 +39,9 @@ module Rigor
|
|
|
38
39
|
# ADR-87 WD4 boot-slimming probe can read it without loading the inference engine.
|
|
39
40
|
|
|
40
41
|
# ADR-72 — a Gemfile.lock gem name mapped to the opt-in plugin id that ships the SAME core-ext RBS. When
|
|
41
|
-
# that plugin
|
|
42
|
-
# methods (which would raise a duplicate-declaration error). Keyed on the gem name
|
|
43
|
-
# reports.
|
|
42
|
+
# that plugin's signatures are reachable the auto-overlay for the gem stands down, so the two never both
|
|
43
|
+
# declare the methods (which would raise a duplicate-declaration error). Keyed on the gem name
|
|
44
|
+
# `RbsCoverageReport` reports.
|
|
44
45
|
GEM_OVERLAY_PLUGIN_IDS = { "activesupport" => "activesupport-core-ext" }.freeze
|
|
45
46
|
|
|
46
47
|
attr_reader :class_registry, :rbs_loader, :plugin_registry, :dependency_source_index,
|
|
@@ -88,6 +89,10 @@ module Rigor
|
|
|
88
89
|
# --no-stats` from doing the RBS env build at all.
|
|
89
90
|
@hkt_registry_base = hkt_registry || Inference::HktRegistry::EMPTY
|
|
90
91
|
@hkt_registry_holder = HktRegistryHolder.new
|
|
92
|
+
# Issue #784 — where either stage of the HKT-registry build (the plugin overlay, #791; the RBS
|
|
93
|
+
# `type`-alias scan) records a raise, so it surfaces once for the run instead of once per file — or,
|
|
94
|
+
# for the overlay, instead of aborting the run outright (see {#hkt_registry}).
|
|
95
|
+
@hkt_scan_failure = FailureSlot.new
|
|
91
96
|
@constant_type_cache = ConstantTypeCacheHolder.new
|
|
92
97
|
# ADR-82 WD9 — `[gem_name, version]` pairs for the locked gems with no resolvable RBS. The
|
|
93
98
|
# root-constant ownership index over them is built lazily (first unresolved constant read) so runs
|
|
@@ -104,21 +109,93 @@ module Rigor
|
|
|
104
109
|
# beat plugin entries, which beat the bundled JSON_VALUE. Memoised; single-threaded use only (under the
|
|
105
110
|
# Ractor pool path each worker has its own Environment so cross-worker mutation is impossible; the LSP
|
|
106
111
|
# single-publish-at-a-time invariant serialises here).
|
|
112
|
+
#
|
|
113
|
+
# BOTH stages are guarded (#784, #791): whichever raises is recorded on {#hkt_scan_failure} with the
|
|
114
|
+
# stage that failed and the other stage still runs, so this getter never raises into its callers —
|
|
115
|
+
# neither into a file's `analyze_body` rescue nor out of the run-owned demands, which have no rescue
|
|
116
|
+
# above them at all.
|
|
107
117
|
def hkt_registry
|
|
108
118
|
@hkt_registry_holder.fetch do
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
pre_scan = pre_scan_hkt_registry
|
|
120
|
+
begin
|
|
121
|
+
Inference::HktRegistry.scan_rbs_loader(
|
|
122
|
+
@rbs_loader,
|
|
123
|
+
base: pre_scan,
|
|
124
|
+
reporter: rbs_extended_reporter
|
|
125
|
+
)
|
|
126
|
+
rescue StandardError => e
|
|
127
|
+
# Issue #784 — the seam. This build is shared and memoised, and it is first demanded from inside
|
|
128
|
+
# a file's analysis, so a raise here would otherwise land in every file's `analyze_body` rescue
|
|
129
|
+
# and turn the whole run into N identical `internal analyzer error` rows (issue #776). Record it
|
|
130
|
+
# and degrade to the pre-scan registry: analysis proceeds, HKT inference from RBS `type` aliases
|
|
131
|
+
# reads its bound (`Dynamic[top]`, ADR-20 WD2), and the run surfaces ONE
|
|
132
|
+
# `rbs.coverage.hkt-scan-failed` row. Not a silent skip — the row is `:error` and names the
|
|
133
|
+
# exception and raise site — and the same shape as `RbsLoader#record_env_build_failure`.
|
|
134
|
+
# Memoising the degraded registry is what keeps the scan from being re-attempted per file.
|
|
135
|
+
record_hkt_registry_failure(e, stage: :scan)
|
|
136
|
+
pre_scan
|
|
137
|
+
end
|
|
119
138
|
end
|
|
120
139
|
end
|
|
121
140
|
|
|
141
|
+
# Issue #791 — the OTHER build behind {#hkt_registry}, and the reason the seam covers two stages
|
|
142
|
+
# rather than one. Aggregating the loaded plugins' manifest-declared HKT entries runs plugin-authored
|
|
143
|
+
# code (`Plugin::Registry#hkt_overlay_registry` reads each manifest); before this it sat one line ABOVE
|
|
144
|
+
# the #784 rescue, so a raise there escaped {#hkt_registry} entirely. Post-#788 that is not a per-file
|
|
145
|
+
# crash storm but an uncaught abort: {HktRegistryHolder#fetch} memoises only on success, so the raise
|
|
146
|
+
# is re-attempted at every demand, and the run-owned demands (`Runner::PoolCoordinator#hkt_scan_outcome`
|
|
147
|
+
# after the file loop, `Analysis::WorkerSession#drain_reporters`) sit outside any rescue.
|
|
148
|
+
#
|
|
149
|
+
# The degradation is narrower than the scan's: only the plugin overlay is dropped, and the RBS scan
|
|
150
|
+
# still runs on top of the bundled base, so a user's own `.rbs` HKT registrations survive a plugin
|
|
151
|
+
# defect. First-write-wins on the slot means a run whose overlay AND scan both raise reports the
|
|
152
|
+
# overlay — the actionable one, since the plugin is what a user can remove.
|
|
153
|
+
#
|
|
154
|
+
# @return [Rigor::Inference::HktRegistry] the registry the RBS scan is layered on top of.
|
|
155
|
+
def pre_scan_hkt_registry
|
|
156
|
+
return @hkt_registry_base unless @plugin_registry.respond_to?(:hkt_overlay_registry)
|
|
157
|
+
|
|
158
|
+
@hkt_registry_base.merge(@plugin_registry.hkt_overlay_registry)
|
|
159
|
+
rescue StandardError => e
|
|
160
|
+
record_hkt_registry_failure(e, stage: :overlay)
|
|
161
|
+
@hkt_registry_base
|
|
162
|
+
end
|
|
163
|
+
private :pre_scan_hkt_registry
|
|
164
|
+
|
|
165
|
+
# Issue #784 — the `[error_class_name, first_message_line, raw_frame_or_nil, stage]` tuple the seam in
|
|
166
|
+
# {#hkt_registry} recorded, or nil when both stages built (or were never demanded). `stage` is `:scan`
|
|
167
|
+
# for the RBS `type`-alias scan and `:overlay` for the plugin-manifest aggregation (#791); it decides
|
|
168
|
+
# the row's wording, because "the implicit HKT scan over RBS `type` aliases raised" would point a user
|
|
169
|
+
# at their own `.rbs` for a plugin's defect. The run reads the tuple only after demanding
|
|
170
|
+
# {#hkt_registry} itself — after the file loop on the sequential paths, at drain time in each pool
|
|
171
|
+
# worker (the coordinator never analyses a file under the pool, so the drain is how its snapshot learns
|
|
172
|
+
# the outcome: the #696 lesson), and from a resolved environment when the analyze set is empty — so the
|
|
173
|
+
# row never depends on which files happened to be analysed.
|
|
174
|
+
#
|
|
175
|
+
# @return [Array(String, String, String, Symbol), Array(String, String, nil, Symbol), nil]
|
|
176
|
+
def hkt_scan_failure
|
|
177
|
+
@hkt_scan_failure.value
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def record_hkt_registry_failure(error, stage:)
|
|
181
|
+
frames = error.backtrace || []
|
|
182
|
+
frame = frames.find { |f| f.include?("/lib/rigor/") } || frames.first
|
|
183
|
+
# `Class#name` is nil for an anonymous exception class; its nearest NAMED ancestor is the stable
|
|
184
|
+
# description (`inspect` embeds an object address, which would make the row's text differ across
|
|
185
|
+
# processes). Each String is frozen individually, not just the Array: the tuple crosses the fork
|
|
186
|
+
# boundary Marshal-clean and the drain channel's stated invariant
|
|
187
|
+
# ({Analysis::WorkerSession#drain_reporters}) is that its payload is also `Ractor.shareable?`, which a
|
|
188
|
+
# shallow freeze over `chomp`'s fresh String would not satisfy.
|
|
189
|
+
named = error.class.ancestors.find { |a| a.is_a?(Class) && a.name }
|
|
190
|
+
@hkt_scan_failure.record([
|
|
191
|
+
(error.class.name || "anonymous #{named&.name || 'Exception'}").dup.freeze,
|
|
192
|
+
error.message.to_s.lines.first.to_s.chomp.freeze,
|
|
193
|
+
frame&.dup&.freeze,
|
|
194
|
+
stage
|
|
195
|
+
])
|
|
196
|
+
end
|
|
197
|
+
private :record_hkt_registry_failure
|
|
198
|
+
|
|
122
199
|
# ADR-82 WD9 — the gem name owning `root_constant_name`, when that gem is locked in the project's
|
|
123
200
|
# Gemfile.lock, ships no resolvable RBS, and its entry file declares the constant at top level. Nil for
|
|
124
201
|
# everything else — the caller then keeps the generic provenance cause. The index is built once, lazily,
|
|
@@ -190,7 +267,7 @@ module Rigor
|
|
|
190
267
|
# {Environment::RbsLoader} so constant lookups (and, in later v0.0.9 slices, other reflection
|
|
191
268
|
# artefacts) consult the cache. Pass `nil` (the default) to skip caching for this environment.
|
|
192
269
|
# @return [Rigor::Environment]
|
|
193
|
-
# rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
|
|
270
|
+
# rubocop:disable-next Metrics/MethodLength, Metrics/ParameterLists
|
|
194
271
|
def for_project(root: Dir.pwd, libraries: [], signature_paths: nil, cache_store: nil,
|
|
195
272
|
plugin_registry: nil, dependency_source_index: nil,
|
|
196
273
|
rbs_extended_reporter: nil, boundary_cross_reporter: nil,
|
|
@@ -253,11 +330,13 @@ module Rigor
|
|
|
253
330
|
# extensions resolve (e.g. `Integer#minutes` on a Rails project) — turning a systematic false
|
|
254
331
|
# `call.undefined-method` into a no-op while a project WITHOUT the gem still sees the genuine
|
|
255
332
|
# diagnostic. Appended last so any RBS the project already supplies wins, and skipped for a gem whose
|
|
256
|
-
# opt-in plugin twin is loaded
|
|
257
|
-
#
|
|
333
|
+
# opt-in plugin twin is REACHABLE — loaded as a plugin, or wired in by `signature_paths:` (#672) —
|
|
334
|
+
# so the two never both declare the methods. The paths ride in `loader_signature_paths`, so the env
|
|
335
|
+
# cache descriptor digests them for free.
|
|
258
336
|
missing_gems, overlay_paths = missing_gems_and_overlay_paths(
|
|
259
337
|
locked: locked, default_libraries: merged_libraries, bundle_sig_paths: gem_sig_paths,
|
|
260
|
-
rbs_collection_paths: collection_paths, plugin_registry: plugin_registry
|
|
338
|
+
rbs_collection_paths: collection_paths, plugin_registry: plugin_registry,
|
|
339
|
+
signature_paths: resolved_paths
|
|
261
340
|
)
|
|
262
341
|
loader_signature_paths = resolved_paths + plugin_sig_paths + gem_sig_paths +
|
|
263
342
|
collection_paths + overlay_paths
|
|
@@ -276,7 +355,11 @@ module Rigor
|
|
|
276
355
|
libraries: merged_libraries,
|
|
277
356
|
signature_paths: loader_signature_paths,
|
|
278
357
|
cache_store: cache_store,
|
|
279
|
-
virtual_rbs: virtual_rbs
|
|
358
|
+
virtual_rbs: virtual_rbs,
|
|
359
|
+
# Issue #610 — the plugin-contributed subset is loaded LAST and may stand down against a colliding
|
|
360
|
+
# generic arity. Only the bundled plugins' own `sig/` is deferred: the user's `signature_paths:`,
|
|
361
|
+
# the bundle walk and `rbs collection install` all stay authoritative.
|
|
362
|
+
deferred_signature_paths: plugin_sig_paths
|
|
280
363
|
)
|
|
281
364
|
# ADR-20 slice 2c + 2e — seed hkt_registry with the bundled builtins. The Environment's
|
|
282
365
|
# `#hkt_registry` getter then LAZILY merges in the RBS env scan on first call so fast paths that
|
|
@@ -297,7 +380,6 @@ module Rigor
|
|
|
297
380
|
missing_rbs_bundle_path: bundle_root
|
|
298
381
|
)
|
|
299
382
|
end
|
|
300
|
-
# rubocop:enable Metrics/MethodLength, Metrics/ParameterLists
|
|
301
383
|
|
|
302
384
|
private
|
|
303
385
|
|
|
@@ -330,27 +412,45 @@ module Rigor
|
|
|
330
412
|
# and consumed twice: as `[gem_name, version]` pairs for the ADR-82 WD9 constant-ownership index, and
|
|
331
413
|
# as the eligible-gem set for the ADR-72 overlay resolution. Returns `[pairs, overlay_paths]`.
|
|
332
414
|
def missing_gems_and_overlay_paths(locked:, default_libraries:, bundle_sig_paths:,
|
|
333
|
-
rbs_collection_paths:, plugin_registry:)
|
|
415
|
+
rbs_collection_paths:, plugin_registry:, signature_paths: [])
|
|
334
416
|
return [[], []] if locked.empty?
|
|
335
417
|
|
|
336
418
|
rows = RbsCoverageReport.classify(
|
|
337
419
|
locked_gems: locked, default_libraries: default_libraries,
|
|
338
420
|
bundle_sig_paths: bundle_sig_paths, rbs_collection_paths: rbs_collection_paths
|
|
339
421
|
).select { |row| row.source == :missing }
|
|
340
|
-
overlays = gem_overlay_paths(
|
|
422
|
+
overlays = gem_overlay_paths(
|
|
423
|
+
missing_gem_names: rows.map(&:gem_name), plugin_registry: plugin_registry,
|
|
424
|
+
signature_paths: signature_paths
|
|
425
|
+
)
|
|
341
426
|
[rows.map { |row| [row.gem_name, row.version] }, overlays]
|
|
342
427
|
end
|
|
343
428
|
|
|
344
429
|
# ADR-72 — resolve the bundled RBS overlay directories to load for this project. A gem is eligible when
|
|
345
|
-
# it is classified `:missing` (see {missing_rbs_gem_rows}) and its conflicting opt-in plugin (if
|
|
346
|
-
# is
|
|
347
|
-
|
|
430
|
+
# it is classified `:missing` (see {missing_rbs_gem_rows}) and its conflicting opt-in plugin twin (if
|
|
431
|
+
# any) is NOT already reachable. Returns `[Pathname]`, deterministically ordered, or `[]` when no
|
|
432
|
+
# eligible gem.
|
|
433
|
+
#
|
|
434
|
+
# "Reachable" is two questions, not one. The plugin being LOADED is the route ADR-72 anticipated. The
|
|
435
|
+
# second is the route issue #672 found: a project that wires the plugin's signatures through
|
|
436
|
+
# `signature_paths:` instead of `plugins:` reaches the same `.rbs` with no registry entry to key on, so
|
|
437
|
+
# both halves loaded, `RBS::DefinitionBuilder` raised `DuplicatedMethodDefinitionError` for every class
|
|
438
|
+
# they share, and each one silently degraded to `Dynamic[top]` — the run reported LESS while exiting 0.
|
|
439
|
+
#
|
|
440
|
+
# This method keeps the POLICY (which gem maps to which plugin id, and when an overlay is eligible at
|
|
441
|
+
# all); `RbsLoader.bundled_overlay_twin_signatures_loaded?` answers the filesystem half, beside the
|
|
442
|
+
# overlay-side answer to the same question, and carries the reasoning for why it asks what an entry
|
|
443
|
+
# LOADS rather than what it looks like.
|
|
444
|
+
def gem_overlay_paths(missing_gem_names:, plugin_registry:, signature_paths: [])
|
|
348
445
|
return [] if missing_gem_names.empty?
|
|
349
446
|
|
|
350
447
|
loaded_ids = plugin_registry ? plugin_registry.ids.to_set : Set.new
|
|
351
448
|
eligible = missing_gem_names.reject do |gem_name|
|
|
352
449
|
plugin_id = GEM_OVERLAY_PLUGIN_IDS[gem_name]
|
|
353
|
-
|
|
450
|
+
next false unless plugin_id
|
|
451
|
+
|
|
452
|
+
loaded_ids.include?(plugin_id) ||
|
|
453
|
+
RbsLoader.bundled_overlay_twin_signatures_loaded?(plugin_id, signature_paths)
|
|
354
454
|
end.sort
|
|
355
455
|
RbsLoader.gem_overlay_sig_paths(eligible)
|
|
356
456
|
end
|
|
@@ -23,7 +23,7 @@ module Rigor
|
|
|
23
23
|
# `Nominal[Hash, [K, V]]` the shape is projected to its underlying nominal so the existing
|
|
24
24
|
# generic-acceptance pipeline continues to apply; the converse direction (a Tuple receiver accepting a
|
|
25
25
|
# generic Array) stays conservative because the analyzer cannot verify arity from a raw nominal alone.
|
|
26
|
-
# rubocop:disable Metrics/ModuleLength
|
|
26
|
+
# rubocop:disable-next Metrics/ModuleLength
|
|
27
27
|
module Acceptance
|
|
28
28
|
module_function
|
|
29
29
|
|
|
@@ -67,11 +67,15 @@ module Rigor
|
|
|
67
67
|
Type::Refined => :accepts_refined,
|
|
68
68
|
Type::Intersection => :accepts_intersection,
|
|
69
69
|
Type::Tuple => :accepts_tuple,
|
|
70
|
-
Type::HashShape => :accepts_hash_shape
|
|
70
|
+
Type::HashShape => :accepts_hash_shape,
|
|
71
|
+
Type::Result => :accepts_result,
|
|
72
|
+
Type::Maybe => :accepts_maybe,
|
|
73
|
+
Type::StructClass => :accepts_class_factory,
|
|
74
|
+
Type::DataClass => :accepts_class_factory
|
|
71
75
|
}.freeze
|
|
72
76
|
private_constant :TYPE_HANDLERS
|
|
73
77
|
|
|
74
|
-
# rubocop:disable Metrics/ClassLength
|
|
78
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
75
79
|
class << self
|
|
76
80
|
private
|
|
77
81
|
|
|
@@ -165,20 +169,28 @@ module Rigor
|
|
|
165
169
|
end
|
|
166
170
|
end
|
|
167
171
|
|
|
168
|
-
# Singleton[C] only accepts another Singleton[D] where D is a subclass
|
|
169
|
-
# carrier (instance, constant, ...) is no, because the singleton type's
|
|
170
|
-
# objects themselves.
|
|
172
|
+
# Singleton[C] only accepts another Singleton[D] (or StructClass / DataClass) where D is a subclass
|
|
173
|
+
# of (or equal to) C. Any other carrier (instance, constant, ...) is no, because the singleton type's
|
|
174
|
+
# inhabitants are the class objects themselves.
|
|
171
175
|
def accepts_singleton(self_type, other_type, mode)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
actual_name =
|
|
177
|
+
case other_type
|
|
178
|
+
when Type::Singleton then other_type.class_name
|
|
179
|
+
when Type::StructClass, Type::DataClass
|
|
180
|
+
return Type::AcceptsResult.yes(mode: mode, reasons: "exact name match") if
|
|
181
|
+
other_type.class_name == self_type.class_name
|
|
182
|
+
|
|
183
|
+
class_factory_base_name(other_type)
|
|
184
|
+
else
|
|
185
|
+
return Type::AcceptsResult.no(
|
|
186
|
+
mode: mode,
|
|
187
|
+
reasons: "Singleton[#{self_type.class_name}] does not accept #{other_type.class}"
|
|
188
|
+
)
|
|
189
|
+
end
|
|
178
190
|
|
|
179
191
|
class_subtype_result(
|
|
180
192
|
target_name: self_type.class_name,
|
|
181
|
-
actual_name:
|
|
193
|
+
actual_name: actual_name,
|
|
182
194
|
mode: mode,
|
|
183
195
|
kind: :singleton
|
|
184
196
|
)
|
|
@@ -195,11 +207,13 @@ module Rigor
|
|
|
195
207
|
# - HashShape{*} when self is the Hash (or a supertype) family, projected to
|
|
196
208
|
# `Nominal[Hash, [union(keys), union(values)]]`.
|
|
197
209
|
# - Singleton: never (wrong value kind).
|
|
210
|
+
# - StructClass / DataClass: class-side carriers, accepted when C is a meta nominal (Class, Module, ...).
|
|
198
211
|
def accepts_nominal(self_type, other_type, mode)
|
|
199
212
|
case other_type
|
|
200
213
|
when Type::Nominal then accepts_nominal_from_nominal(self_type, other_type, mode)
|
|
201
214
|
when Type::Constant then accepts_nominal_from_constant(self_type, other_type, mode)
|
|
202
215
|
when Type::Singleton then accepts_nominal_from_singleton(self_type, other_type, mode)
|
|
216
|
+
when Type::StructClass, Type::DataClass then accepts_nominal_from_class_factory(self_type, other_type, mode)
|
|
203
217
|
when Type::IntegerRange then accepts_nominal_from_integer_range(self_type, other_type, mode)
|
|
204
218
|
else accepts_nominal_from_shape(self_type, other_type, mode)
|
|
205
219
|
end
|
|
@@ -234,6 +248,8 @@ module Rigor
|
|
|
234
248
|
# is bounded because every refinement carrier's `base` is closer to the nominal layer.
|
|
235
249
|
accepts(self_type, other_type.base, mode: mode)
|
|
236
250
|
.with_reason("projected #{other_type.class.name.split('::').last} to its base")
|
|
251
|
+
when Type::Result, Type::Maybe
|
|
252
|
+
accepts_nominal_from_monad(self_type, other_type, mode)
|
|
237
253
|
else
|
|
238
254
|
Type::AcceptsResult.no(
|
|
239
255
|
mode: mode,
|
|
@@ -242,6 +258,32 @@ module Rigor
|
|
|
242
258
|
end
|
|
243
259
|
end
|
|
244
260
|
|
|
261
|
+
def accepts_nominal_from_monad(self_type, other_type, mode)
|
|
262
|
+
cls = self_type.class_name.sub(/\A::/, "")
|
|
263
|
+
case other_type
|
|
264
|
+
when Type::Result
|
|
265
|
+
unless %w[Dry::Monads::Result Result].include?(cls)
|
|
266
|
+
return Type::AcceptsResult.no(mode: mode, reasons: "Nominal[#{self_type.class_name}] rejects Result")
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
target = Type::Combinator.nominal_of(
|
|
270
|
+
self_type.class_name,
|
|
271
|
+
type_args: [other_type.ok_type, other_type.err_type]
|
|
272
|
+
)
|
|
273
|
+
accepts(self_type, target, mode: mode).with_reason("projected Result to Nominal")
|
|
274
|
+
when Type::Maybe
|
|
275
|
+
unless %w[Dry::Monads::Maybe Maybe].include?(cls)
|
|
276
|
+
return Type::AcceptsResult.no(mode: mode, reasons: "Nominal[#{self_type.class_name}] rejects Maybe")
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
target = Type::Combinator.nominal_of(
|
|
280
|
+
self_type.class_name,
|
|
281
|
+
type_args: [other_type.value_type]
|
|
282
|
+
)
|
|
283
|
+
accepts(self_type, target, mode: mode).with_reason("projected Maybe to Nominal")
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
245
287
|
# `Nominal[Integer]` (and anything Integer is-a, like Numeric) accepts any `IntegerRange`; nothing
|
|
246
288
|
# else does. Argument-bearing `Nominal`s never accept `IntegerRange` because IntegerRange has no
|
|
247
289
|
# type args.
|
|
@@ -275,11 +317,11 @@ module Rigor
|
|
|
275
317
|
# introspection patterns. The rule conservatively answers `:yes` for `Module` (every singleton is at
|
|
276
318
|
# least a Module) and for `Class` / `Object` / `BasicObject` (the class object inherits from those).
|
|
277
319
|
# Other Nominals fall through to the default `:no`.
|
|
278
|
-
|
|
279
|
-
private_constant :
|
|
320
|
+
CLASS_OBJECT_NOMINALS = %w[Module Class Object Kernel BasicObject].freeze
|
|
321
|
+
private_constant :CLASS_OBJECT_NOMINALS
|
|
280
322
|
|
|
281
323
|
def accepts_nominal_from_singleton(self_type, other_type, mode)
|
|
282
|
-
if
|
|
324
|
+
if CLASS_OBJECT_NOMINALS.include?(self_type.class_name)
|
|
283
325
|
return Type::AcceptsResult.yes(
|
|
284
326
|
mode: mode,
|
|
285
327
|
reasons: "Singleton[#{other_type.class_name}] is-a #{self_type.class_name}"
|
|
@@ -292,6 +334,38 @@ module Rigor
|
|
|
292
334
|
)
|
|
293
335
|
end
|
|
294
336
|
|
|
337
|
+
def accepts_nominal_from_class_factory(self_type, other_type, mode)
|
|
338
|
+
if CLASS_OBJECT_NOMINALS.include?(self_type.class_name) && self_type.type_args.empty?
|
|
339
|
+
return Type::AcceptsResult.yes(
|
|
340
|
+
mode: mode,
|
|
341
|
+
reasons: "#{other_type.describe} is-a #{self_type.class_name}"
|
|
342
|
+
)
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
if self_type.class_name == "Class" && self_type.type_args.one?
|
|
346
|
+
return accepts_class_factory_instance_arg(self_type.type_args.first, other_type, mode)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
Type::AcceptsResult.no(
|
|
350
|
+
mode: mode,
|
|
351
|
+
reasons: "Nominal[#{self_type.class_name}] rejects #{other_type.class}"
|
|
352
|
+
)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def accepts_class_factory_instance_arg(formal, factory, mode)
|
|
356
|
+
if factory.class_name
|
|
357
|
+
named = accepts(formal, Type::Combinator.nominal_of(factory.class_name), mode: mode)
|
|
358
|
+
return named.with_reason("matched the factory class name") if named.yes?
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
accepts(formal, Type::Combinator.nominal_of(class_factory_base_name(factory)), mode: mode)
|
|
362
|
+
.with_reason("projected the factory instance type")
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def class_factory_base_name(factory)
|
|
366
|
+
factory.is_a?(Type::StructClass) ? "Struct" : "Data"
|
|
367
|
+
end
|
|
368
|
+
|
|
295
369
|
def accepts_nominal_from_nominal(self_type, other_type, mode)
|
|
296
370
|
class_result = class_subtype_result(
|
|
297
371
|
target_name: self_type.class_name,
|
|
@@ -753,6 +827,63 @@ module Rigor
|
|
|
753
827
|
Type::AcceptsResult.no(mode: mode, reasons: reason)
|
|
754
828
|
end
|
|
755
829
|
|
|
830
|
+
def accepts_result(self_type, other_type, mode)
|
|
831
|
+
case other_type
|
|
832
|
+
when Type::Result
|
|
833
|
+
combine_arg_results(
|
|
834
|
+
[
|
|
835
|
+
accepts(self_type.ok_type, other_type.ok_type, mode: mode),
|
|
836
|
+
accepts(self_type.err_type, other_type.err_type, mode: mode)
|
|
837
|
+
],
|
|
838
|
+
mode
|
|
839
|
+
)
|
|
840
|
+
when Type::Nominal
|
|
841
|
+
cls = other_type.class_name.sub(/\A::/, "")
|
|
842
|
+
if %w[Dry::Monads::Result Result].include?(cls) && other_type.type_args.size == 2
|
|
843
|
+
combine_arg_results(
|
|
844
|
+
[
|
|
845
|
+
accepts(self_type.ok_type, other_type.type_args[0], mode: mode),
|
|
846
|
+
accepts(self_type.err_type, other_type.type_args[1], mode: mode)
|
|
847
|
+
],
|
|
848
|
+
mode
|
|
849
|
+
)
|
|
850
|
+
else
|
|
851
|
+
Type::AcceptsResult.no(mode: mode, reasons: "Result does not accept Nominal[#{other_type.class_name}]")
|
|
852
|
+
end
|
|
853
|
+
else
|
|
854
|
+
Type::AcceptsResult.no(
|
|
855
|
+
mode: mode,
|
|
856
|
+
reasons: "Result does not accept #{other_type.class}"
|
|
857
|
+
)
|
|
858
|
+
end
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
def accepts_maybe(self_type, other_type, mode)
|
|
862
|
+
case other_type
|
|
863
|
+
when Type::Maybe
|
|
864
|
+
accepts(self_type.value_type, other_type.value_type, mode: mode)
|
|
865
|
+
when Type::Nominal
|
|
866
|
+
cls = other_type.class_name.sub(/\A::/, "")
|
|
867
|
+
if %w[Dry::Monads::Maybe Maybe].include?(cls) && other_type.type_args.size == 1
|
|
868
|
+
accepts(self_type.value_type, other_type.type_args[0], mode: mode)
|
|
869
|
+
else
|
|
870
|
+
Type::AcceptsResult.no(mode: mode, reasons: "Maybe does not accept Nominal[#{other_type.class_name}]")
|
|
871
|
+
end
|
|
872
|
+
else
|
|
873
|
+
Type::AcceptsResult.no(
|
|
874
|
+
mode: mode,
|
|
875
|
+
reasons: "Maybe does not accept #{other_type.class}"
|
|
876
|
+
)
|
|
877
|
+
end
|
|
878
|
+
end
|
|
879
|
+
|
|
880
|
+
def accepts_class_factory(self_type, other_type, mode)
|
|
881
|
+
Type::AcceptsResult.no(
|
|
882
|
+
mode: mode,
|
|
883
|
+
reasons: "#{self_type.describe} rejects #{other_type.class}"
|
|
884
|
+
)
|
|
885
|
+
end
|
|
886
|
+
|
|
756
887
|
# Uses Ruby's actual class hierarchy via Object.const_get to answer "is D a subclass of C?" for core,
|
|
757
888
|
# stdlib, and application classes. When either name fails to resolve we surface "maybe": the caller
|
|
758
889
|
# (overload selector) treats yes/maybe identically, so the conservative answer keeps overload
|
|
@@ -791,14 +922,35 @@ module Rigor
|
|
|
791
922
|
end
|
|
792
923
|
end
|
|
793
924
|
|
|
925
|
+
# Resolves a class name taken from the analysed program without triggering `const_missing`
|
|
926
|
+
# or executing a pending autoload.
|
|
927
|
+
#
|
|
928
|
+
# `const_defined?(part, false)` answers true for a REGISTERED-BUT-NOT-YET-TRIGGERED autoload,
|
|
929
|
+
# so on its own it does not mean "already in memory" and `const_get` would execute the target
|
|
930
|
+
# file inside the analyzer. `Module#autoload?` separates the two: it returns the registered
|
|
931
|
+
# path while the autoload is pending and nil once it has run, so declining on it declines
|
|
932
|
+
# exactly the dangerous case and still resolves a class genuinely loaded ([#680], [#689]).
|
|
933
|
+
#
|
|
934
|
+
# The rescue catches `StandardError`, `ScriptError` and `SystemExit`. `Interrupt`,
|
|
935
|
+
# `SignalException` and `NoMemoryError` stay uncaught on purpose.
|
|
794
936
|
def resolve_class(name)
|
|
795
|
-
|
|
796
|
-
|
|
937
|
+
return nil unless name.is_a?(::String)
|
|
938
|
+
|
|
939
|
+
parts = name.delete_prefix("::").split("::")
|
|
940
|
+
return nil if parts.empty?
|
|
941
|
+
|
|
942
|
+
mod = ::Object
|
|
943
|
+
parts.each do |part|
|
|
944
|
+
return nil unless mod.is_a?(::Module) && mod.const_defined?(part, false)
|
|
945
|
+
return nil if mod.autoload?(part)
|
|
946
|
+
|
|
947
|
+
mod = mod.const_get(part, false)
|
|
948
|
+
end
|
|
949
|
+
mod.is_a?(::Module) ? mod : nil
|
|
950
|
+
rescue ::StandardError, ::ScriptError, ::SystemExit
|
|
797
951
|
nil
|
|
798
952
|
end
|
|
799
953
|
end
|
|
800
|
-
# rubocop:enable Metrics/ClassLength
|
|
801
954
|
end
|
|
802
|
-
# rubocop:enable Metrics/ModuleLength
|
|
803
955
|
end
|
|
804
956
|
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../source/node_walker"
|
|
6
|
+
require_relative "block_parameter_binder"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Inference
|
|
10
|
+
# The outer locals a block body can REBIND — the one name set that ADR-56's captured-local write-back
|
|
11
|
+
# (`StatementEvaluator#write_back_block_captures`), the escaping-block narrowing drop
|
|
12
|
+
# (`StatementEvaluator#drop_captured_narrowing`) and issue #587's per-element fold
|
|
13
|
+
# (`ExpressionTyper#per_element_captured_bindings`) all key on. It lives in one place so the four
|
|
14
|
+
# cannot disagree about what "captured" means.
|
|
15
|
+
#
|
|
16
|
+
# A write counts across every local-write form — plain `=` (`LocalVariableWriteNode`), the operator /
|
|
17
|
+
# `||=` / `&&=` compound forms, and a multi-assign target (`x, y = …` → `LocalVariableTargetNode` under
|
|
18
|
+
# a `MultiWriteNode`) — at ANY depth: a block is a closure, so a write inside a nested block binds the
|
|
19
|
+
# same outer variable. Block-introduced names (parameters, numbered parameters, `;`-locals) and names
|
|
20
|
+
# not bound in the outer scope are excluded; a write to either is not a captured rebind of an outer
|
|
21
|
+
# variable.
|
|
22
|
+
module CapturedLocals
|
|
23
|
+
LOCAL_WRITE_NODES = [
|
|
24
|
+
Prism::LocalVariableWriteNode,
|
|
25
|
+
Prism::LocalVariableOperatorWriteNode,
|
|
26
|
+
Prism::LocalVariableOrWriteNode,
|
|
27
|
+
Prism::LocalVariableAndWriteNode,
|
|
28
|
+
Prism::LocalVariableTargetNode
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
# @param block_node [Prism::BlockNode]
|
|
34
|
+
# @param base_scope [Rigor::Scope] the call-site scope the block closes over.
|
|
35
|
+
# @return [Array<Symbol>] the captured names the body writes, each once, in first-write order.
|
|
36
|
+
def writes(block_node, base_scope)
|
|
37
|
+
body = block_node.body
|
|
38
|
+
return [] if body.nil?
|
|
39
|
+
|
|
40
|
+
introduced = introduced_locals(block_node)
|
|
41
|
+
outer_writes = []
|
|
42
|
+
Source::NodeWalker.each(body) do |descendant|
|
|
43
|
+
next unless LOCAL_WRITE_NODES.any? { |klass| descendant.is_a?(klass) }
|
|
44
|
+
next if introduced.include?(descendant.name)
|
|
45
|
+
next unless base_scope.locals.key?(descendant.name)
|
|
46
|
+
|
|
47
|
+
outer_writes << descendant.name
|
|
48
|
+
end
|
|
49
|
+
outer_writes.uniq
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Names the block itself introduces: parameters (numbered parameters included, via
|
|
53
|
+
# `BlockParameterBinder`) plus the explicit `;`-prefixed block-locals on `BlockParametersNode`.
|
|
54
|
+
def introduced_locals(block_node)
|
|
55
|
+
introduced = Set.new(BlockParameterBinder.new.bind(block_node).keys)
|
|
56
|
+
params_root = block_node.parameters
|
|
57
|
+
params_root.locals.each { |loc| introduced << loc.name } if params_root.is_a?(Prism::BlockParametersNode)
|
|
58
|
+
introduced
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|