rigortype 0.4.4 → 0.4.6
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/docs/handbook/11-sig-gen.md +100 -18
- data/docs/manual/02-cli-reference.md +85 -16
- data/docs/manual/03-configuration.md +12 -9
- data/docs/manual/04-diagnostics.md +17 -6
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +2 -1
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +17 -2
- data/docs/manual/12-caching.md +8 -1
- data/docs/manual/18-removing-dead-code.md +18 -7
- data/docs/manual/plugins/rigor-actionmailer.md +30 -0
- data/docs/manual/plugins/rigor-actionpack.md +30 -0
- data/docs/manual/plugins/rigor-devise.md +15 -0
- data/docs/manual/plugins/rigor-rspec.md +102 -1
- data/lib/rigor/analysis/buffer_binding.rb +14 -0
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +115 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +253 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +64 -4
- data/lib/rigor/analysis/check_rules.rb +319 -21
- data/lib/rigor/analysis/dependency_recorder.rb +11 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/edit_replay.rb +67 -0
- data/lib/rigor/analysis/incremental.rb +86 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +336 -46
- data/lib/rigor/analysis/path_expansion.rb +44 -1
- data/lib/rigor/analysis/reachability/graph.rb +90 -13
- data/lib/rigor/analysis/reachability/scan.rb +279 -39
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +22 -5
- data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
- data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
- data/lib/rigor/analysis/runner/pool_coordinator.rb +82 -16
- data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
- data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
- data/lib/rigor/analysis/runner.rb +222 -111
- data/lib/rigor/analysis/severity_stamp.rb +11 -0
- data/lib/rigor/analysis/worker_session.rb +24 -1
- data/lib/rigor/cache/descriptor.rb +40 -7
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +49 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/check_command.rb +93 -4
- data/lib/rigor/cli/command.rb +3 -1
- data/lib/rigor/cli/diff_command.rb +2 -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/lens_command.rb +5 -5
- data/lib/rigor/cli/mcp_command.rb +2 -1
- data/lib/rigor/cli/options.rb +20 -3
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +7 -1
- data/lib/rigor/cli/plugins_renderer.rb +24 -2
- data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +73 -13
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/type_of_project_scope.rb +27 -0
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +72 -16
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +43 -7
- 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 +136 -7
- data/lib/rigor/environment/required_features.rb +136 -29
- data/lib/rigor/environment/ruby_library_roots.rb +122 -0
- data/lib/rigor/environment.rb +4 -2
- data/lib/rigor/inference/block_call_timing.rb +7 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
- data/lib/rigor/inference/captured_locals.rb +44 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/deep_erasure.rb +138 -0
- data/lib/rigor/inference/def_return_typer.rb +159 -9
- data/lib/rigor/inference/definer_resolution.rb +101 -20
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +97 -18
- data/lib/rigor/inference/global_write_census.rb +36 -1
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/ivar_writer_census.rb +195 -0
- data/lib/rigor/inference/loop_value_record.rb +53 -0
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +278 -42
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
- data/lib/rigor/inference/method_dispatcher.rb +113 -19
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +149 -42
- data/lib/rigor/inference/mutation_widening.rb +5 -2
- data/lib/rigor/inference/narrowing.rb +116 -11
- data/lib/rigor/inference/object_mixins.rb +13 -1
- 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 +121 -0
- data/lib/rigor/inference/project_yielder.rb +128 -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 +6 -6
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
- data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
- data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
- data/lib/rigor/inference/scope_indexer.rb +1144 -197
- data/lib/rigor/inference/self_call_ivar_writes.rb +29 -12
- 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 +1088 -213
- data/lib/rigor/inference/synthetic_method_index.rb +23 -0
- data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/language_server/buffer_table.rb +6 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
- data/lib/rigor/language_server/project_context.rb +59 -3
- data/lib/rigor/language_server/server.rb +5 -3
- data/lib/rigor/lens/contract_reader.rb +45 -2
- data/lib/rigor/lens/document.rb +2 -1
- data/lib/rigor/lens/file_reader.rb +12 -2
- data/lib/rigor/lens/members.rb +16 -2
- data/lib/rigor/lens/run.rb +34 -7
- data/lib/rigor/mcp/server.rb +11 -3
- data/lib/rigor/plugin/base.rb +29 -13
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/plugin/manifest.rb +29 -3
- data/lib/rigor/plugin/registry.rb +38 -0
- data/lib/rigor/protection/discovery_seed.rb +64 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +36 -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 +59 -7
- 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 +465 -49
- data/lib/rigor/sig_gen/layout_index.rb +5 -2
- data/lib/rigor/sig_gen/observation_collector.rb +141 -12
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +16 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- 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 +5 -3
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/combinator.rb +4 -3
- data/lib/rigor/type/constant.rb +54 -3
- data/lib/rigor/type/hash_shape.rb +14 -9
- data/lib/rigor/type/struct_instance.rb +3 -2
- 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 +61 -44
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -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-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
- 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-activejob/lib/rigor/plugin/activejob.rb +4 -2
- 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-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -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 +102 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
- 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/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +3 -2
- data/sig/rigor/effects/file_collection.rbs +5 -6
- data/sig/rigor/effects/registry.rbs +8 -6
- data/sig/rigor/effects/summary.rbs +6 -5
- data/sig/rigor/environment.rbs +5 -3
- data/sig/rigor/inference/optimistic_origin.rbs +11 -0
- data/sig/rigor/inference.rbs +6 -0
- data/sig/rigor/plugin/additional_initializer.rbs +5 -5
- data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
- data/sig/rigor/plugin/effect_attribution.rbs +25 -10
- data/sig/rigor/plugin/effect_edge.rbs +12 -5
- data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
- data/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/plugin/manifest.rbs +16 -18
- data/sig/rigor/plugin/protocol_contract.rbs +10 -9
- data/sig/rigor/plugin/registry.rbs +1 -1
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +34 -52
- data/sig/rigor/type.rbs +0 -6
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +32 -1
|
@@ -5,12 +5,14 @@ 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"
|
|
11
12
|
require_relative "../analysis/self_call_resolution_recorder"
|
|
12
13
|
require_relative "block_call_timing"
|
|
13
14
|
require_relative "block_repetition"
|
|
15
|
+
require_relative "project_yielder"
|
|
14
16
|
require_relative "block_parameter_binder"
|
|
15
17
|
require_relative "method_parameter_binder"
|
|
16
18
|
require_relative "body_fixpoint"
|
|
@@ -41,6 +43,7 @@ require_relative "receiver_alias"
|
|
|
41
43
|
require_relative "repeated_or_writes"
|
|
42
44
|
require_relative "singleton_object_constant"
|
|
43
45
|
require_relative "refined_dispatch"
|
|
46
|
+
require_relative "singleton_external_ahead"
|
|
44
47
|
require_relative "stored_block_call"
|
|
45
48
|
require_relative "struct_fold_safety"
|
|
46
49
|
require_relative "unknown_store_widening"
|
|
@@ -1049,7 +1052,7 @@ module Rigor
|
|
|
1049
1052
|
# optimistic-carrier decline) and one terminating-arm rule, and cannot drift apart again. Only the
|
|
1050
1053
|
# value is read; the post-scope belongs to whichever walk owns the statement.
|
|
1051
1054
|
def type_of_conditional(node)
|
|
1052
|
-
scope.evaluate(node, tracer: tracer).first
|
|
1055
|
+
LoopValueRecord.fetch(node, scope) || scope.evaluate(node, tracer: tracer).first
|
|
1053
1056
|
end
|
|
1054
1057
|
|
|
1055
1058
|
def type_of_else(node)
|
|
@@ -1208,17 +1211,22 @@ module Rigor
|
|
|
1208
1211
|
Type::Combinator.constant_of(nil)
|
|
1209
1212
|
end
|
|
1210
1213
|
|
|
1211
|
-
rescue_types = rescue_chain_types(rescue_clause)
|
|
1214
|
+
rescue_types = rescue_chain_types(rescue_clause, node.statements)
|
|
1212
1215
|
Type::Combinator.union(primary_type, *rescue_types)
|
|
1213
1216
|
end
|
|
1214
1217
|
|
|
1215
1218
|
# Issue #1360 — the evaluator binds `$!` in a rescue clause it enters; a clause typed here, such as one in a
|
|
1216
1219
|
# `begin` passed as an argument, reads `$!`, `$@` and `$?` unbound rather than an enclosing clause's.
|
|
1217
|
-
|
|
1218
|
-
|
|
1220
|
+
#
|
|
1221
|
+
# Issue #1839 — it also reads each name the `body` writes as `Dynamic[top]` ({ClauseWidening}), as the evaluator's
|
|
1222
|
+
# rescue clause does: `[begin; conn = make; work; rescue; conn; end]` holds the connection, not the entry's `nil`.
|
|
1223
|
+
def rescue_chain_types(rescue_node, body)
|
|
1224
|
+
arm_scope = rescue_arm_scope
|
|
1225
|
+
writes = ClauseWidening.writes([body])
|
|
1219
1226
|
types = []
|
|
1220
1227
|
current = rescue_node
|
|
1221
1228
|
while current
|
|
1229
|
+
arm_typer = typer_under(ClauseWidening.widen(arm_scope, writes, current.statements) || arm_scope)
|
|
1222
1230
|
types << arm_typer.send(:statements_or_nil, current.statements)
|
|
1223
1231
|
current = current.subsequent
|
|
1224
1232
|
end
|
|
@@ -1265,11 +1273,22 @@ module Rigor
|
|
|
1265
1273
|
Type::Combinator.bot
|
|
1266
1274
|
end
|
|
1267
1275
|
|
|
1268
|
-
# `while`
|
|
1269
|
-
#
|
|
1270
|
-
#
|
|
1271
|
-
|
|
1272
|
-
|
|
1276
|
+
# A `while` / `until` / `for` loop in value position. Like {#type_of_conditional}, it delegates to the
|
|
1277
|
+
# statement evaluator, which owns the loop's value: `nil` or a `while` / `until` loop's `break` values
|
|
1278
|
+
# (issue #2031), and a `for` loop's collection or its `break` values (issue #1216).
|
|
1279
|
+
#
|
|
1280
|
+
# A `while` / `until` loop no `break` leaves is `nil` without evaluating its body, as
|
|
1281
|
+
# {StatementEvaluator#loop_value} answers it: a loop that is a call's receiver or argument is typed again by
|
|
1282
|
+
# every narrowing and rule that types that operand, and evaluating it each time re-ran the passes of every loop
|
|
1283
|
+
# nested in it.
|
|
1284
|
+
def type_of_loop(node)
|
|
1285
|
+
return Type::Combinator.constant_of(nil) if break_free_condition_loop?(node)
|
|
1286
|
+
|
|
1287
|
+
LoopValueRecord.fetch(node, scope) || scope.evaluate(node, tracer: tracer).first
|
|
1288
|
+
end
|
|
1289
|
+
|
|
1290
|
+
def break_free_condition_loop?(node)
|
|
1291
|
+
!node.is_a?(Prism::ForNode) && !block_level_jump?(node.statements, Prism::BreakNode)
|
|
1273
1292
|
end
|
|
1274
1293
|
|
|
1275
1294
|
def type_of_lambda(_node)
|
|
@@ -1279,11 +1298,23 @@ module Rigor
|
|
|
1279
1298
|
def type_of_range(node)
|
|
1280
1299
|
left_static, left = static_range_endpoint(node.left)
|
|
1281
1300
|
right_static, right = static_range_endpoint(node.right)
|
|
1282
|
-
|
|
1301
|
+
if left_static && right_static
|
|
1302
|
+
range = static_range_value(left, right, node.exclude_end?)
|
|
1303
|
+
return Type::Combinator.constant_of(range) if range
|
|
1304
|
+
end
|
|
1283
1305
|
|
|
1284
1306
|
nominal_range_for_endpoints(node.left, node.right)
|
|
1285
1307
|
end
|
|
1286
1308
|
|
|
1309
|
+
# The `Range` the static endpoints denote, or nil when Ruby would refuse to build it (`1.."a"`,
|
|
1310
|
+
# `"a"..1` raise `ArgumentError: bad value for range`). The literal is a runtime error,
|
|
1311
|
+
# not a constant, so the fold declines and the caller types it as a plain Range.
|
|
1312
|
+
def static_range_value(left, right, exclude_end)
|
|
1313
|
+
Range.new(left, right, exclude_end)
|
|
1314
|
+
rescue ArgumentError
|
|
1315
|
+
nil
|
|
1316
|
+
end
|
|
1317
|
+
|
|
1287
1318
|
# Derives `Nominal[Range, [T]]` from the endpoint expression types when at least one endpoint is
|
|
1288
1319
|
# statically typeable. The element parameter is the union of the endpoint types (lifted from
|
|
1289
1320
|
# `Constant<v>` to `Nominal<v.class>` so the carrier matches what `Range#each` would yield). Falls back
|
|
@@ -2583,6 +2614,10 @@ module Rigor
|
|
|
2583
2614
|
|
|
2584
2615
|
def_node, owner = resolve_singleton_def_with_owner(receiver.class_name, method_name)
|
|
2585
2616
|
return nil if def_node.nil?
|
|
2617
|
+
# Issue #1936 — a contested slot holds a `def` a later definer may have replaced
|
|
2618
|
+
# (`singleton_class.alias_method :x, :y` after `def self.x`), or one that may not have run: its body is not
|
|
2619
|
+
# proven to be the method Ruby calls, so the call falls through to `Dynamic`.
|
|
2620
|
+
return nil if scope.discovery.contested?(:discovered_singleton_def_nodes, [owner, method_name.to_sym])
|
|
2586
2621
|
|
|
2587
2622
|
result = infer_user_method_return(def_node, receiver, arg_types, yield_type: block_type)
|
|
2588
2623
|
return result if result.nil?
|
|
@@ -2597,6 +2632,10 @@ module Rigor
|
|
|
2597
2632
|
# `discovered_def_nodes` / `discovered_superclasses` / `discovered_includes`, and this walk reads a
|
|
2598
2633
|
# fourth table those three do not imply. Keying on what the answer actually depends on is the #682
|
|
2599
2634
|
# lesson — a memo that outlives its inputs serves one scope's answer to another.
|
|
2635
|
+
#
|
|
2636
|
+
# It is the TYPING read only, so it also withholds (`[nil, nil]`, the call types `Dynamic`) where an external
|
|
2637
|
+
# ancestor may answer ahead of the definer found (#1572: `extend M; extend Enumerable` makes `C.to_a`
|
|
2638
|
+
# `Enumerable#to_a`, which the reader passes over). The existence reads keep the reader's own answer.
|
|
2600
2639
|
def resolve_singleton_def_with_owner(class_name, method_name)
|
|
2601
2640
|
by_singleton = (class_graph_buckets[:singleton_def] ||= {}.compare_by_identity)
|
|
2602
2641
|
cache = (by_singleton[scope.discovered_singleton_def_nodes] ||= {})
|
|
@@ -2604,7 +2643,10 @@ module Rigor
|
|
|
2604
2643
|
key = method_name.to_sym
|
|
2605
2644
|
return table[key] if table.key?(key)
|
|
2606
2645
|
|
|
2607
|
-
|
|
2646
|
+
found = scope.singleton_def_through_ancestors(class_name, method_name)
|
|
2647
|
+
owner = found.last
|
|
2648
|
+
found = [nil, nil] if owner && SingletonExternalAhead.withhold?(scope, class_name, method_name, owner)
|
|
2649
|
+
table[key] = found
|
|
2608
2650
|
end
|
|
2609
2651
|
|
|
2610
2652
|
# The project-side singleton-method band: a `Foo.bar` call resolved against a `class << …` / `def self.…`
|
|
@@ -3288,21 +3330,34 @@ module Rigor
|
|
|
3288
3330
|
cache = class_graph_buckets[:yields]
|
|
3289
3331
|
return cache[def_node] if cache.key?(def_node)
|
|
3290
3332
|
|
|
3291
|
-
cache[def_node] = yield_reachable?(def_node.body)
|
|
3333
|
+
cache[def_node] = yield_reachable?(def_node.body, def_node.parameters&.block&.name)
|
|
3292
3334
|
end
|
|
3293
3335
|
|
|
3294
|
-
def yield_reachable?(node)
|
|
3336
|
+
def yield_reachable?(node, block_name)
|
|
3295
3337
|
return false if node.nil?
|
|
3296
|
-
return true if node.is_a?(Prism::YieldNode)
|
|
3338
|
+
return true if node.is_a?(Prism::YieldNode) || forwards_block_argument?(node, block_name)
|
|
3297
3339
|
|
|
3298
3340
|
node.rigor_each_child do |child|
|
|
3299
3341
|
next if YIELD_BOUNDARY_NODES.include?(child.class)
|
|
3300
3342
|
|
|
3301
|
-
return true if yield_reachable?(child)
|
|
3343
|
+
return true if yield_reachable?(child, block_name)
|
|
3302
3344
|
end
|
|
3303
3345
|
false
|
|
3304
3346
|
end
|
|
3305
3347
|
|
|
3348
|
+
# Issue #1157 — a call that hands on its method's block (`&`, or `&blk` naming the block parameter) reaches the
|
|
3349
|
+
# caller's block as a `yield` does. Whether `blk` is still the method's own block (not rebound or shadowed) is
|
|
3350
|
+
# decided where it is typed ({#forwarded_block_return_type}); this only keeps the call-site dimension for a def
|
|
3351
|
+
# that might.
|
|
3352
|
+
def forwards_block_argument?(node, block_name)
|
|
3353
|
+
block = node.respond_to?(:block) ? node.block : nil
|
|
3354
|
+
return false unless block.is_a?(Prism::BlockArgumentNode)
|
|
3355
|
+
|
|
3356
|
+
expression = block.expression
|
|
3357
|
+
expression.nil? || (!block_name.nil? && expression.is_a?(Prism::LocalVariableReadNode) &&
|
|
3358
|
+
expression.name == block_name)
|
|
3359
|
+
end
|
|
3360
|
+
|
|
3306
3361
|
# The candidate-frame memo path: consult the current run generation's bucket, and on a miss compute
|
|
3307
3362
|
# and store a FINAL result. Reached only when `memo_candidate?` held (see `infer_user_method_return`).
|
|
3308
3363
|
# The store gate (ADR-84 WD3): a result is stored when no transient-machinery event during the
|
|
@@ -4298,9 +4353,13 @@ module Rigor
|
|
|
4298
4353
|
|
|
4299
4354
|
# Whether the call may run its block more than once, so a later run reads a captured binding an earlier
|
|
4300
4355
|
# run rebound — the premise of laying the #587 (b) binding ({#captured_block_bindings}) under the block.
|
|
4301
|
-
# {BlockRepetition.may_repeat?} holds the rule, which the statement pass shares (issue #1412).
|
|
4356
|
+
# {BlockRepetition.may_repeat?} holds the rule, which the statement pass shares (issue #1412). Issue #2301 — a
|
|
4357
|
+
# project callee that hands the block on ({ProjectYielder.hands_block_on?}) also counts: the block's value is what
|
|
4358
|
+
# the forward's target `yield`s, and that target may run it many times, so a captured local the block rebinds must
|
|
4359
|
+
# not read its entry binding.
|
|
4302
4360
|
def block_may_repeat?(call_node, receiver_type)
|
|
4303
|
-
BlockRepetition.may_repeat?(method_name: call_node.name, receiver_type: receiver_type, scope: scope)
|
|
4361
|
+
BlockRepetition.may_repeat?(method_name: call_node.name, receiver_type: receiver_type, scope: scope) ||
|
|
4362
|
+
ProjectYielder.hands_block_on?(call_node, receiver_type, scope)
|
|
4304
4363
|
end
|
|
4305
4364
|
|
|
4306
4365
|
EMPTY_BREAK_ARMS = [].freeze
|
|
@@ -4396,10 +4455,22 @@ module Rigor
|
|
|
4396
4455
|
)
|
|
4397
4456
|
type_block_body(block_arg, entry, captured: captured)
|
|
4398
4457
|
when Prism::BlockArgumentNode
|
|
4399
|
-
symbol_block_return_type(block_arg, expected)
|
|
4458
|
+
forwarded_block_return_type(block_arg) || symbol_block_return_type(block_arg, expected)
|
|
4400
4459
|
end
|
|
4401
4460
|
end
|
|
4402
4461
|
|
|
4462
|
+
# Issue #1157 — `&` / `&blk` hands the callee the block this method was itself called with, so the callee's
|
|
4463
|
+
# `yield` is worth what the CALLER's block returns: the value {.current_yield_value_type} already holds for
|
|
4464
|
+
# this frame (the one a `yield` written here reads). Without it a name resolver that passes its block on
|
|
4465
|
+
# (`def walk(x, &) = first_hit(x, &)`) read as `Dynamic[top]` through the forward and was skipped by
|
|
4466
|
+
# `sig-gen`. Nil when the argument is not a forwarded block or the call site passed no block.
|
|
4467
|
+
def forwarded_block_return_type(block_arg)
|
|
4468
|
+
frame = scope.match_frame
|
|
4469
|
+
return nil unless frame&.forwards_block?(block_arg)
|
|
4470
|
+
|
|
4471
|
+
ExpressionTyper.current_yield_value_type
|
|
4472
|
+
end
|
|
4473
|
+
|
|
4403
4474
|
# The scope a block body is typed under: the surrounding scope plus the parameter bindings the receiving
|
|
4404
4475
|
# method's signature implies, with the #587 (b) `captured` binding ({#repeating_captured_bindings}) laid
|
|
4405
4476
|
# between the two, so the body reads what a captured local, instance variable, class variable or global
|
|
@@ -4494,6 +4565,14 @@ module Rigor
|
|
|
4494
4565
|
# A failure in the re-answer falls back to the plain tail-only answer, for the reason
|
|
4495
4566
|
# {#threaded_block_body_type} gives: a raise reaching `block_return_type_for` would report "no block".
|
|
4496
4567
|
def tail_only_block_body_type(body, block_scope, captured)
|
|
4568
|
+
StatementEvaluator.provisional_returns { tail_only_block_body_type_unrecorded(body, block_scope, captured) }
|
|
4569
|
+
end
|
|
4570
|
+
|
|
4571
|
+
# Issue #2299 — the tail alone, typed from `block_scope`, is not the walk the method's returns are collected
|
|
4572
|
+
# from: a `return` in it reads a name the skipped prefix binds as unbound. Its records are provisional
|
|
4573
|
+
# ({StatementEvaluator.provisional_returns}): the statement walk's record of the same `return` replaces one,
|
|
4574
|
+
# and one stands only for a block the walk never enters (a block in expression position).
|
|
4575
|
+
def tail_only_block_body_type_unrecorded(body, block_scope, captured)
|
|
4497
4576
|
return block_scope.type_of(body) unless block_body_threading_suppressed? && body.is_a?(Prism::StatementsNode)
|
|
4498
4577
|
|
|
4499
4578
|
answered =
|
|
@@ -4,6 +4,7 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../source/constant_path"
|
|
6
6
|
require_relative "../source/node_children"
|
|
7
|
+
require_relative "ivar_writer_census"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module Inference
|
|
@@ -26,6 +27,8 @@ module Rigor
|
|
|
26
27
|
# kept apart from `[:defines, …]`.
|
|
27
28
|
# - `[:main_mixin, name]` — a module a top-level `include`, `prepend` or `extend` mixes into `Object` (or
|
|
28
29
|
# `main`), as written. A module RBS does not know may carry any method, so it reaches every class.
|
|
30
|
+
# - the instance-variable writer entries of {IvarWriterCensus} (issue #1154): `[:ivar_set, name]`,
|
|
31
|
+
# `[:ivar_set_class, class_name]` and `[:own_write, class_name, name, path]`.
|
|
29
32
|
#
|
|
30
33
|
# The census is a `Set` of those frozen entries, merged by union across files, the project pre-pass and the
|
|
31
34
|
# `pre_eval:` files. It is recorded inside descents that already visit every node ({Collector#visit}), never
|
|
@@ -74,12 +77,40 @@ module Rigor
|
|
|
74
77
|
census.include?(DEFINES_ANY) || names.any? { |name| census.include?([:defines, name]) }
|
|
75
78
|
end
|
|
76
79
|
|
|
80
|
+
# #1154 — the {IvarWriterCensus} entries of `census`, for the incremental diff and its readers.
|
|
81
|
+
IVAR_KINDS = %i[ivar_set ivar_set_class own_write].to_set.freeze
|
|
82
|
+
|
|
83
|
+
def ivar_set_entries(census) = census.select { |entry| IVAR_KINDS.include?(entry.first) }.to_set
|
|
84
|
+
|
|
85
|
+
# The negative-dependency key an {IvarWriterCensus} entry fires, the one `UnseenIvarWriters` records.
|
|
86
|
+
def ivar_key(entry)
|
|
87
|
+
case entry.first
|
|
88
|
+
when :ivar_set then "ivar_set:#{entry[1]}"
|
|
89
|
+
when :ivar_set_class then "ivar_set_class:#{entry[1]}"
|
|
90
|
+
else "own_write:#{entry[1]}##{entry[2]}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
77
94
|
# Whether some refinement may add `write`.
|
|
78
95
|
def refines_write?(census) = census.include?(REFINES_ANY) || census.include?(%i[refines write])
|
|
79
96
|
|
|
80
97
|
# The module names the top-level mixins spell.
|
|
81
98
|
def main_mixins(census) = census.filter_map { |entry| entry[1] if entry[0] == :main_mixin }
|
|
82
99
|
|
|
100
|
+
# Issue #2158 — the `global_census:` dependency key a census entry moves, without the kind prefix: `alias:$x`,
|
|
101
|
+
# `defines:write`, `refines:write`, `defines_any`, `refines_any`, and `main_mixin` for every top-level mixin: its
|
|
102
|
+
# reader asks about each, and one shared key is a deliberate precision trade-off (a mixin moving anywhere
|
|
103
|
+
# re-checks every write that read them). The rules that read the census record these keys
|
|
104
|
+
# (`CheckRules#record_global_census`), and `Analysis::Incremental.changed_global_census_keys` fires them for
|
|
105
|
+
# every entry that appeared in or vanished from a file's census.
|
|
106
|
+
def dependency_key(entry)
|
|
107
|
+
case entry
|
|
108
|
+
when DEFINES_ANY then "defines_any"
|
|
109
|
+
when REFINES_ANY then "refines_any"
|
|
110
|
+
else entry[0] == :main_mixin ? "main_mixin" : "#{entry[0]}:#{entry[1]}"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
83
114
|
# A standalone walk, for a file no existing descent visits (a `pre_eval:` entry).
|
|
84
115
|
def scan(root)
|
|
85
116
|
collector = Collector.new(InEffectRefinements.new(root))
|
|
@@ -107,12 +138,16 @@ module Rigor
|
|
|
107
138
|
def initialize(in_effect)
|
|
108
139
|
@census = Set.new
|
|
109
140
|
@refine_ranges = []
|
|
141
|
+
@ivar_writers = IvarWriterCensus.new(@census)
|
|
110
142
|
@in_effect = in_effect
|
|
111
143
|
end
|
|
112
144
|
|
|
113
145
|
# A node the collector cannot read must not fail the analysis of its file, nor of every file the pre-pass
|
|
114
146
|
# feeds: the census then counts it as a definition of every name, which only declines a report.
|
|
115
|
-
|
|
147
|
+
# `owner` is the qualified name of the class whose body the node sits in, for {IvarWriterCensus} (nil where
|
|
148
|
+
# the host walk does not track it, or no class owns an instance there).
|
|
149
|
+
def visit(node, top_level:, owner: nil)
|
|
150
|
+
@ivar_writers.visit(node, owner)
|
|
116
151
|
case node
|
|
117
152
|
when Prism::AliasGlobalVariableNode then visit_global_alias(node)
|
|
118
153
|
when Prism::DefNode then record(node, node.name)
|
|
@@ -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
|