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
|
@@ -23,7 +23,7 @@ module Rigor
|
|
|
23
23
|
# API stays purely functional.
|
|
24
24
|
#
|
|
25
25
|
# See docs/internal-spec/inference-engine.md for the binding contract.
|
|
26
|
-
# rubocop:disable Metrics/ClassLength
|
|
26
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
27
27
|
class RbsLoader
|
|
28
28
|
# Buffer name stamped on the `module` declarations synthesized by {.synthesize_missing_namespaces}.
|
|
29
29
|
# Re-read off the built env by {#synthesized_namespaces} so the analysis layer can surface an `:info`
|
|
@@ -38,6 +38,10 @@ module Rigor
|
|
|
38
38
|
# into the set {MethodDispatcher} resolves to `Dynamic[Top]` (no false `call.undefined-method`).
|
|
39
39
|
SYNTHETIC_STUB_BUFFER = "(rigor: synthesized stub types)"
|
|
40
40
|
|
|
41
|
+
# The buffer name `Cache::RbsEnvironmentMarshalPatch` reconstructs every `RBS::Location` behind on a
|
|
42
|
+
# cache HIT. Never a real path, so it must never be reported as one (issue #696).
|
|
43
|
+
CACHED_LOCATION_BUFFER_NAME = "<cached>"
|
|
44
|
+
|
|
41
45
|
# Cap on how many quarantined `signature_paths:` files {#warn_about_quarantined_signatures} lists by name
|
|
42
46
|
# before collapsing the tail to "… and N more" — a broken generator can emit many, and a wall of parse
|
|
43
47
|
# errors buries the signal.
|
|
@@ -63,7 +67,7 @@ module Rigor
|
|
|
63
67
|
# Vendored gem stubs (`data/vendored_gem_sigs/<gem>/`) are loaded on top of `signature_paths` so the
|
|
64
68
|
# per-gem RBS bundled with Rigor itself is in scope for every analysis run. The gem stubs are
|
|
65
69
|
# intentionally read-only and appended LAST so user-supplied `signature_paths` win on name conflicts.
|
|
66
|
-
def build_env_for(libraries:, signature_paths:, virtual_rbs: [])
|
|
70
|
+
def build_env_for(libraries:, signature_paths:, virtual_rbs: [], deferred_signature_paths: [])
|
|
67
71
|
rbs_loader = RBS::EnvironmentLoader.new
|
|
68
72
|
libraries = libraries_without_shadowed_bigdecimal_math(libraries)
|
|
69
73
|
loaded_libraries = libraries.select do |library|
|
|
@@ -80,11 +84,14 @@ module Rigor
|
|
|
80
84
|
# sigs are Rigor-shipped and trusted, so they stay on the loader's fast batch path.
|
|
81
85
|
add_bundled_signatures(rbs_loader, loaded_libraries.to_set(&:to_s))
|
|
82
86
|
env = RBS::Environment.from_loader(rbs_loader)
|
|
83
|
-
|
|
87
|
+
project_files = project_sig_files(signature_paths)
|
|
88
|
+
add_project_signatures(env, signature_paths, deferred_signature_paths)
|
|
84
89
|
add_virtual_rbs(env, virtual_rbs)
|
|
85
90
|
synthesize_missing_namespaces(env)
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
# Issue #777 — resolve-time backstop: also unload colliding PROJECT buffers (class-vs-module /
|
|
92
|
+
# constant redeclarations against bundled RBS). Virtual culprits are preferred when both appear.
|
|
93
|
+
env, resolved = resolve_quarantining_virtual_collisions(env, virtual_rbs, project_files: project_files)
|
|
94
|
+
stub_missing_referenced_types(env, resolved, project_files)
|
|
88
95
|
end
|
|
89
96
|
|
|
90
97
|
# rbs ships `stdlib/bigdecimal/` and `stdlib/bigdecimal-math/` as two libraries, so
|
|
@@ -153,32 +160,61 @@ module Rigor
|
|
|
153
160
|
false
|
|
154
161
|
end
|
|
155
162
|
|
|
156
|
-
# Backstop for a
|
|
157
|
-
#
|
|
158
|
-
#
|
|
159
|
-
# range spans `>= 3.0, < 5.0` (ADR-79) and WHERE duplicate detection fires is an
|
|
160
|
-
# this code must not depend on. Resolution
|
|
161
|
-
#
|
|
162
|
-
#
|
|
163
|
-
#
|
|
164
|
-
# `#unload` (rbs 3.x), re-raises into
|
|
163
|
+
# Backstop for a `RBS::DuplicatedDeclarationError` that only materialises at `resolve_type_names`
|
|
164
|
+
# (which rebuilds the env from `sources`). {.add_virtual_rbs} / {.add_project_parsed_decls}
|
|
165
|
+
# transactionally handle the add-time case — empirically everything on rbs 4.x — but the rbs
|
|
166
|
+
# gemspec range spans `>= 3.0, < 5.0` (ADR-79) and WHERE duplicate detection fires is an
|
|
167
|
+
# rbs-internal choice this code must not depend on. Resolution preference: drop colliding VIRTUAL
|
|
168
|
+
# buffers first (explicit `.rbs` wins), else drop colliding PROJECT `signature_paths:` buffers
|
|
169
|
+
# (issue #777 — bundled RBS wins over a kind-colliding project file), then retry. Every pass
|
|
170
|
+
# removes at least one buffer, so the loop is bounded. A duplicate with neither virtual nor
|
|
171
|
+
# project culprits (bundled-vs-bundled), or an env without `#unload` (rbs 3.x), re-raises into
|
|
172
|
+
# the existing one-warning total-failure path.
|
|
165
173
|
#
|
|
166
174
|
# The dropped set is not returned: consumers recover it from the built env via
|
|
167
175
|
# {#virtual_rbs_collision_quarantined}, which also works on a cache HIT where this build never ran.
|
|
168
|
-
def resolve_quarantining_virtual_collisions(env, virtual_rbs)
|
|
176
|
+
def resolve_quarantining_virtual_collisions(env, virtual_rbs, project_files: [])
|
|
169
177
|
virtual_names = virtual_rbs.to_set { |name, _content| name.to_s }
|
|
170
|
-
|
|
178
|
+
project_names = Array(project_files).to_set(&:to_s)
|
|
179
|
+
# Bound by virtual + project culprit candidates: each pass removes at least one buffer.
|
|
180
|
+
(virtual_names.size + project_names.size + 1).times do
|
|
171
181
|
return [env, env.resolve_type_names]
|
|
172
182
|
rescue ::RBS::DuplicatedDeclarationError => e
|
|
173
|
-
|
|
183
|
+
env = unload_duplicated_declaration_culprits(
|
|
184
|
+
env, e, virtual_names: virtual_names, project_names: project_names
|
|
185
|
+
)
|
|
186
|
+
end
|
|
187
|
+
[env, env.resolve_type_names]
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Prefer dropping colliding VIRTUAL buffers (explicit `.rbs` wins), else PROJECT `signature_paths:`
|
|
191
|
+
# buffers (issue #777 — bundled RBS wins over a kind-colliding project file). A duplicate with
|
|
192
|
+
# neither virtual nor project culprits (bundled-vs-bundled), or an env without `#unload`, re-raises.
|
|
193
|
+
def unload_duplicated_declaration_culprits(env, error, virtual_names:, project_names:)
|
|
194
|
+
raise unless env.respond_to?(:unload)
|
|
195
|
+
|
|
196
|
+
culprits = duplicated_declaration_buffer_names(error)
|
|
197
|
+
virtual_culprits = culprits.select { |name| virtual_names.include?(name.to_s) }
|
|
198
|
+
return env.unload(virtual_culprits) unless virtual_culprits.empty?
|
|
174
199
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
200
|
+
# Prefer dropping the project buffer(s): the bundled declaration stays, Greeter/core stay
|
|
201
|
+
# usable, and only the conflicting file is absent.
|
|
202
|
+
project_culprits = select_project_collision_culprits(culprits, project_names)
|
|
203
|
+
raise if project_culprits.empty?
|
|
178
204
|
|
|
179
|
-
|
|
205
|
+
env.unload(project_culprits)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def duplicated_declaration_buffer_names(error)
|
|
209
|
+
error.decls.filter_map { |decl| decl.location&.buffer&.name }.uniq
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def select_project_collision_culprits(culprits, project_names)
|
|
213
|
+
culprits.select do |name|
|
|
214
|
+
project_names.include?(File.expand_path(name.to_s))
|
|
215
|
+
rescue ArgumentError, TypeError
|
|
216
|
+
project_names.include?(name.to_s)
|
|
180
217
|
end
|
|
181
|
-
[env, env.resolve_type_names]
|
|
182
218
|
end
|
|
183
219
|
|
|
184
220
|
# ADR-5 robustness, second tier. A project `signature_paths:` RBS that *references* a type no loaded
|
|
@@ -287,13 +323,112 @@ module Rigor
|
|
|
287
323
|
# Buffer names are the file's absolute path (matching {.project_sig_files}) so {.project_entry?} — which
|
|
288
324
|
# attributes a `class_decls` entry to the project by buffer name — still recognises these declarations.
|
|
289
325
|
# Sorted for a deterministic add order (the env feeds the cache, ADR-54).
|
|
290
|
-
def add_project_signatures(env, signature_paths)
|
|
291
|
-
project_sig_files(
|
|
326
|
+
def add_project_signatures(env, signature_paths, deferred_paths = [])
|
|
327
|
+
deferred = project_sig_files(deferred_paths)
|
|
328
|
+
(project_sig_files(signature_paths) - deferred).sort.each do |file|
|
|
292
329
|
parsed = parse_signature_file(file)
|
|
293
330
|
next if parsed.nil? # quarantined (unparseable) or unreadable — skip so the env survives
|
|
294
331
|
|
|
295
332
|
buffer, directives, decls = parsed
|
|
296
|
-
|
|
333
|
+
add_project_parsed_decls(env, buffer, directives, decls)
|
|
334
|
+
end
|
|
335
|
+
add_deferred_signatures(env, deferred)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Issue #777 — add one project signature transactionally. `RBS::Environment#add_source` appends to
|
|
339
|
+
# `sources` BEFORE inserting decls, so a mid-insert `RBS::DuplicatedDeclarationError` (class-vs-module
|
|
340
|
+
# kind collision against bundled RBS, or a constant redeclared from core) would otherwise leave a
|
|
341
|
+
# poisoned source that `resolve_type_names` re-raises outside every per-file rescue, collapsing the
|
|
342
|
+
# WHOLE env to nil. Mirror {.add_virtual_rbs}: drop the poisoned source and continue so Greeter /
|
|
343
|
+
# core / every non-conflicting project file still load. Without a `sources` table (rbs 3.x) re-raise
|
|
344
|
+
# into the existing total-failure path — we cannot make the skip transactional there.
|
|
345
|
+
def add_project_parsed_decls(env, buffer, directives, decls)
|
|
346
|
+
add_parsed_decls(env, buffer, directives, decls)
|
|
347
|
+
rescue ::RBS::DuplicatedDeclarationError
|
|
348
|
+
raise unless env.respond_to?(:sources)
|
|
349
|
+
|
|
350
|
+
env.sources.reject! { |source| source.buffer.name == buffer.name }
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Issue #610 — the DEFERRED half: signature files a bundled plugin contributes through its manifest,
|
|
354
|
+
# added after every other source so each can be asked a question the eager half cannot be asked —
|
|
355
|
+
# "does something already declare this class, at a different generic arity?".
|
|
356
|
+
#
|
|
357
|
+
# `plugins/rigor-activerecord/sig` declares `class Relation[Elem]`; `rbs collection install` ships a
|
|
358
|
+
# NON-generic `ActiveRecord::Relation`. Two declarations of one class with different arity make
|
|
359
|
+
# `RBS::DefinitionBuilder` raise `GenericParameterMismatchError`, and Rigor keeps the class KNOWN
|
|
360
|
+
# after a failed build, so every call INTO a relation reads `Dynamic[top]` — the plugin and
|
|
361
|
+
# `rigor-project-init`'s own advice to install the collection cancel each other out.
|
|
362
|
+
#
|
|
363
|
+
# The plugin stands down, not the user's source, for the reason `Reflection.constant_type_at`
|
|
364
|
+
# already encodes: the project's own sources are authoritative for the project. Standing down is
|
|
365
|
+
# strictly better than the collision it replaces — the collection's own declaration builds, where
|
|
366
|
+
# today NEITHER does — and it cannot regress a project without a colliding declaration, because
|
|
367
|
+
# nothing triggers.
|
|
368
|
+
#
|
|
369
|
+
# Granularity is the FILE, not the declaration: a bundled plugin's `sig/` file is single-purpose
|
|
370
|
+
# (rigor-activerecord's is `relation.rbs`, declaring exactly `Relation`), so dropping the file drops
|
|
371
|
+
# exactly the conflicting declaration. Splitting a plugin file that mixes a colliding class with
|
|
372
|
+
# innocent ones is the change to make if that ever stops being true.
|
|
373
|
+
def add_deferred_signatures(env, deferred_files)
|
|
374
|
+
deferred_files.sort.each do |file|
|
|
375
|
+
parsed = parse_signature_file(file)
|
|
376
|
+
next if parsed.nil?
|
|
377
|
+
|
|
378
|
+
buffer, directives, decls = parsed
|
|
379
|
+
next if generic_arity_conflict(env, decls)
|
|
380
|
+
|
|
381
|
+
add_project_parsed_decls(env, buffer, directives, decls)
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# Issue #610 — which deferred (plugin-contributed) files stood down, as
|
|
386
|
+
# `[absolute_path, class_name, existing_arity, incoming_arity]`, sorted. DERIVED from a built env
|
|
387
|
+
# rather than recorded during the build, for {RbsLoader#quarantined_signatures}' reason: a cache HIT
|
|
388
|
+
# never runs the build, and a condition only the build knew would silently disappear on the second
|
|
389
|
+
# run. Re-asking the same question of the FINAL env gives the same answer — the file's declaration is
|
|
390
|
+
# absent from it, and the source that displaced it is still there with its own arity.
|
|
391
|
+
def deferred_standdowns(env, deferred_paths)
|
|
392
|
+
return [] if env.nil? || deferred_paths.nil? || deferred_paths.empty?
|
|
393
|
+
|
|
394
|
+
project_sig_files(deferred_paths).sort.filter_map do |file|
|
|
395
|
+
parsed = parse_signature_file(file)
|
|
396
|
+
next if parsed.nil?
|
|
397
|
+
|
|
398
|
+
conflict = generic_arity_conflict(env, parsed[2])
|
|
399
|
+
conflict && [file, *conflict]
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# The first `[class_name, existing_arity, incoming_arity]` where `decls` would re-declare a class the
|
|
404
|
+
# env already has with a DIFFERENT number of type parameters, or nil when they agree everywhere.
|
|
405
|
+
# Equal arity is not a conflict: RBS reopens the class, which is the ordinary and supported case, so
|
|
406
|
+
# the stand-down must not fire on it.
|
|
407
|
+
def generic_arity_conflict(env, decls)
|
|
408
|
+
each_declared_class(decls) do |name, arity|
|
|
409
|
+
entry = env.class_decls[::RBS::TypeName.parse(name)]
|
|
410
|
+
next if entry.nil? || !entry.respond_to?(:type_params)
|
|
411
|
+
|
|
412
|
+
existing = entry.type_params&.size
|
|
413
|
+
return [name, existing, arity] if existing && existing != arity
|
|
414
|
+
end
|
|
415
|
+
nil
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# Yields `[absolute_class_name, type_param_count]` for every class declared anywhere in `decls`,
|
|
419
|
+
# descending through modules and nested classes. Names inside a `module`/`class` body are parsed
|
|
420
|
+
# RELATIVE, so the enclosing path is accumulated here — the env is keyed by absolute name, and
|
|
421
|
+
# comparing a relative `Relation` against it would silently never match.
|
|
422
|
+
def each_declared_class(decls, prefix = [], &block)
|
|
423
|
+
decls.each do |decl|
|
|
424
|
+
case decl
|
|
425
|
+
when ::RBS::AST::Declarations::Module
|
|
426
|
+
each_declared_class(decl.members, prefix + [decl.name.to_s], &block)
|
|
427
|
+
when ::RBS::AST::Declarations::Class
|
|
428
|
+
inner = prefix + [decl.name.to_s]
|
|
429
|
+
block.call("::#{inner.join('::')}", decl.type_params.size)
|
|
430
|
+
each_declared_class(decl.members, inner, &block)
|
|
431
|
+
end
|
|
297
432
|
end
|
|
298
433
|
end
|
|
299
434
|
|
|
@@ -348,6 +483,55 @@ module Rigor
|
|
|
348
483
|
end
|
|
349
484
|
end
|
|
350
485
|
|
|
486
|
+
# Issue #777 — project `signature_paths:` files that PARSE but are absent from the built env because
|
|
487
|
+
# {.add_project_parsed_decls} / {.resolve_quarantining_virtual_collisions} quarantined a duplicated
|
|
488
|
+
# declaration (typically class-vs-module or a constant already shipped by bundled RBS). Derived from
|
|
489
|
+
# the env like {#virtual_rbs_collision_quarantined}, so a cache HIT reports the same condition.
|
|
490
|
+
#
|
|
491
|
+
# Path membership alone is not enough: the suite's {RbsEnvMemo} reuses a byte-identical env across
|
|
492
|
+
# `Dir.mktmpdir` roots when `signature_paths:` are relative (`sig/sink.rbs`), leaving buffers named
|
|
493
|
+
# after the FIRST path. A later example's absolute path is then absent from `present` even though
|
|
494
|
+
# the declarations loaded — that used to spam `duplicated declaration against bundled RBS` banners
|
|
495
|
+
# (~60 per CI shard) for fixtures that never collided. Basename + content equality catches that
|
|
496
|
+
# reuse without hiding a real quarantine (e.g. project `class Base64` vs bundled `module Base64`,
|
|
497
|
+
# same basename, different bytes).
|
|
498
|
+
COLLISION_QUARANTINE_NOTE =
|
|
499
|
+
"duplicated declaration against bundled RBS — quarantined so the rest of the RBS env still loads"
|
|
500
|
+
|
|
501
|
+
def collision_quarantined_project_signatures(env, signature_paths)
|
|
502
|
+
return [] if env.nil?
|
|
503
|
+
|
|
504
|
+
present = env.buffers.to_set do |buffer|
|
|
505
|
+
name = buffer.name
|
|
506
|
+
next name.to_s unless name
|
|
507
|
+
|
|
508
|
+
File.expand_path(name.to_s)
|
|
509
|
+
rescue ArgumentError, TypeError
|
|
510
|
+
name.to_s
|
|
511
|
+
end
|
|
512
|
+
buffers_by_basename = env.buffers.group_by { |buffer| File.basename(buffer.name.to_s) }
|
|
513
|
+
project_sig_files(signature_paths).sort.filter_map do |file|
|
|
514
|
+
next if present.include?(file)
|
|
515
|
+
next if parse_signature_file(file).nil? # parse / encoding quarantine owns these
|
|
516
|
+
next if project_signature_loaded_under_other_path?(file, buffers_by_basename)
|
|
517
|
+
|
|
518
|
+
[file, "#{file}: #{COLLISION_QUARANTINE_NOTE}"]
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# True when `file`'s bytes are already in `env` under a different absolute path (same basename).
|
|
523
|
+
# See {#collision_quarantined_project_signatures} — keeps memoised env reuse from looking like a
|
|
524
|
+
# collision quarantine.
|
|
525
|
+
def project_signature_loaded_under_other_path?(file, buffers_by_basename)
|
|
526
|
+
peers = buffers_by_basename[File.basename(file)]
|
|
527
|
+
return false if peers.nil? || peers.empty?
|
|
528
|
+
|
|
529
|
+
content = File.read(file, encoding: "UTF-8")
|
|
530
|
+
peers.any? { |buffer| buffer.content == content }
|
|
531
|
+
rescue Errno::ENOENT, Errno::EISDIR, Errno::EACCES
|
|
532
|
+
false
|
|
533
|
+
end
|
|
534
|
+
|
|
351
535
|
# The `::`-stripped names of every type a PROJECT signature references that no loaded declaration
|
|
352
536
|
# provides — the input to {.append_stub_declarations}.
|
|
353
537
|
#
|
|
@@ -758,6 +942,79 @@ module Rigor
|
|
|
758
942
|
end
|
|
759
943
|
end
|
|
760
944
|
|
|
945
|
+
# @param path [String, Pathname] typically an entry from an {RbsLoader} instance's
|
|
946
|
+
# {#signature_paths}.
|
|
947
|
+
# @return [Boolean] whether `path` sits under the bundled gem-overlay root — what
|
|
948
|
+
# `CheckRules::GEM_OVERLAY_OPEN_RECEIVERS`'s gate consults so that a class name's membership in
|
|
949
|
+
# that list alone never grants the open-receiver exemption; the overlay directory that made the
|
|
950
|
+
# entry true must have actually loaded THIS run too (issue #632, tracked further by #660).
|
|
951
|
+
# `GEM_OVERLAY_SIGS_ROOT` is defined inside this `class << self` block, so it is reachable from
|
|
952
|
+
# here directly but NOT as `RbsLoader::GEM_OVERLAY_SIGS_ROOT` from outside — this method is the
|
|
953
|
+
# public seam callers outside this class use instead of reaching for the constant themselves.
|
|
954
|
+
def under_gem_overlay_root?(path)
|
|
955
|
+
path.to_s.start_with?("#{GEM_OVERLAY_SIGS_ROOT}/")
|
|
956
|
+
end
|
|
957
|
+
|
|
958
|
+
# The twin of {.under_gem_overlay_root?}, for the OTHER copy of the same declarations. ADR-72 ships
|
|
959
|
+
# each overlay's surface twice — as `data/gem_overlay/<gem>/` and as the opt-in plugin's own `sig/` —
|
|
960
|
+
# and a project can reach the second WITHOUT the plugin registry, by naming it in `signature_paths:`
|
|
961
|
+
# (issue #672). Two callers ask, for different reasons:
|
|
962
|
+
#
|
|
963
|
+
# - `Environment.gem_overlay_paths` asks per gem, so the overlay stands down instead of loading
|
|
964
|
+
# alongside the twin and collapsing every class they share;
|
|
965
|
+
# - `CheckRules#gem_overlay_loaded?` asks across all of them, because the twin's `sig/` carries no
|
|
966
|
+
# plugin manifest, so `open_receivers:` cannot protect the partial `ActiveSupport::Duration`
|
|
967
|
+
# declaration it nonetheless loads.
|
|
968
|
+
#
|
|
969
|
+
# **Both ask what an entry LOADS, not what it looks like.** Comparing path strings is wrong in both
|
|
970
|
+
# directions, and the false-positive direction is the expensive one: an entry naming the twin's
|
|
971
|
+
# `.rbs` FILE, or a subdirectory of the twin that does not exist, matches as a string while the
|
|
972
|
+
# loader reads nothing from it ({.project_sig_files} takes directories only — the same fact
|
|
973
|
+
# {Rigor::SignaturePathAudit} reports as `:not_directory` / `:missing`). Standing an overlay down for
|
|
974
|
+
# those leaves the project with NEITHER copy, so ordinary `3.minutes` draws `call.undefined-method`
|
|
975
|
+
# on correct code. The quiet direction is the mirror: a symlink to the twin, or a case-variant
|
|
976
|
+
# spelling of it where the filesystem folds case, loads the twin while matching no prefix. Asking
|
|
977
|
+
# {.project_sig_files} for the twin's files and canonicalising both sides answers both.
|
|
978
|
+
#
|
|
979
|
+
# Lives here rather than on {Rigor::Environment} because it is an internal seam, not ADR-2 public
|
|
980
|
+
# API: `spec/rigor/public_api_drift_spec.rb` pins `Environment`'s singleton surface, and a predicate
|
|
981
|
+
# two call sites share is not a promise to plugin authors. Here it sits beside the overlay-side
|
|
982
|
+
# answer to the very same question, on the class that already owns the overlay's layout
|
|
983
|
+
# ({GEM_OVERLAY_SIGS_ROOT}, {.gem_overlay_sig_paths}).
|
|
984
|
+
#
|
|
985
|
+
# @param signature_paths [Array<String, Pathname>] typically an {RbsLoader} instance's
|
|
986
|
+
# {#signature_paths}. Takes the whole list rather than one entry so the twin's file set resolves
|
|
987
|
+
# once per question — `CheckRules` asks it per `ActiveSupport::Duration` receiver.
|
|
988
|
+
def gem_overlay_twin_signatures_loaded?(signature_paths)
|
|
989
|
+
# `GEM_OVERLAY_PLUGIN_IDS` is ADR-72 eligibility policy and stays owned by `Environment`; it is
|
|
990
|
+
# read here only to enumerate which bundled plugins HAVE an overlay twin.
|
|
991
|
+
GEM_OVERLAY_PLUGIN_IDS.each_value.any? do |plugin_id|
|
|
992
|
+
bundled_overlay_twin_signatures_loaded?(plugin_id, signature_paths)
|
|
993
|
+
end
|
|
994
|
+
end
|
|
995
|
+
|
|
996
|
+
# The per-plugin half of {.gem_overlay_twin_signatures_loaded?}. `Environment` owns the gem → plugin
|
|
997
|
+
# id mapping and passes the id; this resolves the id to the engine's own bundled `sig/` and answers
|
|
998
|
+
# the filesystem question.
|
|
999
|
+
#
|
|
1000
|
+
# @param plugin_id [String] a manifest id, e.g. `"activesupport-core-ext"`.
|
|
1001
|
+
def bundled_overlay_twin_signatures_loaded?(plugin_id, signature_paths)
|
|
1002
|
+
return false if signature_paths.nil? || signature_paths.empty?
|
|
1003
|
+
|
|
1004
|
+
# In-method for the reason `Plugin::FirstParty` documents at its own `require`: the plugin
|
|
1005
|
+
# subsystem's load graph reaches back into the environment, and only the overlay path calls this —
|
|
1006
|
+
# a run with no eligible overlay gem never pays them.
|
|
1007
|
+
require_relative "../plugin/first_party"
|
|
1008
|
+
require_relative "../plugin/loader"
|
|
1009
|
+
twin = Plugin::Loader.bundled_plugin_sig_path("#{Plugin::FirstParty::GEM_PREFIX}#{plugin_id}")
|
|
1010
|
+
return false unless twin
|
|
1011
|
+
|
|
1012
|
+
twin_files = project_sig_files([twin]).filter_map { |file| canonical_real_path(file) }
|
|
1013
|
+
return false if twin_files.empty?
|
|
1014
|
+
|
|
1015
|
+
signature_paths.any? { |entry| signature_entry_loads?(entry, twin_files) }
|
|
1016
|
+
end
|
|
1017
|
+
|
|
761
1018
|
def vendored_gem_sig_paths
|
|
762
1019
|
return [] unless File.directory?(VENDORED_GEM_SIGS_ROOT)
|
|
763
1020
|
|
|
@@ -778,6 +1035,34 @@ module Rigor
|
|
|
778
1035
|
File.file?(File.join(VENDORED_GEM_SIGS_ROOT, child))
|
|
779
1036
|
end
|
|
780
1037
|
end
|
|
1038
|
+
|
|
1039
|
+
# Everything below is internal to {.bundled_overlay_twin_signatures_loaded?}. `private` (not
|
|
1040
|
+
# `private_class_method`) because inside `class << self` these are instance methods of the
|
|
1041
|
+
# singleton class; `private_class_method` looks for them one level further out and raises NameError
|
|
1042
|
+
# at load. Placed last so the modifier cannot capture a method that is meant to be reachable.
|
|
1043
|
+
private
|
|
1044
|
+
|
|
1045
|
+
# Whether one `signature_paths:` entry is a directory the loader would read one of `twin_files`
|
|
1046
|
+
# from. Both sides are canonicalised first, which is what makes a symlinked or case-variant
|
|
1047
|
+
# spelling of the twin answer the same as the twin itself.
|
|
1048
|
+
def signature_entry_loads?(entry, twin_files)
|
|
1049
|
+
dir = canonical_real_path(entry)
|
|
1050
|
+
return false unless dir && File.directory?(dir)
|
|
1051
|
+
|
|
1052
|
+
prefix = "#{dir}#{File::SEPARATOR}"
|
|
1053
|
+
twin_files.any? { |file| file.start_with?(prefix) }
|
|
1054
|
+
end
|
|
1055
|
+
|
|
1056
|
+
# `File.realpath` rather than `File.expand_path`: it resolves symlinks and, on a case-insensitive
|
|
1057
|
+
# filesystem, the on-disk spelling — the two ways a path can reach the twin's files without looking
|
|
1058
|
+
# like it. It raises for a path that does not exist, which is the answer we want (an entry that
|
|
1059
|
+
# resolves to nothing loads nothing), so the rescue returns nil rather than falling back to a
|
|
1060
|
+
# lexical expansion that would resurrect the string match.
|
|
1061
|
+
def canonical_real_path(path)
|
|
1062
|
+
File.realpath(path.to_s)
|
|
1063
|
+
rescue SystemCallError
|
|
1064
|
+
nil
|
|
1065
|
+
end
|
|
781
1066
|
end
|
|
782
1067
|
|
|
783
1068
|
attr_reader :libraries, :signature_paths, :cache_store, :virtual_rbs
|
|
@@ -797,15 +1082,24 @@ module Rigor
|
|
|
797
1082
|
# synthesised from project source by a plugin's `Manifest#source_rbs_synthesizer`. Merged into the
|
|
798
1083
|
# env after `signature_paths:` and the vendored stubs. Pass `[]` (the default) when no
|
|
799
1084
|
# synthesizer-emitting plugin is loaded.
|
|
800
|
-
def initialize(libraries: [], signature_paths: [], cache_store: nil, virtual_rbs: []
|
|
1085
|
+
def initialize(libraries: [], signature_paths: [], cache_store: nil, virtual_rbs: [],
|
|
1086
|
+
deferred_signature_paths: [])
|
|
801
1087
|
@libraries = libraries.map(&:to_s).freeze
|
|
802
1088
|
@signature_paths = signature_paths.map { |p| Pathname(p) }.freeze
|
|
1089
|
+
# Issue #610 — the subset of `signature_paths:` a bundled PLUGIN contributed. Loaded last and
|
|
1090
|
+
# allowed to stand down against a colliding generic arity ({.add_deferred_signatures}). Every entry
|
|
1091
|
+
# is also in `@signature_paths`, so the env cache descriptor already digests these files.
|
|
1092
|
+
@deferred_signature_paths = deferred_signature_paths.map { |p| Pathname(p) }.freeze
|
|
803
1093
|
@cache_store = cache_store
|
|
804
1094
|
@virtual_rbs = virtual_rbs.map { |name, content| [name.to_s.dup.freeze, content.to_s.dup.freeze].freeze }.freeze
|
|
805
1095
|
# Per-loader memoization bucket. Held as a single mutable Hash so the loader instance itself can be
|
|
806
1096
|
# `.freeze`d (per ADR-15 reflection-facade contract) without losing the lazy-memo behaviour. Slot
|
|
807
1097
|
# names currently consulted: `:env`, `:env_loaded`, `:env_build_warned`, `:definition_build_warned`,
|
|
808
|
-
# `:
|
|
1098
|
+
# `:definition_build_details`, `:definition_build_reported`, `:definition_build_failures`,
|
|
1099
|
+
# `:definition_build_deferred_count`, `:definition_build_deferred_first`,
|
|
1100
|
+
# `:definition_build_summary_warned`, `:internal_demand`, `:internal_demand_status`, `:builder`,
|
|
1101
|
+
# `:reflection`,
|
|
1102
|
+
# `:instance_definitions_table`, `:singleton_definitions_table`.
|
|
809
1103
|
# Constructed via `Hash.new` (NOT a `{ ... }` literal) so Rigor's `HashShape` narrowing doesn't
|
|
810
1104
|
# infer a fixed key set from the initial state and fold post-initial slot reads (e.g.
|
|
811
1105
|
# `@state[:env_loaded]`) to a constant `nil`.
|
|
@@ -835,16 +1129,75 @@ module Rigor
|
|
|
835
1129
|
#
|
|
836
1130
|
# @return [Array<Array(String, String)>] empty when every `signature_paths:` file parses.
|
|
837
1131
|
def quarantined_signatures
|
|
838
|
-
@state[:quarantined] ||=
|
|
1132
|
+
@state[:quarantined] ||= begin
|
|
1133
|
+
parse_quarantined = self.class.quarantined_project_signatures(@signature_paths)
|
|
1134
|
+
collision_quarantined = self.class.collision_quarantined_project_signatures(
|
|
1135
|
+
@state[:env], @signature_paths
|
|
1136
|
+
)
|
|
1137
|
+
parse_paths = parse_quarantined.to_set { |path, _note| path }
|
|
1138
|
+
(
|
|
1139
|
+
parse_quarantined +
|
|
1140
|
+
collision_quarantined.reject { |entry| parse_paths.include?(entry[0]) }
|
|
1141
|
+
).freeze
|
|
1142
|
+
end
|
|
1143
|
+
end
|
|
1144
|
+
|
|
1145
|
+
# Issue #610 — plugin-contributed signature files that stood down against a colliding generic arity,
|
|
1146
|
+
# as `[absolute_path, class_name, existing_arity, incoming_arity]`. Empty whenever no plugin sig
|
|
1147
|
+
# collides, which is every project that has not run `rbs collection install` for a gem a bundled
|
|
1148
|
+
# plugin also declares.
|
|
1149
|
+
def signature_standdowns
|
|
1150
|
+
@state[:signature_standdowns] ||= self.class.deferred_standdowns(env, @deferred_signature_paths).freeze
|
|
1151
|
+
end
|
|
1152
|
+
|
|
1153
|
+
# Issue #735 — the class / module names whose PRIMARY declaration lives under the project's own
|
|
1154
|
+
# `signature_paths:`, top-level prefix stripped ("::Admin::Widget" reads back as "Admin::Widget").
|
|
1155
|
+
#
|
|
1156
|
+
# The distinction it exists to draw: a BUNDLED declaration (core, stdlib, a gem's RBS) is
|
|
1157
|
+
# authoritative about its class, so a project `def` on that class is a monkey-patch and ADR-17 has
|
|
1158
|
+
# the analyzer report it rather than adopt it. A declaration the project wrote itself — by hand or
|
|
1159
|
+
# through `rigor sig-gen --write` — is a sidecar of the same source tree, and a `def` in another of
|
|
1160
|
+
# the project's own files is not a monkey-patch, just the second file of an ordinary class. Reading
|
|
1161
|
+
# a partial sidecar as authoritative made `sig-gen --write` on redmine report 5.9x more
|
|
1162
|
+
# `call.undefined-method` than the same project with no `sig/` at all.
|
|
1163
|
+
#
|
|
1164
|
+
# Memoised per loader: one pass over `class_decls` (~1-2k entries), and every consumer is a check
|
|
1165
|
+
# rule on the analysis hot path. Attribution is by buffer NAME, which survives the ADR-54 environment
|
|
1166
|
+
# cache since #725 — an environment blob written before that carries the `<cached>` sentinel instead,
|
|
1167
|
+
# which lands every class outside this set and leaves the pre-#735 behaviour, never a new silence.
|
|
1168
|
+
def project_declared_classes
|
|
1169
|
+
@state[:project_declared_classes] ||= build_project_declared_classes
|
|
1170
|
+
end
|
|
1171
|
+
|
|
1172
|
+
def build_project_declared_classes
|
|
1173
|
+
environment = env
|
|
1174
|
+
return Set.new.freeze if environment.nil?
|
|
1175
|
+
|
|
1176
|
+
project_files = self.class.project_sig_files(@signature_paths)
|
|
1177
|
+
return Set.new.freeze if project_files.empty?
|
|
1178
|
+
|
|
1179
|
+
names = environment.class_decls.each_with_object(Set.new) do |(rbs_name, entry), acc|
|
|
1180
|
+
acc << rbs_name.to_s.delete_prefix("::") if self.class.project_entry?(entry, project_files)
|
|
1181
|
+
end
|
|
1182
|
+
names.freeze
|
|
1183
|
+
rescue ::RBS::BaseError
|
|
1184
|
+
Set.new.freeze
|
|
1185
|
+
end
|
|
1186
|
+
private :build_project_declared_classes
|
|
1187
|
+
|
|
1188
|
+
# True when `class_name`'s RBS declaration is one the project wrote (see {#project_declared_classes}).
|
|
1189
|
+
def project_declared_class?(class_name)
|
|
1190
|
+
project_declared_classes.include?(class_name.to_s.delete_prefix("::"))
|
|
839
1191
|
end
|
|
840
1192
|
|
|
841
1193
|
# The total RBS-environment build failure captured this run, or nil when the env built. Unlike
|
|
842
|
-
# {#quarantined_signatures} — which the env survives, one file lighter, and which is re-derived
|
|
843
|
-
#
|
|
844
|
-
#
|
|
845
|
-
#
|
|
846
|
-
#
|
|
847
|
-
# re-
|
|
1194
|
+
# {#quarantined_signatures} — which the env survives, one file lighter, and which is re-derived so a
|
|
1195
|
+
# cache HIT reports it too — a total failure collapses the WHOLE env to nil. Project-vs-bundled
|
|
1196
|
+
# `RBS::DuplicatedDeclarationError`s are quarantined per-file since #777; this slot remains for
|
|
1197
|
+
# unrecoverable build errors (e.g. bundled-vs-bundled collisions, or hosts without a transactional
|
|
1198
|
+
# `sources`/`unload` API). A failed build produces no cached success to hide behind (nothing is
|
|
1199
|
+
# persisted, so every run re-attempts and re-raises), so this is captured directly in {#env}'s rescue
|
|
1200
|
+
# rather than re-derived. Forcing `env` (any query does) populates it.
|
|
848
1201
|
#
|
|
849
1202
|
# @return [Array(String, String, Array<String>), nil] `[error_class_name, first_error_line,
|
|
850
1203
|
# conflicting_buffer_names]`, or nil when the environment built successfully.
|
|
@@ -853,6 +1206,30 @@ module Rigor
|
|
|
853
1206
|
@state[:env_build_failure]
|
|
854
1207
|
end
|
|
855
1208
|
|
|
1209
|
+
# Issue #696 — the PER-CLASS sibling of {#env_build_failure}, one tier quieter in consequence: an
|
|
1210
|
+
# `RBS::DefinitionBuilder` failure ({#build_instance_definition} / {#build_singleton_definition}'s
|
|
1211
|
+
# rescue) leaves the class KNOWN but with no method surface, so every call on it — real methods and
|
|
1212
|
+
# typos alike — reads `Dynamic[top]` and stops being checkable. `class_known?` consults
|
|
1213
|
+
# {#known_class_names_set}, never a definition build, so nothing downstream can tell the difference.
|
|
1214
|
+
#
|
|
1215
|
+
# NOT forced the way {#env_build_failure} forces `env`, and it MUST NOT be: definition builds are lazy
|
|
1216
|
+
# (ADR-54 WD1 — per class, on first demand), so this answers "which classes has THIS loader failed to
|
|
1217
|
+
# build so far". A reader that forces would have to build every known class, which is a different and
|
|
1218
|
+
# far more expensive question than the run asked. The consequence for callers is a timing contract: a
|
|
1219
|
+
# snapshot taken before the per-file loop reads empty. `Runner::PoolCoordinator` reads it after.
|
|
1220
|
+
#
|
|
1221
|
+
# Recorded, not derived. The two conditions this sits beside are re-derivable from the built env
|
|
1222
|
+
# ({#quarantined_signatures} re-parses; {#virtual_rbs_collision_quarantined} inspects buffers), but a
|
|
1223
|
+
# definition-build failure leaves no trace in the env at all — the env is fine; it is the BUILD over it
|
|
1224
|
+
# that raised — so the rescue is the only place that ever knows.
|
|
1225
|
+
#
|
|
1226
|
+
# @return [Array<Array(String, String, String, Array<String>)>] `[class_name, error_class_name,
|
|
1227
|
+
# first_error_line, conflicting_buffer_names]`, one per class, in first-failure order. Empty for a
|
|
1228
|
+
# healthy sig set, which is the common case.
|
|
1229
|
+
def definition_build_failures
|
|
1230
|
+
(@state[:definition_build_failures] || []).dup.freeze
|
|
1231
|
+
end
|
|
1232
|
+
|
|
856
1233
|
# Virtual (inline-synthesized) contributions dropped by the collision quarantine
|
|
857
1234
|
# ({.resolve_quarantining_virtual_collisions}): buffer names absent from the built env even though the
|
|
858
1235
|
# entry's content is non-empty and parses (a parse failure is the synthesizer's own WD6 skip, reported
|
|
@@ -951,8 +1328,25 @@ module Rigor
|
|
|
951
1328
|
# silently the way the v0.0.9 cache `Cache::Descriptor` regression did.
|
|
952
1329
|
end
|
|
953
1330
|
|
|
1331
|
+
# Yields every type-alias declaration loaded into the environment (`type foo = ...`).
|
|
1332
|
+
# Yields `(RBS::TypeName, RBS::Environment::TypeAliasEntry)` pairs.
|
|
1333
|
+
def each_type_alias_decl
|
|
1334
|
+
return enum_for(:each_type_alias_decl) unless block_given?
|
|
1335
|
+
return unless env
|
|
1336
|
+
|
|
1337
|
+
env.type_alias_decls.each do |type_name, decl_entry|
|
|
1338
|
+
yield [type_name, decl_entry]
|
|
1339
|
+
end
|
|
1340
|
+
end
|
|
1341
|
+
|
|
954
1342
|
# ADR-20 slice 2e — iterates over every `%a{...}` annotation attached to a class- or module-level
|
|
955
1343
|
# declaration in the loaded RBS environment, yielding `(annotation_string, source_location)` pairs.
|
|
1344
|
+
#
|
|
1345
|
+
# Declarations come from {.entry_declarations}, NOT from a direct `entry.each_decl`: that accessor is
|
|
1346
|
+
# RBS 4.x-only, so the direct call raised `NoMethodError` on every RBS 3.x host — inside a rescue that
|
|
1347
|
+
# deliberately does not swallow `NoMethodError`. It stayed latent because nothing the `rbs-compat` job
|
|
1348
|
+
# runs reached an annotated declaration until #672 added a spec under `spec/rigor/environment` that
|
|
1349
|
+
# drives a whole `Runner`.
|
|
956
1350
|
# Used by {Rigor::Inference::HktRegistry.scan_rbs_loader} to find `rigor:v1:hkt_register` /
|
|
957
1351
|
# `rigor:v1:hkt_define` directives in user-authored overlays and merge them into the per-`Environment`
|
|
958
1352
|
# HKT registry. Yields nothing when the env failed to build (fail-soft, same shape as
|
|
@@ -962,7 +1356,7 @@ module Rigor
|
|
|
962
1356
|
return if env.nil?
|
|
963
1357
|
|
|
964
1358
|
env.class_decls.each_value do |entry|
|
|
965
|
-
entry.
|
|
1359
|
+
self.class.entry_declarations(entry).each do |decl|
|
|
966
1360
|
next unless decl.respond_to?(:annotations)
|
|
967
1361
|
|
|
968
1362
|
decl.annotations.each { |a| yield a.string, a.location }
|
|
@@ -983,7 +1377,7 @@ module Rigor
|
|
|
983
1377
|
return if env.nil?
|
|
984
1378
|
|
|
985
1379
|
env.class_decls.each do |rbs_name, entry|
|
|
986
|
-
entry.
|
|
1380
|
+
self.class.entry_declarations(entry).each do |decl|
|
|
987
1381
|
next unless decl.respond_to?(:annotations)
|
|
988
1382
|
|
|
989
1383
|
decl.annotations.each { |a| yield rbs_name.to_s, a.string, a.location }
|
|
@@ -1029,11 +1423,22 @@ module Rigor
|
|
|
1029
1423
|
# keeps the per-process short-circuit. ADR-54 WD1 retired the definitions disk blob: given a cached
|
|
1030
1424
|
# env, `Marshal.load`-ing every definition was measurably slower (and allocation-heavier) than
|
|
1031
1425
|
# rebuilding the ones a run actually touches.
|
|
1426
|
+
#
|
|
1427
|
+
# Issue #696 — the report fires HERE, on the result, rather than in `build_*`'s rescue, and it fires on
|
|
1428
|
+
# a memo hit too. `#prewarm`'s cached producers walk every known class through this method, so by the
|
|
1429
|
+
# time a run demands `String` the nil is already memoised and the rescue will never run again; a report
|
|
1430
|
+
# wired to the rescue would say nothing. The guard keeps the cost on the hot path at one `@state` read,
|
|
1431
|
+
# and only on the nil branch — `nil` here is overwhelmingly an unknown class, not a failed build.
|
|
1032
1432
|
def instance_definition(class_name)
|
|
1033
1433
|
key = class_name.to_s
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1434
|
+
definition =
|
|
1435
|
+
if @instance_definition_cache.key?(key)
|
|
1436
|
+
@instance_definition_cache[key]
|
|
1437
|
+
else
|
|
1438
|
+
@instance_definition_cache[key] = build_instance_definition(class_name)
|
|
1439
|
+
end
|
|
1440
|
+
report_definition_build_failure(class_name) if definition.nil? && @state[:definition_build_details]
|
|
1441
|
+
definition
|
|
1037
1442
|
end
|
|
1038
1443
|
|
|
1039
1444
|
# @return [RBS::Definition::Method, nil]
|
|
@@ -1091,7 +1496,14 @@ module Rigor
|
|
|
1091
1496
|
name = name.absolute! unless name.absolute?
|
|
1092
1497
|
return nil unless env.type_alias_decls.key?(name)
|
|
1093
1498
|
|
|
1094
|
-
|
|
1499
|
+
# Memoized per (name, args): the env is immutable for the loader's lifetime, so the expansion is a
|
|
1500
|
+
# pure function of the pair, and #529's translator wiring re-expands the same handful of aliases
|
|
1501
|
+
# (`Prism::node`, `int`, `string`, …) at every call site — the memo recoups most of that wall cost.
|
|
1502
|
+
memo = (@state[:type_alias_expansions] ||= {})
|
|
1503
|
+
key = [name, rbs_alias.args]
|
|
1504
|
+
return memo[key] if memo.key?(key)
|
|
1505
|
+
|
|
1506
|
+
memo[key] = builder.expand_alias2(name, rbs_alias.args)
|
|
1095
1507
|
rescue ::RBS::BaseError, StandardError
|
|
1096
1508
|
nil
|
|
1097
1509
|
end
|
|
@@ -1103,11 +1515,18 @@ module Rigor
|
|
|
1103
1515
|
#
|
|
1104
1516
|
# Built on demand from the env with a per-process memo; the same on-demand discipline as
|
|
1105
1517
|
# {#instance_definition} (ADR-54 WD1).
|
|
1518
|
+
#
|
|
1519
|
+
# The singleton twin of {#instance_definition}, reporting on the same terms (issue #696).
|
|
1106
1520
|
def singleton_definition(class_name)
|
|
1107
1521
|
key = class_name.to_s
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1522
|
+
definition =
|
|
1523
|
+
if @singleton_definition_cache.key?(key)
|
|
1524
|
+
@singleton_definition_cache[key]
|
|
1525
|
+
else
|
|
1526
|
+
@singleton_definition_cache[key] = build_singleton_definition(class_name)
|
|
1527
|
+
end
|
|
1528
|
+
report_definition_build_failure(class_name) if definition.nil? && @state[:definition_build_details]
|
|
1529
|
+
definition
|
|
1111
1530
|
end
|
|
1112
1531
|
|
|
1113
1532
|
# @return [RBS::Definition::Method, nil] the class method on `class_name`. For example,
|
|
@@ -1150,6 +1569,47 @@ module Rigor
|
|
|
1150
1569
|
@hierarchy.class_ordering(lhs, rhs)
|
|
1151
1570
|
end
|
|
1152
1571
|
|
|
1572
|
+
# The ancestor chain of ONE class, as {RbsHierarchy} asks it to order two classes.
|
|
1573
|
+
#
|
|
1574
|
+
# Issue #696 review (second pass) — this used to live in the hierarchy, which branched on
|
|
1575
|
+
# `cache_store`: with a store it built the whole `RbsClassAncestorTable` and read one key, without a
|
|
1576
|
+
# store it demanded that one class. Three configurations, three different answers to "which classes
|
|
1577
|
+
# failed to build" for one project — 504 classes on a cold store, 0 on a warm one, 2 with no store —
|
|
1578
|
+
# and on the DEFAULT `--workers=0` path, where nothing pre-warms the store first, the cold answer was
|
|
1579
|
+
# the one written into the run-result cache and replayed on every warm run after it.
|
|
1580
|
+
#
|
|
1581
|
+
# The fix is {#during_internal_demand} on BOTH sides, not the removal of the branch. Collapsing to a
|
|
1582
|
+
# per-class demand everywhere was tried first and measured 50x slower on the warm path a cached run
|
|
1583
|
+
# actually takes (200 orderings: 0.0035s reading the table, 0.18s building 400 definitions), which is
|
|
1584
|
+
# the wrong trade for a determinism the marking already buys. What has to be identical across cache
|
|
1585
|
+
# states is the diagnostic, and with both sides marked neither contributes to it: the reported set is
|
|
1586
|
+
# the classes whose METHOD SURFACE the analysis demanded, in every configuration. The memo-hit report
|
|
1587
|
+
# in {#instance_definition} is what makes that hold — a class this path built and memoised as nil is
|
|
1588
|
+
# still reported when a real demand for it arrives later.
|
|
1589
|
+
#
|
|
1590
|
+
# The table side goes through {#ancestor_names_table}, the loader's own marked accessor, rather than
|
|
1591
|
+
# `Cache::RbsClassAncestorTable.fetch` directly. That direct fetch was the bypass: it walked every
|
|
1592
|
+
# known class through the public {#instance_definition} with nothing marking it.
|
|
1593
|
+
#
|
|
1594
|
+
# Answers are identical on both sides — the table's producer computes exactly this, keyed the same
|
|
1595
|
+
# way, and both yield `[]` for an unknown or unbuildable class. Pinned by spec across all three cache
|
|
1596
|
+
# states.
|
|
1597
|
+
#
|
|
1598
|
+
# @return [Array<String>] `::`-stripped ancestor names, or `[]` for an unknown or unbuildable class.
|
|
1599
|
+
def ancestor_names_for(class_name)
|
|
1600
|
+
key = class_name.to_s.delete_prefix("::")
|
|
1601
|
+
during_internal_demand do
|
|
1602
|
+
next ancestor_names_table.fetch(key, [].freeze) if cache_store
|
|
1603
|
+
|
|
1604
|
+
definition = instance_definition(key)
|
|
1605
|
+
next [].freeze if definition.nil?
|
|
1606
|
+
|
|
1607
|
+
definition.ancestors.ancestors.map { |ancestor| ancestor.name.to_s.delete_prefix("::") }.uniq.freeze
|
|
1608
|
+
end
|
|
1609
|
+
rescue ::RBS::BaseError, StandardError
|
|
1610
|
+
[].freeze
|
|
1611
|
+
end
|
|
1612
|
+
|
|
1153
1613
|
# @return [Array<String>] every RBS-declared constant name (top-level prefixed, e.g., `"::Math::PI"`)
|
|
1154
1614
|
# currently loaded into the environment. Used by the cache producer that materialises the
|
|
1155
1615
|
# constant-type table; ordinary callers should keep using {#constant_type} for point lookups.
|
|
@@ -1210,16 +1670,23 @@ module Rigor
|
|
|
1210
1670
|
# No-op when `cache_store` is nil — without a Store the worker has no choice but to build env via the
|
|
1211
1671
|
# loader, so the caller MUST ensure pool mode runs with caching enabled. Returns `self` so the call
|
|
1212
1672
|
# chains cleanly from the `Runner` pre-spawn hook.
|
|
1673
|
+
#
|
|
1674
|
+
# Issue #696 — the whole body is a {#during_internal_demand}, not only the producers that happen to walk
|
|
1675
|
+
# definitions today. On a COLD store `RbsClassTypeParamNames.compute` and `RbsClassAncestorTable.compute`
|
|
1676
|
+
# ask {#instance_definition} for every known class; marking each producer covers that, and marking
|
|
1677
|
+
# `#prewarm` itself covers whichever producer grows the same appetite next.
|
|
1213
1678
|
def prewarm
|
|
1214
1679
|
return self if cache_store.nil?
|
|
1215
1680
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1681
|
+
during_internal_demand do
|
|
1682
|
+
env
|
|
1683
|
+
known_class_names_set
|
|
1684
|
+
constant_type_table
|
|
1685
|
+
type_param_names_table
|
|
1686
|
+
ancestor_names_table
|
|
1687
|
+
instance_definitions_table
|
|
1688
|
+
singleton_definitions_table
|
|
1689
|
+
end
|
|
1223
1690
|
self
|
|
1224
1691
|
end
|
|
1225
1692
|
|
|
@@ -1318,21 +1785,21 @@ module Rigor
|
|
|
1318
1785
|
end
|
|
1319
1786
|
|
|
1320
1787
|
def constant_type_table
|
|
1321
|
-
@constant_type_table ||=
|
|
1788
|
+
@constant_type_table ||= during_internal_demand do
|
|
1322
1789
|
require_relative "../cache/rbs_constant_table"
|
|
1323
1790
|
fetch_or_compute_producer(Cache::RbsConstantTable)
|
|
1324
1791
|
end
|
|
1325
1792
|
end
|
|
1326
1793
|
|
|
1327
1794
|
def known_class_names_set
|
|
1328
|
-
@known_class_names_set ||=
|
|
1795
|
+
@known_class_names_set ||= during_internal_demand do
|
|
1329
1796
|
require_relative "../cache/rbs_known_class_names"
|
|
1330
1797
|
fetch_or_compute_producer(Cache::RbsKnownClassNames)
|
|
1331
1798
|
end
|
|
1332
1799
|
end
|
|
1333
1800
|
|
|
1334
1801
|
def type_param_names_table
|
|
1335
|
-
@type_param_names_table ||=
|
|
1802
|
+
@type_param_names_table ||= during_internal_demand do
|
|
1336
1803
|
require_relative "../cache/rbs_class_type_param_names"
|
|
1337
1804
|
fetch_or_compute_producer(Cache::RbsClassTypeParamNames)
|
|
1338
1805
|
end
|
|
@@ -1352,7 +1819,7 @@ module Rigor
|
|
|
1352
1819
|
# the existing `RbsClassAncestorTable` producer when `cache_store` is set; falls back to the
|
|
1353
1820
|
# producer's `compute` otherwise. Used by {#reflection}.
|
|
1354
1821
|
def ancestor_names_table
|
|
1355
|
-
@ancestor_names_table ||=
|
|
1822
|
+
@ancestor_names_table ||= during_internal_demand do
|
|
1356
1823
|
require_relative "../cache/rbs_class_ancestor_table"
|
|
1357
1824
|
fetch_or_compute_producer(Cache::RbsClassAncestorTable)
|
|
1358
1825
|
end
|
|
@@ -1378,11 +1845,10 @@ module Rigor
|
|
|
1378
1845
|
end
|
|
1379
1846
|
|
|
1380
1847
|
# The RBS environment for this loader. Memoised both on success AND on failure: when the env build
|
|
1381
|
-
# raises
|
|
1382
|
-
#
|
|
1383
|
-
#
|
|
1384
|
-
#
|
|
1385
|
-
# `warn_about_env_build_failure_once` so the broken `signature_paths:` entry is identifiable.
|
|
1848
|
+
# raises an *unrecoverable* `RBS::BaseError` (project-vs-bundled duplicates are quarantined per-file
|
|
1849
|
+
# since #777), retrying on every subsequent `env` call would re-parse and re-resolve the whole sig set
|
|
1850
|
+
# per AST node touched during analysis, multiplying per-file analysis cost by ~100x. Failures
|
|
1851
|
+
# short-circuit to `nil` here and are surfaced to the user via `warn_about_env_build_failure_once`.
|
|
1386
1852
|
def env
|
|
1387
1853
|
return @state[:env] if @state[:env_loaded]
|
|
1388
1854
|
|
|
@@ -1420,10 +1886,11 @@ module Rigor
|
|
|
1420
1886
|
lines = listed.map { |_path, first_line| " - #{first_line}" }
|
|
1421
1887
|
lines << " … and #{more} more" if more.positive?
|
|
1422
1888
|
warn(
|
|
1423
|
-
"rigor: skipped #{quarantined.size}
|
|
1424
|
-
"They were QUARANTINED so the rest of your RBS env
|
|
1425
|
-
"declare are absent — calls into them read
|
|
1426
|
-
"are reduced. Fix the parse error(s)
|
|
1889
|
+
"rigor: skipped #{quarantined.size} RBS file(s) under `signature_paths:` (unparseable or\n " \
|
|
1890
|
+
"duplicated against bundled RBS). They were QUARANTINED so the rest of your RBS env\n " \
|
|
1891
|
+
"still loads, but the types they declare are absent — calls into them read\n " \
|
|
1892
|
+
"`Dynamic[top]`, so coverage and diagnostics are reduced. Fix the parse error(s) or\n " \
|
|
1893
|
+
"remove the conflicting declaration(s) to restore that coverage:\n" \
|
|
1427
1894
|
"#{lines.join("\n")}"
|
|
1428
1895
|
)
|
|
1429
1896
|
end
|
|
@@ -1509,15 +1976,20 @@ module Rigor
|
|
|
1509
1976
|
# all definitions from a cached env is faster), so the eager-table cost is now a compute, not a load.
|
|
1510
1977
|
# Keys stay in `RBS::TypeName#to_s` form (top-level prefixed `"::Hash"`) — the shape
|
|
1511
1978
|
# {Environment::Reflection} documents.
|
|
1979
|
+
#
|
|
1980
|
+
# {#during_internal_demand} (issue #696) — these two walk EVERY known class, so a failure they hit is a
|
|
1981
|
+
# failure of the sig set, not of anything the run asked about, and it must not reach
|
|
1982
|
+
# {#definition_build_failures}. The stderr fallback stays armed, but {#during_internal_demand} folds
|
|
1983
|
+
# every failure found by one outermost walk into one bounded summary (issue #718).
|
|
1512
1984
|
def instance_definitions_table
|
|
1513
|
-
@state[:instance_definitions_table] ||=
|
|
1514
|
-
build_instance_definition(name)
|
|
1985
|
+
@state[:instance_definitions_table] ||= during_internal_demand do
|
|
1986
|
+
build_definitions_table { |name| build_instance_definition(name) }
|
|
1515
1987
|
end
|
|
1516
1988
|
end
|
|
1517
1989
|
|
|
1518
1990
|
def singleton_definitions_table
|
|
1519
|
-
@state[:singleton_definitions_table] ||=
|
|
1520
|
-
build_singleton_definition(name)
|
|
1991
|
+
@state[:singleton_definitions_table] ||= during_internal_demand do
|
|
1992
|
+
build_definitions_table { |name| build_singleton_definition(name) }
|
|
1521
1993
|
end
|
|
1522
1994
|
end
|
|
1523
1995
|
|
|
@@ -1538,7 +2010,8 @@ module Rigor
|
|
|
1538
2010
|
self.class.build_env_for(
|
|
1539
2011
|
libraries: @libraries,
|
|
1540
2012
|
signature_paths: @signature_paths,
|
|
1541
|
-
virtual_rbs: @virtual_rbs
|
|
2013
|
+
virtual_rbs: @virtual_rbs,
|
|
2014
|
+
deferred_signature_paths: @deferred_signature_paths
|
|
1542
2015
|
)
|
|
1543
2016
|
end
|
|
1544
2017
|
|
|
@@ -1552,6 +2025,7 @@ module Rigor
|
|
|
1552
2025
|
|
|
1553
2026
|
builder.build_instance(rbs_name)
|
|
1554
2027
|
rescue ::RBS::BaseError => e
|
|
2028
|
+
store_definition_build_detail(class_name, e)
|
|
1555
2029
|
warn_about_definition_build_failure(class_name, e)
|
|
1556
2030
|
nil
|
|
1557
2031
|
end
|
|
@@ -1566,10 +2040,115 @@ module Rigor
|
|
|
1566
2040
|
|
|
1567
2041
|
builder.build_singleton(rbs_name)
|
|
1568
2042
|
rescue ::RBS::BaseError => e
|
|
2043
|
+
store_definition_build_detail(class_name, e)
|
|
1569
2044
|
warn_about_definition_build_failure(class_name, e)
|
|
1570
2045
|
nil
|
|
1571
2046
|
end
|
|
1572
2047
|
|
|
2048
|
+
# Issue #696 — the DETAIL half of the same rescue: what went wrong for this class, remembered so
|
|
2049
|
+
# {#definition_build_failures} can report it. Split from the REPORTING decision on purpose, and the
|
|
2050
|
+
# split is the whole fix.
|
|
2051
|
+
#
|
|
2052
|
+
# Recording in the rescue is wrong in both directions at once. Too loud: `#prewarm` and the cached
|
|
2053
|
+
# table producers walk EVERY known class, so on a cold store they enter this rescue for every class a
|
|
2054
|
+
# collision took down — 1,336 of them for one `class Object` duplicate — and the diagnostic named all
|
|
2055
|
+
# of them under `--workers=N` with a cold cache while naming 3 under every other configuration. Too
|
|
2056
|
+
# quiet: {#instance_definition} memoises the nil, so once that walk has run, the FIRST real demand
|
|
2057
|
+
# never re-enters the rescue and would record nothing at all.
|
|
2058
|
+
#
|
|
2059
|
+
# So the rescue only remembers, keyed by the `::`-stripped name (the eager walk spells it `::Acme`,
|
|
2060
|
+
# a dispatch site spells it `Acme`, and they are one class — the {#synthesized_namespaces}
|
|
2061
|
+
# convention), and {#report_definition_build_failure} decides. First writer wins: the instance and
|
|
2062
|
+
# singleton sides fail for the same underlying collision, and the first is the one a caller hit.
|
|
2063
|
+
def store_definition_build_detail(class_name, error)
|
|
2064
|
+
key = class_name.to_s.delete_prefix("::")
|
|
2065
|
+
details = (@state[:definition_build_details] ||= {})
|
|
2066
|
+
return if details.key?(key)
|
|
2067
|
+
|
|
2068
|
+
details[key] = [key, error.class.name.to_s, definition_build_member(error),
|
|
2069
|
+
definition_build_conflict_buffers(error)].freeze
|
|
2070
|
+
end
|
|
2071
|
+
|
|
2072
|
+
# Issue #696 — the REPORTING half: promote a remembered detail into {#definition_build_failures}
|
|
2073
|
+
# because something the run was actually doing asked for this class's definition and got nothing.
|
|
2074
|
+
#
|
|
2075
|
+
# Called from {#instance_definition} / {#singleton_definition} — the demand entries — rather than from
|
|
2076
|
+
# the rescue, so it fires on a MEMO HIT too. That is what survives a pre-warm: the walk builds the
|
|
2077
|
+
# definition, fails, memoises nil, and never enters the rescue again, but the next real demand still
|
|
2078
|
+
# reads nil here and still reports. Reporting from the rescue could only ever see the first build.
|
|
2079
|
+
#
|
|
2080
|
+
# Silent inside a whole-universe walk ({#during_internal_demand}): a producer that touches every known
|
|
2081
|
+
# class is asking a question the RUN did not ask, and letting it contribute makes the reported class
|
|
2082
|
+
# list depend on whether a cache was cold — the same project saying different things under
|
|
2083
|
+
# `--workers=N` than under `--workers=0`, which is the defect this diagnostic exists to end.
|
|
2084
|
+
def report_definition_build_failure(class_name)
|
|
2085
|
+
return if @state[:internal_demand]
|
|
2086
|
+
|
|
2087
|
+
details = @state[:definition_build_details]
|
|
2088
|
+
return if details.nil?
|
|
2089
|
+
|
|
2090
|
+
key = class_name.to_s.delete_prefix("::")
|
|
2091
|
+
detail = details[key]
|
|
2092
|
+
return if detail.nil?
|
|
2093
|
+
|
|
2094
|
+
reported = (@state[:definition_build_reported] ||= {})
|
|
2095
|
+
return if reported[key]
|
|
2096
|
+
|
|
2097
|
+
reported[key] = true
|
|
2098
|
+
(@state[:definition_build_failures] ||= []) << detail
|
|
2099
|
+
end
|
|
2100
|
+
|
|
2101
|
+
# Marks a definition demand that is RIGOR'S OWN, not the analysed program's, so
|
|
2102
|
+
# {#report_definition_build_failure} stays silent for the duration.
|
|
2103
|
+
#
|
|
2104
|
+
# The diagnostic reports classes whose METHOD SURFACE the analysis asked to resolve — that is what its
|
|
2105
|
+
# message promises, and it is the only demand set that does not vary with how the run was invoked. Two
|
|
2106
|
+
# kinds of demand are Rigor's own and must be excluded:
|
|
2107
|
+
#
|
|
2108
|
+
# - a whole-universe cache producer ({#prewarm}'s tables), which asks about every known class;
|
|
2109
|
+
# - the hierarchy oracle's ancestry lookup ({#ancestor_names_for}), which asks whether two classes are
|
|
2110
|
+
# ordered, not whether either one's methods resolve.
|
|
2111
|
+
#
|
|
2112
|
+
# Both were cache-state-dependent before this, and each produced a DIFFERENT class list per
|
|
2113
|
+
# configuration for the same project — the second one on the DEFAULT `--workers=0` path (issue #696
|
|
2114
|
+
# review, second pass).
|
|
2115
|
+
#
|
|
2116
|
+
# Save-and-restore rather than a bare flag: `#prewarm` wraps a body whose members wrap themselves, and
|
|
2117
|
+
# a nested demand must not un-mark its caller on the way out. A successful outermost exit flushes the
|
|
2118
|
+
# deferred stderr summary; an outermost exit that raises keeps the detailed fallback banner armed even
|
|
2119
|
+
# when an earlier internal-demand episode already emitted the loader-instance summary.
|
|
2120
|
+
#
|
|
2121
|
+
# Per LOADER, not per thread. Nesting and a raise mid-demand are both handled, and the fork pool forks
|
|
2122
|
+
# after `#prewarm` returns, so no CLI path shares a loader across concurrent analyses. An in-process
|
|
2123
|
+
# host that did (`language_server/debouncer.rb` runs analysis on a `Thread`) could have one analysis
|
|
2124
|
+
# silence another's reporting; not reachable today, and not worth a thread-local until it is.
|
|
2125
|
+
def during_internal_demand
|
|
2126
|
+
previous = @state[:internal_demand]
|
|
2127
|
+
outermost = !previous
|
|
2128
|
+
if outermost
|
|
2129
|
+
@state[:definition_build_deferred_count] = 0
|
|
2130
|
+
@state[:definition_build_deferred_first] = nil
|
|
2131
|
+
end
|
|
2132
|
+
@state[:internal_demand] = true
|
|
2133
|
+
# `rescue`/`else` keeps the normal block result precise for the analyzer; the pending marker covers
|
|
2134
|
+
# non-local exits (e.g. `throw`) that unwind through `ensure` without entering either branch.
|
|
2135
|
+
@state[:internal_demand_status] = :pending if outermost
|
|
2136
|
+
begin
|
|
2137
|
+
result = yield
|
|
2138
|
+
@state[:internal_demand_status] = :completed if outermost
|
|
2139
|
+
rescue StandardError, ScriptError => e
|
|
2140
|
+
@state[:internal_demand_status] = :aborted if outermost
|
|
2141
|
+
warn_about_aborted_definition_build_failures if outermost
|
|
2142
|
+
raise e
|
|
2143
|
+
else
|
|
2144
|
+
warn_about_deferred_definition_build_failures if outermost
|
|
2145
|
+
result
|
|
2146
|
+
ensure
|
|
2147
|
+
@state[:internal_demand] = previous
|
|
2148
|
+
warn_about_aborted_definition_build_failures if outermost && @state[:internal_demand_status] == :pending
|
|
2149
|
+
end
|
|
2150
|
+
end
|
|
2151
|
+
|
|
1573
2152
|
# The third twin of {#warn_about_quarantined_signatures} / {#warn_about_virtual_rbs_collisions}: name,
|
|
1574
2153
|
# once per class per PROCESS, a `RBS::DefinitionBuilder` failure caught in
|
|
1575
2154
|
# {#build_instance_definition} / {#build_singleton_definition}'s rescue. Without it, `class_known?`
|
|
@@ -1580,21 +2159,33 @@ module Rigor
|
|
|
1580
2159
|
# {#env}, because the whole env is built eagerly and every quarantine/collision is already known by
|
|
1581
2160
|
# then. Definition builds are LAZY (ADR-54 WD1 — built on demand per class the FIRST time a caller asks,
|
|
1582
2161
|
# long after {#env} has already run), so there is no later central checkpoint to fire from before the
|
|
1583
|
-
# affected classes even exist.
|
|
1584
|
-
#
|
|
1585
|
-
#
|
|
1586
|
-
#
|
|
1587
|
-
#
|
|
1588
|
-
#
|
|
1589
|
-
# `@state
|
|
1590
|
-
#
|
|
1591
|
-
#
|
|
2162
|
+
# affected classes even exist. An ordinary demand therefore warns inline at the rescue site; Rigor's
|
|
2163
|
+
# own internal demand defers until its outermost boundary and collapses the whole walk to one summary
|
|
2164
|
+
# (issue #718). Both routes share `@state[:definition_build_warned]` (keyed by normalized class name),
|
|
2165
|
+
# so the instance and singleton sides — and any re-entry once the per-loader-instance definition caches memoize
|
|
2166
|
+
# the failure — count or warn at most once per class name, cache-hit runs included.
|
|
2167
|
+
#
|
|
2168
|
+
# `@state` is per LOADER INSTANCE, not process-global. A fork-pool prewarm finishes before the fork, so
|
|
2169
|
+
# that loader instance emits its summary once in the parent and its dedupe state is inherited by the
|
|
2170
|
+
# workers. A separate loader instance (for example, a parameter-inference pre-pass) has its own summary
|
|
2171
|
+
# budget. A failure first reached by an ordinary demand can still warn once in each worker that reaches
|
|
2172
|
+
# it; deduplicating that across processes is out of scope here — see [#295](https://github.com/rigortype/rigor/issues/295).
|
|
1592
2173
|
def warn_about_definition_build_failure(class_name, error)
|
|
1593
2174
|
warned = (@state[:definition_build_warned] ||= {})
|
|
1594
|
-
key = class_name.to_s
|
|
2175
|
+
key = class_name.to_s.delete_prefix("::")
|
|
1595
2176
|
return if warned[key]
|
|
1596
2177
|
|
|
1597
2178
|
warned[key] = true
|
|
2179
|
+
if @state[:internal_demand]
|
|
2180
|
+
@state[:definition_build_deferred_count] = @state.fetch(:definition_build_deferred_count, 0) + 1
|
|
2181
|
+
@state[:definition_build_deferred_first] ||= [class_name.to_s, error]
|
|
2182
|
+
return
|
|
2183
|
+
end
|
|
2184
|
+
|
|
2185
|
+
warn(definition_build_failure_warning(class_name, error))
|
|
2186
|
+
end
|
|
2187
|
+
|
|
2188
|
+
def definition_build_failure_warning(class_name, error)
|
|
1598
2189
|
first_line = error.message.to_s.lines.first.to_s.strip
|
|
1599
2190
|
buffers = definition_build_conflict_buffers(error)
|
|
1600
2191
|
collisions =
|
|
@@ -1607,13 +2198,42 @@ module Rigor
|
|
|
1607
2198
|
lines << " … and #{more} more" if more.positive?
|
|
1608
2199
|
"\n Colliding declaration(s):\n#{lines.join("\n")}"
|
|
1609
2200
|
end
|
|
1610
|
-
|
|
1611
|
-
"rigor: RBS definition build failed for `#{class_name}`: #{error.class}: #{first_line}\n " \
|
|
2201
|
+
"rigor: RBS definition build failed for `#{class_name}`: #{error.class}: #{first_line}\n " \
|
|
1612
2202
|
"Rigor still treats the class as known, so calls into it now silently degrade to\n " \
|
|
1613
2203
|
"`Dynamic[top]` — real methods and typos alike — instead of resolving normally.#{collisions}"
|
|
2204
|
+
end
|
|
2205
|
+
|
|
2206
|
+
def warn_about_deferred_definition_build_failures
|
|
2207
|
+
return if @state[:definition_build_summary_warned]
|
|
2208
|
+
|
|
2209
|
+
count = @state[:definition_build_deferred_count]
|
|
2210
|
+
return if count.nil? || count.zero?
|
|
2211
|
+
|
|
2212
|
+
@state[:definition_build_summary_warned] = true
|
|
2213
|
+
first_class, first_error = @state[:definition_build_deferred_first]
|
|
2214
|
+
affected = if count == 1
|
|
2215
|
+
"1 class affected"
|
|
2216
|
+
else
|
|
2217
|
+
more = count - 1
|
|
2218
|
+
noun = more == 1 ? "class" : "classes"
|
|
2219
|
+
"… and #{more} more #{noun} affected (#{count} total)"
|
|
2220
|
+
end
|
|
2221
|
+
warn(
|
|
2222
|
+
"#{definition_build_failure_warning(first_class, first_error)}\n " \
|
|
2223
|
+
"Internal whole-universe demand found #{affected}; the " \
|
|
2224
|
+
"`rbs.coverage.definition-build-failed` diagnostic reports the classes the analysis demanded and " \
|
|
2225
|
+
"the culprit member and signature files."
|
|
1614
2226
|
)
|
|
1615
2227
|
end
|
|
1616
2228
|
|
|
2229
|
+
def warn_about_aborted_definition_build_failures
|
|
2230
|
+
count = @state[:definition_build_deferred_count]
|
|
2231
|
+
return if count.nil? || count.zero?
|
|
2232
|
+
|
|
2233
|
+
first_class, first_error = @state[:definition_build_deferred_first]
|
|
2234
|
+
warn(definition_build_failure_warning(first_class, first_error))
|
|
2235
|
+
end
|
|
2236
|
+
|
|
1617
2237
|
# The definition-build twin of {#env_build_conflict_buffers}: the declaration source file(s) named by a
|
|
1618
2238
|
# `RBS::DefinitionBuilder` failure, so {#warn_about_definition_build_failure} can name the colliding
|
|
1619
2239
|
# declarations rather than only the exception's class and message. The payload shape varies by error
|
|
@@ -1624,22 +2244,53 @@ module Rigor
|
|
|
1624
2244
|
# `#location`. Falls back to an empty list — the warning still names the class and the exception's own
|
|
1625
2245
|
# message — for the remainder (e.g. `SuperclassMismatchError`, which carries neither).
|
|
1626
2246
|
def definition_build_conflict_buffers(error)
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
elsif error.respond_to?(:member) && error.member
|
|
1631
|
-
[error.member.location]
|
|
1632
|
-
elsif error.respond_to?(:location)
|
|
1633
|
-
[error.location]
|
|
1634
|
-
else
|
|
1635
|
-
[]
|
|
1636
|
-
end
|
|
1637
|
-
|
|
1638
|
-
locations.compact.filter_map { |loc| loc.buffer&.name }.map(&:to_s).uniq.freeze
|
|
2247
|
+
definition_build_error_locations(error)
|
|
2248
|
+
.compact.filter_map { |loc| loc.buffer&.name }.map(&:to_s)
|
|
2249
|
+
.reject { |name| name == CACHED_LOCATION_BUFFER_NAME }.uniq.freeze
|
|
1639
2250
|
rescue ::RBS::BaseError, StandardError
|
|
1640
2251
|
[].freeze
|
|
1641
2252
|
end
|
|
1642
2253
|
|
|
2254
|
+
# The `RBS::Location`s an error carries, by whichever accessor its class happens to expose (see the
|
|
2255
|
+
# shape survey above). Split out so the buffer-name extraction reads as one pipeline.
|
|
2256
|
+
def definition_build_error_locations(error)
|
|
2257
|
+
return Array(error.members).map(&:location) if error.respond_to?(:members) && error.members
|
|
2258
|
+
return [error.member.location] if error.respond_to?(:member) && error.member
|
|
2259
|
+
return [error.location] if error.respond_to?(:location)
|
|
2260
|
+
|
|
2261
|
+
[]
|
|
2262
|
+
end
|
|
2263
|
+
|
|
2264
|
+
# The member the failure is ABOUT, taken off the error object rather than parsed out of its message.
|
|
2265
|
+
#
|
|
2266
|
+
# Issue #696 — the message cannot serve here. It is built from `RBS::Location`s, and the ADR-54 env
|
|
2267
|
+
# cache drops their POSITIONS, so a warm run's message would name no line and the same project would
|
|
2268
|
+
# report different text cold and warm. These accessors are `RBS::TypeName`s and Symbols, so they
|
|
2269
|
+
# survive the marshal round trip unchanged — and they name the class that actually CARRIES the
|
|
2270
|
+
# duplicate, which the failed-class list does not: a collision on `::Object#blank?` fails `String`,
|
|
2271
|
+
# `Integer` and `Array`, none of which is where the fix goes. (The buffer NAME does survive, since the
|
|
2272
|
+
# second review pass; it is the file list that uses it, not this.)
|
|
2273
|
+
#
|
|
2274
|
+
# Shapes vary by error class (`references/rbs`'s `lib/rbs/errors.rb`): the two duplicated-definition
|
|
2275
|
+
# errors expose `#qualified_method_name` directly; `InvalidOverloadMethodError` has `#type_name` +
|
|
2276
|
+
# `#method_name`; `UnknownMethodAliasError` and `NoSuperclassFoundError` have `#type_name`;
|
|
2277
|
+
# `SuperclassMismatchError` has `#name`. `RecursiveAncestorError` has none of them and yields nil, and
|
|
2278
|
+
# the diagnostic then omits the clause rather than inventing one.
|
|
2279
|
+
#
|
|
2280
|
+
# @return [String, nil]
|
|
2281
|
+
def definition_build_member(error)
|
|
2282
|
+
return error.qualified_method_name.to_s if error.respond_to?(:qualified_method_name)
|
|
2283
|
+
|
|
2284
|
+
type_name = error.respond_to?(:type_name) ? error.type_name : nil
|
|
2285
|
+
type_name ||= error.respond_to?(:name) ? error.name : nil
|
|
2286
|
+
return nil if type_name.nil?
|
|
2287
|
+
|
|
2288
|
+
method_name = error.respond_to?(:method_name) ? error.method_name : nil
|
|
2289
|
+
method_name.nil? ? type_name.to_s : "#{type_name}##{method_name}"
|
|
2290
|
+
rescue ::RBS::BaseError, StandardError
|
|
2291
|
+
nil
|
|
2292
|
+
end
|
|
2293
|
+
|
|
1643
2294
|
# Resolve an RBS class/module ALIAS to its canonical declared name. `class Mutex = Thread::Mutex`
|
|
1644
2295
|
# lives only in `class_alias_decls`, so `class_known?` reports it (it checks that table) but the
|
|
1645
2296
|
# definition builder — which only knows `class_decls` — could not enumerate its methods, leaving alias
|
|
@@ -1688,6 +2339,5 @@ module Rigor
|
|
|
1688
2339
|
false
|
|
1689
2340
|
end
|
|
1690
2341
|
end
|
|
1691
|
-
# rubocop:enable Metrics/ClassLength
|
|
1692
2342
|
end
|
|
1693
2343
|
end
|