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/scope.rb
CHANGED
|
@@ -16,16 +16,16 @@ module Rigor
|
|
|
16
16
|
# #type_of(node), the Rigor counterpart of PHPStan's $scope->getType($node).
|
|
17
17
|
#
|
|
18
18
|
# See docs/internal-spec/inference-engine.md for the binding contract.
|
|
19
|
-
# rubocop:disable Metrics/ClassLength,Metrics/ParameterLists
|
|
19
|
+
# rubocop:disable-next Metrics/ClassLength,Metrics/ParameterLists
|
|
20
20
|
class Scope
|
|
21
21
|
attr_reader :environment, :locals, :fact_store, :self_type,
|
|
22
22
|
:ivars, :cvars, :globals,
|
|
23
23
|
:indexed_narrowings, :method_chain_narrowings,
|
|
24
24
|
:declaration_sourced,
|
|
25
25
|
:source_path, :discovery, :struct_fold_safe_locals,
|
|
26
|
-
:opaque_block_self,
|
|
26
|
+
:opaque_block_self, :lexical_nesting,
|
|
27
27
|
:dynamic_origins, :local_origins, :ivar_origins,
|
|
28
|
-
:void_origins,
|
|
28
|
+
:void_origins, :plugin_typed_calls,
|
|
29
29
|
:optimistic_origins, :optimistic_locals, :optimistic_ivars
|
|
30
30
|
|
|
31
31
|
# ADR-53 Track A — the seed-time discovery tables live on the {DiscoveryIndex} the scope carries by a single
|
|
@@ -50,6 +50,50 @@ module Rigor
|
|
|
50
50
|
def discovered_superclasses = @discovery.discovered_superclasses
|
|
51
51
|
def discovered_includes = @discovery.discovered_includes
|
|
52
52
|
def discovered_class_sources = @discovery.discovered_class_sources
|
|
53
|
+
# Issue #644 — `{qualified constant name => Set[declaring file]}`; seeded only on an ADR-46 recording run.
|
|
54
|
+
def constant_sources = @discovery.constant_sources
|
|
55
|
+
def published_constant_names = @discovery.published_constant_names
|
|
56
|
+
def local_constant_names = @discovery.local_constant_names
|
|
57
|
+
|
|
58
|
+
# Issue #644 — true when the constant reference `name` (as written: `MODE` or `AppConfig::MODE`) is
|
|
59
|
+
# published by the cross-file value-constant table and is NOT assigned by the file being analysed: a
|
|
60
|
+
# value this file's author cannot see the declaration of. {Analysis::CheckRules::PublishedConstantGuard}
|
|
61
|
+
# is the only caller — a rule MUST ask through the guard rather than re-derive the question here, exactly
|
|
62
|
+
# as ADR-58's mark is asked through `DeclarationSourcedGuard`.
|
|
63
|
+
#
|
|
64
|
+
# The two halves match at deliberately different granularities, because the two errors are not
|
|
65
|
+
# symmetric. The PUBLISHED half matches on the last segment and so over-answers (a reader of a nested
|
|
66
|
+
# `MyApp::MODE` answers true for a top-level `MODE`) — over-answering only ever WITHHOLDS a firing, the
|
|
67
|
+
# safe direction, and it is the grammar the ADR-46 negative keys already use. The LOCAL half is an
|
|
68
|
+
# exemption, so over-answering there would UN-withhold: matching it on the last segment too would let an
|
|
69
|
+
# unrelated `Local::MODE` in this file make a read of `AppConfig::MODE` fire.
|
|
70
|
+
#
|
|
71
|
+
# The exemption therefore matches a **suffix relation on qualified names**, not the spelling and not the
|
|
72
|
+
# last segment: a local assignment exempts a reference when it IS that reference or ENDS with `::` plus
|
|
73
|
+
# it. That is what a lexically relative spelling resolves to — inside `module AppConfig`, the reference
|
|
74
|
+
# `Nested::X` is the file's own `AppConfig::Nested::X` — and it subsumes the bare case (`OWNED` matched
|
|
75
|
+
# by `Owner::OWNED`) without matching the unrelated `Local::MODE` above. It is deliberately a relation
|
|
76
|
+
# over already-qualified names rather than a second lexical ladder: the engine's ladder
|
|
77
|
+
# (`Reflection.resolve_constant_type`) answers with a TYPE and not with the candidate that won, and
|
|
78
|
+
# reimplementing the walk here is the very thing that made the receiver resolution wrong once already.
|
|
79
|
+
def published_constant?(name)
|
|
80
|
+
names = @discovery.published_constant_names
|
|
81
|
+
return false if names.empty?
|
|
82
|
+
return false unless names.include?(name.split("::").last)
|
|
83
|
+
|
|
84
|
+
!locally_declared_constant?(name)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def locally_declared_constant?(reference_name)
|
|
88
|
+
local = @discovery.local_constant_names
|
|
89
|
+
return false if local.empty?
|
|
90
|
+
return true if local.include?(reference_name)
|
|
91
|
+
|
|
92
|
+
suffix = "::#{reference_name}"
|
|
93
|
+
local.any? { |name| name.end_with?(suffix) }
|
|
94
|
+
end
|
|
95
|
+
private :locally_declared_constant?
|
|
96
|
+
|
|
53
97
|
def data_member_layouts = @discovery.data_member_layouts
|
|
54
98
|
def struct_member_layouts = @discovery.struct_member_layouts
|
|
55
99
|
# ADR-67 WD3 — call-site-inferred parameter types, keyed by `[class_name, method_name, kind]`.
|
|
@@ -135,6 +179,23 @@ module Rigor
|
|
|
135
179
|
self
|
|
136
180
|
end
|
|
137
181
|
|
|
182
|
+
# Issue #653 — records that a plugin's `dynamic_return` answered the return type for `node`, i.e. that
|
|
183
|
+
# `MethodDispatcher`'s plugin tier (which sits ABOVE `RbsDispatch`) is where the site's type came from.
|
|
184
|
+
# `Analysis::CheckRules` consults it so `call.undefined-method` does not then read the receiver's RBS to
|
|
185
|
+
# prove the same call undefined: the engine already decided the plugin outranks the signature here, and a
|
|
186
|
+
# partially-declared receiver is not a closed world (ADR-5). Mirrors {#record_void_origin} exactly —
|
|
187
|
+
# identity-keyed advisory metadata on a table threaded by reference through `#join` / `#rebuild`,
|
|
188
|
+
# excluded from `==` / `hash`, so it never forks a flow-dedup or cache key.
|
|
189
|
+
def record_plugin_typed_call(node)
|
|
190
|
+
@plugin_typed_calls[node] = true
|
|
191
|
+
self
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Issue #653 — whether a plugin answered the return type for `node` on this run.
|
|
195
|
+
def plugin_typed_call?(node)
|
|
196
|
+
@plugin_typed_calls.key?(node)
|
|
197
|
+
end
|
|
198
|
+
|
|
138
199
|
def initialize(
|
|
139
200
|
environment:, locals:,
|
|
140
201
|
fact_store: Analysis::FactStore.empty,
|
|
@@ -149,10 +210,12 @@ module Rigor
|
|
|
149
210
|
source_path: nil,
|
|
150
211
|
struct_fold_safe_locals: EMPTY_FOLD_SAFE,
|
|
151
212
|
opaque_block_self: false,
|
|
213
|
+
lexical_nesting: nil,
|
|
152
214
|
dynamic_origins: {}.compare_by_identity,
|
|
153
215
|
local_origins: EMPTY_ORIGINS,
|
|
154
216
|
ivar_origins: EMPTY_ORIGINS,
|
|
155
217
|
void_origins: {}.compare_by_identity,
|
|
218
|
+
plugin_typed_calls: {}.compare_by_identity,
|
|
156
219
|
optimistic_origins: {}.compare_by_identity,
|
|
157
220
|
optimistic_locals: EMPTY_ORIGINS,
|
|
158
221
|
optimistic_ivars: EMPTY_ORIGINS
|
|
@@ -171,10 +234,12 @@ module Rigor
|
|
|
171
234
|
@source_path = source_path
|
|
172
235
|
@struct_fold_safe_locals = struct_fold_safe_locals
|
|
173
236
|
@opaque_block_self = opaque_block_self
|
|
237
|
+
@lexical_nesting = lexical_nesting
|
|
174
238
|
@dynamic_origins = dynamic_origins
|
|
175
239
|
@local_origins = local_origins
|
|
176
240
|
@ivar_origins = ivar_origins
|
|
177
241
|
@void_origins = void_origins
|
|
242
|
+
@plugin_typed_calls = plugin_typed_calls
|
|
178
243
|
@optimistic_origins = optimistic_origins
|
|
179
244
|
@optimistic_locals = optimistic_locals
|
|
180
245
|
@optimistic_ivars = optimistic_ivars
|
|
@@ -264,6 +329,29 @@ module Rigor
|
|
|
264
329
|
rebuild(self_type: type)
|
|
265
330
|
end
|
|
266
331
|
|
|
332
|
+
# Issue #652 — installs the body's REAL `Module.nesting`, innermost first, as recorded at declaration
|
|
333
|
+
# time by `Inference::StatementEvaluator` (`["Admin::UsersController"]` for a compact
|
|
334
|
+
# `class Admin::UsersController`, `["Admin::UsersController", "Admin"]` for the nested spelling). A
|
|
335
|
+
# qualified name cannot be un-flattened back into the chain that produced it, so the chain has to travel
|
|
336
|
+
# with the scope; `Reflection.lexical_nesting_chain` is the single reader.
|
|
337
|
+
#
|
|
338
|
+
# The value is THREE-valued, and issue #716 is what the third state is for:
|
|
339
|
+
#
|
|
340
|
+
# - a non-empty chain — the body's real `Module.nesting`, recorded at declaration time;
|
|
341
|
+
# - `[]` — recorded, and the body is written at the TOP LEVEL. Ruby's `Module.nesting` there is empty,
|
|
342
|
+
# so a top-level `def helper = Post.new` names `::Post` no matter which namespace calls it.
|
|
343
|
+
# `Reflection.resolve_constant_type` consults the top level FIRST for such a scope and only then the
|
|
344
|
+
# caller-derived rungs, instead of letting the caller's namespace answer;
|
|
345
|
+
# - `nil` — NOT recorded, for a scope built outside a declaration walk (a callee body re-entered through
|
|
346
|
+
# `Scope#evaluate`, a plugin-constructed scope). The reader then peels `self_type`'s class name — a
|
|
347
|
+
# gradual answer, never a new firing.
|
|
348
|
+
#
|
|
349
|
+
# Collapsing `[]` into `nil` is precisely the bug #716 fixed, so a writer that has an empty chain must
|
|
350
|
+
# pass it rather than skip the stamp.
|
|
351
|
+
def with_lexical_nesting(chain)
|
|
352
|
+
rebuild(lexical_nesting: chain)
|
|
353
|
+
end
|
|
354
|
+
|
|
267
355
|
# ADR-28 / ADR-52 slice 5a — per-file source path carried on the scope. The analyzer stamps the current file's
|
|
268
356
|
# path onto the seed scope; nested rebuilds propagate it so plugin rules (`dynamic_return`'s `file_methods:`
|
|
269
357
|
# gate, sigil checks) can resolve "which file does this call site belong to?" without thread-locals.
|
|
@@ -602,6 +690,15 @@ module Rigor
|
|
|
602
690
|
site
|
|
603
691
|
end
|
|
604
692
|
|
|
693
|
+
# Issue #735 — the singleton-side mirror of {#user_def_site_for}: the `"path:line"` where the project
|
|
694
|
+
# defines `class_name.method_name`, or nil. Records the same cross-file edge, keyed `"Class.method"`.
|
|
695
|
+
def user_singleton_def_site_for(class_name, method_name)
|
|
696
|
+
table = @discovery.discovered_singleton_def_sources[class_name.to_s]
|
|
697
|
+
site = table && table[method_name.to_sym]
|
|
698
|
+
record_cross_file_method(class_name, method_name, site, singleton: true) if Analysis::DependencyRecorder.active?
|
|
699
|
+
site
|
|
700
|
+
end
|
|
701
|
+
|
|
605
702
|
# ADR-24 slice 2 — per-class table mapping a fully qualified user-class name to its superclass name AS WRITTEN
|
|
606
703
|
# at the `class Foo < Bar` declaration (`"Bar"`, possibly a qualified `"A::B"`). Populated by `ScopeIndexer` —
|
|
607
704
|
# per-file plus the cross-file project pre-pass — and consumed by
|
|
@@ -647,6 +744,223 @@ module Rigor
|
|
|
647
744
|
@discovery.discovered_includes[class_name.to_s] || []
|
|
648
745
|
end
|
|
649
746
|
|
|
747
|
+
# ADR-24 slice 2 — the user-side ancestor walk: resolves `method_name` against `class_name`'s own `def`s,
|
|
748
|
+
# then breadth-first through its included / prepended modules (mixins first, as Ruby orders them) and its
|
|
749
|
+
# superclass chain. Returns `[def_node, owner]`, or `[nil, nil]` when nothing in the project defines it.
|
|
750
|
+
# RBS-known / third-party ancestors resolve to no discovered class and end that branch.
|
|
751
|
+
#
|
|
752
|
+
# It lives HERE, not in a consumer, because it reads nothing but this scope's frozen discovery index
|
|
753
|
+
# (`user_def_for` / `includes_of` / `superclass_of` / the `discovered_*` tables) — the same property that
|
|
754
|
+
# lets `ExpressionTyper` memoise its results run-wide. Two callers depend on that single answer:
|
|
755
|
+
# `ExpressionTyper#compute_user_def_with_owner`, which wraps it in the run-scoped memo for the dispatch hot
|
|
756
|
+
# path, and {Inference::MethodDispatcher::StructMaterialization}, whose `.with` guard must see a `with`
|
|
757
|
+
# contributed by an included module. They forked once — the guard resolved own-class-only while the
|
|
758
|
+
# dispatcher walked ancestors — and answered differently about the same receiver expression (#598 review).
|
|
759
|
+
#
|
|
760
|
+
# `name_memo` is a CACHE, not semantics: the per-edge as-written-name resolutions, so a caller resolving
|
|
761
|
+
# many methods against one class pays each ancestor edge once. Omit it and the walk is identical, just
|
|
762
|
+
# uncached.
|
|
763
|
+
def user_def_through_ancestors(class_name, method_name, name_memo: {})
|
|
764
|
+
queue = [class_name.to_s]
|
|
765
|
+
seen = {}
|
|
766
|
+
visited = 0
|
|
767
|
+
until queue.empty?
|
|
768
|
+
current = queue.shift
|
|
769
|
+
next if current.nil? || seen[current]
|
|
770
|
+
|
|
771
|
+
seen[current] = true
|
|
772
|
+
visited += 1
|
|
773
|
+
return ancestor_walk_gave_up if visited > ANCESTOR_WALK_LIMIT
|
|
774
|
+
|
|
775
|
+
found = user_def_for(current, method_name)
|
|
776
|
+
return [found, current] if found
|
|
777
|
+
|
|
778
|
+
enqueue_ancestors(current, queue, name_memo)
|
|
779
|
+
end
|
|
780
|
+
[nil, nil]
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
# Issue #731 — the singleton-side twin of {#user_def_through_ancestors}: resolves `method_name` against
|
|
784
|
+
# `class_name`'s own `def self.` / `class << self` bodies, then up the SUPERCLASS chain. Returns
|
|
785
|
+
# `[def_node, owner]`, or `[nil, nil]`.
|
|
786
|
+
#
|
|
787
|
+
# Superclasses only, and that is the whole shape of class-method inheritance in Ruby: a subclass
|
|
788
|
+
# inherits its parent's class methods, while an `include`d module contributes INSTANCE methods and its
|
|
789
|
+
# own `def self.x` is not callable on the includer. `extend M` needs no walk either — `ScopeIndexer`
|
|
790
|
+
# folds an extend into the extender's own singleton entries before the table is frozen — so the one
|
|
791
|
+
# edge this misses is a module's singleton surface reached through some other route, which nothing
|
|
792
|
+
# resolves today.
|
|
793
|
+
def singleton_def_through_ancestors(class_name, method_name, name_memo: {})
|
|
794
|
+
queue = [class_name.to_s]
|
|
795
|
+
seen = {}
|
|
796
|
+
visited = 0
|
|
797
|
+
until queue.empty?
|
|
798
|
+
current = queue.shift
|
|
799
|
+
next if current.nil? || seen[current]
|
|
800
|
+
|
|
801
|
+
seen[current] = true
|
|
802
|
+
visited += 1
|
|
803
|
+
return ancestor_walk_gave_up if visited > ANCESTOR_WALK_LIMIT
|
|
804
|
+
|
|
805
|
+
found = singleton_def_for(current, method_name)
|
|
806
|
+
return [found, current] if found
|
|
807
|
+
|
|
808
|
+
enqueue_ancestors(current, queue, name_memo, mixins: false)
|
|
809
|
+
end
|
|
810
|
+
[nil, nil]
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
# The BFS node cap; a hierarchy past it gives up rather than walking unboundedly (ADR-41 WD4).
|
|
814
|
+
ANCESTOR_WALK_LIMIT = 100
|
|
815
|
+
|
|
816
|
+
def ancestor_walk_gave_up
|
|
817
|
+
Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT)
|
|
818
|
+
[nil, nil]
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
# Issue #723 — the same ancestor walk as {#user_def_through_ancestors}, asked of the DISCOVERY table
|
|
822
|
+
# rather than the def-node table: does the project define `method_name` on `class_name` or on any
|
|
823
|
+
# ancestor the project itself declares? The two tables are not interchangeable —
|
|
824
|
+
# `discovered_methods` also carries `define_method`, `attr_*` and the whole singleton side, none of
|
|
825
|
+
# which contributes a `Prism::DefNode` — and the suppression probe shared by the `call.*` check rules
|
|
826
|
+
# needs the broader one.
|
|
827
|
+
#
|
|
828
|
+
# Why it must walk at all: `Analysis::CheckRules#source_declared_method?` asked `discovered_method?`,
|
|
829
|
+
# which is keyed on the receiver's OWN name, while the typer resolves the same call through this
|
|
830
|
+
# ancestry. A project class whose `sig/` declares it without its project superclass therefore drew
|
|
831
|
+
# `call.undefined-method` on a method `dump_type` resolved on the same line of the same run — writing
|
|
832
|
+
# MORE RBS made the run worse, the incentive #653 removed for plugin-typed calls.
|
|
833
|
+
#
|
|
834
|
+
# `kind: :singleton` follows the superclass chain ONLY: a class method is inherited by a subclass, but
|
|
835
|
+
# an `include`d module contributes instance methods and its own `def self.x` is not callable on the
|
|
836
|
+
# includer. `extend M` is not lost to that narrowing — `Inference::ScopeIndexer` folds an extend into
|
|
837
|
+
# the extender's own singleton entries before this table is frozen.
|
|
838
|
+
def discovered_method_through_ancestors?(class_name, method_name, kind, name_memo: {})
|
|
839
|
+
return false if class_name.nil?
|
|
840
|
+
|
|
841
|
+
queue = [class_name.to_s]
|
|
842
|
+
seen = {}
|
|
843
|
+
visited = 0
|
|
844
|
+
until queue.empty?
|
|
845
|
+
current = queue.shift
|
|
846
|
+
next if current.nil? || seen[current]
|
|
847
|
+
|
|
848
|
+
seen[current] = true
|
|
849
|
+
visited += 1
|
|
850
|
+
# Budget exhaustion is uncertainty, not absence: answering "not declared" here would hand a
|
|
851
|
+
# `call.undefined-method` a fired verdict it has no evidence for. Suppress and record the hit.
|
|
852
|
+
if visited > ANCESTOR_WALK_LIMIT
|
|
853
|
+
Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT)
|
|
854
|
+
return true
|
|
855
|
+
end
|
|
856
|
+
|
|
857
|
+
return true if discovered_method?(current, method_name, kind)
|
|
858
|
+
|
|
859
|
+
enqueue_ancestors(current, queue, name_memo, mixins: kind != :singleton)
|
|
860
|
+
end
|
|
861
|
+
false
|
|
862
|
+
end
|
|
863
|
+
|
|
864
|
+
# Pushes `current`'s direct ancestors onto the BFS queue: included / prepended modules first (Ruby places
|
|
865
|
+
# mixins nearer than the superclass), then the superclass. Each as-written name is resolved against the
|
|
866
|
+
# nesting `current`'s declaration header is written in ({#ancestor_name_candidates}); names that resolve
|
|
867
|
+
# to no project class/module are dropped. `mixins: false` walks the superclass chain alone, for the
|
|
868
|
+
# singleton-side question where an `include` contributes nothing.
|
|
869
|
+
def enqueue_ancestors(current, queue, name_memo, mixins: true)
|
|
870
|
+
if mixins
|
|
871
|
+
includes_of(current).each do |raw|
|
|
872
|
+
resolved = resolve_ancestor_class_name(current, raw, name_memo)
|
|
873
|
+
queue.push(resolved) if resolved
|
|
874
|
+
end
|
|
875
|
+
end
|
|
876
|
+
raw_super = superclass_of(current)
|
|
877
|
+
return if raw_super.nil?
|
|
878
|
+
|
|
879
|
+
resolved_super = resolve_ancestor_class_name(current, raw_super, name_memo)
|
|
880
|
+
queue.push(resolved_super) if resolved_super
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
# Resolves an ancestor name AS WRITTEN (`"Base"`, or a qualified `"A::B"`) to a project-discovered class,
|
|
884
|
+
# following Ruby's `Module.nesting` constant lookup from the subclass's declaration HEADER, innermost
|
|
885
|
+
# first, then bare. nil when no candidate names a discovered user class.
|
|
886
|
+
def resolve_ancestor_class_name(subclass_qualified, raw_ancestor, name_memo)
|
|
887
|
+
by_subclass = (name_memo[subclass_qualified] ||= {})
|
|
888
|
+
return by_subclass[raw_ancestor] if by_subclass.key?(raw_ancestor)
|
|
889
|
+
|
|
890
|
+
by_subclass[raw_ancestor] = compute_ancestor_class_name(subclass_qualified, raw_ancestor)
|
|
891
|
+
end
|
|
892
|
+
private :ancestor_walk_gave_up, :resolve_ancestor_class_name
|
|
893
|
+
|
|
894
|
+
# Issue #682 — the candidate names an ancestor spelled `raw_ancestor` can denote in `subclass_qualified`,
|
|
895
|
+
# in Ruby's lookup order, most-qualified first and the bare name last. The single owner of the question:
|
|
896
|
+
# three walks resolved a superclass / include name apiece ({#enqueue_ancestors} for method lookup,
|
|
897
|
+
# `Reflection.resolve_ancestor_name` for the constant ladder's ancestor rung, and
|
|
898
|
+
# `Analysis::CheckRules`' override-visibility rule), and each derived the order by PEELING the
|
|
899
|
+
# subclass's qualified name one `::` segment at a time.
|
|
900
|
+
#
|
|
901
|
+
# That peel is the NESTED spelling's answer, given to both spellings. Ruby evaluates a superclass
|
|
902
|
+
# expression before entering the body, so the cref that governs it is the one the declaration's HEADER
|
|
903
|
+
# is written in — `Module.nesting` minus the declaration's own entry. For `module Admin; class Widget <
|
|
904
|
+
# Base` that is `["Admin"]` and the peel agrees; for the compact `class Admin::Widget < Base` written at
|
|
905
|
+
# the top level it is EMPTY, and the peel searched an `Admin::Base` Ruby never looks at — a wrong class
|
|
906
|
+
# for the ancestor rung of constant lookup and for method dispatch alike.
|
|
907
|
+
#
|
|
908
|
+
# The header nesting is a property of the DECLARATION, not of the reader, so it is read from the
|
|
909
|
+
# discovery table `Inference::ScopeIndexer` records it in rather than from this scope's own
|
|
910
|
+
# `#lexical_nesting`. Answering off the reader's chain would be wrong past the first hop of the ancestor
|
|
911
|
+
# BFS (the chain describes the reader's class, not the ancestor being resolved) and would silently
|
|
912
|
+
# corrupt the run-scoped ancestor memos in `Inference::ExpressionTyper#class_graph_buckets` and
|
|
913
|
+
# `Reflection.ancestor_constant_scopes`, both of which key on the class name alone because this walk is
|
|
914
|
+
# a pure function of the frozen discovery tables.
|
|
915
|
+
#
|
|
916
|
+
# The peel survives as the FALLBACK, for a class no declaration walk recorded — a scope seeded without
|
|
917
|
+
# the table, an anonymous `Class.new` name, a class reached only through RBS. It is the same answer this
|
|
918
|
+
# walk gave before, so an unrecorded class is unchanged rather than degraded.
|
|
919
|
+
#
|
|
920
|
+
# An include / prepend name is written INSIDE the body, so Ruby also tries `<subclass>::<raw>` ahead of
|
|
921
|
+
# everything here. That rung is deliberately still missing — it was missing from the peel too, and
|
|
922
|
+
# adding it is a widening this change does not need.
|
|
923
|
+
def ancestor_name_candidates(subclass_qualified, raw_ancestor)
|
|
924
|
+
# Issue #722 residue 1 / #637 — a ROOTED ancestor name is anchored at the top level and has no
|
|
925
|
+
# candidate list: `class Rooted < ::Base` names `::Base` wherever it is written, exactly as
|
|
926
|
+
# `Source::ConstantPath.declaration_prefix` already re-anchors a rooted HEADER. The marker is the
|
|
927
|
+
# leading `::` `Inference::ScopeIndexer.recorded_ancestor_name` preserves.
|
|
928
|
+
raw = raw_ancestor.to_s
|
|
929
|
+
return [raw.delete_prefix("::")] if raw.start_with?("::")
|
|
930
|
+
|
|
931
|
+
recorded = @discovery.discovered_header_nestings[subclass_qualified.to_s]
|
|
932
|
+
entries = recorded || peeled_header_nesting(subclass_qualified)
|
|
933
|
+
entries.map { |entry| "#{entry}::#{raw_ancestor}" } << raw_ancestor.to_s
|
|
934
|
+
end
|
|
935
|
+
|
|
936
|
+
# The pre-#682 reading of a qualified class name: every proper prefix of it, innermost first, as if the
|
|
937
|
+
# class had been declared one `module` keyword per segment.
|
|
938
|
+
def peeled_header_nesting(subclass_qualified)
|
|
939
|
+
segments = subclass_qualified.to_s.split("::")
|
|
940
|
+
(segments.length - 1).downto(1).map { |i| segments[0, i].join("::") }
|
|
941
|
+
end
|
|
942
|
+
private :peeled_header_nesting
|
|
943
|
+
|
|
944
|
+
def compute_ancestor_class_name(subclass_qualified, raw_ancestor)
|
|
945
|
+
ancestor_name_candidates(subclass_qualified, raw_ancestor).find { |c| known_user_class?(c) }
|
|
946
|
+
end
|
|
947
|
+
|
|
948
|
+
# Issue #723 — `discovered_methods` is in the list because the other three miss a class whose only
|
|
949
|
+
# project-side content is CLASS methods: `class Base; def self.build = :built; end` records no instance
|
|
950
|
+
# def node, no superclass and no include, so the ancestor-name resolver did not recognise `Base` as a
|
|
951
|
+
# project class at all and every walk through it ended one hop early. The table that answers "the
|
|
952
|
+
# project defines something here" on both sides is this one.
|
|
953
|
+
def known_user_class?(name)
|
|
954
|
+
discovered_superclasses.key?(name) || discovered_def_nodes.key?(name) ||
|
|
955
|
+
discovered_includes.key?(name) || discovered_methods.key?(name)
|
|
956
|
+
end
|
|
957
|
+
# `known_user_class?` is PUBLIC (issue #530): `Inference::ExpressionTyper`'s external-gem ancestry probe
|
|
958
|
+
# has to ask the same "is this name a project class?" question this walk asks, and answering it with a
|
|
959
|
+
# narrower predicate (`discovered_classes` alone) would read a project class as having left the project
|
|
960
|
+
# and attribute its root name to a same-named locked gem — a WRONG provenance label, which is the one
|
|
961
|
+
# direction ADR-82 forbids.
|
|
962
|
+
private :ancestor_walk_gave_up, :compute_ancestor_class_name
|
|
963
|
+
|
|
650
964
|
# Records, for a resolved cross-class ancestry read, every file that declares `class_name` (its declaration /
|
|
651
965
|
# reopening / superclass / include sites). The `discovered_class_sources` table it reads is populated only by
|
|
652
966
|
# the cross-file project pre-pass ({Inference::ScopeIndexer.discovered_def_index_for_paths}) and only when
|
|
@@ -660,6 +974,22 @@ module Rigor
|
|
|
660
974
|
end
|
|
661
975
|
private :record_class_dependency
|
|
662
976
|
|
|
977
|
+
# Issue #644 — the positive ADR-46 edge for a cross-file VALUE constant. `Reflection.constant_type_at`
|
|
978
|
+
# calls this the moment a candidate resolves through `in_source_constants`, so the reader depends on the
|
|
979
|
+
# file that wrote the constant: editing the literal, or deleting the file, re-checks the reader. Recorded
|
|
980
|
+
# WITHOUT a symbol (a file-granularity / ancestry edge) because a constant's published value is a
|
|
981
|
+
# declaration-level fact of the whole file, which is also the granularity
|
|
982
|
+
# `ScopeIndexer#append_constant_signature` moves the declaration signature at.
|
|
983
|
+
#
|
|
984
|
+
# `constant_sources` is seeded only when dependency recording is on, so on every ordinary run the table
|
|
985
|
+
# is empty and this is one Hash miss; the caller additionally gates on the recorder being active.
|
|
986
|
+
def record_constant_dependency(name)
|
|
987
|
+
sites = @discovery.constant_sources[name]
|
|
988
|
+
return if sites.nil?
|
|
989
|
+
|
|
990
|
+
sites.each { |site| Analysis::DependencyRecorder.read_site(site) }
|
|
991
|
+
end
|
|
992
|
+
|
|
663
993
|
# v0.1.2 — per-class table mapping `method_name (Symbol) → :public | :private | :protected`. Populated by
|
|
664
994
|
# `ScopeIndexer` for every `def` it sees inside a class body, with the visibility taken from the surrounding
|
|
665
995
|
# `private` / `protected` / `public` modifier state plus any post-hoc `private :name, ...` named-argument
|
|
@@ -815,10 +1145,12 @@ module Rigor
|
|
|
815
1145
|
source_path: @source_path,
|
|
816
1146
|
struct_fold_safe_locals: @struct_fold_safe_locals,
|
|
817
1147
|
opaque_block_self: @opaque_block_self,
|
|
1148
|
+
lexical_nesting: @lexical_nesting,
|
|
818
1149
|
dynamic_origins: @dynamic_origins,
|
|
819
1150
|
local_origins: @local_origins,
|
|
820
1151
|
ivar_origins: @ivar_origins,
|
|
821
1152
|
void_origins: @void_origins,
|
|
1153
|
+
plugin_typed_calls: @plugin_typed_calls,
|
|
822
1154
|
optimistic_origins: @optimistic_origins,
|
|
823
1155
|
optimistic_locals: @optimistic_locals,
|
|
824
1156
|
optimistic_ivars: @optimistic_ivars
|
|
@@ -834,10 +1166,12 @@ module Rigor
|
|
|
834
1166
|
source_path: source_path,
|
|
835
1167
|
struct_fold_safe_locals: struct_fold_safe_locals,
|
|
836
1168
|
opaque_block_self: opaque_block_self,
|
|
1169
|
+
lexical_nesting: lexical_nesting,
|
|
837
1170
|
dynamic_origins: dynamic_origins,
|
|
838
1171
|
local_origins: local_origins,
|
|
839
1172
|
ivar_origins: ivar_origins,
|
|
840
1173
|
void_origins: void_origins,
|
|
1174
|
+
plugin_typed_calls: plugin_typed_calls,
|
|
841
1175
|
optimistic_origins: optimistic_origins,
|
|
842
1176
|
optimistic_locals: optimistic_locals,
|
|
843
1177
|
optimistic_ivars: optimistic_ivars
|
|
@@ -875,16 +1209,64 @@ module Rigor
|
|
|
875
1209
|
# flow-live and `possible-nil-receiver` fires as before.
|
|
876
1210
|
declaration_sourced: join_declaration_sourced(other),
|
|
877
1211
|
source_path: source_path,
|
|
1212
|
+
# Issue #589 — the fold-safe set MUST survive a merge. It was simply absent from this constructor
|
|
1213
|
+
# call, so it fell back to the empty default and every `if` / `while` in a method silently revoked
|
|
1214
|
+
# struct member folding for the whole body after it: `s = S.new("r"); while c; i += 1; end; s.raw`
|
|
1215
|
+
# answered `Dynamic[top]` while the straight-line sibling folded to `"r"`. The carrier was never
|
|
1216
|
+
# the casualty — `s` still reads `S(raw: "r")` at the merge — only the grant that lets a read
|
|
1217
|
+
# consult it, which is why the shape looked like carrier erasure from the outside.
|
|
1218
|
+
#
|
|
1219
|
+
# The set is a property of the method BODY (a static scan over its root, stamped once by
|
|
1220
|
+
# `ScopeIndexer` / `StatementEvaluator` and threaded by `rebuild`), so both arms of a merge
|
|
1221
|
+
# normally carry the identical set and the intersection below is that set. Intersecting rather
|
|
1222
|
+
# than unioning is the FP-safe direction anyway: the grant licenses a FOLD, so retaining one an
|
|
1223
|
+
# arm did not have could fold a stale constant, while dropping one only costs precision.
|
|
1224
|
+
struct_fold_safe_locals: join_struct_fold_safe(other),
|
|
1225
|
+
# Issue #600 — the sibling omission of the same class, and the same failure mode: absent from this
|
|
1226
|
+
# constructor call, the flag fell back to `false`, so ANY merge inside a block re-armed the #316/#319
|
|
1227
|
+
# decline gate. `RSpec.describe do; if c; end; output; end` bound a cross-file top-level `def output`
|
|
1228
|
+
# again after the `if`, which is the wrong-bind direction the gate exists to prevent — the block's real
|
|
1229
|
+
# `self` may have gained a public same-named method by `include`, and that wins the MRO over the
|
|
1230
|
+
# private `Object` def.
|
|
1231
|
+
#
|
|
1232
|
+
# `||`, not `&&`: opacity is the DECLINING state, so keeping it once either arm has it is the FP-safe
|
|
1233
|
+
# merge. Like the fold-safe set this is normally a property of the enclosing block (stamped once at
|
|
1234
|
+
# block entry by `entering_opaque_block` and inherited through `rebuild`), so both arms usually carry
|
|
1235
|
+
# the same value and the `||` is that value.
|
|
1236
|
+
opaque_block_self: @opaque_block_self || other.opaque_block_self,
|
|
1237
|
+
# Issue #652 — the recorded `Module.nesting`, stamped once at body entry and threaded by `rebuild`
|
|
1238
|
+
# exactly as the fold-safe set is. A join is a control-flow merge INSIDE one body, so both arms
|
|
1239
|
+
# always carry the identical chain; taking this scope's is that chain. Dropping it would silently
|
|
1240
|
+
# re-enable the name-peel for everything after the first `if` in a body.
|
|
1241
|
+
lexical_nesting: @lexical_nesting,
|
|
878
1242
|
dynamic_origins: @dynamic_origins,
|
|
879
1243
|
local_origins: join_origins(@local_origins, other.local_origins),
|
|
880
1244
|
ivar_origins: join_origins(@ivar_origins, other.ivar_origins),
|
|
881
1245
|
void_origins: @void_origins,
|
|
1246
|
+
plugin_typed_calls: @plugin_typed_calls,
|
|
882
1247
|
optimistic_origins: @optimistic_origins,
|
|
883
1248
|
optimistic_locals: join_origins(@optimistic_locals, other.optimistic_locals),
|
|
884
1249
|
optimistic_ivars: join_origins(@optimistic_ivars, other.optimistic_ivars)
|
|
885
1250
|
)
|
|
886
1251
|
end
|
|
887
1252
|
|
|
1253
|
+
# Issue #589 — intersect the struct fold-safe grants. Zero-alloc on the common path, where both arms
|
|
1254
|
+
# carry the same frozen set the body scan stamped.
|
|
1255
|
+
# A `nil` on EITHER side yields no grants. The field is keyword-defaulted to `EMPTY_FOLD_SAFE` and
|
|
1256
|
+
# threaded by `rebuild`, so `nil` should be unreachable — but the two sides are deliberately symmetric
|
|
1257
|
+
# rather than one arm trusting the other, because the asymmetric form kept every grant when the OTHER
|
|
1258
|
+
# side was nil, which is the one direction that can fold a stale value.
|
|
1259
|
+
def join_struct_fold_safe(other)
|
|
1260
|
+
mine = @struct_fold_safe_locals
|
|
1261
|
+
theirs = other.struct_fold_safe_locals
|
|
1262
|
+
return EMPTY_FOLD_SAFE if mine.nil? || theirs.nil?
|
|
1263
|
+
return mine if mine.equal?(theirs) || mine == theirs
|
|
1264
|
+
return EMPTY_FOLD_SAFE if mine.empty? || theirs.empty?
|
|
1265
|
+
|
|
1266
|
+
intersected = mine & theirs
|
|
1267
|
+
intersected.empty? ? EMPTY_FOLD_SAFE : intersected.freeze
|
|
1268
|
+
end
|
|
1269
|
+
|
|
888
1270
|
# ADR-82 WD1 — merge two branches' propagated origins (self wins on a name conflict; advisory metadata, so
|
|
889
1271
|
# a disagreement is harmless). Zero-alloc when either side is empty — the common case, keeping the join
|
|
890
1272
|
# hot-path cost negligible.
|
|
@@ -986,5 +1368,4 @@ module Rigor
|
|
|
986
1368
|
filtered.size == @method_chain_narrowings.size ? @method_chain_narrowings : filtered.freeze
|
|
987
1369
|
end
|
|
988
1370
|
end
|
|
989
|
-
# rubocop:enable Metrics/ClassLength,Metrics/ParameterLists
|
|
990
1371
|
end
|
|
@@ -16,6 +16,11 @@ module Rigor
|
|
|
16
16
|
EQUIVALENT = :equivalent
|
|
17
17
|
SKIPPED = :skipped
|
|
18
18
|
|
|
19
|
+
# The classifications that actually produce a line in a generated `sig/`. Consulted by the renderer,
|
|
20
|
+
# the writer and the generator's own post-passes; it lived as a private constant in the first two,
|
|
21
|
+
# which is one fork too many for a list this load-bearing.
|
|
22
|
+
EMITTABLE = [NEW_FILE, NEW_METHOD, TIGHTER_RETURN].freeze
|
|
23
|
+
|
|
19
24
|
DIAGNOSTIC_IDS = {
|
|
20
25
|
NEW_FILE => "sig.generated.new-file",
|
|
21
26
|
NEW_METHOD => "sig.generated.new-method",
|
|
@@ -29,7 +34,13 @@ module Rigor
|
|
|
29
34
|
# The generator rendered a line `rbs` itself rejects. Skipping the method keeps sig-gen useful on a
|
|
30
35
|
# project with one pathological def, where failing the command outright would deny the user every other
|
|
31
36
|
# signature; the count is reported so the defect is not silent. See {SigGen::RbsValidity}.
|
|
32
|
-
unrenderable_rbs: "sig.skipped.unrenderable-rbs"
|
|
37
|
+
unrenderable_rbs: "sig.skipped.unrenderable-rbs",
|
|
38
|
+
# Issue #735 — the class's superclass chain does not terminate in a class the RBS environment knows,
|
|
39
|
+
# so emitting the declaration would collapse the class on the next run rather than type it.
|
|
40
|
+
unresolvable_superclass: "sig.skipped.unresolvable-superclass",
|
|
41
|
+
# Issue #744 — a project subclass overrides this method and its override is NOT emitted, so the
|
|
42
|
+
# declaration would be inherited by a subclass it does not describe.
|
|
43
|
+
overridden_by_unsigned_subclass: "sig.skipped.overridden-by-unsigned-subclass"
|
|
33
44
|
}.freeze
|
|
34
45
|
end
|
|
35
46
|
end
|