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
|
@@ -11,6 +11,7 @@ require_relative "dynamic_origin"
|
|
|
11
11
|
require_relative "flow_tracer"
|
|
12
12
|
require_relative "void_tail_summary"
|
|
13
13
|
require_relative "method_dispatcher/call_context"
|
|
14
|
+
require_relative "method_dispatcher/argument_nodes"
|
|
14
15
|
require_relative "method_dispatcher/constant_folding"
|
|
15
16
|
require_relative "method_dispatcher/literal_string_folding"
|
|
16
17
|
require_relative "method_dispatcher/shape_dispatch"
|
|
@@ -18,6 +19,7 @@ require_relative "method_dispatcher/data_folding"
|
|
|
18
19
|
require_relative "method_dispatcher/struct_folding"
|
|
19
20
|
require_relative "method_dispatcher/rbs_dispatch"
|
|
20
21
|
require_relative "method_dispatcher/iterator_dispatch"
|
|
22
|
+
require_relative "method_dispatcher/arithmetic_sequence_elements"
|
|
21
23
|
require_relative "method_dispatcher/reduce_folding"
|
|
22
24
|
require_relative "method_dispatcher/block_folding"
|
|
23
25
|
require_relative "method_dispatcher/array_to_h_folding"
|
|
@@ -38,6 +40,7 @@ require_relative "method_dispatcher/kernel_dispatch"
|
|
|
38
40
|
require_relative "method_dispatcher/universal_object_dispatch"
|
|
39
41
|
require_relative "method_dispatcher/singleton_mixin_dispatch"
|
|
40
42
|
require_relative "method_dispatcher/method_folding"
|
|
43
|
+
require_relative "method_dispatcher/optional_count"
|
|
41
44
|
|
|
42
45
|
module Rigor
|
|
43
46
|
module Inference
|
|
@@ -101,9 +104,19 @@ module Rigor
|
|
|
101
104
|
# the tier commits the buffer only if it goes on to answer. See {#try_composite_receiver}.
|
|
102
105
|
def resolve(receiver_type:, method_name:, arg_types:, # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/ParameterLists
|
|
103
106
|
block_type: nil, environment: nil,
|
|
104
|
-
call_node: nil, scope: nil, plugin_typed_sink: nil)
|
|
107
|
+
call_node: nil, scope: nil, plugin_typed_sink: nil, skip_project_patched: false)
|
|
105
108
|
return nil if receiver_type.nil?
|
|
106
109
|
|
|
110
|
+
# Issue #1393 — a `nil` count on `min` / `max` / `min_by` / `max_by` is the no-count call.
|
|
111
|
+
nil_count = OptionalCount.split(receiver_type, method_name, arg_types, block: !block_type.nil?)
|
|
112
|
+
if nil_count
|
|
113
|
+
return resolve_nil_count(nil_count.first, receiver_type: receiver_type, method_name: method_name,
|
|
114
|
+
block_type: block_type, environment: environment,
|
|
115
|
+
call_node: call_node, scope: scope,
|
|
116
|
+
plugin_typed_sink: plugin_typed_sink,
|
|
117
|
+
skip_project_patched: skip_project_patched)
|
|
118
|
+
end
|
|
119
|
+
|
|
107
120
|
# Build the call context once and thread it — unchanged — through every tier
|
|
108
121
|
# (`_DispatchTier#try_dispatch`). The dispatcher's own private fallback tiers still read
|
|
109
122
|
# the positional locals below; only the tier modules consume the context object.
|
|
@@ -205,7 +218,10 @@ module Rigor
|
|
|
205
218
|
# the pre-pass populated `ProjectPatchedMethods` with the `(class, method, kind)` triple;
|
|
206
219
|
# this tier surfaces it as `Dynamic[top]` so the patched call resolves cross-file without
|
|
207
220
|
# `call.undefined-method`.
|
|
208
|
-
patched_result =
|
|
221
|
+
patched_result =
|
|
222
|
+
unless skip_project_patched
|
|
223
|
+
try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment)
|
|
224
|
+
end
|
|
209
225
|
if patched_result
|
|
210
226
|
scope&.record_dynamic_origin(call_node, DynamicOrigin::EXTERNAL_GEM_WITHOUT_RBS)
|
|
211
227
|
return patched_result
|
|
@@ -284,6 +300,20 @@ module Rigor
|
|
|
284
300
|
try_composite_receiver(context)
|
|
285
301
|
end
|
|
286
302
|
|
|
303
|
+
# The `nil` part of an optional count dispatches as the no-argument call, the rest as written; a count
|
|
304
|
+
# that is both answers the union of the two, or declines when either declines. See {OptionalCount}.
|
|
305
|
+
def resolve_nil_count(rest, **common)
|
|
306
|
+
return resolve(arg_types: [], **common) if rest.nil?
|
|
307
|
+
|
|
308
|
+
# The counted leg runs first: a leg's plugin answer records on the scope (or buffers in the sink, #1101),
|
|
309
|
+
# and a declining counted leg must not leave the no-argument leg's record behind for a call that answers nil.
|
|
310
|
+
counted = resolve(arg_types: [rest], **common)
|
|
311
|
+
return nil if counted.nil?
|
|
312
|
+
|
|
313
|
+
without = resolve(arg_types: [], **common)
|
|
314
|
+
without && Type::Combinator.union(without, counted)
|
|
315
|
+
end
|
|
316
|
+
|
|
287
317
|
# ADR-100 WD4 — records the transitive `-> void` provenance when the called method is a
|
|
288
318
|
# discovered project `def` whose {VoidTailSummary} chain ends in an author-declared `-> void`
|
|
289
319
|
# leaf on the same owner. Guards cheapest-first: the internal dispatcher callers nil `scope` /
|
|
@@ -614,7 +644,7 @@ module Rigor
|
|
|
614
644
|
# `def to_url; "hello"; end` patched onto `String` now resolves `s.to_url` to
|
|
615
645
|
# `Dynamic[Nominal[String]]` instead of the pre-3a `Dynamic[Top]`. Falls back to
|
|
616
646
|
# `Dynamic[Top]` when the heuristic declined (non-literal tail expression).
|
|
617
|
-
def try_project_patched_method(receiver_type, method_name, environment)
|
|
647
|
+
def try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment)
|
|
618
648
|
registry = environment&.project_patched_methods
|
|
619
649
|
return nil if registry.nil? || registry.empty?
|
|
620
650
|
|
|
@@ -624,9 +654,25 @@ module Rigor
|
|
|
624
654
|
kind = receiver_type.is_a?(Type::Singleton) ? :singleton : :instance
|
|
625
655
|
entry = registry.lookup(class_name: class_name, method_name: method_name, kind: kind)
|
|
626
656
|
return nil if entry.nil?
|
|
627
|
-
return Type::Combinator.
|
|
657
|
+
return Type::Combinator.dynamic(entry.return_type) if entry.return_type
|
|
658
|
+
|
|
659
|
+
aliased = project_patched_alias_target(entry, receiver_type, arg_types, block_type, environment)
|
|
660
|
+
aliased ? Type::Combinator.dynamic(aliased) : Type::Combinator.untyped
|
|
661
|
+
end
|
|
628
662
|
|
|
629
|
-
|
|
663
|
+
# Issue #1702 — an alias in a `pre_eval:` patch whose old name no earlier patch binding defines
|
|
664
|
+
# (`alias old_plus +` on Integer, or `alias_method :orig_succ, :succ` ahead of the patch's own `def succ`)
|
|
665
|
+
# names the method the class had before the patch, so it answers what that method answers for the same
|
|
666
|
+
# arguments, behind the `Dynamic` every patched method carries. The re-entry skips this tier: a later patch
|
|
667
|
+
# `def` of the old name is not the body the alias copied, and the skip is also what ends an alias cycle. It
|
|
668
|
+
# carries no call node or scope, so it answers a type and records nothing at the call site.
|
|
669
|
+
def project_patched_alias_target(entry, receiver_type, arg_types, block_type, environment)
|
|
670
|
+
return nil if entry.alias_of.nil?
|
|
671
|
+
|
|
672
|
+
resolve(
|
|
673
|
+
receiver_type: receiver_type, method_name: entry.alias_of, arg_types: arg_types,
|
|
674
|
+
block_type: block_type, environment: environment, skip_project_patched: true
|
|
675
|
+
)
|
|
630
676
|
end
|
|
631
677
|
|
|
632
678
|
# ADR-10 slice 2b-ii. Consults the per-run `Analysis::DependencySourceInference::Index`
|
|
@@ -847,7 +893,8 @@ module Rigor
|
|
|
847
893
|
private_constant :STDLIB_SINGLETON_FOLDERS
|
|
848
894
|
|
|
849
895
|
PRECISE_TIERS_TAIL = Ractor.make_shareable([
|
|
850
|
-
MethodFolding, ReduceFolding, ArrayToHFolding, HashTransformKeysFolding, BlockFolding
|
|
896
|
+
MethodFolding, ReduceFolding, ArrayToHFolding, HashTransformKeysFolding, BlockFolding,
|
|
897
|
+
ArithmeticSequenceElements
|
|
851
898
|
].freeze)
|
|
852
899
|
private_constant :PRECISE_TIERS_TAIL
|
|
853
900
|
|
|
@@ -1104,14 +1151,23 @@ module Rigor
|
|
|
1104
1151
|
# `instance_of?(self)` later as the rule catalogue grows; for now only `class` is handled.
|
|
1105
1152
|
def try_meta_introspection(receiver_type, method_name, arg_types = [], context = nil)
|
|
1106
1153
|
case method_name
|
|
1107
|
-
when :class then meta_class(receiver_type)
|
|
1154
|
+
when :class then meta_class(receiver_type, context&.environment)
|
|
1108
1155
|
when :new then meta_new(receiver_type, arg_types, context)
|
|
1109
1156
|
end
|
|
1110
1157
|
end
|
|
1111
1158
|
|
|
1112
|
-
def meta_class(receiver_type)
|
|
1159
|
+
def meta_class(receiver_type, environment = nil)
|
|
1113
1160
|
case receiver_type
|
|
1114
|
-
when Type::Nominal
|
|
1161
|
+
when Type::Nominal
|
|
1162
|
+
# An instance of module `M` is an instance of some class that includes `M`, never of `M`
|
|
1163
|
+
# itself: `self.class` in `M`'s instance method is that includer, which no RBS type names.
|
|
1164
|
+
# `Dynamic[Class]` keeps the surface every class shares (`name`, `ancestors`) and answers
|
|
1165
|
+
# nothing for the includer's own singleton methods.
|
|
1166
|
+
if environment&.rbs_module?(receiver_type.class_name)
|
|
1167
|
+
return Type::Combinator.dynamic(Type::Combinator.nominal_of("Class"))
|
|
1168
|
+
end
|
|
1169
|
+
|
|
1170
|
+
Type::Combinator.singleton_of(receiver_type.class_name)
|
|
1115
1171
|
when Type::Constant then constant_metaclass(receiver_type.value)
|
|
1116
1172
|
end
|
|
1117
1173
|
end
|
|
@@ -1140,7 +1196,7 @@ module Rigor
|
|
|
1140
1196
|
set_lift = set_new_lift(receiver_type.class_name, arg_types)
|
|
1141
1197
|
return set_lift if set_lift
|
|
1142
1198
|
|
|
1143
|
-
hash_lift = hash_new_lift(receiver_type.class_name, arg_types)
|
|
1199
|
+
hash_lift = hash_new_lift(receiver_type.class_name, arg_types, context&.call_node)
|
|
1144
1200
|
return hash_lift if hash_lift
|
|
1145
1201
|
|
|
1146
1202
|
regexp_lift = regexp_new_lift(receiver_type.class_name, arg_types)
|
|
@@ -1415,9 +1471,15 @@ module Rigor
|
|
|
1415
1471
|
# form (`Hash.new { |h, k| … }`) keep the bare `Nominal[Hash]` answer — the block's value
|
|
1416
1472
|
# type is not available at this `:new` dispatch site, and conservatively leaving the read
|
|
1417
1473
|
# as today's behaviour is precision-additive.
|
|
1418
|
-
|
|
1474
|
+
#
|
|
1475
|
+
# Issue #1812 — a brace-less keyword hash is `Hash#initialize`'s `capacity:` keyword, not the
|
|
1476
|
+
# default (`Hash.new(capacity: 10).default` is `nil`), so it is dropped before counting the
|
|
1477
|
+
# positionals. A braced hash (`Hash.new({ capacity: 10 })`) is a positional and stays the default.
|
|
1478
|
+
def hash_new_lift(class_name, arg_types, call_node = nil)
|
|
1419
1479
|
return nil unless class_name == "Hash"
|
|
1420
|
-
|
|
1480
|
+
|
|
1481
|
+
arg_types = hash_new_positionals(arg_types, call_node)
|
|
1482
|
+
return nil unless arg_types&.size == 1
|
|
1421
1483
|
|
|
1422
1484
|
default = arg_types.first
|
|
1423
1485
|
return nil if default.nil?
|
|
@@ -1426,6 +1488,23 @@ module Rigor
|
|
|
1426
1488
|
Type::Combinator.nominal_of("Hash", type_args: [Type::Combinator.untyped, value])
|
|
1427
1489
|
end
|
|
1428
1490
|
|
|
1491
|
+
# `Hash.new`'s positional argument types: `arg_types` without a trailing brace-less keyword hash. A literal
|
|
1492
|
+
# send (`Hash.send(:new, capacity: 1)`) dispatches with the outer call node, whose arguments are the
|
|
1493
|
+
# selector symbol followed by `arg_types`' own, and is read through that offset. When the node's arguments
|
|
1494
|
+
# correspond otherwise, a sole hash argument may be the keyword hash, so the answer is nil (decline) and the
|
|
1495
|
+
# bare `Hash` stands; any other argument cannot be a keyword hash and is kept.
|
|
1496
|
+
def hash_new_positionals(arg_types, call_node)
|
|
1497
|
+
return arg_types[0...-1] if ArgumentNodes.keyword_hash_last?(call_node, arg_types)
|
|
1498
|
+
|
|
1499
|
+
arguments = call_node.respond_to?(:arguments) ? call_node.arguments&.arguments : nil
|
|
1500
|
+
return arg_types if arguments.nil? || arguments.size == arg_types.size
|
|
1501
|
+
if arguments.size == arg_types.size + 1 && arguments.first.is_a?(Prism::SymbolNode)
|
|
1502
|
+
return arguments.last.is_a?(Prism::KeywordHashNode) ? arg_types[0...-1] : arg_types
|
|
1503
|
+
end
|
|
1504
|
+
|
|
1505
|
+
arg_types.last.is_a?(Type::HashShape) ? nil : arg_types
|
|
1506
|
+
end
|
|
1507
|
+
|
|
1429
1508
|
# `Range.new(b, e)` / `Range.new(b, e, excl)` — folds to `Constant[Range]` when both
|
|
1430
1509
|
# endpoints are `Constant[Integer]` or both are `Constant[String]`, and the optional third
|
|
1431
1510
|
# argument is a `Constant[true/false]`. Nil endpoints (beginless / endless ranges) are not
|
|
@@ -1555,13 +1634,16 @@ module Rigor
|
|
|
1555
1634
|
# method definition, or selected overload does not provide statically declared block
|
|
1556
1635
|
# parameter types. Callers MUST treat the empty array as "no information"; the binder
|
|
1557
1636
|
# falls back to `Dynamic[Top]` for every parameter slot in that case.
|
|
1637
|
+
#
|
|
1638
|
+
# `call_node` lets the overload selector read the call's keyword arguments as keywords (#1727, #1737), as
|
|
1639
|
+
# the return path does; without it they are read positionally.
|
|
1558
1640
|
def expected_block_param_types(receiver_type:, method_name:, arg_types:, environment: nil,
|
|
1559
|
-
scope: nil)
|
|
1641
|
+
scope: nil, call_node: nil)
|
|
1560
1642
|
return [] if receiver_type.nil?
|
|
1561
1643
|
|
|
1562
1644
|
context = CallContext.build(
|
|
1563
1645
|
receiver: receiver_type, method_name: method_name,
|
|
1564
|
-
args: arg_types, environment: environment, scope: scope
|
|
1646
|
+
args: arg_types, environment: environment, scope: scope, call_node: call_node
|
|
1565
1647
|
)
|
|
1566
1648
|
iterator_result = IteratorDispatch.block_param_types(context)
|
|
1567
1649
|
return iterator_result if iterator_result
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
|
+
require_relative "../source/literals"
|
|
4
5
|
require_relative "../source/node_children"
|
|
5
6
|
|
|
6
7
|
module Rigor
|
|
@@ -204,7 +205,7 @@ module Rigor
|
|
|
204
205
|
|
|
205
206
|
# The Symbol a `:name` or `"name"` literal argument names, or nil.
|
|
206
207
|
def symbol_argument_name(arg)
|
|
207
|
-
|
|
208
|
+
Source::Literals.symbol_or_string(arg)
|
|
208
209
|
end
|
|
209
210
|
|
|
210
211
|
private_class_method :prescan_call, :prescan_eval_call, :symbol_argument_name
|
|
@@ -10,6 +10,7 @@ require_relative "../rbs_extended"
|
|
|
10
10
|
require_relative "../analysis/fact_store"
|
|
11
11
|
require_relative "../builtins/regex_refinement"
|
|
12
12
|
require_relative "guard_rebinding"
|
|
13
|
+
require_relative "key_presence_guard"
|
|
13
14
|
require_relative "last_line"
|
|
14
15
|
require_relative "operand_effects"
|
|
15
16
|
require_relative "optimistic_origin"
|
|
@@ -1273,7 +1274,7 @@ module Rigor
|
|
|
1273
1274
|
end
|
|
1274
1275
|
|
|
1275
1276
|
def simple_dispatch_name?(name)
|
|
1276
|
-
%i[nil? ! is_a? kind_of? instance_of? == != === =~ !~ match? key? has_key? empty? any?
|
|
1277
|
+
%i[nil? ! is_a? kind_of? instance_of? == != === =~ !~ match? key? has_key? include? member? empty? any?
|
|
1277
1278
|
none? respond_to? nan? finite?].include?(name)
|
|
1278
1279
|
end
|
|
1279
1280
|
|
|
@@ -1286,7 +1287,7 @@ module Rigor
|
|
|
1286
1287
|
when :=== then analyse_case_equality_predicate(node, scope)
|
|
1287
1288
|
when :=~, :!~ then dispatch_regex_match_predicate(node, scope, name)
|
|
1288
1289
|
when :match? then analyse_whole_regex_match_predicate(node, scope)
|
|
1289
|
-
when :key?, :has_key? then analyse_key_presence_predicate(node, scope)
|
|
1290
|
+
when :key?, :has_key?, :include?, :member? then analyse_key_presence_predicate(node, scope)
|
|
1290
1291
|
when :empty?, :any?, :none? then analyse_array_emptiness_predicate(node, scope, name)
|
|
1291
1292
|
when :respond_to? then analyse_respond_to_predicate(node, scope)
|
|
1292
1293
|
when :nan?, :finite? then analyse_float_class_predicate(node, scope, name)
|
|
@@ -1462,14 +1463,16 @@ module Rigor
|
|
|
1462
1463
|
# `h[:foo]` nil-typed). The falsey edge is left unchanged — "key absent" is the
|
|
1463
1464
|
# conservative no-op. Only literal `Symbol`/`String` arguments and
|
|
1464
1465
|
# `LocalVariableReadNode`/`InstanceVariableReadNode` receivers narrow; everything else
|
|
1465
|
-
# (Dynamic, `Nominal[Hash]`, method-chain receivers
|
|
1466
|
-
#
|
|
1466
|
+
# (Dynamic, `Nominal[Hash]`, method-chain receivers) bails to no narrowing. A non-literal
|
|
1467
|
+
# key goes to {#analyse_computed_key_presence_predicate} (issue #1703).
|
|
1467
1468
|
def analyse_key_presence_predicate(node, scope)
|
|
1468
1469
|
return nil if node.arguments.nil?
|
|
1469
1470
|
return nil unless node.arguments.arguments.size == 1
|
|
1470
1471
|
|
|
1471
|
-
key
|
|
1472
|
-
|
|
1472
|
+
# A literal key promotes a shape's optional key only for `key?` / `has_key?`; `include?` / `member?` reach
|
|
1473
|
+
# the computed-key guard alone, so a String or Array receiver's `include?` keeps its own narrowings.
|
|
1474
|
+
key = static_hash_key(node.arguments.arguments.first) if %i[key? has_key?].include?(node.name)
|
|
1475
|
+
return analyse_computed_key_presence_predicate(node, scope) if key.nil?
|
|
1473
1476
|
|
|
1474
1477
|
case node.receiver
|
|
1475
1478
|
when Prism::LocalVariableReadNode
|
|
@@ -1479,6 +1482,17 @@ module Rigor
|
|
|
1479
1482
|
end
|
|
1480
1483
|
end
|
|
1481
1484
|
|
|
1485
|
+
# Issue #1703 — `H.key?(k)` / `has_key?` / `include?` / `member?` with a non-literal key (a local, an ivar,
|
|
1486
|
+
# or a reader chain such as `prop.column_type`) on a closed hash shape held in a local, an ivar or a
|
|
1487
|
+
# constant. The truthy edge records the guard, and a later `H[k]` of the same receiver and structurally the
|
|
1488
|
+
# same key drops the miss `nil` ({KeyPresenceGuard}). The falsey edge is the no-op: an
|
|
1489
|
+
# absent computed key says nothing a shape can record. Nil when the guard does not apply, so a collection's
|
|
1490
|
+
# `include?` still reaches the membership and String predicates.
|
|
1491
|
+
def analyse_computed_key_presence_predicate(node, scope)
|
|
1492
|
+
truthy = KeyPresenceGuard.record(node, scope)
|
|
1493
|
+
truthy && [truthy, scope]
|
|
1494
|
+
end
|
|
1495
|
+
|
|
1482
1496
|
def key_presence_scopes(name, key, scope, reader:, writer:)
|
|
1483
1497
|
current = scope.public_send(reader, name)
|
|
1484
1498
|
return nil if current.nil?
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../reflection"
|
|
4
|
+
require_relative "../analysis/dependency_recorder"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Inference
|
|
8
|
+
# Issue #1697 — the modules the project mixes into `Object`, and what they say about a method name.
|
|
9
|
+
#
|
|
10
|
+
# A top-level `include M` is `main.include`, which is `Object.include`, so M's instance methods exist on
|
|
11
|
+
# every object: bare top-level calls (`main` is an `Object`) and calls on any receiver whose class
|
|
12
|
+
# descends from `Object`. `ScopeIndexer` records that statement under its own owner key
|
|
13
|
+
# ({TOPLEVEL_INCLUDE_KEY}), and an `include M` inside `class Object ... end` on `Object`. The RBS
|
|
14
|
+
# environment knows nothing of either: `Object`'s RBS ancestry is core's, so a direct lookup on an
|
|
15
|
+
# RBS-known receiver misses every one of M's methods. This module is the one place that asks the
|
|
16
|
+
# project's `Object` edges instead, for the
|
|
17
|
+
# `call.unresolved-toplevel` and `call.undefined-method` silences in `Analysis::CheckRules`. Nothing
|
|
18
|
+
# types a call through these edges yet: what Ruby reaches first, even for a bare top-level call, depends
|
|
19
|
+
# on more than the chain shows (a block that rebinds `self`, an `extend` or a singleton `def` on `main`),
|
|
20
|
+
# so typing is issue #1715's.
|
|
21
|
+
#
|
|
22
|
+
# The edges are read through `Scope::ResolutionChain`, the two owners' instance chains, whose search
|
|
23
|
+
# files the ADR-46 class edge of every project entry it passes, the declaring files among them (a file
|
|
24
|
+
# that wrote a top-level `include` is one), so a consumer re-checks when the include that answered for
|
|
25
|
+
# it is edited away. A project that never reopens `Object` and writes no top-level `include` returns
|
|
26
|
+
# before reading anything. The question is a union over the chain, not "which definer does Ruby call":
|
|
27
|
+
# any entry that may define the name answers, so the chain's `settle` verdict is not asked.
|
|
28
|
+
module ObjectMixins
|
|
29
|
+
OWNER = "Object"
|
|
30
|
+
# The owner key `ScopeIndexer` files a top-level `include` under, beside `Object`'s own key, so that
|
|
31
|
+
# no ancestry walk that types a call reaches it (see `ScopeIndexer#toplevel_include_owner`).
|
|
32
|
+
TOPLEVEL_INCLUDE_KEY = "<toplevel-include>"
|
|
33
|
+
|
|
34
|
+
module_function
|
|
35
|
+
|
|
36
|
+
# How the project's `Object` mixins answer `method_name`, nearest edge first:
|
|
37
|
+
#
|
|
38
|
+
# - `[:rbs, module_name, definition]` — an RBS-declared module declares it; `definition` is its
|
|
39
|
+
# `RBS::Definition::Method`. `:private_rbs` is the same for a private declaration, which a bare call
|
|
40
|
+
# reaches and a call with a receiver does not.
|
|
41
|
+
# - `[:source, module_name]` — a project module (or `Object` itself) defines it.
|
|
42
|
+
# - `[:singleton_function, module_name]` — an RBS-declared module declares it only as a singleton method.
|
|
43
|
+
# Core declares `Math`'s functions that way (`def self.sqrt`) although they are `module_function`s,
|
|
44
|
+
# whose private instance copies a bare call reaches after `include Math`. RBS cannot tell such a
|
|
45
|
+
# function from a plain `def self.`, so the answer silences a bare call only.
|
|
46
|
+
# - `[:opaque, module_name]` — a module neither RBS nor the project declares. Its surface cannot be
|
|
47
|
+
# enumerated, so it may define the name.
|
|
48
|
+
# - `[:truncated, "Object"]` — the chain was cut at its budget with no answer before the cut: a definer
|
|
49
|
+
# may sit past it, which is uncertainty, not absence.
|
|
50
|
+
# - `nil` — no edge, or every edge names a module known not to declare it.
|
|
51
|
+
#
|
|
52
|
+
# `skip:` names the kinds a caller cannot use; an entry of one of them is passed over and the search
|
|
53
|
+
# goes on to the next, so the answer is a union over the chain for those callers rather than the
|
|
54
|
+
# nearest entry only.
|
|
55
|
+
def answer(scope, method_name, skip: NO_SKIP)
|
|
56
|
+
return nil if scope.nil?
|
|
57
|
+
|
|
58
|
+
# ADR-46 — the answer depends on whether a top-level `include` exists anywhere, so a consumer checked
|
|
59
|
+
# while none did must re-check when one appears. The key appears as a declared "class" of the file that
|
|
60
|
+
# writes it (`discovered_class_sources`), which is what this name-keyed existence edge listens for.
|
|
61
|
+
if Analysis::DependencyRecorder.active?
|
|
62
|
+
Analysis::DependencyRecorder.read_last_segment(:class, TOPLEVEL_INCLUDE_KEY)
|
|
63
|
+
end
|
|
64
|
+
EDGE_OWNERS.each do |owner|
|
|
65
|
+
next unless scope.known_user_class?(owner)
|
|
66
|
+
|
|
67
|
+
found = answer_on_chain(scope, owner, method_name, skip)
|
|
68
|
+
return found if found
|
|
69
|
+
end
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Where `Object`'s project edges are recorded: a top-level `include` under the indexer's own key, and
|
|
74
|
+
# `Object` itself for `include M` in `class Object` (and `Object`'s own definitions).
|
|
75
|
+
EDGE_OWNERS = [TOPLEVEL_INCLUDE_KEY, OWNER].freeze
|
|
76
|
+
private_constant :EDGE_OWNERS
|
|
77
|
+
|
|
78
|
+
def answer_on_chain(scope, owner, method_name, skip)
|
|
79
|
+
chain = Scope::ResolutionChain.for(scope, owner, :instance, :methods)
|
|
80
|
+
found = chain.search(scope, side: :instance) do |entry|
|
|
81
|
+
entry_answer = answer_for_entry(scope, entry, method_name)
|
|
82
|
+
entry_answer unless entry_answer.nil? || skip.include?(entry_answer.first)
|
|
83
|
+
end
|
|
84
|
+
return found if found
|
|
85
|
+
|
|
86
|
+
chain.truncated? ? [:truncated, owner] : nil
|
|
87
|
+
end
|
|
88
|
+
private_class_method :answer_on_chain
|
|
89
|
+
|
|
90
|
+
NO_SKIP = [].freeze
|
|
91
|
+
# An explicit receiver reaches neither a private method (a `module_function`'s instance copy among
|
|
92
|
+
# them) nor, on #746's
|
|
93
|
+
# reading of an ancestor's unknown include (the receiver's own includes only), a module whose surface
|
|
94
|
+
# is unknown: one top-level `include` of an undeclared module would otherwise silence
|
|
95
|
+
# `call.undefined-method` on every receiver in the project.
|
|
96
|
+
EXPLICIT_RECEIVER_SKIP = %i[private_rbs singleton_function opaque].freeze
|
|
97
|
+
private_constant :NO_SKIP, :EXPLICIT_RECEIVER_SKIP
|
|
98
|
+
|
|
99
|
+
# Whether some `Object` mixin may define `method_name`: the question the silences ask. A bare
|
|
100
|
+
# top-level call (`receiver: :implicit`, `call.unresolved-toplevel`) counts every answer, an unknown
|
|
101
|
+
# module's included: such an include is the likeliest source of a bare name, and silencing a typo
|
|
102
|
+
# there is the cheaper error. A call with a receiver (`receiver: :explicit`, `call.undefined-method`)
|
|
103
|
+
# counts only a declared, public definition.
|
|
104
|
+
def may_define?(scope, method_name, receiver:)
|
|
105
|
+
skip = receiver == :explicit ? EXPLICIT_RECEIVER_SKIP : NO_SKIP
|
|
106
|
+
!answer(scope, method_name, skip: skip).nil?
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Whether a receiver of `class_name` reaches `Object`'s mixins at all: `Object` itself, or an RBS
|
|
110
|
+
# class the environment orders below it. A `BasicObject` subclass does not; a class the environment
|
|
111
|
+
# cannot order is not claimed.
|
|
112
|
+
def reaches_object?(scope, class_name)
|
|
113
|
+
name = class_name.to_s.delete_prefix("::")
|
|
114
|
+
return true if name == OWNER
|
|
115
|
+
|
|
116
|
+
environment = scope&.environment
|
|
117
|
+
!environment.nil? && environment.class_ordering(name, OWNER) == :subclass
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# A project entry (`Object` itself, or a project module mixed into it) answers when it defines the name;
|
|
121
|
+
# an external one when RBS declares the name on one of the names its spelling can denote (as an instance
|
|
122
|
+
# method, or as a module's singleton function), and is opaque when none of those names is declared at
|
|
123
|
+
# all.
|
|
124
|
+
def answer_for_entry(scope, entry, method_name)
|
|
125
|
+
unless entry.external?
|
|
126
|
+
return source_defines?(scope, entry.name, method_name) ? [:source, entry.name] : nil
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
entry.candidates.each do |candidate|
|
|
130
|
+
definition = Rigor::Reflection.instance_method_definition(candidate, method_name, scope: scope)
|
|
131
|
+
return [private_definition?(definition) ? :private_rbs : :rbs, candidate, definition] if definition
|
|
132
|
+
return [:singleton_function, candidate] if singleton_function?(scope, candidate, method_name)
|
|
133
|
+
end
|
|
134
|
+
return nil if entry.candidates.any? { |candidate| declared?(scope, candidate) }
|
|
135
|
+
|
|
136
|
+
[:opaque, entry.raw.to_s]
|
|
137
|
+
end
|
|
138
|
+
private_class_method :answer_for_entry
|
|
139
|
+
|
|
140
|
+
def private_definition?(definition)
|
|
141
|
+
definition.respond_to?(:accessibility) && definition.accessibility == :private
|
|
142
|
+
end
|
|
143
|
+
private_class_method :private_definition?
|
|
144
|
+
|
|
145
|
+
def singleton_function?(scope, name, method_name)
|
|
146
|
+
environment = scope.environment
|
|
147
|
+
return false if environment.nil? || !environment.rbs_module?(name)
|
|
148
|
+
|
|
149
|
+
definition = Rigor::Reflection.singleton_method_definition(name, method_name, scope: scope)
|
|
150
|
+
# Declared on the module itself: its singleton class also answers every `Module` method
|
|
151
|
+
# (`include`, `name`), which no `include` of it brings to the top level.
|
|
152
|
+
!definition.nil? && definition.defined_in.to_s.delete_prefix("::") == name
|
|
153
|
+
end
|
|
154
|
+
private_class_method :singleton_function?
|
|
155
|
+
|
|
156
|
+
# Asked of both tables a project method lands in, as `RbsDispatch#source_declares_through_ancestors?`
|
|
157
|
+
# does: `discovered_methods` withholds a plain `def` written in another file, which is exactly where a
|
|
158
|
+
# top-level `include` usually finds its module. The chain already expands each module's own ancestry, so
|
|
159
|
+
# one entry's own tables are asked.
|
|
160
|
+
def source_defines?(scope, name, method_name)
|
|
161
|
+
scope.discovered_method?(name, method_name, :instance) || !scope.user_def_for(name, method_name).nil?
|
|
162
|
+
end
|
|
163
|
+
private_class_method :source_defines?
|
|
164
|
+
|
|
165
|
+
# The same "is this mixin's surface enumerable" reading `Analysis::CheckRules#known_mixin?` gives a
|
|
166
|
+
# class's own includes (#746): a project class or module, or an RBS declaration Rigor did not
|
|
167
|
+
# synthesize to keep a broken signature buildable.
|
|
168
|
+
def declared?(scope, name)
|
|
169
|
+
return true if scope.discovered_classes.key?(name)
|
|
170
|
+
return false unless Rigor::Reflection.rbs_class_known?(name, scope: scope)
|
|
171
|
+
|
|
172
|
+
loader = scope.environment&.rbs_loader
|
|
173
|
+
return true if loader.nil? || !loader.respond_to?(:synthesized_type_names)
|
|
174
|
+
|
|
175
|
+
!loader.synthesized_type_names.include?(name)
|
|
176
|
+
end
|
|
177
|
+
private_class_method :declared?
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../source/node_children"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Inference
|
|
9
|
+
module ProjectPatchedScanner
|
|
10
|
+
# Finds the `def`s of a `pre_eval:` file written under a definedness guard naming the same method
|
|
11
|
+
# (`def foo = 1 unless method_defined?(:foo)`): polyfills that run only when nothing bound the name before.
|
|
12
|
+
module DefinednessGuard
|
|
13
|
+
DEFINEDNESS_PREDICATES = %i[
|
|
14
|
+
method_defined? private_method_defined? public_method_defined? protected_method_defined? respond_to?
|
|
15
|
+
].freeze
|
|
16
|
+
INSTANCE_METHOD_LISTS = %i[
|
|
17
|
+
instance_methods public_instance_methods private_instance_methods protected_instance_methods
|
|
18
|
+
].freeze
|
|
19
|
+
private_constant :DEFINEDNESS_PREDICATES, :INSTANCE_METHOD_LISTS
|
|
20
|
+
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
# The `def` nodes (by identity) that sit on the "name not yet defined" side of a definedness guard naming the
|
|
24
|
+
# same method: `def foo = 1 unless method_defined?(:foo)`, or the same inside an `unless` / `if !` block.
|
|
25
|
+
def defs(root)
|
|
26
|
+
found = {}.compare_by_identity
|
|
27
|
+
collect_guarded(root, found)
|
|
28
|
+
found
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def collect_guarded(node, found)
|
|
32
|
+
return unless node.is_a?(Prism::Node)
|
|
33
|
+
|
|
34
|
+
if node.is_a?(Prism::IfNode) || node.is_a?(Prism::UnlessNode)
|
|
35
|
+
name, negated = definedness_guard(node.predicate)
|
|
36
|
+
mark_guarded_defs(undefined_branch(node, negated), name, found) if name
|
|
37
|
+
end
|
|
38
|
+
node.rigor_each_child { |child| collect_guarded(child, found) }
|
|
39
|
+
end
|
|
40
|
+
private_class_method :collect_guarded
|
|
41
|
+
|
|
42
|
+
# `[method_name, negated]` for a predicate like `method_defined?(:m)` / `!respond_to?(:m)`, else nil.
|
|
43
|
+
def definedness_guard(predicate)
|
|
44
|
+
predicate = predicate.body if predicate.is_a?(Prism::ParenthesesNode) && predicate.body
|
|
45
|
+
predicate = predicate.body.first if predicate.is_a?(Prism::StatementsNode) && predicate.body.size == 1
|
|
46
|
+
if predicate.is_a?(Prism::CallNode) && predicate.name == :! && predicate.receiver
|
|
47
|
+
name, negated = definedness_guard(predicate.receiver)
|
|
48
|
+
return name && [name, !negated]
|
|
49
|
+
end
|
|
50
|
+
name = guard_method_name(predicate)
|
|
51
|
+
name && [name, false]
|
|
52
|
+
end
|
|
53
|
+
private_class_method :definedness_guard
|
|
54
|
+
|
|
55
|
+
def guard_method_name(call)
|
|
56
|
+
return unless call.is_a?(Prism::CallNode)
|
|
57
|
+
|
|
58
|
+
listed = %i[include? member?].include?(call.name)
|
|
59
|
+
listed &&= call.receiver.is_a?(Prism::CallNode) && INSTANCE_METHOD_LISTS.include?(call.receiver.name)
|
|
60
|
+
return unless listed || DEFINEDNESS_PREDICATES.include?(call.name)
|
|
61
|
+
|
|
62
|
+
arg = call.arguments&.arguments&.first
|
|
63
|
+
arg.unescaped.to_sym if arg.is_a?(Prism::SymbolNode) || arg.is_a?(Prism::StringNode)
|
|
64
|
+
end
|
|
65
|
+
private_class_method :guard_method_name
|
|
66
|
+
|
|
67
|
+
# The branch that runs when the guarded name is NOT defined yet.
|
|
68
|
+
def undefined_branch(node, negated)
|
|
69
|
+
branch_when_true = node.is_a?(Prism::IfNode) ? node.statements : node.else_clause
|
|
70
|
+
branch_when_false = node.is_a?(Prism::IfNode) ? node.subsequent : node.statements
|
|
71
|
+
# The predicate is true when the name is defined (unless negated).
|
|
72
|
+
negated ? branch_when_true : branch_when_false
|
|
73
|
+
end
|
|
74
|
+
private_class_method :undefined_branch
|
|
75
|
+
|
|
76
|
+
def mark_guarded_defs(node, name, found)
|
|
77
|
+
return unless node.is_a?(Prism::Node)
|
|
78
|
+
|
|
79
|
+
found[node] = true if node.is_a?(Prism::DefNode) && node.name == name
|
|
80
|
+
node.rigor_each_child { |child| mark_guarded_defs(child, name, found) } unless node.is_a?(Prism::DefNode)
|
|
81
|
+
end
|
|
82
|
+
private_class_method :mark_guarded_defs
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -7,9 +7,9 @@ module Rigor
|
|
|
7
7
|
# ADR-17 § "Inference contract" — project-wide patched-method registry populated by the pre-eval
|
|
8
8
|
# pre-pass (slice 2) from the user's `.rigor.yml` `pre_eval:` list.
|
|
9
9
|
#
|
|
10
|
-
# Each entry records one `def` declaration the pre-pass observed inside a class /
|
|
11
|
-
# dispatcher's `try_project_patched_method` tier consults this registry between the plugin
|
|
12
|
-
# the dependency-source tier so project-side `lib/core_ext/string_extensions.rb` patches are visible
|
|
10
|
+
# Each entry records one `def`, `alias` or `alias_method` declaration the pre-pass observed inside a class /
|
|
11
|
+
# module body. The dispatcher's `try_project_patched_method` tier consults this registry between the plugin
|
|
12
|
+
# tier and the dependency-source tier so project-side `lib/core_ext/string_extensions.rb` patches are visible
|
|
13
13
|
# to cross-file dispatch.
|
|
14
14
|
#
|
|
15
15
|
# The dispatcher answers `Dynamic[T]` (with a heuristic static facet) when `Entry#return_type` is
|
|
@@ -22,8 +22,13 @@ module Rigor
|
|
|
22
22
|
# the {Analysis::DependencySourceInference::ReturnTypeHeuristic}-extracted static facet (a
|
|
23
23
|
# `Rigor::Type::*`) or `nil` when the heuristic declined. The dispatcher wraps a non-nil
|
|
24
24
|
# `return_type` in `Dynamic[T]`; a `nil` `return_type` falls back to `Dynamic[top]`.
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
#
|
|
26
|
+
# Issue #1702 — an `alias new old` / `alias_method :new, :old` in a patch is recorded too. When a `def` or
|
|
27
|
+
# alias earlier in the `pre_eval:` files binds `old`, the alias's entry copies that entry's `return_type` (and
|
|
28
|
+
# its `alias_of`); otherwise `alias_of` names `old`, a method the class had before the patch, and the
|
|
29
|
+
# dispatcher answers the alias with what that method answers. `alias_of` is nil for a `def`.
|
|
30
|
+
Entry = Data.define(:class_name, :method_name, :kind, :source_path, :source_line, :return_type, :alias_of) do
|
|
31
|
+
def initialize(class_name:, method_name:, kind:, source_path:, source_line:, return_type: nil, alias_of: nil)
|
|
27
32
|
super
|
|
28
33
|
end
|
|
29
34
|
end
|
|
@@ -31,14 +36,14 @@ module Rigor
|
|
|
31
36
|
attr_reader :by_key, :write_census
|
|
32
37
|
|
|
33
38
|
# @param entries — flat list of declarations observed during the pre-pass.
|
|
34
|
-
#
|
|
39
|
+
# Last-write-wins on `(class_name, method_name, kind)` duplicates, as at runtime (ADR-17); the
|
|
35
40
|
# `pre-eval.duplicate-declaration` diagnostic emission stays decoupled from registry behaviour.
|
|
36
41
|
# @param write_census — issue #1367: the `pre_eval:` files' {GlobalWriteCensus}, which the `global.*` write
|
|
37
42
|
# rules join with the project's, since a patch file is loaded ahead of the code it patches.
|
|
38
43
|
def initialize(entries: [], write_census: GlobalWriteCensus::EMPTY)
|
|
39
44
|
@by_key = entries.each_with_object({}) do |entry, acc|
|
|
40
45
|
key = [entry.class_name, entry.method_name, entry.kind]
|
|
41
|
-
acc[key]
|
|
46
|
+
acc[key] = entry
|
|
42
47
|
end.freeze
|
|
43
48
|
@write_census = write_census.frozen? ? write_census : write_census.dup.freeze
|
|
44
49
|
freeze
|