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/reflection.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "type"
|
|
4
|
+
require_relative "analysis/dependency_recorder"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
# Read-side facade over Rigor's three reflection sources:
|
|
@@ -76,6 +77,21 @@ module Rigor
|
|
|
76
77
|
loader.class_known?(class_name)
|
|
77
78
|
end
|
|
78
79
|
|
|
80
|
+
# Issue #735 — true when the class's RBS declaration is the PROJECT's own (a file under
|
|
81
|
+
# `signature_paths:`) rather than bundled core / stdlib / gem RBS. The two carry different authority:
|
|
82
|
+
# a bundled signature describes a class the project does not own, so a project `def` on it is the
|
|
83
|
+
# ADR-17 monkey-patch the analyzer reports; a project sidecar describes the very source being
|
|
84
|
+
# analysed, and a `def` in another of the project's files is that class's own definition.
|
|
85
|
+
#
|
|
86
|
+
# False whenever the question cannot be answered (no loader, no environment, an environment cached
|
|
87
|
+
# before #725 preserved buffer names), which is the pre-#735 reading in every case.
|
|
88
|
+
def project_declared_class?(class_name, scope: nil, environment: nil)
|
|
89
|
+
loader = rbs_loader_for(scope, environment)
|
|
90
|
+
return false if loader.nil?
|
|
91
|
+
|
|
92
|
+
loader.project_declared_class?(class_name)
|
|
93
|
+
end
|
|
94
|
+
|
|
79
95
|
# @return [Symbol] one of `:equal`, `:subclass`, `:superclass`,
|
|
80
96
|
# `:disjoint`, `:unknown`.
|
|
81
97
|
def class_ordering(lhs, rhs, scope: Scope.empty)
|
|
@@ -118,22 +134,35 @@ module Rigor
|
|
|
118
134
|
# RBS because the user's source is authoritative for its own constants. Returns the matched
|
|
119
135
|
# `Rigor::Type`, or nil when no source knows the constant.
|
|
120
136
|
#
|
|
137
|
+
# `rooted:` reports that the reference was written with a leading `::` (`::Foo`,
|
|
138
|
+
# `::Foo::Bar`). Ruby's absolute form names the TOP-LEVEL constant unconditionally, so the
|
|
139
|
+
# lexical ladder (steps 1 and 2) MUST be skipped and only the bare candidate consulted. The
|
|
140
|
+
# name itself stays un-rooted — every discovered-constant table is keyed that way — so the
|
|
141
|
+
# marker travels beside it rather than inside it (#614). When no top-level constant answers,
|
|
142
|
+
# the result is nil exactly as for an unknown constant: a lexically nearer shadow is never the
|
|
143
|
+
# answer to `::Foo`, and falling back to it was the bug (`::Rails` inside `module MyApp` typed
|
|
144
|
+
# as `MyApp::Rails`).
|
|
145
|
+
#
|
|
121
146
|
# This is the shared owner of the lexical-constant resolution: `Inference::ExpressionTyper`
|
|
122
147
|
# reads it to type a constant read, and `Inference::Narrowing` reads it to recognise a
|
|
123
148
|
# value-pinned `Constant[Regexp]` match-predicate operand.
|
|
124
|
-
def resolve_constant_type(name, scope: Scope.empty)
|
|
125
|
-
|
|
149
|
+
def resolve_constant_type(name, scope: Scope.empty, rooted: false)
|
|
150
|
+
# Issue #644 — the ADR-46 name edge, recorded ONCE PER REFERENCE and BEFORE the ladder, so it does not
|
|
151
|
+
# depend on which resolver answers. A reference that resolves through RBS (`Math::PI`) or through the
|
|
152
|
+
# class registry is exactly as dependent on the project's constant write set as one that resolves
|
|
153
|
+
# through `in_source_constants`: a project file that later assigns the name wins over the RBS answer,
|
|
154
|
+
# and without this edge the reader keeps serving the pre-assignment type. That staleness is NEW with
|
|
155
|
+
# the cross-file publication — before it, a project value write never won at a reader, so nothing could
|
|
156
|
+
# go stale. Over-recording is ADR-46's safe direction.
|
|
157
|
+
record_constant_reference(name) if Analysis::DependencyRecorder.active?
|
|
158
|
+
return constant_type_at(name, scope) if rooted
|
|
159
|
+
# Issue #716 — a body whose `Module.nesting` was RECORDED EMPTY is definitively top level, and Ruby
|
|
160
|
+
# resolves its constants there no matter which namespace calls it. Steps 1 and 2 both derive from the
|
|
161
|
+
# caller and are both wrong for it, so it takes its own ladder.
|
|
162
|
+
return toplevel_first_constant_type(name, scope) if recorded_toplevel_nesting?(scope)
|
|
126
163
|
|
|
127
164
|
# Step 1 — `Module.nesting`, innermost first. Each entry contributes only its OWN constants.
|
|
128
|
-
|
|
129
|
-
while walker && !walker.empty?
|
|
130
|
-
hit = constant_type_at("#{walker}::#{name}", scope)
|
|
131
|
-
return hit if hit
|
|
132
|
-
|
|
133
|
-
idx = walker.rindex("::")
|
|
134
|
-
walker = idx ? walker[0, idx] : nil
|
|
135
|
-
end
|
|
136
|
-
|
|
165
|
+
#
|
|
137
166
|
# Step 2 (#354) — the ancestors of the innermost cresting scope, which Ruby consults BEFORE
|
|
138
167
|
# falling back to the top level. Skipping this step did not merely lose a resolution: when the
|
|
139
168
|
# same name also exists at top level, step 3 answered a lookup Ruby gives to the ancestor, so
|
|
@@ -141,16 +170,67 @@ module Rigor
|
|
|
141
170
|
# wrong type on correct code, which outranks any worst-case reading (AGENTS.md
|
|
142
171
|
# § "Implementation Guidelines"). Only project ancestors are walked; an RBS-known superclass
|
|
143
172
|
# contributes no name here (see {.ancestor_constant_scopes}).
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
173
|
+
#
|
|
174
|
+
# Step 3 — the bare name (top level).
|
|
175
|
+
first_constant_hit(lexical_nesting_chain(scope), name, scope) ||
|
|
176
|
+
ancestor_constant_type(name, scope, enclosing_class_path(scope)) ||
|
|
177
|
+
constant_type_at(name, scope)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# The first candidate `<entry>::<name>` any source knows, walking `entries` in order, or nil. The one
|
|
181
|
+
# place a qualifying prefix is joined to a name, so every rung of the ladder consults its candidates
|
|
182
|
+
# identically.
|
|
183
|
+
def first_constant_hit(entries, name, scope)
|
|
184
|
+
entries.each do |entry|
|
|
185
|
+
hit = constant_type_at("#{entry}::#{name}", scope)
|
|
186
|
+
return hit if hit
|
|
149
187
|
end
|
|
188
|
+
nil
|
|
189
|
+
end
|
|
190
|
+
private_class_method :first_constant_hit
|
|
150
191
|
|
|
151
|
-
|
|
152
|
-
|
|
192
|
+
# Step 2's rung on its own: nil when there is no enclosing class path to take ancestors of.
|
|
193
|
+
def ancestor_constant_type(name, scope, prefix)
|
|
194
|
+
return nil if prefix.nil? || prefix.empty?
|
|
195
|
+
|
|
196
|
+
first_constant_hit(ancestor_constant_scopes(prefix, scope), name, scope)
|
|
153
197
|
end
|
|
198
|
+
private_class_method :ancestor_constant_type
|
|
199
|
+
|
|
200
|
+
# Issue #716 — whether the scope carries a RECORDED empty `Module.nesting`, which is the declaration
|
|
201
|
+
# walk's positive statement that the body is written at the top level (`Inference::ScopeIndexer#
|
|
202
|
+
# walk_def_nestings`). Distinct from `nil`, which still means "no declaration walk built this scope" and
|
|
203
|
+
# keeps the peel ({.lexical_nesting_chain}); the two are only distinguishable because the table is
|
|
204
|
+
# populated by that walk alone.
|
|
205
|
+
def recorded_toplevel_nesting?(scope) = scope.lexical_nesting.is_a?(Array) && scope.lexical_nesting.empty?
|
|
206
|
+
private_class_method :recorded_toplevel_nesting?
|
|
207
|
+
|
|
208
|
+
# Issue #716 — the ladder for a top-level body: the top level FIRST, then the caller-derived rungs
|
|
209
|
+
# unchanged. The candidate SET is exactly what {.resolve_constant_type}'s three steps consult for the
|
|
210
|
+
# same scope — only the order moves — so no read that resolves today stops resolving, and any read where
|
|
211
|
+
# the two disagree is one where the top level is Ruby's answer and the caller's namespace never was.
|
|
212
|
+
#
|
|
213
|
+
# Keeping the caller-derived rungs at all is deliberate and unsound against Ruby: they cover reads whose
|
|
214
|
+
# name exists ONLY under some namespace, where Ruby raises `NameError` (486 such reads on gitlab, 33 on
|
|
215
|
+
# dependabot-core — `docs/notes/20260905-toplevel-def-cref-movable-sites.md`). Rigor reports nothing
|
|
216
|
+
# about them either way, so retracting them would trade a silent wrong answer for a silent absent one at
|
|
217
|
+
# no gain. Making them FIRE is a separate question with its own false-positive budget.
|
|
218
|
+
def toplevel_first_constant_type(name, scope)
|
|
219
|
+
constant_type_at(name, scope) || caller_derived_constant_type(name, scope)
|
|
220
|
+
end
|
|
221
|
+
private_class_method :toplevel_first_constant_type
|
|
222
|
+
|
|
223
|
+
# The peel and its ancestors — {.resolve_constant_type}'s steps 1 and 2 as they answer for a scope with
|
|
224
|
+
# no recorded chain, factored out so the top-level ladder above can consult them BELOW the top level
|
|
225
|
+
# instead of above it.
|
|
226
|
+
def caller_derived_constant_type(name, scope)
|
|
227
|
+
prefix = enclosing_class_path(scope)
|
|
228
|
+
return nil if prefix.nil? || prefix.empty?
|
|
229
|
+
|
|
230
|
+
first_constant_hit(peeled_nesting(prefix), name, scope) ||
|
|
231
|
+
ancestor_constant_type(name, scope, prefix)
|
|
232
|
+
end
|
|
233
|
+
private_class_method :caller_derived_constant_type
|
|
154
234
|
|
|
155
235
|
# One candidate name, consulted in source-precedence order: the class registry (yielding a
|
|
156
236
|
# `Singleton[C]`), source-discovered classes, in-source value constants, then RBS-side
|
|
@@ -166,12 +246,27 @@ module Rigor
|
|
|
166
246
|
return in_source_class if in_source_class
|
|
167
247
|
|
|
168
248
|
in_source_value = scope.in_source_constants[candidate]
|
|
169
|
-
|
|
249
|
+
if in_source_value
|
|
250
|
+
scope.record_constant_dependency(candidate) if Analysis::DependencyRecorder.active?
|
|
251
|
+
return in_source_value
|
|
252
|
+
end
|
|
170
253
|
|
|
171
254
|
env.constant_for_name(candidate)
|
|
172
255
|
end
|
|
173
256
|
private_class_method :constant_type_at
|
|
174
257
|
|
|
258
|
+
# Issue #644 — the load-bearing ADR-46 edge for a constant reference: `constant:<last segment>`, keyed on
|
|
259
|
+
# the NAME rather than on a file. The published value is a function of the whole project's write set for
|
|
260
|
+
# the name, so it moves when a SECOND file starts or stops assigning it — a file this reader has no other
|
|
261
|
+
# relationship with, and which no positive edge could reach. Matched against the publication diff
|
|
262
|
+
# ({Analysis::Incremental.changed_constant_publications}). Gated by the caller on the recorder being
|
|
263
|
+
# active, so an ordinary run pays one integer read.
|
|
264
|
+
def record_constant_reference(name)
|
|
265
|
+
segment = name.delete_prefix("::").split("::").last
|
|
266
|
+
Analysis::DependencyRecorder.read_name(:constant, segment) if segment
|
|
267
|
+
end
|
|
268
|
+
private_class_method :record_constant_reference
|
|
269
|
+
|
|
175
270
|
# #354 — the project classes and modules whose own constants `class_name` inherits, in Ruby's
|
|
176
271
|
# ancestor order: included / prepended modules before the superclass (Ruby places mixins nearer),
|
|
177
272
|
# transitively, breadth-first. `class_name` itself is excluded — step 1 already covered it.
|
|
@@ -235,17 +330,14 @@ module Rigor
|
|
|
235
330
|
end
|
|
236
331
|
private_class_method :compute_ancestor_constant_scopes
|
|
237
332
|
|
|
238
|
-
# Resolves an ancestor name AS WRITTEN (`"Base"`, or a qualified `"A::B"`) against the
|
|
239
|
-
# subclass's
|
|
240
|
-
# `
|
|
241
|
-
#
|
|
333
|
+
# Resolves an ancestor name AS WRITTEN (`"Base"`, or a qualified `"A::B"`) against the nesting in
|
|
334
|
+
# force where the subclass's header is written — `Scope#ancestor_name_candidates`, the single
|
|
335
|
+
# owner of that order, which `Scope#enqueue_ancestors` reads for method lookup and the
|
|
336
|
+
# override-visibility rule reads for its own walk. Returns nil when no candidate names a
|
|
337
|
+
# discovered project class or module.
|
|
242
338
|
def resolve_ancestor_name(subclass_qualified, raw, scope)
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
candidate = (segments[0, i] + [raw]).join("::")
|
|
246
|
-
return candidate if known_project_namespace?(candidate, scope)
|
|
247
|
-
end
|
|
248
|
-
nil
|
|
339
|
+
scope.ancestor_name_candidates(subclass_qualified, raw)
|
|
340
|
+
.find { |candidate| known_project_namespace?(candidate, scope) }
|
|
249
341
|
end
|
|
250
342
|
private_class_method :resolve_ancestor_name
|
|
251
343
|
|
|
@@ -266,6 +358,49 @@ module Rigor
|
|
|
266
358
|
end
|
|
267
359
|
private_class_method :enclosing_class_path
|
|
268
360
|
|
|
361
|
+
# Ruby's `Module.nesting` for the body `scope` sits in, innermost first — the chain step 1 of
|
|
362
|
+
# {.resolve_constant_type} walks, and the chain `Inference::Narrowing` walks to resolve a class-guard
|
|
363
|
+
# name. The single owner of the question, so a constant read and the `is_a?` / `case`-`when` / `===`
|
|
364
|
+
# guard over the same spelling cannot disagree about which constant it names.
|
|
365
|
+
#
|
|
366
|
+
# The chain the analyzer RECORDED at declaration time wins ({Scope#with_lexical_nesting}). It is not
|
|
367
|
+
# recoverable from `self_type`: `class Admin::UsersController` and `module Admin; class
|
|
368
|
+
# UsersController` produce the identical `class_name`, while Ruby's nesting is
|
|
369
|
+
# `[Admin::UsersController]` for the first and `[Admin::UsersController, Admin]` for the second — so a
|
|
370
|
+
# bare `User` in the compact form names `::User`, and peeling the name reached `Admin::User` and
|
|
371
|
+
# reported `undefined method` on correct code ([#652](https://github.com/rigortype/rigor/issues/652)).
|
|
372
|
+
#
|
|
373
|
+
# The peel survives only as the FALLBACK, for a scope no declaration walk built (a callee body
|
|
374
|
+
# re-entered through `Scope#evaluate`, a plugin-constructed scope). A `class << expr` body is NOT
|
|
375
|
+
# one of them — not even `class << SomeOtherConstant`, whose class-frame stack the evaluator
|
|
376
|
+
# resets: the body inherits the enclosing chain, because Ruby pushes no `Module.nesting` entry
|
|
377
|
+
# there either. Answering an empty chain there would retract resolutions the
|
|
378
|
+
# engine makes today and turn correct code into `Dynamic`; a stale-but-gradual rung costs precision
|
|
379
|
+
# only, which is the direction AGENTS.md § "Implementation Guidelines" mandates.
|
|
380
|
+
#
|
|
381
|
+
# Issue #716 — the recorded value is three-valued, and a RECORDED `[]` is not the absence of one: it is
|
|
382
|
+
# the declaration walk saying "top level", and this returns it verbatim so `Module.nesting` reads what
|
|
383
|
+
# Ruby reads. {.resolve_constant_type} keeps the peel for that case as a rung BELOW the top level rather
|
|
384
|
+
# than through this reader, because a chain is not where an out-of-order fallback belongs.
|
|
385
|
+
def lexical_nesting_chain(scope)
|
|
386
|
+
recorded = scope.lexical_nesting
|
|
387
|
+
return recorded if recorded
|
|
388
|
+
|
|
389
|
+
base = enclosing_class_path(scope)
|
|
390
|
+
return [] if base.nil? || base.empty?
|
|
391
|
+
|
|
392
|
+
peeled_nesting(base)
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# The peel itself: a qualified name split into the chain the NESTED spelling would have produced
|
|
396
|
+
# (`"Admin::Users::Show"` → `["Admin::Users::Show", "Admin::Users", "Admin"]`). Shared by the fallback
|
|
397
|
+
# above and by issue #716's below-the-top-level rung, so the two cannot drift apart.
|
|
398
|
+
def peeled_nesting(base)
|
|
399
|
+
parts = base.split("::")
|
|
400
|
+
parts.each_index.map { |i| parts[0..-(i + 1)].join("::") }
|
|
401
|
+
end
|
|
402
|
+
private_class_method :peeled_nesting
|
|
403
|
+
|
|
269
404
|
# Returns the RBS `RBS::Definition::Method` for the instance method, or nil when the
|
|
270
405
|
# class or method is not in RBS. The source-side discovered-method facts are reachable
|
|
271
406
|
# through {.discovered_method?}; a future slice will unify the two under a
|
|
@@ -340,8 +475,25 @@ module Rigor
|
|
|
340
475
|
# @param kind [:instance, :singleton]
|
|
341
476
|
# @return [Boolean] true when the ScopeIndexer recorded a `def` for the given method on
|
|
342
477
|
# the given class with the matching kind.
|
|
478
|
+
#
|
|
479
|
+
# ADR-46 — a MISS records a negative cross-file dependency, so a consumer whose analysis
|
|
480
|
+
# turned on "the project does not define this method" is re-checked once a later edit
|
|
481
|
+
# defines it. The engine's own dispatch gets that edge from
|
|
482
|
+
# `Scope#record_cross_file_method`, but this facade is what a PLUGIN gate reads, and a
|
|
483
|
+
# contribution tier answers before dispatch reaches the engine's recording accessors —
|
|
484
|
+
# rigor-railties declines its `Rails.logger` typing exactly on this probe. Without the
|
|
485
|
+
# edge a warm `--incremental` run kept serving the plugin's answer after the project
|
|
486
|
+
# added `def self.logger`. The key grammar is the one `IncrementalSession#negative_key_for`
|
|
487
|
+
# inverts against (`Class#method` instance-side, `Class.method` singleton-side), which is
|
|
488
|
+
# also what `Incremental.appeared_symbols` emits for a newly-defined method.
|
|
343
489
|
def discovered_method?(class_name, method_name, kind: :instance, scope: Scope.empty)
|
|
344
|
-
scope.discovered_method?(class_name, method_name, kind)
|
|
490
|
+
return true if scope.discovered_method?(class_name, method_name, kind)
|
|
491
|
+
|
|
492
|
+
if Analysis::DependencyRecorder.active?
|
|
493
|
+
symbol = "#{class_name}#{kind == :singleton ? '.' : '#'}#{method_name}"
|
|
494
|
+
Analysis::DependencyRecorder.read_missing(:method, symbol)
|
|
495
|
+
end
|
|
496
|
+
false
|
|
345
497
|
end
|
|
346
498
|
end
|
|
347
499
|
end
|
|
@@ -19,13 +19,18 @@ module Rigor
|
|
|
19
19
|
:in_source_constants,
|
|
20
20
|
:discovered_methods,
|
|
21
21
|
:discovered_def_nodes,
|
|
22
|
+
:discovered_def_nestings,
|
|
22
23
|
:discovered_singleton_def_nodes,
|
|
23
24
|
:discovered_def_sources,
|
|
24
25
|
:discovered_singleton_def_sources,
|
|
25
26
|
:discovered_method_visibilities,
|
|
26
27
|
:discovered_superclasses,
|
|
28
|
+
:discovered_header_nestings,
|
|
27
29
|
:discovered_includes,
|
|
28
30
|
:discovered_class_sources,
|
|
31
|
+
:constant_sources,
|
|
32
|
+
:published_constant_names,
|
|
33
|
+
:local_constant_names,
|
|
29
34
|
:data_member_layouts,
|
|
30
35
|
:struct_member_layouts,
|
|
31
36
|
:param_inferred_types,
|
|
@@ -35,7 +40,8 @@ module Rigor
|
|
|
35
40
|
class DiscoveryIndex
|
|
36
41
|
EMPTY_NODE_TABLE = {}.compare_by_identity.freeze
|
|
37
42
|
EMPTY_TABLE = {}.freeze
|
|
38
|
-
|
|
43
|
+
EMPTY_NAME_SET = Set.new.freeze
|
|
44
|
+
private_constant :EMPTY_NODE_TABLE, :EMPTY_TABLE, :EMPTY_NAME_SET
|
|
39
45
|
|
|
40
46
|
# The third value a `discovered_methods` entry can hold, beside `:instance` and `:singleton`. One name may
|
|
41
47
|
# legitimately be defined on both sides of the same class (`def helper` plus a `class << self` twin), and the
|
|
@@ -57,13 +63,45 @@ module Rigor
|
|
|
57
63
|
in_source_constants: EMPTY_TABLE,
|
|
58
64
|
discovered_methods: EMPTY_TABLE,
|
|
59
65
|
discovered_def_nodes: EMPTY_TABLE,
|
|
66
|
+
# Issue #681 — `{Prism::DefNode => Module.nesting}` for every `def` the declaration walk reaches,
|
|
67
|
+
# recorded by `Inference::ScopeIndexer` where the declaration is indexed. Read by
|
|
68
|
+
# `Inference::ExpressionTyper#build_user_method_body_scope`, which rebuilds a callee's body scope
|
|
69
|
+
# from the receiver's type alone and so has no prefix of its own to derive one from. Keyed by node
|
|
70
|
+
# IDENTITY, because an inherited body is re-walked with the subclass as receiver while its
|
|
71
|
+
# constants resolve under the declaration that owns it.
|
|
72
|
+
#
|
|
73
|
+
# Issue #716 — PRESENT with `[]` and ABSENT are different facts, and only this table can tell them
|
|
74
|
+
# apart: it is populated by the declaration walk alone, so `[]` means "walked, and written at the top
|
|
75
|
+
# level" (Ruby's `Module.nesting` there) while an absent entry still means "not recorded" and
|
|
76
|
+
# `Reflection.lexical_nesting_chain` keeps its peel fallback for it.
|
|
77
|
+
discovered_def_nestings: EMPTY_NODE_TABLE,
|
|
60
78
|
discovered_singleton_def_nodes: EMPTY_TABLE,
|
|
61
79
|
discovered_def_sources: EMPTY_TABLE,
|
|
62
80
|
discovered_singleton_def_sources: EMPTY_TABLE,
|
|
63
81
|
discovered_method_visibilities: EMPTY_TABLE,
|
|
64
82
|
discovered_superclasses: EMPTY_TABLE,
|
|
83
|
+
# Issue #682 — `{qualified class name => Module.nesting where its declaration HEADER is written}`,
|
|
84
|
+
# innermost first and EXCLUDING the declaration's own entry. Read by `Scope#ancestor_name_candidates`,
|
|
85
|
+
# which resolves a superclass / include name in that cref instead of peeling the subclass's qualified
|
|
86
|
+
# name — the peel is the nested spelling's answer, and it searched an `Admin::Base` that a compact
|
|
87
|
+
# `class Admin::Widget < Base` written at the top level never reaches. An absent entry means "not
|
|
88
|
+
# recorded" and keeps the peel, so a scope that never saw a declaration walk is unchanged.
|
|
89
|
+
discovered_header_nestings: EMPTY_TABLE,
|
|
65
90
|
discovered_includes: EMPTY_TABLE,
|
|
66
91
|
discovered_class_sources: EMPTY_TABLE,
|
|
92
|
+
# Issue #644 — `{qualified constant name => Set[declaring file]}`, the write attribution behind the
|
|
93
|
+
# cross-file value-constant table. Read only by `Scope#record_constant_dependency` during ADR-46
|
|
94
|
+
# dependency recording, so the runner seeds it only on a recording run; every other run leaves it
|
|
95
|
+
# empty and the edge costs one nil check.
|
|
96
|
+
constant_sources: EMPTY_TABLE,
|
|
97
|
+
# Issue #644 — the two halves of `Scope#published_constant?`, the question
|
|
98
|
+
# {Analysis::CheckRules::PublishedConstantGuard} asks. `published_constant_names` is the LAST
|
|
99
|
+
# SEGMENTS of the project-wide published table (run-wide, seeded by the runner);
|
|
100
|
+
# `local_constant_names` is the last segments the ANALYSED FILE itself assigns (per file, seeded by
|
|
101
|
+
# `ScopeIndexer.index`). A name in the first and not the second was declared somewhere the reader's
|
|
102
|
+
# author cannot see. Both empty outside a runner-seeded scope, which makes the guard a no-op there.
|
|
103
|
+
published_constant_names: EMPTY_NAME_SET,
|
|
104
|
+
local_constant_names: EMPTY_NAME_SET,
|
|
67
105
|
data_member_layouts: EMPTY_TABLE,
|
|
68
106
|
struct_member_layouts: EMPTY_TABLE,
|
|
69
107
|
# ADR-67 WD3 — the call-site parameter-inference table, keyed by `[class_name, method_name, kind]` (the
|