rigortype 0.4.1 → 0.4.3
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/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/11-sig-gen.md +18 -5
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/manual/02-cli-reference.md +19 -1
- data/docs/manual/03-configuration.md +7 -5
- data/docs/manual/04-diagnostics.md +2 -1
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/06-baseline.md +2 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/12-caching.md +18 -1
- data/docs/manual/14-rails-quickstart.md +2 -2
- data/docs/manual/18-removing-dead-code.md +41 -4
- 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 +11 -0
- data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
- data/lib/rigor/analysis/check_rules.rb +292 -59
- data/lib/rigor/analysis/dependency_recorder.rb +15 -0
- 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/effects_cache_probe.rb +1 -1
- 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 +30 -5
- data/lib/rigor/analysis/incremental_run_slot.rb +6 -4
- data/lib/rigor/analysis/incremental_session.rb +103 -23
- data/lib/rigor/analysis/path_expansion.rb +62 -6
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
- data/lib/rigor/analysis/reachability/graph.rb +241 -37
- data/lib/rigor/analysis/reachability/scan.rb +490 -77
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +16 -1
- data/lib/rigor/analysis/run_cache_probe.rb +1 -1
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +113 -6
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -19
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +62 -13
- data/lib/rigor/analysis/worker_session.rb +8 -5
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +55 -5
- 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 +140 -40
- data/lib/rigor/cli/docs_command.rb +5 -4
- data/lib/rigor/cli/lens_command.rb +144 -0
- data/lib/rigor/cli/lsp_command.rb +5 -2
- data/lib/rigor/cli/skill_command.rb +3 -3
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/trace_command.rb +5 -0
- data/lib/rigor/cli/type_of_command.rb +6 -3
- data/lib/rigor/cli/unused_command.rb +166 -27
- data/lib/rigor/cli.rb +39 -5
- data/lib/rigor/configuration/severity_profile.rb +6 -0
- data/lib/rigor/configuration.rb +61 -2
- data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/installed_gem_set.rb +38 -11
- data/lib/rigor/environment/lockfile_resolver.rb +37 -6
- data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +316 -14
- data/lib/rigor/environment/required_features.rb +208 -0
- data/lib/rigor/environment.rb +33 -8
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/builtins/cruby_definers.rb +78 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- data/lib/rigor/inference/captured_locals.rb +43 -3
- data/lib/rigor/inference/def_node_resolver.rb +23 -1
- data/lib/rigor/inference/expression_typer.rb +261 -29
- data/lib/rigor/inference/global_write_census.rb +7 -4
- data/lib/rigor/inference/guard_rebinding.rb +1 -1
- 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 +286 -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 +59 -12
- data/lib/rigor/inference/match_rebinding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -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/kernel_dispatch.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
- data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +247 -28
- data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
- data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +96 -14
- data/lib/rigor/inference/module_function_state.rb +2 -1
- data/lib/rigor/inference/narrowing.rb +20 -6
- data/lib/rigor/inference/object_mixins.rb +180 -0
- data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
- data/lib/rigor/inference/project_patched_methods.rb +12 -7
- data/lib/rigor/inference/project_patched_scanner.rb +108 -26
- data/lib/rigor/inference/refine_census.rb +239 -0
- data/lib/rigor/inference/refine_self.rb +317 -0
- data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +475 -0
- data/lib/rigor/inference/refined_dispatch.rb +320 -0
- data/lib/rigor/inference/repeated_or_writes.rb +2 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
- data/lib/rigor/inference/scope_indexer.rb +827 -65
- data/lib/rigor/inference/statement_evaluator.rb +294 -51
- data/lib/rigor/inference/string_mutation.rb +9 -3
- data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
- data/lib/rigor/inference/untyped_rejoin.rb +75 -0
- data/lib/rigor/inference/version_guard.rb +19 -10
- data/lib/rigor/language_server/project_context.rb +87 -5
- data/lib/rigor/language_server/server.rb +18 -0
- data/lib/rigor/lens/anchor.rb +115 -0
- data/lib/rigor/lens/contract_reader.rb +118 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +433 -0
- data/lib/rigor/lens/members.rb +153 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +280 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +40 -0
- data/lib/rigor/plugin/inflector.rb +1 -1
- data/lib/rigor/plugin/io_boundary.rb +42 -3
- data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
- data/lib/rigor/project_environment.rb +48 -6
- data/lib/rigor/protection/mutation_cache.rb +3 -2
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/reflection/constant_ancestors.rb +127 -11
- data/lib/rigor/reflection/constant_path.rb +13 -2
- data/lib/rigor/reflection.rb +40 -8
- data/lib/rigor/scope/discovery_index.rb +40 -1
- data/lib/rigor/scope/resolution_chain.rb +159 -11
- data/lib/rigor/scope.rb +211 -21
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +100 -10
- 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 +34 -2
- 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-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -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 +7 -6
- 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/plugin/io_boundary.rbs +3 -0
- data/sig/rigor/scope.rbs +30 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +9 -4
- data/skills/rigor-project-init/references/02-configure.md +2 -2
- metadata +83 -1
|
@@ -54,7 +54,7 @@ module Rigor
|
|
|
54
54
|
# by `rigor plugins` (ADR-90) to probe {available?} at activation time — so a standalone install
|
|
55
55
|
# where inflection silently degrades reports the degradation instead of an unqualified `[OK]`. When
|
|
56
56
|
# a new plugin adopts the helper, add its manifest id here.
|
|
57
|
-
CONSUMER_PLUGIN_IDS = %w[actionmailer actionpack activerecord factorybot rails-routes].freeze
|
|
57
|
+
CONSUMER_PLUGIN_IDS = %w[actionmailer actionpack activerecord alba factorybot rails-routes].freeze
|
|
58
58
|
|
|
59
59
|
# Raised when `ActiveSupport::Inflector` is required for an inflection but cannot be loaded. Caught by
|
|
60
60
|
# the per-plugin isolation boundary, so it surfaces as "this plugin produced no diagnostics" rather than
|
|
@@ -199,6 +199,44 @@ module Rigor
|
|
|
199
199
|
entries
|
|
200
200
|
end
|
|
201
201
|
|
|
202
|
+
# Issue #1652 — the MEMBERSHIP fingerprint of a `Dir.glob` pattern under `root`, `**` included. Returns the
|
|
203
|
+
# matching paths and records one `:names` {Cache::Descriptor::GlobEntry} over `root` and `pattern`: the row
|
|
204
|
+
# reads stale when a match appears or disappears, in a subdirectory created after the read too, and NOT
|
|
205
|
+
# when a match's bytes or stat tuple move. A caller whose answer depends on a match's content MUST
|
|
206
|
+
# {#read_file} each match it reads; that row carries the edit. A stat signature here would read stale on
|
|
207
|
+
# every `touch`, checkout or restored CI cache, though the content the caller read is unchanged (#979's
|
|
208
|
+
# reasoning). {#list_directory} keeps its `:stat` listing row.
|
|
209
|
+
#
|
|
210
|
+
# The pattern must stay under `root`, since the policy is checked against `root` alone: a `..` segment, a
|
|
211
|
+
# brace (`{..,x}`) or a backslash escape (`.\\./`) raises {AccessDeniedError} (`:read_outside_scope`).
|
|
212
|
+
# The last two are refused outright because `Dir.glob` can spell `..` with them. Out of policy, `root` is
|
|
213
|
+
# globbed truthfully and contributes no row, as in {#list_directory}.
|
|
214
|
+
#
|
|
215
|
+
# @param root — project directory; relative paths expand against the working directory
|
|
216
|
+
# @param pattern — a `Dir.glob` pattern relative to `root`, such as `"ext/**/extconf.rb"`
|
|
217
|
+
# @return the absolute matching paths in `Dir.glob` order
|
|
218
|
+
def glob(root, pattern)
|
|
219
|
+
absolute = File.absolute_path(root.to_s)
|
|
220
|
+
pattern = pattern.to_s
|
|
221
|
+
if pattern.match?(/[{\\]/) || pattern.split("/").include?("..")
|
|
222
|
+
raise AccessDeniedError.new(
|
|
223
|
+
"plugin #{@plugin_id.inspect} cannot glob #{pattern.inspect} under #{absolute.inspect}: " \
|
|
224
|
+
"a `..` segment, a brace or a backslash can leave the root the trusted-read scope was checked against",
|
|
225
|
+
reason: :read_outside_scope,
|
|
226
|
+
resource: File.join(absolute, pattern)
|
|
227
|
+
)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
entries = Dir.glob(File.join(absolute, pattern))
|
|
231
|
+
unless @policy.allow_read?(absolute)
|
|
232
|
+
record_refusal(absolute)
|
|
233
|
+
return entries
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
record_glob_entry(absolute, pattern, mode: :names)
|
|
237
|
+
entries
|
|
238
|
+
end
|
|
239
|
+
|
|
202
240
|
# Fetches the URL when the policy permits it. Returns the response body. Raises {AccessDeniedError} when
|
|
203
241
|
# the policy is `:disabled`, the URL scheme is not `https`, the host is not on the allowlist, the
|
|
204
242
|
# response is non-2xx, the body exceeds {URL_MAX_BYTES}, or the request times out
|
|
@@ -352,11 +390,12 @@ module Rigor
|
|
|
352
390
|
self.class.attribute(entry)
|
|
353
391
|
end
|
|
354
392
|
|
|
355
|
-
# ADR-45 WD1c (#629) — the listing row for {#list_directory}
|
|
393
|
+
# ADR-45 WD1c (#629) — the listing row for {#list_directory} (and the `:names` row for {#glob}),
|
|
394
|
+
# deduplicated per (root, pattern, mode) slot.
|
|
356
395
|
# Recomputed on each call rather than `||=`d, so a directory listed again after a mid-run mutation
|
|
357
396
|
# carries the signature of the state the LAST reader saw.
|
|
358
|
-
def record_glob_entry(root, pattern)
|
|
359
|
-
entry = Cache::Descriptor::GlobEntry.compute(root: root, pattern: pattern)
|
|
397
|
+
def record_glob_entry(root, pattern, mode: :stat)
|
|
398
|
+
entry = Cache::Descriptor::GlobEntry.compute(root: root, pattern: pattern, mode: mode)
|
|
360
399
|
@mutex.synchronize { @glob_entries[entry.slot_key] = entry }
|
|
361
400
|
self.class.attribute(entry)
|
|
362
401
|
end
|
|
@@ -42,8 +42,20 @@ module Rigor
|
|
|
42
42
|
# `Nominal[Grape::Endpoint]` (the block is `instance_eval`'d on an instance of that class, e.g. a
|
|
43
43
|
# route body on `Grape::Endpoint`), and `"singleton(Grape::API::Instance)"` binds
|
|
44
44
|
# `Singleton[Grape::API::Instance]` (the block is `instance_eval`'d on that *class object*, e.g.
|
|
45
|
-
# a `namespace` body on `Grape::API::Instance`).
|
|
46
|
-
#
|
|
45
|
+
# a `namespace` body on `Grape::API::Instance`). `:lexical` (issue #1667) leaves `self` as the caller's:
|
|
46
|
+
# the method runs the block where it was written (`block.refined(M).call`), so the entry only declares
|
|
47
|
+
# `refinements:`. Reserved Symbol names (`:receiver_singleton`, `:dsl_recorder`) remain unaccepted.
|
|
48
|
+
# - `refinements` — Array of fully-qualified module-name Strings (default `[]`, issue #1667, ADR-121 WD5).
|
|
49
|
+
# The method runs the block under `Proc#refined` with these modules (`instance_exec(&block.refined(M))`),
|
|
50
|
+
# so they are appended to the block body's in-effect refinements after its lexical list, in the declared
|
|
51
|
+
# order (the last one wins). Nested blocks inherit them. A module that refines nothing the project or a
|
|
52
|
+
# gem's source inference makes visible contributes nothing.
|
|
53
|
+
#
|
|
54
|
+
# ## Matching
|
|
55
|
+
#
|
|
56
|
+
# A `:receiver_instance` or `singleton(...)` entry matches `Singleton[X]` receivers only (a class-level DSL
|
|
57
|
+
# call). A named instance binding (`"Foo::Bar"`) and a `:lexical` entry also match `Nominal[X]` receivers:
|
|
58
|
+
# a `:lexical` method is as often an instance method (`ActiveRecord::Relation#where`) as a class-level one.
|
|
47
59
|
#
|
|
48
60
|
# ## Ractor-shareability
|
|
49
61
|
#
|
|
@@ -51,7 +63,8 @@ module Rigor
|
|
|
51
63
|
# mutable array does not leak into the value. `Ractor.shareable?` returns true after `#initialize`.
|
|
52
64
|
class BlockAsMethod
|
|
53
65
|
SELF_TYPE_RECEIVER_INSTANCE = :receiver_instance
|
|
54
|
-
|
|
66
|
+
SELF_TYPE_LEXICAL = :lexical
|
|
67
|
+
VALID_SELF_TYPES = [SELF_TYPE_RECEIVER_INSTANCE, SELF_TYPE_LEXICAL].freeze
|
|
55
68
|
|
|
56
69
|
CLASS_NAME_PATTERN = /[A-Z]\w*(?:::[A-Z]\w*)*/
|
|
57
70
|
# `self_type: "Foo::Bar"` — the block is `instance_eval`'d on an instance of `Foo::Bar`.
|
|
@@ -60,16 +73,18 @@ module Rigor
|
|
|
60
73
|
# object itself (Grape's `namespace`/`route_param` bodies run on `Grape::API::Instance`).
|
|
61
74
|
SINGLETON_SELF_TYPE_PATTERN = /\Asingleton\((#{CLASS_NAME_PATTERN})\)\z/
|
|
62
75
|
|
|
63
|
-
attr_reader :receiver_constraint, :method_names, :self_type
|
|
76
|
+
attr_reader :receiver_constraint, :method_names, :self_type, :refinements
|
|
64
77
|
|
|
65
|
-
def initialize(receiver_constraint:, method_names:, self_type: SELF_TYPE_RECEIVER_INSTANCE)
|
|
78
|
+
def initialize(receiver_constraint:, method_names:, self_type: SELF_TYPE_RECEIVER_INSTANCE, refinements: [])
|
|
66
79
|
validate_receiver_constraint!(receiver_constraint)
|
|
67
80
|
validate_method_names!(method_names)
|
|
68
81
|
validate_self_type!(self_type)
|
|
82
|
+
validate_refinements!(refinements)
|
|
69
83
|
|
|
70
84
|
@receiver_constraint = receiver_constraint.dup.freeze
|
|
71
85
|
@method_names = method_names.map(&:to_sym).freeze
|
|
72
86
|
@self_type = self_type.is_a?(String) ? self_type.dup.freeze : self_type
|
|
87
|
+
@refinements = refinements.map { |name| name.dup.freeze }.freeze
|
|
73
88
|
freeze
|
|
74
89
|
end
|
|
75
90
|
|
|
@@ -77,7 +92,8 @@ module Rigor
|
|
|
77
92
|
{
|
|
78
93
|
"receiver_constraint" => receiver_constraint,
|
|
79
94
|
"method_names" => method_names.map(&:to_s),
|
|
80
|
-
"self_type" => self_type.to_s
|
|
95
|
+
"self_type" => self_type.to_s,
|
|
96
|
+
"refinements" => refinements
|
|
81
97
|
}
|
|
82
98
|
end
|
|
83
99
|
|
|
@@ -85,12 +101,18 @@ module Rigor
|
|
|
85
101
|
other.is_a?(BlockAsMethod) &&
|
|
86
102
|
receiver_constraint == other.receiver_constraint &&
|
|
87
103
|
method_names == other.method_names &&
|
|
88
|
-
self_type == other.self_type
|
|
104
|
+
self_type == other.self_type &&
|
|
105
|
+
refinements == other.refinements
|
|
89
106
|
end
|
|
90
107
|
alias eql? ==
|
|
91
108
|
|
|
92
109
|
def hash
|
|
93
|
-
[receiver_constraint, method_names, self_type].hash
|
|
110
|
+
[receiver_constraint, method_names, self_type, refinements].hash
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Whether the block keeps the caller's `self` (`self_type: :lexical`).
|
|
114
|
+
def lexical_self?
|
|
115
|
+
self_type == SELF_TYPE_LEXICAL
|
|
94
116
|
end
|
|
95
117
|
|
|
96
118
|
# The class name a String `self_type` binds `self` to — `"Foo::Bar"` for both the plain and the
|
|
@@ -118,6 +140,12 @@ module Rigor
|
|
|
118
140
|
self_type.is_a?(String) && !singleton_binding?
|
|
119
141
|
end
|
|
120
142
|
|
|
143
|
+
# Whether `Nominal[X]` receivers match, beside `Singleton[X]` ones: a named instance binding, or a
|
|
144
|
+
# `:lexical` entry.
|
|
145
|
+
def matches_instance_receivers?
|
|
146
|
+
named_instance_binding? || lexical_self?
|
|
147
|
+
end
|
|
148
|
+
|
|
121
149
|
private
|
|
122
150
|
|
|
123
151
|
def validate_receiver_constraint!(value)
|
|
@@ -143,6 +171,21 @@ module Rigor
|
|
|
143
171
|
end
|
|
144
172
|
end
|
|
145
173
|
|
|
174
|
+
def validate_refinements!(refinements)
|
|
175
|
+
unless refinements.is_a?(Array)
|
|
176
|
+
raise ArgumentError,
|
|
177
|
+
"Plugin::Macro::BlockAsMethod#refinements must be an Array, got #{refinements.inspect}"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
refinements.each do |name|
|
|
181
|
+
next if name.is_a?(String) && name.match?(NAMED_SELF_TYPE_PATTERN)
|
|
182
|
+
|
|
183
|
+
raise ArgumentError,
|
|
184
|
+
"Plugin::Macro::BlockAsMethod#refinements entries must be module-name Strings ('Foo::Bar'), " \
|
|
185
|
+
"got #{name.inspect}"
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
146
189
|
def validate_self_type!(self_type)
|
|
147
190
|
return if VALID_SELF_TYPES.include?(self_type)
|
|
148
191
|
return if self_type.is_a?(String) &&
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "environment"
|
|
4
|
+
require_relative "inference/project_patched_scanner"
|
|
4
5
|
require_relative "plugin"
|
|
5
6
|
require_relative "plugin/loader"
|
|
6
7
|
require_relative "plugin/services"
|
|
@@ -53,16 +54,55 @@ module Rigor
|
|
|
53
54
|
# ADR-93 auto-wired `rigor-rbs-inline` entry when the library is resolvable).
|
|
54
55
|
# @param source_files — the file(s) the command inspects. Threaded so each loaded plugin's
|
|
55
56
|
# `source_rbs_synthesizer` runs over them at env-build time; an empty list contributes no synthesized RBS.
|
|
56
|
-
|
|
57
|
+
# @param feature_sources — path => source text the required-feature scan reads in place of that file (an
|
|
58
|
+
# editor buffer bound with `--tmp-file` / `--instead-of`).
|
|
59
|
+
# @param project_patched_methods — the `pre_eval:` registry (see {.project_patched_methods}); nil keeps
|
|
60
|
+
# the unpatched universe.
|
|
61
|
+
def build(configuration:, source_files:, feature_sources: {}, project_patched_methods: nil)
|
|
62
|
+
features = required_features(configuration, source_files, sources: feature_sources)
|
|
57
63
|
Environment.for_project(
|
|
64
|
+
project_patched_methods: project_patched_methods,
|
|
58
65
|
libraries: configuration.libraries,
|
|
59
66
|
signature_paths: configuration.signature_paths,
|
|
60
67
|
plugin_registry: load_plugin_registry(configuration),
|
|
61
68
|
source_files: source_files,
|
|
69
|
+
required_features: features,
|
|
62
70
|
**dependency_discovery_options(configuration)
|
|
63
71
|
)
|
|
64
72
|
rescue StandardError
|
|
65
|
-
bare(configuration)
|
|
73
|
+
bare(configuration, features)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The patched-method registry `rigor check` types against for the project's `pre_eval:` files. It depends
|
|
77
|
+
# on those files alone, so it is built by the same {Inference::ProjectPatchedScanner} the Runner's project
|
|
78
|
+
# pre-pass calls, over the same resolved `configuration.pre_eval` entries (glob-expanded at config load,
|
|
79
|
+
# missing files skipped), without the pre-pass's plugin load / `#prepare` / dependency-source work. An
|
|
80
|
+
# editor `buffer` is honoured, so a `--tmp-file` edit of a pre_eval file is read in flight. Nil when the
|
|
81
|
+
# project lists no `pre_eval:` entry, so a project without one stays byte-identical. Fail-soft.
|
|
82
|
+
def project_patched_methods(configuration, buffer: nil)
|
|
83
|
+
return nil if configuration.pre_eval.empty?
|
|
84
|
+
|
|
85
|
+
existing = configuration.pre_eval.select { |path| File.file?(path) }
|
|
86
|
+
Inference::ProjectPatchedScanner.scan(existing, buffer: buffer).registry
|
|
87
|
+
rescue StandardError
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Issue #1700 — the {Environment::RequiredFeatures} the environment gates on: the configured paths plus
|
|
92
|
+
# `extra_files` (the file a probe inspects, which may lie outside them), the same set `rigor check` scans
|
|
93
|
+
# whichever files it is given, so a position types as `rigor check` types it.
|
|
94
|
+
def required_features(configuration, extra_files = [], sources: {})
|
|
95
|
+
Environment::RequiredFeatures.for_configuration(configuration, extra_files, sources: sources)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The required-feature scan's view of an editor buffer (`--tmp-file` / `--instead-of`): its logical path
|
|
99
|
+
# read from its physical file. Empty without a buffer or when the file cannot be read.
|
|
100
|
+
def buffer_sources(buffer)
|
|
101
|
+
return {} if buffer.nil?
|
|
102
|
+
|
|
103
|
+
{ buffer.logical_path => File.binread(buffer.physical_path) }
|
|
104
|
+
rescue SystemCallError
|
|
105
|
+
{}
|
|
66
106
|
end
|
|
67
107
|
|
|
68
108
|
# The dependency-discovery axes `check` reads off the configuration, as a keyword hash. Extracted so the
|
|
@@ -116,22 +156,24 @@ module Rigor
|
|
|
116
156
|
|
|
117
157
|
# The first fail-soft floor: no plugin tier and no synthesized RBS, but still the project's own dependency
|
|
118
158
|
# sources — dropping those is what issue #821 was, so a plugin-loading failure must not cost them.
|
|
119
|
-
def bare(configuration)
|
|
159
|
+
def bare(configuration, features = required_features(configuration))
|
|
120
160
|
Environment.for_project(
|
|
121
161
|
libraries: configuration.libraries,
|
|
122
162
|
signature_paths: configuration.signature_paths,
|
|
163
|
+
required_features: features,
|
|
123
164
|
**dependency_discovery_options(configuration)
|
|
124
165
|
)
|
|
125
166
|
rescue StandardError
|
|
126
|
-
minimal(configuration)
|
|
167
|
+
minimal(configuration, features)
|
|
127
168
|
end
|
|
128
169
|
|
|
129
170
|
# The last floor: RBS core plus the project's own signature paths, which cannot fail on anything outside
|
|
130
171
|
# the project itself.
|
|
131
|
-
def minimal(configuration)
|
|
172
|
+
def minimal(configuration, features = [])
|
|
132
173
|
Environment.for_project(
|
|
133
174
|
libraries: configuration.libraries,
|
|
134
|
-
signature_paths: configuration.signature_paths
|
|
175
|
+
signature_paths: configuration.signature_paths,
|
|
176
|
+
required_features: features
|
|
135
177
|
)
|
|
136
178
|
end
|
|
137
179
|
end
|
|
@@ -84,7 +84,8 @@ module Rigor
|
|
|
84
84
|
|
|
85
85
|
# Human-readable reasons the caller prints. Each names something the operator can act on.
|
|
86
86
|
NO_SNAPSHOT = "no reusable `rigor check --incremental` snapshot"
|
|
87
|
-
OPAQUE_PLUGIN = "a plugin
|
|
87
|
+
OPAQUE_PLUGIN = "a plugin is opaque to the incremental fingerprint " \
|
|
88
|
+
"(no fingerprint surface, or a value it cannot digest)"
|
|
88
89
|
UNDIGESTIBLE = "the project-scan tables could not be digested"
|
|
89
90
|
UNIDENTIFIED_ENGINE = "the engine's own source tree could not be digested"
|
|
90
91
|
|
|
@@ -220,7 +221,7 @@ module Rigor
|
|
|
220
221
|
def patched_rows(patched)
|
|
221
222
|
patched.by_key.map do |(class_name, method_name, kind), entry|
|
|
222
223
|
[class_name.to_s, method_name.to_s, kind.to_s, entry.source_path.to_s, entry.source_line,
|
|
223
|
-
entry.return_type&.describe(:short).to_s]
|
|
224
|
+
entry.return_type&.describe(:short).to_s, entry.alias_of.to_s]
|
|
224
225
|
end.sort
|
|
225
226
|
end
|
|
226
227
|
|
|
@@ -27,11 +27,13 @@ module Rigor
|
|
|
27
27
|
# ancestors are `[C, A, M, Base]`. It reads the chain's `:constants` flavor, whose predicate for a
|
|
28
28
|
# project class also admits a module that holds only constants.
|
|
29
29
|
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
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}.
|
|
35
37
|
#
|
|
36
38
|
# Memoised per run because step 2 runs on every constant reference whose lexical candidates all
|
|
37
39
|
# miss — which is the common case for a core-class reference (`String` inside `class Foo`). The
|
|
@@ -52,7 +54,7 @@ module Rigor
|
|
|
52
54
|
scopes, chain = ancestor_constant_worlds(class_name, scope)
|
|
53
55
|
owner, hit = first_ancestor_hit(scopes, &)
|
|
54
56
|
verdict = chain.settle(scope, owner, owner: owner) do |retro|
|
|
55
|
-
first_ancestor_hit(constant_scopes_of(retro, class_name), &)&.first
|
|
57
|
+
first_ancestor_hit(constant_scopes_of(retro, class_name, scope), &)&.first
|
|
56
58
|
end
|
|
57
59
|
return hit if verdict == :chain
|
|
58
60
|
|
|
@@ -109,7 +111,9 @@ module Rigor
|
|
|
109
111
|
worlds = bucket[class_name]
|
|
110
112
|
return bucket[class_name] = compute_ancestor_constant_scopes(class_name, scope) if worlds.nil?
|
|
111
113
|
|
|
112
|
-
Scope::ResolutionChain.for(scope, class_name.to_s, :instance, :constants)
|
|
114
|
+
chain = Scope::ResolutionChain.for(scope, class_name.to_s, :instance, :constants)
|
|
115
|
+
chain.record(scope)
|
|
116
|
+
record_external_owner_edges(chain)
|
|
113
117
|
worlds
|
|
114
118
|
end
|
|
115
119
|
private_class_method :recorded_ancestor_constant_scopes
|
|
@@ -118,22 +122,134 @@ module Rigor
|
|
|
118
122
|
def compute_ancestor_constant_scopes(class_name, scope)
|
|
119
123
|
chain = Scope::ResolutionChain.for(scope, class_name.to_s, :instance, :constants)
|
|
120
124
|
chain.record(scope)
|
|
121
|
-
|
|
125
|
+
record_external_owner_edges(chain)
|
|
126
|
+
scopes = constant_scopes_of(chain, class_name, scope)
|
|
122
127
|
[scopes, chain].freeze
|
|
123
128
|
end
|
|
124
129
|
private_class_method :compute_ancestor_constant_scopes
|
|
125
130
|
|
|
126
|
-
# The chain's project entries in order, each once, without `class_name` itself
|
|
127
|
-
|
|
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)
|
|
128
134
|
out = []
|
|
129
135
|
chain.entries.each do |entry|
|
|
130
|
-
name = entry.name
|
|
136
|
+
name = entry.external? ? external_constant_owner(entry, scope) : entry.name
|
|
131
137
|
out << name unless name.nil? || name == class_name || out.include?(name)
|
|
132
138
|
end
|
|
133
139
|
out.freeze
|
|
134
140
|
end
|
|
135
141
|
private_class_method :constant_scopes_of
|
|
136
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
|
|
245
|
+
|
|
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)
|
|
250
|
+
end
|
|
251
|
+
private_class_method :project_written_candidate?
|
|
252
|
+
|
|
137
253
|
# Whether `name` is a namespace the project declares — the predicate the chain's `:constants` flavor
|
|
138
254
|
# resolves ancestor names with, and the path walk's (`reflection/constant_path.rb`) test for a head.
|
|
139
255
|
def known_project_namespace?(name, scope)
|
|
@@ -118,11 +118,15 @@ module Rigor
|
|
|
118
118
|
|
|
119
119
|
# {.agreed_ancestor_hit} over the budget-bounded scope lists: the first answer where
|
|
120
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.
|
|
121
125
|
def bounded_agreed_hit(class_name, scope, &)
|
|
122
126
|
scopes, chain = ancestor_constant_worlds(class_name, scope)
|
|
123
|
-
owner, hit = first_ancestor_hit(bounded(scopes), &)
|
|
127
|
+
owner, hit = first_ancestor_hit(bounded(project_scopes(scopes, scope)), &)
|
|
124
128
|
verdict = chain.settle(scope, owner, owner: owner) do |retro|
|
|
125
|
-
first_ancestor_hit(bounded(constant_scopes_of(retro, class_name)), &)&.first
|
|
129
|
+
first_ancestor_hit(bounded(project_scopes(constant_scopes_of(retro, class_name, scope), scope)), &)&.first
|
|
126
130
|
end
|
|
127
131
|
return hit if verdict == :chain
|
|
128
132
|
|
|
@@ -130,6 +134,13 @@ module Rigor
|
|
|
130
134
|
end
|
|
131
135
|
private_class_method :bounded_agreed_hit
|
|
132
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
|
+
|
|
133
144
|
# How many of {.ancestor_constant_scopes}' entries one segment may consult, under
|
|
134
145
|
# `Scope::ANCESTOR_WALK_LIMIT` — the budget `Scope`'s method lookup already spends on this same
|
|
135
146
|
# graph, reused rather than re-chosen so one pathological ancestry costs one bound. The list
|
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
|
|