rigortype 0.4.3 → 0.4.5
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 +5 -5
- data/data/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +104 -15
- data/docs/manual/02-cli-reference.md +43 -12
- data/docs/manual/03-configuration.md +20 -7
- data/docs/manual/04-diagnostics.md +56 -3
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +3 -2
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +40 -4
- data/docs/manual/18-removing-dead-code.md +23 -2
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-rspec.md +101 -1
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +60 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
- data/lib/rigor/analysis/check_rules.rb +274 -51
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +61 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +270 -28
- data/lib/rigor/analysis/path_expansion.rb +3 -1
- data/lib/rigor/analysis/reachability/graph.rb +111 -14
- data/lib/rigor/analysis/reachability/scan.rb +305 -23
- data/lib/rigor/analysis/rule_catalog.rb +22 -4
- data/lib/rigor/analysis/run_cache_key.rb +31 -7
- data/lib/rigor/analysis/run_cache_probe.rb +5 -3
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +48 -12
- data/lib/rigor/analysis/runner/pool_coordinator.rb +179 -31
- data/lib/rigor/analysis/runner/project_pre_passes.rb +29 -4
- data/lib/rigor/analysis/runner.rb +102 -72
- data/lib/rigor/analysis/severity_stamp.rb +45 -1
- data/lib/rigor/analysis/worker_session.rb +69 -2
- data/lib/rigor/cache/descriptor.rb +79 -12
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +39 -5
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/command.rb +3 -1
- data/lib/rigor/cli/docs_command.rb +3 -2
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +3 -2
- data/lib/rigor/cli/mcp_command.rb +1 -1
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +32 -11
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +163 -22
- data/lib/rigor/configuration/dependencies.rb +19 -11
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +29 -3
- data/lib/rigor/effects/signature_sources.rb +3 -1
- data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
- data/lib/rigor/environment/gated_signature_guard.rb +2 -1
- data/lib/rigor/environment/gated_signature_plan.rb +2 -1
- data/lib/rigor/environment/installed_gem_set.rb +2 -1
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
- data/lib/rigor/environment/rbs_loader.rb +67 -11
- data/lib/rigor/inference/block_call_timing.rb +12 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/captured_locals.rb +67 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/definer_resolution.rb +101 -25
- data/lib/rigor/inference/expression_typer.rb +94 -9
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher.rb +67 -3
- data/lib/rigor/inference/module_function_state.rb +213 -51
- data/lib/rigor/inference/narrowing.rb +235 -75
- data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
- data/lib/rigor/inference/project_patched_methods.rb +168 -4
- data/lib/rigor/inference/project_patched_mixins.rb +162 -0
- data/lib/rigor/inference/project_patched_scanner.rb +51 -12
- data/lib/rigor/inference/project_patched_visibility.rb +214 -0
- data/lib/rigor/inference/project_seed_tables.rb +120 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
- data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +414 -1
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer.rb +1061 -122
- data/lib/rigor/inference/self_call_ivar_writes.rb +346 -0
- data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
- data/lib/rigor/inference/statement_evaluator.rb +512 -150
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/lens/anchor.rb +3 -3
- data/lib/rigor/lens/contract_reader.rb +60 -2
- data/lib/rigor/lens/document.rb +4 -3
- data/lib/rigor/lens/file_reader.rb +452 -73
- data/lib/rigor/lens/members.rb +81 -17
- data/lib/rigor/lens/run.rb +73 -4
- data/lib/rigor/lens.rb +2 -1
- data/lib/rigor/mcp/server.rb +63 -22
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/project_environment.rb +6 -3
- data/lib/rigor/protection/discovery_seed.rb +9 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +27 -0
- data/lib/rigor/scope/discovery_index.rb +5 -0
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
- data/lib/rigor/scope/resolution_chain.rb +40 -5
- data/lib/rigor/scope.rb +56 -8
- data/lib/rigor/sig_gen/alias_index.rb +2 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +167 -22
- data/lib/rigor/sig_gen/layout_index.rb +40 -1
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/observation_collector.rb +137 -11
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- data/lib/rigor/sig_gen/renderer.rb +13 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- data/lib/rigor/sig_gen/write_reporter.rb +203 -0
- data/lib/rigor/sig_gen/write_result.rb +3 -1
- data/lib/rigor/sig_gen/writer.rb +50 -35
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/constant.rb +10 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +57 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
- data/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +4 -3
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +16 -1
|
@@ -5,6 +5,7 @@ require "prism"
|
|
|
5
5
|
require_relative "../type"
|
|
6
6
|
require_relative "../reflection"
|
|
7
7
|
require_relative "../ast"
|
|
8
|
+
require_relative "clause_widening"
|
|
8
9
|
require_relative "../source/constant_path"
|
|
9
10
|
require_relative "../source/node_children"
|
|
10
11
|
require_relative "../source/node_walker"
|
|
@@ -41,6 +42,7 @@ require_relative "receiver_alias"
|
|
|
41
42
|
require_relative "repeated_or_writes"
|
|
42
43
|
require_relative "singleton_object_constant"
|
|
43
44
|
require_relative "refined_dispatch"
|
|
45
|
+
require_relative "singleton_external_ahead"
|
|
44
46
|
require_relative "stored_block_call"
|
|
45
47
|
require_relative "struct_fold_safety"
|
|
46
48
|
require_relative "unknown_store_widening"
|
|
@@ -321,6 +323,37 @@ module Rigor
|
|
|
321
323
|
try_project_singleton_inference(receiver, node, arg_types, method_name: :[])
|
|
322
324
|
end
|
|
323
325
|
|
|
326
|
+
# Issue #1889 — the type a block-form `inject` / `reduce` accumulator parameter holds across the block's runs,
|
|
327
|
+
# or nil when the call is not one {#try_block_inject_fold} folds. The RBS tier binds the parameter to the seed
|
|
328
|
+
# alone, so the statement pass and the ADR-56 write-back typed every run's accumulator as `0` for `inject(0)`,
|
|
329
|
+
# and a capture assigned from it (`m = a`) was written back as `0`.
|
|
330
|
+
#
|
|
331
|
+
# The fold records the answer as it types the call ({#record_inject_accumulator}): the constant arm the
|
|
332
|
+
# accumulator values the block receives (`0 | 1` for `[1, 2].inject(0) { |a, e| a + e }`), the nominal
|
|
333
|
+
# arm its seed joined with the converged accumulator. The statement pass types the call from the same scope
|
|
334
|
+
# just before it enters the block, so the recorded answer is read back rather than computed again — each
|
|
335
|
+
# computation types the body, and a nested `inject` multiplied that by its depth.
|
|
336
|
+
def inject_accumulator_type(call_node, receiver, arg_types)
|
|
337
|
+
block = call_node.block
|
|
338
|
+
return nil unless INJECT_METHODS.include?(call_node.name) && block.is_a?(Prism::BlockNode)
|
|
339
|
+
|
|
340
|
+
recorded = self.class.recorded_inject_accumulator(block, scope)
|
|
341
|
+
return recorded if recorded
|
|
342
|
+
|
|
343
|
+
try_block_inject_fold(call_node, receiver, arg_types)
|
|
344
|
+
self.class.recorded_inject_accumulator(block, scope)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
INJECT_ACCUMULATOR_KEY = :__rigor_inject_accumulator__
|
|
348
|
+
private_constant :INJECT_ACCUMULATOR_KEY
|
|
349
|
+
|
|
350
|
+
# The accumulator type {#record_inject_accumulator} stored for `block` when the call was typed from `scope`
|
|
351
|
+
# itself (by identity), else nil.
|
|
352
|
+
def self.recorded_inject_accumulator(block, scope)
|
|
353
|
+
slot = Thread.current[INJECT_ACCUMULATOR_KEY]&.[](block)
|
|
354
|
+
slot && slot[0].equal?(scope) ? slot[1] : nil
|
|
355
|
+
end
|
|
356
|
+
|
|
324
357
|
# ADR-89 WD2 — the current run's return memo bucket as `{ def_node => [MemoEntry, …] }` (only entries
|
|
325
358
|
# that carry a call descriptor, i.e. every stored entry). Read by the incremental session right after a
|
|
326
359
|
# recording run to harvest each analyzed callee's observed call keys → return descriptors. Returns an
|
|
@@ -1047,12 +1080,15 @@ module Rigor
|
|
|
1047
1080
|
#
|
|
1048
1081
|
# The `case ... in` pattern-matching form (`CaseMatchNode`) and the predicate-less form (`case; when
|
|
1049
1082
|
# c1; ...`) bypass the `===` analysis: pattern matching has richer semantics, and a predicate-less
|
|
1050
|
-
# `case` reduces to a `if c1; ...; elsif c2` chain
|
|
1083
|
+
# `case` reduces to a `if c1; ...; elsif c2` chain. Issue #1495 — like {#type_of_conditional}, that
|
|
1084
|
+
# chain delegates to the statement evaluator, which narrows each arm by its conditions' edges, so
|
|
1085
|
+
# `[case; when x.nil? then "n"; else x end]` reads `"n" | String` as the statement form does.
|
|
1051
1086
|
# Issue #1429 — each arm is typed under the subject's clause narrowing (`Narrowing.case_when_scopes`), the
|
|
1052
1087
|
# scope the statement evaluator runs the arm in: `when Symbol then n` on `n: Integer | Symbol` answers
|
|
1053
1088
|
# `Symbol`, and the `else` arm reads the subject every earlier clause has ruled out.
|
|
1054
1089
|
def type_of_case(node)
|
|
1055
|
-
return type_of_case_simple_union(node) if node.is_a?(Prism::CaseMatchNode)
|
|
1090
|
+
return type_of_case_simple_union(node) if node.is_a?(Prism::CaseMatchNode)
|
|
1091
|
+
return scope.evaluate(node, tracer: tracer).first if node.predicate.nil?
|
|
1056
1092
|
|
|
1057
1093
|
subject_type = type_of(node.predicate)
|
|
1058
1094
|
candidates = []
|
|
@@ -1174,17 +1210,22 @@ module Rigor
|
|
|
1174
1210
|
Type::Combinator.constant_of(nil)
|
|
1175
1211
|
end
|
|
1176
1212
|
|
|
1177
|
-
rescue_types = rescue_chain_types(rescue_clause)
|
|
1213
|
+
rescue_types = rescue_chain_types(rescue_clause, node.statements)
|
|
1178
1214
|
Type::Combinator.union(primary_type, *rescue_types)
|
|
1179
1215
|
end
|
|
1180
1216
|
|
|
1181
1217
|
# Issue #1360 — the evaluator binds `$!` in a rescue clause it enters; a clause typed here, such as one in a
|
|
1182
1218
|
# `begin` passed as an argument, reads `$!`, `$@` and `$?` unbound rather than an enclosing clause's.
|
|
1183
|
-
|
|
1184
|
-
|
|
1219
|
+
#
|
|
1220
|
+
# Issue #1839 — it also reads each name the `body` writes as `Dynamic[top]` ({ClauseWidening}), as the evaluator's
|
|
1221
|
+
# rescue clause does: `[begin; conn = make; work; rescue; conn; end]` holds the connection, not the entry's `nil`.
|
|
1222
|
+
def rescue_chain_types(rescue_node, body)
|
|
1223
|
+
arm_scope = rescue_arm_scope
|
|
1224
|
+
writes = ClauseWidening.writes([body])
|
|
1185
1225
|
types = []
|
|
1186
1226
|
current = rescue_node
|
|
1187
1227
|
while current
|
|
1228
|
+
arm_typer = typer_under(ClauseWidening.widen(arm_scope, writes, current.statements) || arm_scope)
|
|
1188
1229
|
types << arm_typer.send(:statements_or_nil, current.statements)
|
|
1189
1230
|
current = current.subsequent
|
|
1190
1231
|
end
|
|
@@ -1245,11 +1286,23 @@ module Rigor
|
|
|
1245
1286
|
def type_of_range(node)
|
|
1246
1287
|
left_static, left = static_range_endpoint(node.left)
|
|
1247
1288
|
right_static, right = static_range_endpoint(node.right)
|
|
1248
|
-
|
|
1289
|
+
if left_static && right_static
|
|
1290
|
+
range = static_range_value(left, right, node.exclude_end?)
|
|
1291
|
+
return Type::Combinator.constant_of(range) if range
|
|
1292
|
+
end
|
|
1249
1293
|
|
|
1250
1294
|
nominal_range_for_endpoints(node.left, node.right)
|
|
1251
1295
|
end
|
|
1252
1296
|
|
|
1297
|
+
# The `Range` the static endpoints denote, or nil when Ruby would refuse to build it (`1.."a"`,
|
|
1298
|
+
# `"a"..1` raise `ArgumentError: bad value for range`). The literal is a runtime error,
|
|
1299
|
+
# not a constant, so the fold declines and the caller types it as a plain Range.
|
|
1300
|
+
def static_range_value(left, right, exclude_end)
|
|
1301
|
+
Range.new(left, right, exclude_end)
|
|
1302
|
+
rescue ArgumentError
|
|
1303
|
+
nil
|
|
1304
|
+
end
|
|
1305
|
+
|
|
1253
1306
|
# Derives `Nominal[Range, [T]]` from the endpoint expression types when at least one endpoint is
|
|
1254
1307
|
# statically typeable. The element parameter is the union of the endpoint types (lifted from
|
|
1255
1308
|
# `Constant<v>` to `Nominal<v.class>` so the carrier matches what `Range#each` would yield). Falls back
|
|
@@ -2549,6 +2602,10 @@ module Rigor
|
|
|
2549
2602
|
|
|
2550
2603
|
def_node, owner = resolve_singleton_def_with_owner(receiver.class_name, method_name)
|
|
2551
2604
|
return nil if def_node.nil?
|
|
2605
|
+
# Issue #1936 — a contested slot holds a `def` a later definer may have replaced
|
|
2606
|
+
# (`singleton_class.alias_method :x, :y` after `def self.x`), or one that may not have run: its body is not
|
|
2607
|
+
# proven to be the method Ruby calls, so the call falls through to `Dynamic`.
|
|
2608
|
+
return nil if scope.discovery.contested?(:discovered_singleton_def_nodes, [owner, method_name.to_sym])
|
|
2552
2609
|
|
|
2553
2610
|
result = infer_user_method_return(def_node, receiver, arg_types, yield_type: block_type)
|
|
2554
2611
|
return result if result.nil?
|
|
@@ -2563,6 +2620,10 @@ module Rigor
|
|
|
2563
2620
|
# `discovered_def_nodes` / `discovered_superclasses` / `discovered_includes`, and this walk reads a
|
|
2564
2621
|
# fourth table those three do not imply. Keying on what the answer actually depends on is the #682
|
|
2565
2622
|
# lesson — a memo that outlives its inputs serves one scope's answer to another.
|
|
2623
|
+
#
|
|
2624
|
+
# It is the TYPING read only, so it also withholds (`[nil, nil]`, the call types `Dynamic`) where an external
|
|
2625
|
+
# ancestor may answer ahead of the definer found (#1572: `extend M; extend Enumerable` makes `C.to_a`
|
|
2626
|
+
# `Enumerable#to_a`, which the reader passes over). The existence reads keep the reader's own answer.
|
|
2566
2627
|
def resolve_singleton_def_with_owner(class_name, method_name)
|
|
2567
2628
|
by_singleton = (class_graph_buckets[:singleton_def] ||= {}.compare_by_identity)
|
|
2568
2629
|
cache = (by_singleton[scope.discovered_singleton_def_nodes] ||= {})
|
|
@@ -2570,7 +2631,10 @@ module Rigor
|
|
|
2570
2631
|
key = method_name.to_sym
|
|
2571
2632
|
return table[key] if table.key?(key)
|
|
2572
2633
|
|
|
2573
|
-
|
|
2634
|
+
found = scope.singleton_def_through_ancestors(class_name, method_name)
|
|
2635
|
+
owner = found.last
|
|
2636
|
+
found = [nil, nil] if owner && SingletonExternalAhead.withhold?(scope, class_name, method_name, owner)
|
|
2637
|
+
table[key] = found
|
|
2574
2638
|
end
|
|
2575
2639
|
|
|
2576
2640
|
# The project-side singleton-method band: a `Foo.bar` call resolved against a `class << …` / `def self.…`
|
|
@@ -3883,6 +3947,12 @@ module Rigor
|
|
|
3883
3947
|
# default — or `Dynamic` when the shape is open or absent; `**kwrest`, `&block`, and destructured
|
|
3884
3948
|
# positionals bind `Dynamic`. Binding `Dynamic` is monotone-safe (wider in → wider out), so the only
|
|
3885
3949
|
# declines left are the shapes where positional CORRESPONDENCE itself is unknowable.
|
|
3950
|
+
#
|
|
3951
|
+
# The scope carries no `source_path:`: the body is re-typed for call sites in any file, so a rule that
|
|
3952
|
+
# asks "did this file write it?" by comparing `scope.source_path` with census paths answers wrongly
|
|
3953
|
+
# through it. Record a per-file write kind in the census instead, as {ScopeIndexer::CONSTANT_MEMO} does
|
|
3954
|
+
# (#1276). Setting `source_path` here was rejected: plugins read it, and the return memo would become
|
|
3955
|
+
# caller-dependent.
|
|
3886
3956
|
def build_user_method_body_scope(def_node, receiver, arg_types, self_fold_safe: false)
|
|
3887
3957
|
params = def_node.parameters
|
|
3888
3958
|
locals = bind_params_from_call_types(params, arg_types)
|
|
@@ -3893,7 +3963,7 @@ module Rigor
|
|
|
3893
3963
|
# allocated a fresh frozen Scope per field, run per user-method-call inference (ADR-44). The
|
|
3894
3964
|
# discovery index is inherited whole by reference (ADR-53 Track A); the hand-copied per-field list
|
|
3895
3965
|
# this replaces had silently dropped `data_member_layouts` and `discovered_method_visibilities`.
|
|
3896
|
-
Scope.new(
|
|
3966
|
+
body_scope = Scope.new(
|
|
3897
3967
|
environment: scope.environment,
|
|
3898
3968
|
locals: locals.freeze,
|
|
3899
3969
|
self_type: receiver,
|
|
@@ -3911,6 +3981,7 @@ module Rigor
|
|
|
3911
3981
|
# Issue #1358 — the callee runs in a frame of its own ({Scope#with_match_frame}).
|
|
3912
3982
|
match_frame: MatchRebinding::Frame.new(def_node.body, def_node.parameters)
|
|
3913
3983
|
)
|
|
3984
|
+
body_scope.with_repeated_or_writes(RepeatedOrWrites.straight_line_sites(def_node.body, body_scope))
|
|
3914
3985
|
end
|
|
3915
3986
|
|
|
3916
3987
|
# Issue #681 / #707 — the ONE place in `lib/` that answers "what `Module.nesting` was recorded for this
|
|
@@ -5436,13 +5507,25 @@ module Rigor
|
|
|
5436
5507
|
return nil if acc.nil?
|
|
5437
5508
|
|
|
5438
5509
|
captured = inject_captured_bindings(block, rest.map { |value| Type::Combinator.constant_of(value) })
|
|
5510
|
+
received = [acc]
|
|
5439
5511
|
rest.each_with_index do |element_value, position|
|
|
5440
5512
|
acc = inject_constant_step(block, acc, element_value, captured, position)
|
|
5441
5513
|
return nil if acc.nil?
|
|
5514
|
+
|
|
5515
|
+
received << acc
|
|
5442
5516
|
end
|
|
5517
|
+
received.pop if received.size > 1
|
|
5518
|
+
record_inject_accumulator(block, Type::Combinator.union(*received))
|
|
5443
5519
|
acc
|
|
5444
5520
|
end
|
|
5445
5521
|
|
|
5522
|
+
# Records `type` as the accumulator `block` receives when typed from this typer's scope
|
|
5523
|
+
# ({.recorded_inject_accumulator}). The keys are weak, so a finished file's nodes do not stay reachable, and
|
|
5524
|
+
# the values strong, so a record outlives a collection while its node does. Prism nodes compare by identity.
|
|
5525
|
+
def record_inject_accumulator(block, type)
|
|
5526
|
+
(Thread.current[INJECT_ACCUMULATOR_KEY] ||= ObjectSpace::WeakKeyMap.new)[block] = [scope, type].freeze
|
|
5527
|
+
end
|
|
5528
|
+
|
|
5446
5529
|
# Issue #1332 — the #587 (b) captured-block binding ({#captured_block_bindings}) both inject arms lay under
|
|
5447
5530
|
# every step, as the Tuple / Range per-element folds and the per-pair `HashShape` fold do. Without it each
|
|
5448
5531
|
# step reads a captured variable at its entry binding, so a body that rebinds or mutates one (`n += 1`,
|
|
@@ -5550,7 +5633,9 @@ module Rigor
|
|
|
5550
5633
|
result.nil? ? {} : { __inject_acc__: result }
|
|
5551
5634
|
end
|
|
5552
5635
|
)
|
|
5553
|
-
converged[:__inject_acc__] || seed_acc
|
|
5636
|
+
result = converged[:__inject_acc__] || seed_acc
|
|
5637
|
+
record_inject_accumulator(block, Type::Combinator.union(seed_acc, result))
|
|
5638
|
+
result
|
|
5554
5639
|
end
|
|
5555
5640
|
|
|
5556
5641
|
def assemble_per_element_result(method_name, per_position, element_types)
|
|
@@ -223,6 +223,12 @@ module Rigor
|
|
|
223
223
|
end || Type::Combinator.untyped
|
|
224
224
|
end
|
|
225
225
|
|
|
226
|
+
# Issue #1596 — true when `name` runs code chosen by name or by a String, or a block or String it is given
|
|
227
|
+
# ({CODE_RUNNING_NAMES}, {BLOCK_OR_CODE_NAMES}), whatever its receiver.
|
|
228
|
+
def code_running_name?(name)
|
|
229
|
+
CODE_RUNNING_NAMES.include?(name) || BLOCK_OR_CODE_NAMES.include?(name)
|
|
230
|
+
end
|
|
231
|
+
|
|
226
232
|
# True when `method_name` on a value of `type` may run foreign code ({.foreign_target?}).
|
|
227
233
|
def type_method_foreign?(type, method_name, scope)
|
|
228
234
|
targets = ProjectMethodOwnership.targets(type)
|
|
@@ -326,8 +332,7 @@ module Rigor
|
|
|
326
332
|
private_class_method :scan, :guarded_ivar_write?, :scan_call, :receiver_targets, :foreign_target?,
|
|
327
333
|
:universal_delegate_foreign?, :method_owner, :deferred_block_call?,
|
|
328
334
|
:compound_write_foreign?,
|
|
329
|
-
:compound_receiver_type, :compound_accessors, :compound_read_type, :variable_type
|
|
330
|
-
:type_method_foreign?
|
|
335
|
+
:compound_receiver_type, :compound_accessors, :compound_read_type, :variable_type
|
|
331
336
|
end
|
|
332
337
|
|
|
333
338
|
module GuardRebinding
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../type"
|
|
6
|
+
require_relative "reflective_ivar_write"
|
|
7
|
+
require_relative "refined_dispatch/own_method"
|
|
8
|
+
require_relative "self_call_ivar_writes"
|
|
9
|
+
|
|
10
|
+
module Rigor
|
|
11
|
+
module Inference
|
|
12
|
+
# The intervening-call reset of instance variables. An implicit-self or `self.` call may write any instance
|
|
13
|
+
# variable of `self`, so past one each instance variable whose binding is narrower than its class-ivar seed reads
|
|
14
|
+
# the union of the two again; a call proven to write only some ({SelfCallIvarWrites.of}, issue #1596) widens only
|
|
15
|
+
# those. A call on another receiver cannot reach `self`'s instance variables and widens nothing.
|
|
16
|
+
#
|
|
17
|
+
# {.widen} is the reset itself, which the statement evaluator applies past a statement call
|
|
18
|
+
# (`StatementEvaluator#invalidate_ivars_for_intervening_call`). Issue #1963 — {.after_operand} applies it past
|
|
19
|
+
# the calls in a condition operand, where a narrowing an earlier operand or clause made would otherwise survive
|
|
20
|
+
# them: `if @x.nil? && reset!` read `@x` as `nil` in the body although `reset!` assigns it.
|
|
21
|
+
module InterveningIvarReset
|
|
22
|
+
# Nodes whose body does not run where it is written, or runs on another `self`: a `def`, and a class or
|
|
23
|
+
# module body, where `@x` is the class's own instance variable.
|
|
24
|
+
NON_RUNNING_NODES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
25
|
+
# Issue #1402 — the nodes that run code the program does not show on `self`'s behalf: `yield` runs the
|
|
26
|
+
# method's block, and `super` the superclass's method on `self`.
|
|
27
|
+
UNKNOWN_CODE_NODES = [Prism::YieldNode, Prism::SuperNode, Prism::ForwardingSuperNode].freeze
|
|
28
|
+
# Issue #1402 — the names of a proc's call (`blk.call`, `blk.()`, `blk.yield`), which runs the block the method
|
|
29
|
+
# was given as `yield` does.
|
|
30
|
+
PROC_CALL_NAMES = Set[:call, :yield].freeze
|
|
31
|
+
# Issue #1402 — the calls whose block runs with `self` bound to the receiver: on a receiver that is provably
|
|
32
|
+
# another object ({.foreign_self_block?}), an instance variable the block writes, and a method it calls
|
|
33
|
+
# without a receiver, are that object's.
|
|
34
|
+
SELF_REBINDING_BLOCK_CALLS = Set[
|
|
35
|
+
:instance_eval, :instance_exec, :class_eval, :class_exec, :module_eval, :module_exec, :define_method,
|
|
36
|
+
:define_singleton_method
|
|
37
|
+
].freeze
|
|
38
|
+
# The classes whose instances may be a class or module object: a `self` of one of them may be the receiver of
|
|
39
|
+
# `Foo.class_eval`, and an instance of one of them may be a `self` that is a class object.
|
|
40
|
+
CLASS_OBJECT_CLASSES = Set["Class", "Module", "Object", "BasicObject"].freeze
|
|
41
|
+
private_constant :NON_RUNNING_NODES, :UNKNOWN_CODE_NODES, :PROC_CALL_NAMES, :SELF_REBINDING_BLOCK_CALLS,
|
|
42
|
+
:CLASS_OBJECT_CLASSES
|
|
43
|
+
|
|
44
|
+
module_function
|
|
45
|
+
|
|
46
|
+
# True when `node` is a call that may write `self`'s instance variables: an implicit-self or `self.` call
|
|
47
|
+
# ({.self_call?}). Issue #1402 — and a `yield`, a `super`, or a proc's call on a local ({.proc_call?}), which
|
|
48
|
+
# run code the program does not show; {SelfCallIvarWrites.of} proves no write set for any of them.
|
|
49
|
+
def candidate?(node)
|
|
50
|
+
return true if UNKNOWN_CODE_NODES.any? { |klass| node.is_a?(klass) }
|
|
51
|
+
return false unless node.is_a?(Prism::CallNode)
|
|
52
|
+
|
|
53
|
+
self_call?(node) || proc_call?(node)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Whether `node`, a call, has no receiver or `self` as its receiver.
|
|
57
|
+
def self_call?(node)
|
|
58
|
+
receiver = node.receiver
|
|
59
|
+
receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Issue #1402 — `blk.call` / `blk.()` / `blk.yield` on a local: the method's own `&blk`, or a proc it was
|
|
63
|
+
# handed, runs code that may call back into `self`, as `yield` does. Read by name, since an unannotated
|
|
64
|
+
# `&blk` binds `Dynamic`.
|
|
65
|
+
def proc_call?(node)
|
|
66
|
+
PROC_CALL_NAMES.include?(node.name) && node.receiver.is_a?(Prism::LocalVariableReadNode)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Issue #1402 — whether `call_node`'s block runs on an object that is provably not `self`: a
|
|
70
|
+
# {SELF_REBINDING_BLOCK_CALLS} call whose receiver, typed in `scope`, cannot be `self` ({.other_object?}).
|
|
71
|
+
# A receiver that may be `self` at run time (`obj = self; obj.instance_eval { … }`, a block parameter, an
|
|
72
|
+
# `Object` or `Dynamic` value, a class the analysis cannot order against `self`'s) is not foreign.
|
|
73
|
+
def foreign_self_block?(call_node, scope)
|
|
74
|
+
return false unless call_node.is_a?(Prism::CallNode) && SELF_REBINDING_BLOCK_CALLS.include?(call_node.name)
|
|
75
|
+
return false if self_call?(call_node)
|
|
76
|
+
|
|
77
|
+
other_object?(scope.type_of(call_node.receiver), scope)
|
|
78
|
+
rescue StandardError
|
|
79
|
+
false
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Whether a value of `type` cannot be `scope`'s `self`. Only an instance method of a class answers: `self` is
|
|
83
|
+
# then an instance of that class (one other than `Class`, `Module`, `Object` and `BasicObject`, and no module,
|
|
84
|
+
# declared in source or RBS), and a class object (`Singleton[X]`: `self.class`, a constant naming a class or
|
|
85
|
+
# module) is not one. Where `self` is a class object, or a module's instance (`module_function`, `extend
|
|
86
|
+
# self`), any receiver may be `self` — a subclass, the module itself — and none is foreign. An instance
|
|
87
|
+
# receiver is never foreign: its class may be a subclass of `self`'s that the environment cannot order.
|
|
88
|
+
def other_object?(type, scope)
|
|
89
|
+
self_type = scope.self_type
|
|
90
|
+
return false unless self_type.is_a?(Type::Nominal) && plain_class?(self_type.class_name, scope)
|
|
91
|
+
|
|
92
|
+
type.is_a?(Type::Singleton)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Whether `class_name` names a class whose instances are no class or module object, and not a module.
|
|
96
|
+
def plain_class?(class_name, scope)
|
|
97
|
+
name = class_name.to_s.delete_prefix("::")
|
|
98
|
+
!CLASS_OBJECT_CLASSES.include?(name) && !RefinedDispatch::OwnMethod.module_name?(scope, name) &&
|
|
99
|
+
!module_subclass?(name, scope)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Whether `name` may be a subclass of `Module` (`class Meta < Module`), whose instance is a module object.
|
|
103
|
+
# Read off the class's instance `Scope::ResolutionChain`, which resolves a superclass written relative to the
|
|
104
|
+
# subclass's namespace (`class Meta < Base` in `module NS`) as Ruby does: true when an entry is one the
|
|
105
|
+
# environment orders `:subclass` or `:equal` to `Module` (`Module` itself, `Class`, a project class whose
|
|
106
|
+
# signature declares it, an external superclass), when an external superclass names no class the
|
|
107
|
+
# environment knows (it may be `Module`), or when the chain is truncated or unsettled.
|
|
108
|
+
def module_subclass?(name, scope)
|
|
109
|
+
chain = Scope::ResolutionChain.for(scope, name, :instance, :methods)
|
|
110
|
+
return true if chain.truncated? || chain.unsettled?
|
|
111
|
+
|
|
112
|
+
environment = scope.environment
|
|
113
|
+
chain.search(scope) { |entry| module_entry?(entry, environment) || nil } ? true : false
|
|
114
|
+
end
|
|
115
|
+
private_class_method :module_subclass?
|
|
116
|
+
|
|
117
|
+
# Whether a chain entry makes the class a possible `Module` subclass: a project entry {.module_like?}, or an
|
|
118
|
+
# external superclass that names no class the environment knows, or one {.module_like?}. An external mixin
|
|
119
|
+
# (`include Enumerable`) does not.
|
|
120
|
+
def module_entry?(entry, environment)
|
|
121
|
+
return module_like?(entry.name.to_s, environment) unless entry.external?
|
|
122
|
+
return false unless entry.superclass_edge
|
|
123
|
+
|
|
124
|
+
known = entry.candidates.map { |candidate| candidate.to_s.delete_prefix("::") }
|
|
125
|
+
.select { |candidate| environment.class_known?(candidate) }
|
|
126
|
+
known.empty? || known.any? { |candidate| module_like?(candidate, environment) }
|
|
127
|
+
end
|
|
128
|
+
private_class_method :module_entry?
|
|
129
|
+
|
|
130
|
+
def module_like?(name, environment)
|
|
131
|
+
%i[subclass equal].include?(environment.class_ordering(name, "Module"))
|
|
132
|
+
end
|
|
133
|
+
private_class_method :module_like?
|
|
134
|
+
private_class_method :plain_class?
|
|
135
|
+
|
|
136
|
+
# Issue #1402 — `scope` past a statement call whose receiver or arguments run code the program does not
|
|
137
|
+
# show (`@items << yield`, `puts(yield)`, `log(blk.call)`, `items.each(&blk)`): {.widen} over every narrowed
|
|
138
|
+
# name, as past a statement `yield`. Every other call in them is the operand walk's.
|
|
139
|
+
def after_operands(call_node, scope)
|
|
140
|
+
return scope unless narrowed_ivars?(scope)
|
|
141
|
+
|
|
142
|
+
found = []
|
|
143
|
+
collect_calls(call_node.receiver, false, found, scope, unknown_only: true)
|
|
144
|
+
collect_calls(call_node.arguments, false, found, scope, unknown_only: true)
|
|
145
|
+
block = call_node.block
|
|
146
|
+
found << block if block.is_a?(Prism::BlockArgumentNode) && block.expression.is_a?(Prism::LocalVariableReadNode)
|
|
147
|
+
return scope if found.empty?
|
|
148
|
+
|
|
149
|
+
widen(scope, mark: true) { nil }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Issue #1402 — `scope` past a call on another receiver whose literal block may make a {.candidate?} call
|
|
153
|
+
# (`[1].each { reset }`, `items.each { yield }`): {.widen} over the names those calls may write, as
|
|
154
|
+
# {.after_operand} widens past the calls in a condition. In a block that runs on another object
|
|
155
|
+
# ({.foreign_self_block?}) only a `yield`, a `super` or a proc's call counts: its implicit-self calls are that
|
|
156
|
+
# object's, while the method's block runs as it always does. A literal-name
|
|
157
|
+
# `instance_variable_set` on `self` ({ReflectiveIvarWrite.literal_name}) writes the one ivar it names, which
|
|
158
|
+
# the block's captured-binding write-back already joins, so it is not counted.
|
|
159
|
+
def after_block(call_node, scope)
|
|
160
|
+
block = call_node.block
|
|
161
|
+
return scope unless block.is_a?(Prism::BlockNode)
|
|
162
|
+
return scope unless narrowed_ivars?(scope)
|
|
163
|
+
|
|
164
|
+
calls = []
|
|
165
|
+
if foreign_self_block?(call_node, scope)
|
|
166
|
+
collect_calls(block, false, calls, scope, unknown_only: true)
|
|
167
|
+
else
|
|
168
|
+
collect_calls(block, false, calls, scope)
|
|
169
|
+
calls.reject! { |call| ReflectiveIvarWrite.literal_name(call) }
|
|
170
|
+
end
|
|
171
|
+
return scope if calls.empty?
|
|
172
|
+
|
|
173
|
+
widen(scope, mark: true) { proven_writes(calls, scope) }
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# `scope` with each instance variable narrowed below its class-ivar seed widened back over the seed. The block
|
|
177
|
+
# answers the names the call may write, or nil for any; it runs at most once, and only when an instance
|
|
178
|
+
# variable is narrowed, since a proof costs a read of the callee. An instance variable whose binding carries
|
|
179
|
+
# the declaration mark keeps it (issue #1889). With `mark: true` a binding whose narrowing had ruled `nil` out
|
|
180
|
+
# takes the mark when widened, so a `nil` only the seed brings back is not fuel for
|
|
181
|
+
# `call.possible-nil-receiver` (ADR-58), as a declared one is not. A narrowing that kept `nil`
|
|
182
|
+
# (`return if @tag`, `@tag.nil?`) widens unmarked, so a read the program really makes on `nil` still reports.
|
|
183
|
+
def widen(scope, mark: false)
|
|
184
|
+
seed = class_ivar_seed(scope)
|
|
185
|
+
return scope if seed.nil? || seed.empty?
|
|
186
|
+
|
|
187
|
+
computed = false
|
|
188
|
+
writes = nil
|
|
189
|
+
seed.reduce(scope) do |widened, (name, seed_type)|
|
|
190
|
+
local_type = scope.ivar(name)
|
|
191
|
+
next widened if local_type.nil? || local_type == seed_type
|
|
192
|
+
|
|
193
|
+
unless computed
|
|
194
|
+
writes = yield
|
|
195
|
+
computed = true
|
|
196
|
+
end
|
|
197
|
+
next widened if writes && !writes.include?(name)
|
|
198
|
+
|
|
199
|
+
joined = Type::Combinator.union(local_type, seed_type)
|
|
200
|
+
widened = widened.with_ivar(name, joined)
|
|
201
|
+
marked = (mark && nil_free?(local_type)) || scope.declaration_sourced?(:ivar, name)
|
|
202
|
+
marked ? widened.seed_declaration_sourced_ivar(name, joined) : widened
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Issue #1963 — `scope` past the calls `node`, a condition operand, makes: {.widen} over the names every
|
|
207
|
+
# candidate call in it may write, or every name when one of them is not proven. A call's literal block may run,
|
|
208
|
+
# so its calls count; a `def`, a class or module body and (unless `run_lambdas`) a lambda literal run nothing
|
|
209
|
+
# on this `self` there.
|
|
210
|
+
#
|
|
211
|
+
# A binding whose narrowing ruled `nil` out takes the declaration mark when widened ({.widen}'s `mark:`): the
|
|
212
|
+
# reset forgets the narrowing without making the variable newly nil-reportable, since a call the proof cannot
|
|
213
|
+
# clear is far more often one that leaves the variable alone (`if @tag && items.any?` then `t = @tag;
|
|
214
|
+
# t.upcase`) than one that assigns `nil`. A `when` test passes `run_lambdas: true`, since its `===` runs a
|
|
215
|
+
# lambda literal's body.
|
|
216
|
+
def after_operand(node, scope, run_lambdas: false)
|
|
217
|
+
return scope unless narrowed_ivars?(scope)
|
|
218
|
+
|
|
219
|
+
calls = candidate_calls(node, run_lambdas, scope)
|
|
220
|
+
return scope if calls.empty?
|
|
221
|
+
|
|
222
|
+
widen(scope, mark: true) { proven_writes(calls, scope) }
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Whether a candidate call in `node` may write the instance variable `name`. A `when` test passes
|
|
226
|
+
# `run_lambdas: true`, since its `===` runs a lambda literal's body. A write to it in `node` is the caller's
|
|
227
|
+
# to check.
|
|
228
|
+
def may_write?(node, name, scope, run_lambdas: false)
|
|
229
|
+
calls = candidate_calls(node, run_lambdas, scope)
|
|
230
|
+
return false if calls.empty?
|
|
231
|
+
|
|
232
|
+
proven = proven_writes(calls, scope)
|
|
233
|
+
proven.nil? || proven.include?(name)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# The union of the names each call may write ({SelfCallIvarWrites.of}), or nil when one is not proven.
|
|
237
|
+
def proven_writes(calls, scope)
|
|
238
|
+
calls.reduce(Set.new) do |acc, call|
|
|
239
|
+
writes = SelfCallIvarWrites.of(call, scope)
|
|
240
|
+
break nil if writes.nil?
|
|
241
|
+
|
|
242
|
+
acc.merge(writes)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# The candidate calls under `node`, in source order.
|
|
247
|
+
def candidate_calls(node, run_lambdas, scope)
|
|
248
|
+
found = []
|
|
249
|
+
collect_calls(node, run_lambdas, found, scope)
|
|
250
|
+
found
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Issue #1402 — with `unknown_only: true` only the `yield`s, `super`s and proc calls ({.unknown_code?}) count,
|
|
254
|
+
# wherever they sit. A block that runs on another object ({.foreign_self_block?}) is read that way: its
|
|
255
|
+
# implicit-self calls are that object's, while the method's block runs as anywhere else.
|
|
256
|
+
def collect_calls(node, run_lambdas, found, scope, unknown_only: false)
|
|
257
|
+
return unless node.is_a?(Prism::Node)
|
|
258
|
+
return if NON_RUNNING_NODES.any? { |klass| node.is_a?(klass) }
|
|
259
|
+
return if !run_lambdas && node.is_a?(Prism::LambdaNode)
|
|
260
|
+
|
|
261
|
+
found << node if unknown_only ? unknown_code?(node) : candidate?(node)
|
|
262
|
+
node.compact_child_nodes.each do |child|
|
|
263
|
+
foreign = !unknown_only && child.is_a?(Prism::BlockNode) && foreign_self_block?(node, scope)
|
|
264
|
+
collect_calls(child, run_lambdas, found, scope, unknown_only: unknown_only || foreign)
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
private_class_method :collect_calls
|
|
268
|
+
|
|
269
|
+
# Issue #1402 — a `yield`, a `super` or a proc's call ({.proc_call?}).
|
|
270
|
+
def unknown_code?(node)
|
|
271
|
+
UNKNOWN_CODE_NODES.any? { |klass| node.is_a?(klass) } || (node.is_a?(Prism::CallNode) && proc_call?(node))
|
|
272
|
+
end
|
|
273
|
+
private_class_method :unknown_code?
|
|
274
|
+
|
|
275
|
+
def nil_free?(type)
|
|
276
|
+
Narrowing.narrow_non_nil(type) == type
|
|
277
|
+
end
|
|
278
|
+
private_class_method :nil_free?
|
|
279
|
+
|
|
280
|
+
# Whether any instance variable is bound narrower than its class-ivar seed: the only state {.widen} changes.
|
|
281
|
+
def narrowed_ivars?(scope)
|
|
282
|
+
seed = class_ivar_seed(scope)
|
|
283
|
+
return false if seed.nil? || seed.empty?
|
|
284
|
+
|
|
285
|
+
seed.any? do |name, seed_type|
|
|
286
|
+
local_type = scope.ivar(name)
|
|
287
|
+
!local_type.nil? && local_type != seed_type
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
private_class_method :narrowed_ivars?
|
|
291
|
+
|
|
292
|
+
def class_ivar_seed(scope)
|
|
293
|
+
self_type = scope.self_type
|
|
294
|
+
class_name =
|
|
295
|
+
case self_type
|
|
296
|
+
when Type::Nominal, Type::Singleton then self_type.class_name
|
|
297
|
+
end
|
|
298
|
+
class_name && scope.class_ivars_for(class_name)
|
|
299
|
+
end
|
|
300
|
+
private_class_method :class_ivar_seed
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
@@ -45,7 +45,7 @@ module Rigor
|
|
|
45
45
|
# `X.send(:define_method, :m, blk)`), whose body may be a block the file wrote anywhere, an eval of a
|
|
46
46
|
# String, whose code may do any of these, and a `const_set`, which may make a class a `class` body reopens;
|
|
47
47
|
# - one name: a `def` of it anywhere (`def self.gets` at the top level defines it on `main`, which
|
|
48
|
-
# {BlockCallTiming.project_defines_anywhere?} does not
|
|
48
|
+
# {BlockCallTiming.project_defines_anywhere?} does not count), and a Symbol or String literal naming it,
|
|
49
49
|
# which a macro may define it through (`def_delegators :@io, :gets`, `attr_reader :gets`,
|
|
50
50
|
# `undef_method :gets`), other than `&:gets` and the first argument of a call that only names a method
|
|
51
51
|
# (`respond_to?(:gets)`, `send(:gets)`).
|
|
@@ -47,6 +47,25 @@ module Rigor
|
|
|
47
47
|
# same call, another plugin's included, is not consulted. A `:lexical` entry (issue #1667) keeps the caller's
|
|
48
48
|
# `self`, so its `self_type` is the calling scope's.
|
|
49
49
|
def match_for(scope:, call_node:, receiver_type:)
|
|
50
|
+
entry = first_matching_entry(scope, call_node, receiver_type)
|
|
51
|
+
return nil if entry.nil?
|
|
52
|
+
|
|
53
|
+
receiver_name = singleton_receiver_class_name(receiver_type) || nominal_receiver_class_name(receiver_type)
|
|
54
|
+
entry_match(entry, receiver_name, scope, scope.environment)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Whether `call_node`'s block runs as an instance method of the receiver class (issue #1768): its first matching
|
|
58
|
+
# entry binds `:receiver_instance`, so the block body's `self` is the instance {#match_for} narrows to and its
|
|
59
|
+
# `@x` writes are that instance's ivars, as a `def` body's are. A `:lexical` or named-`self_type` entry runs the
|
|
60
|
+
# block on another object, and no entry means no narrowing.
|
|
61
|
+
def runs_as_receiver_instance_method?(scope:, call_node:, receiver_type:)
|
|
62
|
+
entry = first_matching_entry(scope, call_node, receiver_type)
|
|
63
|
+
!entry.nil? && entry.self_type == Plugin::Macro::BlockAsMethod::SELF_TYPE_RECEIVER_INSTANCE
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The first registered entry that matches `call_node` on `receiver_type`, in (plugin registration, declaration)
|
|
67
|
+
# order, or nil.
|
|
68
|
+
def first_matching_entry(scope, call_node, receiver_type)
|
|
50
69
|
return nil if receiver_type.nil?
|
|
51
70
|
|
|
52
71
|
environment = scope&.environment
|
|
@@ -61,12 +80,7 @@ module Rigor
|
|
|
61
80
|
# (plugin registration, declaration) order; the method-name membership is guaranteed
|
|
62
81
|
# by the table key.
|
|
63
82
|
entries = registry.contribution_index.block_entries_for(call_node.name)
|
|
64
|
-
entries.
|
|
65
|
-
next unless entry_matches?(entry, singleton_name, nominal_name, call_node, scope, environment)
|
|
66
|
-
|
|
67
|
-
return entry_match(entry, singleton_name || nominal_name, scope, environment)
|
|
68
|
-
end
|
|
69
|
-
nil
|
|
83
|
+
entries.find { |entry| entry_matches?(entry, singleton_name, nominal_name, call_node, scope, environment) }
|
|
70
84
|
end
|
|
71
85
|
|
|
72
86
|
# The {Match} a matching entry contributes: a `:lexical` one keeps the calling scope's `self`, unknown where the
|