rigortype 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +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 +6 -0
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/manual/03-configuration.md +1 -1
- data/docs/manual/04-diagnostics.md +2 -1
- data/docs/manual/06-baseline.md +2 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/18-removing-dead-code.md +32 -2
- data/docs/manual/plugins/README.md +6 -1
- data/docs/manual/plugins/rigor-activerecord.md +21 -0
- data/docs/manual/plugins/rigor-alba.md +60 -0
- data/docs/manual/plugins/rigor-minitest.md +22 -4
- data/docs/manual/plugins/rigor-typelizer.md +71 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
- data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
- data/lib/rigor/analysis/check_rules.rb +289 -57
- 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/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 +3 -1
- data/lib/rigor/analysis/incremental_session.rb +14 -1
- data/lib/rigor/analysis/reachability/graph.rb +123 -23
- data/lib/rigor/analysis/reachability/scan.rb +290 -36
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +16 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +87 -5
- data/lib/rigor/analysis/runner/pool_coordinator.rb +50 -11
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +34 -3
- data/lib/rigor/analysis/worker_session.rb +7 -4
- data/lib/rigor/cache/incremental_snapshot.rb +22 -0
- data/lib/rigor/cli/annotate_command.rb +5 -0
- data/lib/rigor/cli/baseline_command.rb +7 -18
- data/lib/rigor/cli/check_command.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +5 -0
- data/lib/rigor/cli/type_of_command.rb +5 -3
- data/lib/rigor/cli/unused_command.rb +13 -7
- data/lib/rigor/cli.rb +10 -2
- data/lib/rigor/configuration/severity_profile.rb +6 -0
- data/lib/rigor/configuration.rb +46 -2
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +283 -14
- data/lib/rigor/environment/required_features.rb +206 -0
- data/lib/rigor/environment.rb +21 -3
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- data/lib/rigor/inference/def_node_resolver.rb +23 -1
- data/lib/rigor/inference/expression_typer.rb +215 -16
- 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 +282 -0
- data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
- data/lib/rigor/inference/in_effect_refinements.rb +256 -0
- data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
- data/lib/rigor/inference/key_presence_guard.rb +303 -0
- data/lib/rigor/inference/macro_block_self_type.rb +59 -12
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
- data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
- data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +259 -28
- data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
- data/lib/rigor/inference/method_dispatcher.rb +44 -11
- data/lib/rigor/inference/module_function_state.rb +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_methods.rb +10 -5
- data/lib/rigor/inference/project_patched_scanner.rb +71 -3
- data/lib/rigor/inference/refine_census.rb +239 -0
- data/lib/rigor/inference/refine_self.rb +317 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
- data/lib/rigor/inference/refined_dispatch.rb +272 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
- data/lib/rigor/inference/scope_indexer.rb +687 -60
- data/lib/rigor/inference/statement_evaluator.rb +71 -20
- data/lib/rigor/inference/string_mutation.rb +9 -3
- data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
- data/lib/rigor/inference/version_guard.rb +19 -10
- data/lib/rigor/language_server/project_context.rb +31 -0
- data/lib/rigor/language_server/server.rb +18 -0
- data/lib/rigor/plugin/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 +29 -6
- data/lib/rigor/protection/mutation_cache.rb +1 -1
- 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 +189 -10
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +55 -5
- 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-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 +27 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +3 -1
- metadata +66 -1
|
@@ -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
|
|
@@ -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
|
|
@@ -4,6 +4,7 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "project_patched_methods"
|
|
6
6
|
require_relative "../analysis/dependency_source_inference/return_type_heuristic"
|
|
7
|
+
require_relative "../source/alias_names"
|
|
7
8
|
require_relative "../source/constant_path"
|
|
8
9
|
require_relative "../source/node_children"
|
|
9
10
|
|
|
@@ -11,7 +12,8 @@ module Rigor
|
|
|
11
12
|
module Inference
|
|
12
13
|
# ADR-17 slice 2 — pre-pass scanner. Walks every file the user listed under `pre_eval:` and
|
|
13
14
|
# harvests every `def` / `def self.` declaration inside a class / module body into a
|
|
14
|
-
# {ProjectPatchedMethods} registry the dispatcher consults below the plugin tier
|
|
15
|
+
# {ProjectPatchedMethods} registry the dispatcher consults below the plugin tier, together with every
|
|
16
|
+
# `alias new old` / `alias_method :new, :old` there (issue #1702, read through {Source::AliasNames}).
|
|
15
17
|
#
|
|
16
18
|
# The walker is intentionally a strict subset of {Rigor::Inference::ScopeIndexer}'s machinery: it
|
|
17
19
|
# only needs `class C; def m; end; end` shape recognition, not full inference. Parse errors degrade
|
|
@@ -29,6 +31,10 @@ module Rigor
|
|
|
29
31
|
end
|
|
30
32
|
end
|
|
31
33
|
|
|
34
|
+
# An alias the walk met, before {resolve_aliases} knows whether its old name is one the patch files define.
|
|
35
|
+
PendingAlias = Data.define(:class_name, :new_name, :old_name, :kind, :source_path, :source_line)
|
|
36
|
+
private_constant :PendingAlias
|
|
37
|
+
|
|
32
38
|
module_function
|
|
33
39
|
|
|
34
40
|
# @param paths — absolute paths to the pre-eval files. The runner has already
|
|
@@ -39,10 +45,11 @@ module Rigor
|
|
|
39
45
|
# users editing a monkey-patch file see the in-flight version in their analysis.
|
|
40
46
|
# @return the populated registry plus any per-file warnings.
|
|
41
47
|
def scan(paths, buffer: nil)
|
|
42
|
-
|
|
48
|
+
collected = []
|
|
43
49
|
diagnostics = []
|
|
44
50
|
census = Set.new
|
|
45
|
-
paths.each { |path| scan_file(path,
|
|
51
|
+
paths.each { |path| scan_file(path, collected, diagnostics, buffer, census) }
|
|
52
|
+
entries = resolve_aliases(collected)
|
|
46
53
|
diagnostics.concat(duplicate_declaration_diagnostics(entries))
|
|
47
54
|
Result.new(
|
|
48
55
|
registry: ProjectPatchedMethods.new(entries: entries, write_census: census.freeze),
|
|
@@ -78,6 +85,46 @@ module Rigor
|
|
|
78
85
|
end
|
|
79
86
|
private_class_method :duplicate_declaration_diagnostics
|
|
80
87
|
|
|
88
|
+
# Turns each {PendingAlias} into the entry it publishes, walking the bindings in configuration-then-source
|
|
89
|
+
# order as Ruby runs them. An alias binds to the body its old name has WHEN IT RUNS, so only a `def` or alias
|
|
90
|
+
# earlier in that order may supply its return type: `alias to_m to_modint` after `def to_modint` answers what
|
|
91
|
+
# `to_modint` does, while `alias_method :orig_succ, :succ` ahead of a patch's `def succ` is the method
|
|
92
|
+
# Integer already had. An alias whose old name nothing earlier binds records that name as `alias_of`, for the
|
|
93
|
+
# dispatcher to answer with the class's existing method (`alias old_plus +` on Integer), or with
|
|
94
|
+
# `Dynamic[top]` when nothing knows the name: the alias is still published, since a call to it is not the
|
|
95
|
+
# undefined method a dropped entry would report.
|
|
96
|
+
#
|
|
97
|
+
# Within one file the later binding of a name replaces the earlier, as at runtime: the alias-method-chain
|
|
98
|
+
# idiom (`def foo`, `alias_method :foo_without_x, :foo`, `alias_method :foo, :foo_with_x`) leaves `foo` the
|
|
99
|
+
# alias. Bindings of one name in DIFFERENT files are what `pre-eval.duplicate-declaration` reports, and the
|
|
100
|
+
# registry keeps the first of those, as it always has for `def`s.
|
|
101
|
+
def resolve_aliases(collected)
|
|
102
|
+
bound = {}
|
|
103
|
+
per_file = collected.group_by(&:source_path).values.map do |items|
|
|
104
|
+
final = {}
|
|
105
|
+
items.each do |item|
|
|
106
|
+
entry = item.is_a?(PendingAlias) ? alias_entry(item, bound) : item
|
|
107
|
+
key = [entry.class_name, entry.method_name, entry.kind]
|
|
108
|
+
bound[key] = entry
|
|
109
|
+
final.delete(key)
|
|
110
|
+
final[key] = entry
|
|
111
|
+
end
|
|
112
|
+
final.values
|
|
113
|
+
end
|
|
114
|
+
per_file.flatten(1)
|
|
115
|
+
end
|
|
116
|
+
private_class_method :resolve_aliases
|
|
117
|
+
|
|
118
|
+
def alias_entry(item, bound)
|
|
119
|
+
target = bound[[item.class_name, item.old_name, item.kind]]
|
|
120
|
+
ProjectPatchedMethods::Entry.new(
|
|
121
|
+
class_name: item.class_name, method_name: item.new_name, kind: item.kind,
|
|
122
|
+
source_path: item.source_path, source_line: item.source_line,
|
|
123
|
+
return_type: target&.return_type, alias_of: target ? target.alias_of : item.old_name
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
private_class_method :alias_entry
|
|
127
|
+
|
|
81
128
|
def scan_file(path, entries, diagnostics, buffer = nil, census = Set.new)
|
|
82
129
|
physical = buffer ? buffer.resolve(path) : path
|
|
83
130
|
parse_result =
|
|
@@ -139,6 +186,13 @@ module Rigor
|
|
|
139
186
|
descend_singleton_class(node, qualified_prefix, source_path, entries)
|
|
140
187
|
when Prism::DefNode
|
|
141
188
|
record_def_node(node, qualified_prefix, in_singleton_class, source_path, entries)
|
|
189
|
+
when Prism::AliasMethodNode
|
|
190
|
+
record_alias(node, Source::AliasNames.keyword_names(node), qualified_prefix, in_singleton_class,
|
|
191
|
+
source_path, entries)
|
|
192
|
+
when Prism::CallNode
|
|
193
|
+
record_alias(node, Source::AliasNames.alias_method_call_names(node), qualified_prefix,
|
|
194
|
+
in_singleton_class, source_path, entries)
|
|
195
|
+
walk_children(node, qualified_prefix, in_singleton_class, source_path, entries)
|
|
142
196
|
else
|
|
143
197
|
walk_children(node, qualified_prefix, in_singleton_class, source_path, entries)
|
|
144
198
|
end
|
|
@@ -186,6 +240,20 @@ module Rigor
|
|
|
186
240
|
)
|
|
187
241
|
end
|
|
188
242
|
private_class_method :record_def_node
|
|
243
|
+
|
|
244
|
+
# `names` is the alias's `[new_name, old_name]`, or nil for a computed name or a call that is not an alias.
|
|
245
|
+
# The side follows the body it is written in, as a `def`'s does: `alias` and `alias_method` inside
|
|
246
|
+
# `class << self` both bind on the singleton.
|
|
247
|
+
def record_alias(node, names, qualified_prefix, in_singleton_class, source_path, entries)
|
|
248
|
+
return if names.nil? || qualified_prefix.empty?
|
|
249
|
+
|
|
250
|
+
entries << PendingAlias.new(
|
|
251
|
+
class_name: qualified_prefix.join("::"), new_name: names.first, old_name: names.last,
|
|
252
|
+
kind: in_singleton_class ? :singleton : :instance,
|
|
253
|
+
source_path: source_path, source_line: node.location&.start_line || 1
|
|
254
|
+
)
|
|
255
|
+
end
|
|
256
|
+
private_class_method :record_alias
|
|
189
257
|
end
|
|
190
258
|
end
|
|
191
259
|
end
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../source/alias_names"
|
|
6
|
+
require_relative "../source/node_children"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Inference
|
|
10
|
+
# ADR-121 WD7 (issues #1796, #1799) — what a `refine` body tells the refinement table, read the one way the project
|
|
11
|
+
# walk (`ScopeIndexer#record_refinement_defs`) and the gem source walker
|
|
12
|
+
# (`Analysis::DependencySourceInference::Walker#walk_refine_body`) both use, so the two cannot drift. The table is
|
|
13
|
+
# `{refined class => {method => [refining modules]}}`; `Scope::DiscoveryIndex::REFINEMENT_WILDCARD` stands for
|
|
14
|
+
# what could not be read.
|
|
15
|
+
#
|
|
16
|
+
# A body is fully read when every call on its own `self` (the refinement module) is one that defines a name the
|
|
17
|
+
# walk can spell, or one that defines nothing: a `def`, an `alias` or `alias_method` of two literal names, a
|
|
18
|
+
# `define_method` of a literal name, an `attr_*` of literal names, an `undef`, a visibility call, or
|
|
19
|
+
# `Refinement#target`. Anything
|
|
20
|
+
# else (`import_methods`, `send`, a computed `define_method`, `include`, a nested `refine`, any other call on
|
|
21
|
+
# `self`) may define a name the walk cannot spell, so the body's names are incomplete and the table gets a
|
|
22
|
+
# names-wildcard row for each target. A call on another receiver cannot define a method on the refinement, and
|
|
23
|
+
# a call inside a `def` or `define_method` body runs on an instance of the refined class, so neither counts.
|
|
24
|
+
module RefineCensus
|
|
25
|
+
# `names`, every name the body is read to define, in textual order; `complete`, whether those are all of them;
|
|
26
|
+
# `defs`, the typed refined arm's bodies in textual order: `[name, Prism::DefNode]` for a `def` or an alias of
|
|
27
|
+
# an earlier `def`, and `[name, nil]` for any other definer (`define_method`, `attr_*`, `undef`, an alias of a
|
|
28
|
+
# name the body did not define earlier). A later definer replaces an earlier one, as Ruby's method table
|
|
29
|
+
# keeps the last, so a name whose last definer has no `DefNode` types as `Dynamic[top]`.
|
|
30
|
+
BodyReading = Struct.new(:names, :complete, :defs)
|
|
31
|
+
|
|
32
|
+
# Calls on the refinement that define nothing: visibility, with no argument, literal names, a `def`, or an
|
|
33
|
+
# allowlisted definer call.
|
|
34
|
+
VISIBILITY_CALLS = Set[:private, :public, :protected, :module_function, :private_constant].freeze
|
|
35
|
+
# `Refinement#target`, a reader of the refined class (the other `Refinement` method on Ruby 4.0.5 is
|
|
36
|
+
# `import_methods`).
|
|
37
|
+
READER_CALLS = Set[:target].freeze
|
|
38
|
+
# `attr_*` and whether each defines the reader and the writer.
|
|
39
|
+
ATTR_CALLS = {
|
|
40
|
+
attr_reader: [true, false].freeze, attr_writer: [false, true].freeze, attr_accessor: [true, true].freeze,
|
|
41
|
+
attr: [true, false].freeze
|
|
42
|
+
}.freeze
|
|
43
|
+
|
|
44
|
+
# A3 — the calls whose String argument may hold `refine` as code or as a method name, and the two families
|
|
45
|
+
# whose `self` is their receiver (so `M.send(:refine, …)` runs `refine` on `M`).
|
|
46
|
+
SEND_CALLS = Set[:send, :__send__, :public_send].freeze
|
|
47
|
+
SELF_EVAL_CALLS = Set[:instance_eval, :class_eval, :module_eval, :instance_exec, :class_exec, :module_exec].freeze
|
|
48
|
+
NAMING_CALLS = Set[
|
|
49
|
+
:method, :public_method, :singleton_method, :instance_method, :public_instance_method, :define_method,
|
|
50
|
+
:define_singleton_method, :alias_method
|
|
51
|
+
].freeze
|
|
52
|
+
REFINE_WORD = /\brefine\b/
|
|
53
|
+
|
|
54
|
+
private_constant :VISIBILITY_CALLS, :READER_CALLS, :ATTR_CALLS, :REFINE_WORD
|
|
55
|
+
|
|
56
|
+
module_function
|
|
57
|
+
|
|
58
|
+
def wildcard = Scope::DiscoveryIndex::REFINEMENT_WILDCARD
|
|
59
|
+
|
|
60
|
+
# §2.2 — what `body` (a refine block's body, or nil) defines on the refined class.
|
|
61
|
+
def read_body(body)
|
|
62
|
+
reading = BodyReading.new([], true, [])
|
|
63
|
+
scan_body(body, reading, {}, true) if body
|
|
64
|
+
reading.names.uniq!
|
|
65
|
+
reading
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Records `reading`'s names for each of `targets` under `refining` into `table` (nil starts one), plus a
|
|
69
|
+
# names-wildcard row per target when the body's names are incomplete. Returns the table.
|
|
70
|
+
def record_rows(table, targets, reading, refining)
|
|
71
|
+
table ||= {}
|
|
72
|
+
targets.each do |class_name|
|
|
73
|
+
reading.names.each { |name| add_row(table, class_name, name, refining) }
|
|
74
|
+
add_row(table, class_name, wildcard, refining) unless reading.complete
|
|
75
|
+
end
|
|
76
|
+
table
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# One `{class_name => {method_key => [refining]}}` entry, `refining` once.
|
|
80
|
+
def add_row(table, class_name, method_key, refining)
|
|
81
|
+
modules = ((table[class_name] ||= {})[method_key] ||= [])
|
|
82
|
+
modules << refining unless modules.include?(refining)
|
|
83
|
+
table
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# A3 — a `:refine` Symbol literal: it may name `Module#refine` for `send`, `alias_method`, `method`, ….
|
|
87
|
+
def refine_symbol?(node) = node.is_a?(Prism::SymbolNode) && node.unescaped == "refine"
|
|
88
|
+
|
|
89
|
+
# A3 — a String literal (or a heredoc's literal part) holding the word `refine`, as code an eval runs or as a
|
|
90
|
+
# method name.
|
|
91
|
+
def refine_string?(node)
|
|
92
|
+
case node
|
|
93
|
+
# As bytes: a literal whose bytes are not valid in its encoding (`"\xff …"`) must not raise.
|
|
94
|
+
when Prism::StringNode then REFINE_WORD.match?(node.unescaped.b)
|
|
95
|
+
when Prism::InterpolatedStringNode then node.parts.any? { |part| refine_string?(part) }
|
|
96
|
+
else false
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# A3 — does a String argument of `call` count: an eval, `send` or method-naming call?
|
|
101
|
+
def string_literal_call?(call)
|
|
102
|
+
name = call.name
|
|
103
|
+
name == :eval || SELF_EVAL_CALLS.include?(name) || SEND_CALLS.include?(name) || NAMING_CALLS.include?(name)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# A3 — the `self` a refine literal argument of `call` runs `refine` on: `:receiver` for a `send` or `*_eval`
|
|
107
|
+
# / `*_exec` call (the receiver, or the caller's `self` when it has none or it is `self`), `:unknown` for an
|
|
108
|
+
# `eval` on a binding, `:self` for everything else (the caller's `self`).
|
|
109
|
+
def literal_target(call)
|
|
110
|
+
receiver = call.receiver
|
|
111
|
+
self_receiver = receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
112
|
+
if SEND_CALLS.include?(call.name) || SELF_EVAL_CALLS.include?(call.name)
|
|
113
|
+
self_receiver ? :self : :receiver
|
|
114
|
+
elsif call.name == :eval
|
|
115
|
+
self_receiver ? :self : :unknown
|
|
116
|
+
else
|
|
117
|
+
:self
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# An implicit- or `self`-receiver call: one whose `self` is the walk's `self`.
|
|
122
|
+
def self_call?(node) = node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
123
|
+
|
|
124
|
+
# `classify` is false inside a `define_method` block or its arguments, whose calls run on an instance of the
|
|
125
|
+
# refined class (or before the method exists), so they define nothing on the refinement. `latest` maps each
|
|
126
|
+
# name to the last `def` the body wrote for it so far, which an alias copies.
|
|
127
|
+
def scan_body(node, reading, latest, classify)
|
|
128
|
+
case node
|
|
129
|
+
when Prism::DefNode
|
|
130
|
+
record_def(node, reading, latest) if node.receiver.nil?
|
|
131
|
+
return
|
|
132
|
+
when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode
|
|
133
|
+
return
|
|
134
|
+
when Prism::AliasMethodNode
|
|
135
|
+
return record_alias(Source::AliasNames.keyword_names(node), reading, latest)
|
|
136
|
+
when Prism::UndefNode
|
|
137
|
+
return record_undef(node, reading, latest)
|
|
138
|
+
when Prism::CallNode
|
|
139
|
+
if classify && self_call?(node)
|
|
140
|
+
classify_call(node, reading, latest)
|
|
141
|
+
classify = node.name != :define_method
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
node.rigor_each_child { |child| scan_body(child, reading, latest, classify) }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def record_def(node, reading, latest)
|
|
149
|
+
reading.names << node.name
|
|
150
|
+
reading.defs << [node.name, node]
|
|
151
|
+
latest[node.name] = node
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# `alias new old` / `alias_method :new, :old`: `new` is defined; it copies `old`'s body when the refine body
|
|
155
|
+
# wrote `old` earlier (Ruby copies at the alias, so a later `def old` does not move it), and otherwise it
|
|
156
|
+
# copies the refined class's own method, whose body the walk does not hold.
|
|
157
|
+
def record_alias(pair, reading, latest)
|
|
158
|
+
return reading.complete = false if pair.nil?
|
|
159
|
+
|
|
160
|
+
new_name, old_name = pair
|
|
161
|
+
reading.names << new_name
|
|
162
|
+
def_node = latest[old_name]
|
|
163
|
+
return record_bodiless(new_name, reading, latest) if def_node.nil?
|
|
164
|
+
|
|
165
|
+
reading.defs << [new_name, def_node]
|
|
166
|
+
latest[new_name] = def_node
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# A name a definer with no `DefNode` defines: it replaces any earlier `def` of the name, so the typed arm
|
|
170
|
+
# finds no body for it (`refine(String) { def center(a) = 1; define_method(:center) { |a| "dm" } }` answers
|
|
171
|
+
# `"dm"` on Ruby 4.0.5), and an alias of it copies no body either.
|
|
172
|
+
def record_bodiless(name, reading, latest)
|
|
173
|
+
reading.names << name
|
|
174
|
+
reading.defs << [name, nil]
|
|
175
|
+
latest.delete(name)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def record_undef(node, reading, latest)
|
|
179
|
+
node.names.each do |name|
|
|
180
|
+
symbol = literal_name(name)
|
|
181
|
+
symbol ? record_bodiless(symbol, reading, latest) : reading.complete = false
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Classifies a call on the refinement.
|
|
186
|
+
def classify_call(node, reading, latest)
|
|
187
|
+
name = node.name
|
|
188
|
+
arguments = node.arguments&.arguments || []
|
|
189
|
+
if (attr = ATTR_CALLS[name])
|
|
190
|
+
record_attr(arguments, attr, reading, latest)
|
|
191
|
+
elsif name == :define_method
|
|
192
|
+
record_define_method(arguments, reading, latest)
|
|
193
|
+
elsif name == :alias_method
|
|
194
|
+
record_alias(Source::AliasNames.alias_method_call_names(node), reading, latest)
|
|
195
|
+
else
|
|
196
|
+
reading.complete = false unless defines_nothing?(node, arguments)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# A visibility call with no argument, literal names, a `def` or a call; `Refinement#target` with none.
|
|
201
|
+
def defines_nothing?(node, arguments)
|
|
202
|
+
return arguments.all? { |argument| visibility_argument?(argument) } if VISIBILITY_CALLS.include?(node.name)
|
|
203
|
+
|
|
204
|
+
READER_CALLS.include?(node.name) && arguments.empty? && node.block.nil?
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def record_attr(arguments, (reader, writer), reading, latest)
|
|
208
|
+
arguments.each do |argument|
|
|
209
|
+
name = literal_name(argument)
|
|
210
|
+
next reading.complete = false if name.nil?
|
|
211
|
+
|
|
212
|
+
record_bodiless(name, reading, latest) if reader
|
|
213
|
+
record_bodiless(:"#{name}=", reading, latest) if writer
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def record_define_method(arguments, reading, latest)
|
|
218
|
+
name = arguments.first && literal_name(arguments.first)
|
|
219
|
+
name ? record_bodiless(name, reading, latest) : reading.complete = false
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# `private`, `private :a, "b"`, `private def x`, `private attr_reader :x`: the nested call is classified on
|
|
223
|
+
# its own when the walk descends into it.
|
|
224
|
+
def visibility_argument?(argument)
|
|
225
|
+
argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode) || argument.is_a?(Prism::DefNode) ||
|
|
226
|
+
argument.is_a?(Prism::CallNode)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# A literal's name, or nil for none and for bytes no Symbol holds: `define_method("\xff")` raises
|
|
230
|
+
# EncodingError on Ruby 4.0.5, so the body is not read.
|
|
231
|
+
def literal_name(node)
|
|
232
|
+
return nil unless node.is_a?(Prism::SymbolNode) || node.is_a?(Prism::StringNode)
|
|
233
|
+
|
|
234
|
+
name = node.unescaped
|
|
235
|
+
name.to_sym if name&.valid_encoding?
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|