rigortype 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +4 -3
- data/data/builtins/ruby_core/io.yml +1 -0
- data/data/builtins/ruby_core/proc.yml +4 -0
- data/data/builtins/ruby_core/random.yml +4 -0
- data/data/builtins/ruby_core/struct.yml +3 -0
- data/data/core_overlay/env.rbs +34 -0
- data/data/core_overlay/gc.rbs +50 -0
- data/data/core_overlay/hash.rbs +45 -0
- data/data/core_overlay/integer.rbs +31 -0
- data/data/core_overlay/io_buffer.rbs +27 -0
- data/data/core_overlay/kernel.rbs +40 -0
- data/data/core_overlay/match_data.rbs +28 -0
- data/data/core_overlay/module.rbs +51 -0
- data/data/core_overlay/object_space.rbs +29 -0
- data/data/core_overlay/proc.rbs +45 -0
- data/data/core_overlay/range.rbs +40 -0
- data/data/core_overlay/source_range.rbs +78 -0
- data/data/core_overlay/string.rbs +86 -0
- data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
- data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
- data/docs/handbook/01-getting-started.md +3 -3
- data/docs/handbook/06-classes.md +3 -1
- data/docs/handbook/08-understanding-errors.md +3 -2
- data/docs/handbook/11-sig-gen.md +6 -0
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/install.md +87 -15
- data/docs/manual/02-cli-reference.md +2 -2
- data/docs/manual/03-configuration.md +6 -1
- data/docs/manual/04-diagnostics.md +2 -1
- data/docs/manual/06-baseline.md +2 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/08-skills.md +4 -3
- data/docs/manual/12-caching.md +43 -4
- data/docs/manual/14-rails-quickstart.md +19 -11
- data/docs/manual/18-removing-dead-code.md +32 -2
- data/docs/manual/plugins/README.md +6 -1
- data/docs/manual/plugins/rigor-activerecord.md +21 -0
- data/docs/manual/plugins/rigor-alba.md +60 -0
- data/docs/manual/plugins/rigor-minitest.md +22 -4
- data/docs/manual/plugins/rigor-typelizer.md +71 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
- data/lib/rigor/analysis/check_rules/source_arity.rb +184 -61
- data/lib/rigor/analysis/check_rules.rb +407 -144
- data/lib/rigor/analysis/dependency_recorder.rb +215 -18
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
- data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
- data/lib/rigor/analysis/erb_template_detector.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +29 -9
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +291 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +173 -0
- data/lib/rigor/analysis/incremental_session.rb +287 -50
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
- data/lib/rigor/analysis/reachability/graph.rb +123 -23
- data/lib/rigor/analysis/reachability/scan.rb +290 -36
- data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +43 -9
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +88 -6
- data/lib/rigor/analysis/runner/pool_coordinator.rb +52 -13
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +371 -49
- data/lib/rigor/analysis/shadow_harness.rb +27 -0
- data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
- data/lib/rigor/analysis/template_unit_paths.rb +6 -4
- data/lib/rigor/analysis/template_units.rb +15 -5
- data/lib/rigor/analysis/worker_session.rb +11 -7
- data/lib/rigor/cache/descriptor.rb +40 -1
- data/lib/rigor/cache/file_digest.rb +28 -0
- data/lib/rigor/cache/incremental_snapshot.rb +92 -8
- data/lib/rigor/cache/store.rb +63 -3
- data/lib/rigor/cache/value_digest.rb +325 -0
- data/lib/rigor/cli/annotate_command.rb +5 -0
- data/lib/rigor/cli/baseline_command.rb +7 -18
- data/lib/rigor/cli/check_command.rb +85 -14
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/effects_report.rb +2 -2
- data/lib/rigor/cli/trace_command.rb +5 -0
- data/lib/rigor/cli/type_of_command.rb +5 -3
- data/lib/rigor/cli/unused_command.rb +16 -10
- data/lib/rigor/cli.rb +11 -3
- data/lib/rigor/config_audit.rb +3 -2
- data/lib/rigor/config_path.rb +29 -0
- data/lib/rigor/configuration/severity_profile.rb +11 -0
- data/lib/rigor/configuration.rb +52 -7
- data/lib/rigor/effects/plugin_facts.rb +6 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/lockfile_resolver.rb +10 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +284 -15
- data/lib/rigor/environment/required_features.rb +206 -0
- data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
- data/lib/rigor/environment.rb +34 -73
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/block_parameter_binder.rb +29 -13
- data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- data/lib/rigor/inference/captured_locals.rb +7 -1
- data/lib/rigor/inference/declaration_walk/context.rb +195 -0
- data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
- data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
- data/lib/rigor/inference/declaration_walk.rb +7 -0
- data/lib/rigor/inference/def_node_resolver.rb +26 -3
- data/lib/rigor/inference/definer_resolution.rb +266 -0
- data/lib/rigor/inference/expression_typer.rb +351 -61
- data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
- data/lib/rigor/inference/global_write_census.rb +7 -4
- data/lib/rigor/inference/guard_rebinding.rb +54 -13
- data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
- data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
- data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
- data/lib/rigor/inference/in_effect_refinements.rb +256 -0
- data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
- data/lib/rigor/inference/key_presence_guard.rb +303 -0
- data/lib/rigor/inference/macro_block_self_type.rb +73 -14
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
- data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
- data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +305 -54
- data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
- data/lib/rigor/inference/method_dispatcher.rb +44 -11
- data/lib/rigor/inference/module_function_state.rb +214 -0
- data/lib/rigor/inference/multi_target_binder.rb +21 -16
- data/lib/rigor/inference/narrowing.rb +166 -22
- data/lib/rigor/inference/object_mixins.rb +180 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
- data/lib/rigor/inference/project_patched_methods.rb +10 -5
- data/lib/rigor/inference/project_patched_scanner.rb +71 -3
- data/lib/rigor/inference/refine_census.rb +239 -0
- data/lib/rigor/inference/refine_self.rb +317 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
- data/lib/rigor/inference/refined_dispatch.rb +272 -0
- data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
- data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
- data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
- data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +143 -0
- data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
- data/lib/rigor/inference/scope_indexer.rb +2129 -354
- data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
- data/lib/rigor/inference/statement_evaluator.rb +149 -32
- data/lib/rigor/inference/string_mutation.rb +9 -3
- data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
- data/lib/rigor/inference/version_guard.rb +19 -10
- data/lib/rigor/language_server/project_context.rb +31 -0
- data/lib/rigor/language_server/server.rb +18 -0
- data/lib/rigor/plugin/base.rb +28 -8
- data/lib/rigor/plugin/inflector.rb +1 -1
- data/lib/rigor/plugin/io_boundary.rb +124 -7
- data/lib/rigor/plugin/isolation.rb +1 -1
- data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
- data/lib/rigor/plugin/trust_policy.rb +3 -3
- data/lib/rigor/project_environment.rb +29 -6
- data/lib/rigor/protection/discovery_seed.rb +18 -9
- data/lib/rigor/protection/mutation_cache.rb +4 -4
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/reflection/constant_ancestors.rb +211 -46
- data/lib/rigor/reflection/constant_path.rb +33 -7
- data/lib/rigor/reflection.rb +41 -10
- data/lib/rigor/scope/discovery_index.rb +258 -3
- data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
- data/lib/rigor/scope/resolution_chain.rb +1032 -0
- data/lib/rigor/scope.rb +480 -328
- data/lib/rigor/sig_gen/alias_index.rb +1 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
- data/lib/rigor/sig_gen/generator.rb +77 -21
- data/lib/rigor/sig_gen/method_candidate.rb +2 -2
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
- data/lib/rigor/source/alias_names.rb +52 -0
- data/lib/rigor/source/literals.rb +3 -2
- data/lib/rigor/source/symbol_name.rb +20 -0
- data/lib/rigor/type/combinator.rb +59 -3
- data/lib/rigor/type/float_range.rb +4 -1
- data/lib/rigor/type/nominal.rb +6 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
- data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
- data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
- data/plugins/rigor-alba/sig/alba.rbs +4 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +13 -2
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
- data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
- data/sig/rigor/environment.rbs +2 -0
- data/sig/rigor/plugin/io_boundary.rbs +4 -0
- data/sig/rigor/scope.rbs +69 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +3 -1
- data/skills/rigor-project-init/SKILL.md +37 -11
- data/skills/rigor-project-init/references/02-configure.md +7 -2
- metadata +90 -1
|
@@ -11,18 +11,29 @@ module Rigor
|
|
|
11
11
|
ANCESTOR_SCOPES_KEY = :__rigor_ancestor_constant_scopes__
|
|
12
12
|
private_constant :ANCESTOR_SCOPES_KEY
|
|
13
13
|
|
|
14
|
+
# The recording run's slot. See {.recorded_ancestor_constant_scopes}.
|
|
15
|
+
RECORDED_ANCESTOR_SCOPES_KEY = :__rigor_recorded_ancestor_constant_scopes__
|
|
16
|
+
private_constant :RECORDED_ANCESTOR_SCOPES_KEY
|
|
17
|
+
|
|
14
18
|
module_function
|
|
15
19
|
|
|
16
20
|
# #354 — the project classes and modules whose own constants `class_name` inherits, in Ruby's
|
|
17
|
-
# ancestor order:
|
|
18
|
-
#
|
|
21
|
+
# ancestor order: the entries of `class_name`'s `Scope::ResolutionChain` — prepended modules, then
|
|
22
|
+
# included ones, then the superclass's chain likewise — each once. `class_name` itself is excluded:
|
|
23
|
+
# step 1 already covered it.
|
|
24
|
+
#
|
|
25
|
+
# Issue #1571 — this was a breadth-first walk of its own, which is not Ruby's order: `class C < Base;
|
|
26
|
+
# include A` with `A` including `M` reached `Base::X` (depth 1) before `M::X` (depth 2), while Ruby's
|
|
27
|
+
# ancestors are `[C, A, M, Base]`. It reads the chain's `:constants` flavor, whose predicate for a
|
|
28
|
+
# project class also admits a module that holds only constants.
|
|
19
29
|
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
30
|
+
# Project ancestors appear, and (issue #1698) a module the class body `include`s or `prepend`s that
|
|
31
|
+
# only RBS or the class registry declares: `include AcLibraryRb` puts `AcLibraryRb` at its Ruby
|
|
32
|
+
# position, so `Segtree` inside the class reads `AcLibraryRb::Segtree` as Ruby does. Such an entry
|
|
33
|
+
# contributes its OWN constants only; the RBS ancestry behind it is not expanded, as for every external
|
|
34
|
+
# entry. An external SUPERCLASS (`class Foo < ActiveRecord::Base`) still contributes nothing, and a
|
|
35
|
+
# constant it owns resolves only if the bare name reaches it at step 3: widening to the superclass's
|
|
36
|
+
# RBS graph is a separate question with its own FP surface. See {.external_constant_owner}.
|
|
26
37
|
#
|
|
27
38
|
# Memoised per run because step 2 runs on every constant reference whose lexical candidates all
|
|
28
39
|
# miss — which is the common case for a core-class reference (`String` inside `class Foo`). The
|
|
@@ -30,11 +41,49 @@ module Rigor
|
|
|
30
41
|
# back to the per-file discovery table for runner-less scopes, so a re-run in one process (LSP,
|
|
31
42
|
# ADR-62 warm loop) cannot hit stale entries.
|
|
32
43
|
def ancestor_constant_scopes(class_name, scope)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
ancestor_constant_worlds(class_name, scope).first
|
|
45
|
+
end
|
|
46
|
+
private_class_method :ancestor_constant_scopes
|
|
47
|
+
|
|
48
|
+
# ADR-24 / #1570 — the first ancestor that owns a constant, asked as {.ancestor_constant_scopes} orders
|
|
49
|
+
# them and settled by `Scope::ResolutionChain#settle`: the block's first truthy answer where the chain
|
|
50
|
+
# stands, and otherwise the answer of the breadth-first order this rung used before the chain
|
|
51
|
+
# (`Scope::ResolutionChain::MasterOrder`) — where a skipped `include` would put a different owner first,
|
|
52
|
+
# which constant Ruby reads depends on the order the bodies ran.
|
|
53
|
+
def agreed_ancestor_hit(class_name, scope, &)
|
|
54
|
+
scopes, chain = ancestor_constant_worlds(class_name, scope)
|
|
55
|
+
owner, hit = first_ancestor_hit(scopes, &)
|
|
56
|
+
verdict = chain.settle(scope, owner, owner: owner) do |retro|
|
|
57
|
+
first_ancestor_hit(constant_scopes_of(retro, class_name, scope), &)&.first
|
|
58
|
+
end
|
|
59
|
+
return hit if verdict == :chain
|
|
60
|
+
|
|
61
|
+
first_ancestor_hit(master_constant_scopes(class_name, scope), &)&.last
|
|
62
|
+
end
|
|
63
|
+
private_class_method :agreed_ancestor_hit
|
|
64
|
+
|
|
65
|
+
def master_constant_scopes(class_name, scope)
|
|
66
|
+
Scope::ResolutionChain::MasterOrder.breadth_first(scope, class_name.to_s, :constants)
|
|
67
|
+
end
|
|
68
|
+
private_class_method :master_constant_scopes
|
|
69
|
+
|
|
70
|
+
def first_ancestor_hit(entries)
|
|
71
|
+
entries.each do |entry|
|
|
72
|
+
hit = yield entry
|
|
73
|
+
return [entry, hit] if hit
|
|
74
|
+
end
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
private_class_method :first_ancestor_hit
|
|
78
|
+
|
|
79
|
+
# `[scopes, chain]` — {.ancestor_constant_scopes} and the chain they were read from, which settles the
|
|
80
|
+
# answer.
|
|
81
|
+
def ancestor_constant_worlds(class_name, scope)
|
|
82
|
+
# ADR-46: the answer depends on every class on the chain, and the memo is run-scoped rather than
|
|
83
|
+
# file-scoped — a hit would skip the recording and under-record the edge for every later file. A
|
|
84
|
+
# recording run reads the chain, which is memoised per discovery index, and files its edges on
|
|
85
|
+
# every call.
|
|
86
|
+
return recorded_ancestor_constant_scopes(class_name, scope) if Analysis::DependencyRecorder.active?
|
|
38
87
|
|
|
39
88
|
generation = scope.run_generation || scope.discovered_superclasses
|
|
40
89
|
slot = Thread.current[ANCESTOR_SCOPES_KEY]
|
|
@@ -45,48 +94,164 @@ module Rigor
|
|
|
45
94
|
bucket = slot[1]
|
|
46
95
|
bucket.fetch(class_name) { bucket[class_name] = compute_ancestor_constant_scopes(class_name, scope) }
|
|
47
96
|
end
|
|
48
|
-
private_class_method :
|
|
97
|
+
private_class_method :ancestor_constant_worlds
|
|
98
|
+
|
|
99
|
+
# The recording run's memo. The answer is every project entry of the chain, so its edges are the
|
|
100
|
+
# root's and every entry's — what `Scope::ResolutionChain#record` files, which a computation does and a
|
|
101
|
+
# hit replays, in the same order. One slot keyed on the discovery index: the answer and the edges are
|
|
102
|
+
# both read from it, so the key is exact, and the slot never pins more than the index it serves.
|
|
103
|
+
def recorded_ancestor_constant_scopes(class_name, scope)
|
|
104
|
+
discovery = scope.discovery
|
|
105
|
+
slot = Thread.current[RECORDED_ANCESTOR_SCOPES_KEY]
|
|
106
|
+
unless slot && slot[0].equal?(discovery)
|
|
107
|
+
slot = [discovery, {}]
|
|
108
|
+
Thread.current[RECORDED_ANCESTOR_SCOPES_KEY] = slot
|
|
109
|
+
end
|
|
110
|
+
bucket = slot[1]
|
|
111
|
+
worlds = bucket[class_name]
|
|
112
|
+
return bucket[class_name] = compute_ancestor_constant_scopes(class_name, scope) if worlds.nil?
|
|
113
|
+
|
|
114
|
+
chain = Scope::ResolutionChain.for(scope, class_name.to_s, :instance, :constants)
|
|
115
|
+
chain.record(scope)
|
|
116
|
+
record_external_owner_edges(chain)
|
|
117
|
+
worlds
|
|
118
|
+
end
|
|
119
|
+
private_class_method :recorded_ancestor_constant_scopes
|
|
49
120
|
|
|
121
|
+
# Reads the chain whole, so a recording run files every class on it (a no-op otherwise).
|
|
50
122
|
def compute_ancestor_constant_scopes(class_name, scope)
|
|
51
|
-
|
|
52
|
-
|
|
123
|
+
chain = Scope::ResolutionChain.for(scope, class_name.to_s, :instance, :constants)
|
|
124
|
+
chain.record(scope)
|
|
125
|
+
record_external_owner_edges(chain)
|
|
126
|
+
scopes = constant_scopes_of(chain, class_name, scope)
|
|
127
|
+
[scopes, chain].freeze
|
|
128
|
+
end
|
|
129
|
+
private_class_method :compute_ancestor_constant_scopes
|
|
130
|
+
|
|
131
|
+
# The chain's project entries in order, each once, without `class_name` itself, with each mixin entry
|
|
132
|
+
# the project does not declare standing as the module {.external_constant_owner} names.
|
|
133
|
+
def constant_scopes_of(chain, class_name, scope)
|
|
53
134
|
out = []
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
scope.includes_of(current).each do |raw|
|
|
58
|
-
resolved = resolve_ancestor_name(current, raw, scope)
|
|
59
|
-
next if resolved.nil? || seen[resolved]
|
|
60
|
-
|
|
61
|
-
seen[resolved] = true
|
|
62
|
-
out << resolved
|
|
63
|
-
queue << resolved
|
|
64
|
-
end
|
|
65
|
-
raw_super = scope.superclass_of(current)
|
|
66
|
-
next if raw_super.nil?
|
|
67
|
-
|
|
68
|
-
resolved_super = resolve_ancestor_name(current, raw_super, scope)
|
|
69
|
-
next if resolved_super.nil? || seen[resolved_super]
|
|
70
|
-
|
|
71
|
-
seen[resolved_super] = true
|
|
72
|
-
out << resolved_super
|
|
73
|
-
queue << resolved_super
|
|
135
|
+
chain.entries.each do |entry|
|
|
136
|
+
name = entry.external? ? external_constant_owner(entry, scope) : entry.name
|
|
137
|
+
out << name unless name.nil? || name == class_name || out.include?(name)
|
|
74
138
|
end
|
|
75
139
|
out.freeze
|
|
76
140
|
end
|
|
77
|
-
private_class_method :
|
|
141
|
+
private_class_method :constant_scopes_of
|
|
142
|
+
|
|
143
|
+
# Issue #1698 — the module an external `include` / `prepend` entry names, when the constant ladder may
|
|
144
|
+
# read its constants, or nil. Ruby resolved the spelling at the `include` lexically, so the first of the
|
|
145
|
+
# entry's candidates (most qualified first) that exists is the module. The answer is the first candidate
|
|
146
|
+
# RBS or the class registry knows, and nil when a nearer candidate is a constant the project binds
|
|
147
|
+
# ({.project_written_candidate?}): Ruby's module is then that constant, whose constants nothing here
|
|
148
|
+
# describes. The candidates are the entry's own, behind the namespace of the body that wrote the edge
|
|
149
|
+
# (`entry.owner`), which Ruby searches first and the chain's resolver does not consult. An ambiguous
|
|
150
|
+
# spelling has no candidates and answers nil. A superclass edge answers nil (see
|
|
151
|
+
# {.ancestor_constant_scopes}).
|
|
152
|
+
def external_constant_owner(entry, scope)
|
|
153
|
+
return nil if entry.superclass_edge || entry.candidates.empty?
|
|
154
|
+
|
|
155
|
+
env = scope.environment
|
|
156
|
+
external_owner_candidates(entry).each do |candidate|
|
|
157
|
+
return nil if project_written_candidate?(candidate, scope)
|
|
158
|
+
return candidate if env.class_known?(candidate)
|
|
159
|
+
end
|
|
160
|
+
nil
|
|
161
|
+
end
|
|
162
|
+
private_class_method :external_constant_owner
|
|
163
|
+
|
|
164
|
+
def external_owner_candidates(entry)
|
|
165
|
+
return entry.candidates if entry.owner.nil? || entry.raw.start_with?("::")
|
|
166
|
+
|
|
167
|
+
["#{entry.owner}::#{entry.raw}", *entry.candidates]
|
|
168
|
+
end
|
|
169
|
+
private_class_method :external_owner_candidates
|
|
170
|
+
|
|
171
|
+
# ADR-46 — the name edges of {.external_constant_owner}'s guard, for every mixin entry it read: a file that
|
|
172
|
+
# starts writing or declaring a nearer spelling of the module's name (`User::AcLibraryRb = …` in a file the
|
|
173
|
+
# reader has no other edge to) moves the answer. Keyed on the spelling's last segment, as the ladder's own
|
|
174
|
+
# `constant:` and `class:` edges are. A memo hit replays them beside the chain's own edges.
|
|
175
|
+
def record_external_owner_edges(chain)
|
|
176
|
+
return unless Analysis::DependencyRecorder.active?
|
|
177
|
+
|
|
178
|
+
chain.entries.each do |entry|
|
|
179
|
+
next unless entry.external? && !entry.superclass_edge
|
|
180
|
+
|
|
181
|
+
Analysis::DependencyRecorder.read_last_segment(:constant, entry.raw)
|
|
182
|
+
Analysis::DependencyRecorder.read_last_segment(:class, entry.raw)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
private_class_method :record_external_owner_edges
|
|
186
|
+
|
|
187
|
+
# Issue #1698 — whether the walk must stop at `owner` because a module its RBS ancestry holds may own the name
|
|
188
|
+
# (`candidate` answers for it). Ruby searches that ancestry right after `owner`, before any later entry of
|
|
189
|
+
# the chain and before the top level, and the chain does not expand it. Where no module of it owns the
|
|
190
|
+
# name, where they sit does not matter to this name and the walk goes on.
|
|
191
|
+
#
|
|
192
|
+
# `owner` is an RBS-only mixin (`external`), or a project entry RBS also declares: a project file that
|
|
193
|
+
# reopens an RBS module without restating its `include`s makes it a project entry whose chain lists only
|
|
194
|
+
# the edges the project writes, so its ancestry is read less what that chain lists. The ancestry comes from
|
|
195
|
+
# the declarations (`RbsLoader#declared_ancestry`), so a definition that fails to build still yields what
|
|
196
|
+
# it declares. Where it cannot be read whole, {.ancestry_unknown?} keeps the later RBS-only mixins out
|
|
197
|
+
# instead of stopping here.
|
|
198
|
+
def mixin_ancestry_stop?(owner, scope, external)
|
|
199
|
+
loader = rbs_loader_for(scope, nil)
|
|
200
|
+
return false if loader.nil? || (!external && !loader.class_known?(owner))
|
|
201
|
+
|
|
202
|
+
names, = loader.declared_ancestry(owner)
|
|
203
|
+
listed = nil
|
|
204
|
+
names.any? do |ancestor|
|
|
205
|
+
next false if IMPLICIT_ANCESTORS.include?(ancestor)
|
|
206
|
+
|
|
207
|
+
listed ||= external ? EMPTY_NAMES : project_chain_names(owner, scope)
|
|
208
|
+
!listed.include?(ancestor) && yield(ancestor)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
private_class_method :mixin_ancestry_stop?
|
|
212
|
+
|
|
213
|
+
# Issue #1698 — whether an entry ahead of `entry` on `class_name`'s list has an RBS ancestry that cannot be
|
|
214
|
+
# read whole ({.mixin_ancestry_stop?}): a module it hides may own the name, so `entry`, an RBS-only mixin,
|
|
215
|
+
# MUST NOT answer. Only those entries are kept out; project entries, the superclass chain and the top level
|
|
216
|
+
# answer as they did before #1698.
|
|
217
|
+
def ancestry_unknown_before?(class_name, entry, scope)
|
|
218
|
+
loader = rbs_loader_for(scope, nil)
|
|
219
|
+
return true if loader.nil?
|
|
220
|
+
|
|
221
|
+
ancestor_constant_scopes(class_name, scope).each do |earlier|
|
|
222
|
+
break if earlier == entry
|
|
223
|
+
next unless loader.class_known?(earlier)
|
|
224
|
+
|
|
225
|
+
return true unless loader.declared_ancestry(earlier)[1]
|
|
226
|
+
end
|
|
227
|
+
false
|
|
228
|
+
end
|
|
229
|
+
private_class_method :ancestry_unknown_before?
|
|
230
|
+
|
|
231
|
+
# Ruby places these after every entry of a chain, so a constant they own is the top level's (step 4).
|
|
232
|
+
IMPLICIT_ANCESTORS = %w[Object Kernel BasicObject].freeze
|
|
233
|
+
private_constant :IMPLICIT_ANCESTORS
|
|
234
|
+
|
|
235
|
+
EMPTY_NAMES = [].freeze
|
|
236
|
+
private_constant :EMPTY_NAMES
|
|
237
|
+
|
|
238
|
+
# Every name `owner`'s own instance chain lists: its project entries and each external entry's candidates.
|
|
239
|
+
def project_chain_names(owner, scope)
|
|
240
|
+
Scope::ResolutionChain.for(scope, owner, :instance, :constants).entries.flat_map do |entry|
|
|
241
|
+
entry.external? ? entry.candidates : [entry.name]
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
private_class_method :project_chain_names
|
|
78
245
|
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
# discovered project class or module.
|
|
84
|
-
def resolve_ancestor_name(subclass_qualified, raw, scope)
|
|
85
|
-
scope.ancestor_name_candidates(subclass_qualified, raw)
|
|
86
|
-
.find { |candidate| known_project_namespace?(candidate, scope) }
|
|
246
|
+
# Whether the project binds `candidate` itself: a namespace under the `:constants` flavor's tables, or a
|
|
247
|
+
# constant write the census records (#1290), typed or not.
|
|
248
|
+
def project_written_candidate?(candidate, scope)
|
|
249
|
+
known_project_namespace?(candidate, scope) || scope.shadowing_constant_names(candidate).include?(candidate)
|
|
87
250
|
end
|
|
88
|
-
private_class_method :
|
|
251
|
+
private_class_method :project_written_candidate?
|
|
89
252
|
|
|
253
|
+
# Whether `name` is a namespace the project declares — the predicate the chain's `:constants` flavor
|
|
254
|
+
# resolves ancestor names with, and the path walk's (`reflection/constant_path.rb`) test for a head.
|
|
90
255
|
def known_project_namespace?(name, scope)
|
|
91
256
|
scope.discovered_superclasses.key?(name) ||
|
|
92
257
|
scope.discovered_includes.key?(name) ||
|
|
@@ -81,7 +81,10 @@ module Rigor
|
|
|
81
81
|
|
|
82
82
|
prefix = caller_derived ? enclosing_class_path(scope) : nil
|
|
83
83
|
unless prefix.nil? || prefix.empty?
|
|
84
|
-
hit =
|
|
84
|
+
hit = bounded_agreed_hit(prefix, scope) do |entry|
|
|
85
|
+
candidate = "#{entry}::#{head}"
|
|
86
|
+
candidate if namespace_known?(candidate, scope)
|
|
87
|
+
end
|
|
85
88
|
return hit if hit
|
|
86
89
|
end
|
|
87
90
|
|
|
@@ -106,28 +109,51 @@ module Rigor
|
|
|
106
109
|
candidate = "#{owner}::#{segment}"
|
|
107
110
|
return candidate if yield(candidate)
|
|
108
111
|
|
|
109
|
-
|
|
112
|
+
bounded_agreed_hit(owner, scope) do |ancestor|
|
|
110
113
|
inherited = "#{ancestor}::#{segment}"
|
|
111
|
-
|
|
114
|
+
inherited if yield(inherited)
|
|
112
115
|
end
|
|
113
|
-
nil
|
|
114
116
|
end
|
|
115
117
|
private_class_method :constant_in_namespace
|
|
116
118
|
|
|
119
|
+
# {.agreed_ancestor_hit} over the budget-bounded scope lists: the first answer where
|
|
120
|
+
# `Scope::ResolutionChain#settle` lets the chain stand, and otherwise the breadth-first order's.
|
|
121
|
+
#
|
|
122
|
+
# Issue #1698 — the path walk reads the project entries only: an RBS-only mixin's own ancestry is not on the
|
|
123
|
+
# chain, so the rung that reads such a mixin also stops behind it ({.mixin_ancestry_stop?}), and this
|
|
124
|
+
# walk keeps the answer it gave before.
|
|
125
|
+
def bounded_agreed_hit(class_name, scope, &)
|
|
126
|
+
scopes, chain = ancestor_constant_worlds(class_name, scope)
|
|
127
|
+
owner, hit = first_ancestor_hit(bounded(project_scopes(scopes, scope)), &)
|
|
128
|
+
verdict = chain.settle(scope, owner, owner: owner) do |retro|
|
|
129
|
+
first_ancestor_hit(bounded(project_scopes(constant_scopes_of(retro, class_name, scope), scope)), &)&.first
|
|
130
|
+
end
|
|
131
|
+
return hit if verdict == :chain
|
|
132
|
+
|
|
133
|
+
first_ancestor_hit(bounded(master_constant_scopes(class_name, scope)), &)&.last
|
|
134
|
+
end
|
|
135
|
+
private_class_method :bounded_agreed_hit
|
|
136
|
+
|
|
137
|
+
def project_scopes(scopes, scope)
|
|
138
|
+
return scopes if scopes.all? { |name| known_project_namespace?(name, scope) }
|
|
139
|
+
|
|
140
|
+
scopes.select { |name| known_project_namespace?(name, scope) }
|
|
141
|
+
end
|
|
142
|
+
private_class_method :project_scopes
|
|
143
|
+
|
|
117
144
|
# How many of {.ancestor_constant_scopes}' entries one segment may consult, under
|
|
118
145
|
# `Scope::ANCESTOR_WALK_LIMIT` — the budget `Scope`'s method lookup already spends on this same
|
|
119
146
|
# graph, reused rather than re-chosen so one pathological ancestry costs one bound. The list
|
|
120
147
|
# itself is the memoised one step 2 builds, so this bounds the per-segment consultation the walk
|
|
121
148
|
# adds and not the graph. Reported through the shared counter, so a truncated walk is visible in a
|
|
122
149
|
# budget trace instead of looking like a plain miss.
|
|
123
|
-
def
|
|
124
|
-
scopes = ancestor_constant_scopes(class_name, scope)
|
|
150
|
+
def bounded(scopes)
|
|
125
151
|
return scopes if scopes.size <= Scope::ANCESTOR_WALK_LIMIT
|
|
126
152
|
|
|
127
153
|
Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT)
|
|
128
154
|
scopes.first(Scope::ANCESTOR_WALK_LIMIT)
|
|
129
155
|
end
|
|
130
|
-
private_class_method :
|
|
156
|
+
private_class_method :bounded
|
|
131
157
|
|
|
132
158
|
# Whether `name` can OWN a constant. `class_known?` covers the RBS and registry classes and modules;
|
|
133
159
|
# the project tables cover a namespace the project declares with no RBS for it.
|
data/lib/rigor/reflection.rb
CHANGED
|
@@ -50,6 +50,11 @@ module Rigor
|
|
|
50
50
|
WRITTEN_CANDIDATE = Object.new.freeze
|
|
51
51
|
private_constant :WRITTEN_CANDIDATE
|
|
52
52
|
|
|
53
|
+
# Issue #1698 — what the constant ladder's ancestor rung answers where an entry's RBS ancestry may
|
|
54
|
+
# own the name ({.mixin_ancestry_stop?}). Never a type: {.lexical_constant_type} replaces it.
|
|
55
|
+
MIXIN_ANCESTRY_STOP = Object.new.freeze
|
|
56
|
+
private_constant :MIXIN_ANCESTRY_STOP
|
|
57
|
+
|
|
53
58
|
module_function
|
|
54
59
|
|
|
55
60
|
def class_known?(class_name, scope: Scope.empty)
|
|
@@ -197,10 +202,19 @@ module Rigor
|
|
|
197
202
|
# `P::A < P::Base` is `P::Base::B`, and step 3's `A::B` is a real but different class.
|
|
198
203
|
#
|
|
199
204
|
# Step 3 — the bare name (top level). It takes no `shadows`: nothing sits below it for a stop to replace.
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
205
|
+
#
|
|
206
|
+
# Issue #1698 — step 2 answers {MIXIN_ANCESTRY_STOP} where an RBS-only mixin's own ancestry may own the name
|
|
207
|
+
# ({.mixin_ancestry_stop?}). Ruby reads that ancestry before any later ancestor or the top level, and
|
|
208
|
+
# the chain does not list it, so a lower rung's answer is replaced by `Dynamic[top]`, as #1290's stop is.
|
|
209
|
+
lexical = first_constant_hit(lexical_nesting_chain(scope), name, scope, shadows)
|
|
210
|
+
return lexical if lexical
|
|
211
|
+
|
|
212
|
+
ancestor = ancestor_constant_type(name, scope, enclosing_class_path(scope), shadows)
|
|
213
|
+
stopped = ancestor.equal?(MIXIN_ANCESTRY_STOP)
|
|
214
|
+
return ancestor if ancestor && !stopped
|
|
215
|
+
|
|
216
|
+
below = constant_path_type(name, scope, shadows: shadows) || constant_type_at(name, scope)
|
|
217
|
+
stopped && below ? Type::Combinator.untyped : below
|
|
204
218
|
end
|
|
205
219
|
private_class_method :lexical_constant_type
|
|
206
220
|
|
|
@@ -216,11 +230,28 @@ module Rigor
|
|
|
216
230
|
end
|
|
217
231
|
private_class_method :first_constant_hit
|
|
218
232
|
|
|
219
|
-
# Step 2's rung on its own: nil when there is no enclosing class path to take ancestors of
|
|
233
|
+
# Step 2's rung on its own: nil when there is no enclosing class path to take ancestors of, and
|
|
234
|
+
# {MIXIN_ANCESTRY_STOP} where an RBS-only mixin's ancestry may own the name.
|
|
235
|
+
#
|
|
236
|
+
# Issue #1698 — an entry that stands for an RBS-only mixin ({.external_constant_owner}) is skipped where `self`
|
|
237
|
+
# is a class object: a `class << self` body and a `def` in it run under the singleton class's cref, whose
|
|
238
|
+
# ancestors are not the class's ([#1305](https://github.com/rigortype/rigor/issues/1305)), and the scope does
|
|
239
|
+
# not tell such a body from a `def self.m` or the class body. Skipping keeps the answer this rung gave before
|
|
240
|
+
# #1698 there.
|
|
220
241
|
def ancestor_constant_type(name, scope, prefix, shadows)
|
|
221
242
|
return nil if prefix.nil? || prefix.empty?
|
|
222
243
|
|
|
223
|
-
|
|
244
|
+
class_object_self = scope.self_type.is_a?(Type::Singleton)
|
|
245
|
+
agreed_ancestor_hit(prefix, scope) do |entry|
|
|
246
|
+
external = !known_project_namespace?(entry, scope)
|
|
247
|
+
next nil if external && (class_object_self || ancestry_unknown_before?(prefix, entry, scope))
|
|
248
|
+
|
|
249
|
+
candidate = ->(owner) { constant_type_at("#{owner}::#{name}", scope, shadows) }
|
|
250
|
+
hit = candidate.call(entry)
|
|
251
|
+
next hit if hit
|
|
252
|
+
|
|
253
|
+
MIXIN_ANCESTRY_STOP if mixin_ancestry_stop?(entry, scope, external, &candidate)
|
|
254
|
+
end
|
|
224
255
|
end
|
|
225
256
|
private_class_method :ancestor_constant_type
|
|
226
257
|
|
|
@@ -264,8 +295,9 @@ module Rigor
|
|
|
264
295
|
prefix = enclosing_class_path(scope)
|
|
265
296
|
return nil if prefix.nil? || prefix.empty?
|
|
266
297
|
|
|
267
|
-
first_constant_hit(peeled_nesting(prefix), name, scope, shadows) ||
|
|
268
|
-
|
|
298
|
+
hit = first_constant_hit(peeled_nesting(prefix), name, scope, shadows) ||
|
|
299
|
+
ancestor_constant_type(name, scope, prefix, shadows)
|
|
300
|
+
hit.equal?(MIXIN_ANCESTRY_STOP) ? nil : hit
|
|
269
301
|
end
|
|
270
302
|
private_class_method :caller_derived_constant_type
|
|
271
303
|
|
|
@@ -322,8 +354,7 @@ module Rigor
|
|
|
322
354
|
# ({Analysis::Incremental.changed_constant_publications}). Gated by the caller on the recorder being
|
|
323
355
|
# active, so an ordinary run pays one integer read.
|
|
324
356
|
def record_constant_reference(name)
|
|
325
|
-
|
|
326
|
-
Analysis::DependencyRecorder.read_name(:constant, segment) if segment
|
|
357
|
+
Analysis::DependencyRecorder.read_last_segment(:constant, name)
|
|
327
358
|
end
|
|
328
359
|
private_class_method :record_constant_reference
|
|
329
360
|
|