rigortype 0.4.7 → 0.4.8
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 +3 -1
- data/data/core_overlay/bigdecimal.rbs +33 -0
- data/data/core_overlay/resolv.rbs +30 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
- data/data/gem_overlay/addressable/addressable.rbs +40 -0
- data/docs/handbook/01-getting-started.md +1 -1
- data/docs/handbook/11-sig-gen.md +35 -0
- data/docs/install.md +20 -0
- data/docs/manual/01-installation.md +67 -26
- data/docs/manual/02-cli-reference.md +307 -91
- data/docs/manual/03-configuration.md +41 -3
- data/docs/manual/04-diagnostics.md +5 -4
- data/docs/manual/08-skills.md +11 -0
- data/docs/manual/09-editor-integration.md +8 -0
- data/docs/manual/14-rails-quickstart.md +1 -1
- data/docs/manual/19-effect-labels.md +3 -4
- data/docs/manual/plugins/rigor-actionpack.md +15 -6
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
- data/docs/manual/plugins/rigor-sorbet.md +17 -3
- data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
- data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
- data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
- data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules.rb +598 -99
- data/lib/rigor/analysis/crash_signature.rb +43 -1
- data/lib/rigor/analysis/dependency_recorder.rb +233 -76
- data/lib/rigor/analysis/incremental.rb +11 -1
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +279 -37
- data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
- data/lib/rigor/analysis/rule_catalog.rb +37 -1
- data/lib/rigor/analysis/run_cache_key.rb +27 -3
- data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
- data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
- data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
- data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
- data/lib/rigor/analysis/runner.rb +222 -39
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
- data/lib/rigor/analysis/worker_session.rb +42 -41
- data/lib/rigor/bleeding_edge.rb +19 -4
- data/lib/rigor/cache/file_digest.rb +39 -31
- data/lib/rigor/cache/incremental_snapshot.rb +66 -7
- data/lib/rigor/cache/rbs_descriptor.rb +37 -2
- data/lib/rigor/cache/rbs_environment.rb +5 -1
- data/lib/rigor/cli/baseline_command.rb +15 -2
- data/lib/rigor/cli/check_command.rb +4 -2
- data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
- data/lib/rigor/cli/lens_command.rb +17 -2
- data/lib/rigor/cli/sig_gen_command.rb +8 -0
- data/lib/rigor/cli/skill_describe.rb +1 -0
- data/lib/rigor/cli.rb +5 -2
- data/lib/rigor/config_audit.rb +32 -18
- data/lib/rigor/configuration/severity_profile.rb +9 -0
- data/lib/rigor/configuration.rb +32 -5
- data/lib/rigor/effects/collector.rb +53 -21
- data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
- data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
- data/lib/rigor/environment/default_libraries.rb +17 -0
- data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
- data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
- data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
- data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
- data/lib/rigor/environment/rbs_loader.rb +312 -63
- data/lib/rigor/environment.rb +131 -35
- data/lib/rigor/inference/acceptance.rb +50 -15
- data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
- data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
- data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
- data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
- data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
- data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
- data/lib/rigor/inference/block_auto_splat.rb +1 -1
- data/lib/rigor/inference/block_run_count.rb +9 -2
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
- data/lib/rigor/inference/captured_locals.rb +2 -0
- data/lib/rigor/inference/census_foreign_block.rb +354 -0
- data/lib/rigor/inference/clause_widening.rb +1 -1
- data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
- data/lib/rigor/inference/construction_escape.rb +56 -15
- data/lib/rigor/inference/declaration_walk.rb +4 -0
- data/lib/rigor/inference/def_node_resolver.rb +32 -14
- data/lib/rigor/inference/def_return_typer.rb +1 -1
- data/lib/rigor/inference/definer_resolution.rb +15 -7
- data/lib/rigor/inference/definers_index.rb +72 -16
- data/lib/rigor/inference/error_info.rb +16 -0
- data/lib/rigor/inference/expression_typer.rb +1459 -287
- data/lib/rigor/inference/flow_tracer.rb +31 -14
- data/lib/rigor/inference/fork_map.rb +3 -0
- data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
- data/lib/rigor/inference/global_write_census.rb +8 -6
- data/lib/rigor/inference/in_effect_refinements.rb +7 -0
- data/lib/rigor/inference/indexed_narrowing.rb +57 -8
- data/lib/rigor/inference/ivar_writer_census.rb +106 -11
- data/lib/rigor/inference/key_presence_guard.rb +51 -25
- data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
- data/lib/rigor/inference/last_line.rb +5 -2
- data/lib/rigor/inference/loop_value_record.rb +63 -15
- data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
- data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
- data/lib/rigor/inference/match_rebinding.rb +21 -8
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
- data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
- data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
- data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
- data/lib/rigor/inference/method_dispatcher.rb +66 -18
- data/lib/rigor/inference/method_parameter_binder.rb +136 -56
- data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
- data/lib/rigor/inference/module_function_state.rb +13 -2
- data/lib/rigor/inference/multi_target_binder.rb +1 -1
- data/lib/rigor/inference/mutation_widening.rb +44 -4
- data/lib/rigor/inference/narrowing.rb +201 -27
- data/lib/rigor/inference/operand_effects.rb +43 -2
- data/lib/rigor/inference/optimistic_origin.rb +133 -8
- data/lib/rigor/inference/origin_lookup.rb +1 -1
- data/lib/rigor/inference/precision_scanner.rb +1 -1
- data/lib/rigor/inference/project_method_ownership.rb +28 -10
- data/lib/rigor/inference/project_patched_methods.rb +65 -13
- data/lib/rigor/inference/project_patched_scanner.rb +120 -25
- data/lib/rigor/inference/project_patched_visibility.rb +39 -3
- data/lib/rigor/inference/rbs_type_translator.rb +68 -15
- data/lib/rigor/inference/receiver_writer_call.rb +67 -0
- data/lib/rigor/inference/recursion_rebinding.rb +157 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
- data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
- data/lib/rigor/inference/refined_dispatch.rb +13 -0
- data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
- data/lib/rigor/inference/remover_holder_scan.rb +76 -22
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
- data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
- data/lib/rigor/inference/scope_indexer.rb +2079 -183
- data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
- data/lib/rigor/inference/self_preserving_block.rb +87 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
- data/lib/rigor/inference/singleton_object_constant.rb +32 -2
- data/lib/rigor/inference/statement_evaluator.rb +970 -250
- data/lib/rigor/inference/string_mutation.rb +14 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
- data/lib/rigor/inference/toplevel_def_census.rb +14 -6
- data/lib/rigor/inference/unknown_store_widening.rb +1 -1
- data/lib/rigor/inference/untyped_rejoin.rb +1 -1
- data/lib/rigor/inference/void_tail_summary.rb +29 -10
- data/lib/rigor/lens/claims/comparison.rb +78 -0
- data/lib/rigor/lens/claims/request.rb +131 -0
- data/lib/rigor/lens/claims/type_reader.rb +216 -0
- data/lib/rigor/lens/claims.rb +208 -0
- data/lib/rigor/lens/contract_reader.rb +5 -0
- data/lib/rigor/lens/document.rb +1 -1
- data/lib/rigor/lens/file_reader.rb +21 -8
- data/lib/rigor/lens/run.rb +8 -0
- data/lib/rigor/lens.rb +1 -1
- data/lib/rigor/library_probe.rb +68 -0
- data/lib/rigor/memo_audit.rb +282 -0
- data/lib/rigor/plugin/base.rb +15 -2
- data/lib/rigor/plugin/io_boundary.rb +25 -3
- data/lib/rigor/plugin/manifest.rb +7 -3
- data/lib/rigor/plugin/registry.rb +13 -2
- data/lib/rigor/plugin/trust_policy.rb +46 -0
- data/lib/rigor/protection/mutation_cache.rb +4 -1
- data/lib/rigor/reflection/constant_ancestors.rb +79 -22
- data/lib/rigor/reflection/declaration_collectors.rb +55 -0
- data/lib/rigor/reflection/declarations.rb +58 -0
- data/lib/rigor/reflection/project_method.rb +177 -0
- data/lib/rigor/reflection.rb +2 -0
- data/lib/rigor/rooted_glob.rb +18 -1
- data/lib/rigor/run_scoped_slots.rb +20 -20
- data/lib/rigor/scope/discovery_index.rb +149 -2
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
- data/lib/rigor/scope/resolution_chain.rb +900 -55
- data/lib/rigor/scope.rb +307 -34
- data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
- data/lib/rigor/sig_gen/classification.rb +14 -1
- data/lib/rigor/sig_gen/generator.rb +222 -13
- data/lib/rigor/sig_gen/method_candidate.rb +14 -0
- data/lib/rigor/sig_gen/observation_collector.rb +11 -2
- data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
- data/lib/rigor/sig_gen/writer.rb +21 -0
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/source/forwardable_delegators.rb +122 -0
- data/lib/rigor/source/node_children.rb +13 -0
- data/lib/rigor/type/combinator.rb +164 -8
- data/lib/rigor/type/interface.rb +60 -0
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_of_memo/serving.rb +99 -0
- data/lib/rigor/type_of_memo/stats.rb +43 -0
- data/lib/rigor/type_of_memo.rb +337 -0
- data/lib/rigor/typing_state.rb +1094 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
- data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
- data/sig/rigor/analysis/fact_store.rbs +2 -3
- data/sig/rigor/cache/descriptor.rbs +14 -0
- data/sig/rigor/environment.rbs +4 -1
- data/sig/rigor/plugin/base.rbs +53 -19
- data/sig/rigor/plugin/interfaces.rbs +51 -0
- data/sig/rigor/plugin/io_boundary.rbs +2 -3
- data/sig/rigor/plugin/node_context.rbs +21 -0
- data/sig/rigor/reflection.rbs +37 -0
- data/sig/rigor/scope.rbs +22 -3
- data/sig/rigor/trinary.rbs +1 -0
- data/sig/rigor/type.rbs +9 -3
- data/sig/rigor.rbs +3 -2
- data/skills/rigor-ask/SKILL.md +1 -0
- data/skills/rigor-project-init/references/01-detect.md +1 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
- data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
- data/skills/rigor-rbs-contract/SKILL.md +147 -0
- data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
- data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
- data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
- data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
- data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
- data/skills/rigor-rbs-setup/SKILL.md +3 -1
- data/skills/rigor-type-oracle/SKILL.md +26 -1
- data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
- metadata +48 -2
- data/lib/rigor/inference/memo_audit.rb +0 -55
|
@@ -226,6 +226,8 @@ module Rigor
|
|
|
226
226
|
# arguments nothing will read.
|
|
227
227
|
# A precise `Hash` counts too when its binding carries the `:synthesised` construction fact (issue #2555): the
|
|
228
228
|
# re-join grows it from these arguments.
|
|
229
|
+
# An intersection counts when a member does: `non-empty-lowercase-string`'s witness half reads `tr!`'s
|
|
230
|
+
# replacement ({#widen_string_intersection}).
|
|
229
231
|
def shape_carrier?(type, synthesised: false)
|
|
230
232
|
case type
|
|
231
233
|
when Type::Tuple, Type::HashShape then true
|
|
@@ -237,7 +239,7 @@ module Rigor
|
|
|
237
239
|
MutationRejoin.regrowable_carrier?(type) || MutationRejoin.precise_array?(type) ||
|
|
238
240
|
SetMutation.regrowable?(type) || (synthesised && MutationRejoin.synthesised_carrier?(type))
|
|
239
241
|
when Type::Difference then type.removes_empty_witness?
|
|
240
|
-
when Type::Union then type.members.any? { |m| shape_carrier?(m, synthesised:) }
|
|
242
|
+
when Type::Union, Type::Intersection then type.members.any? { |m| shape_carrier?(m, synthesised:) }
|
|
241
243
|
else false
|
|
242
244
|
end
|
|
243
245
|
end
|
|
@@ -304,6 +306,10 @@ module Rigor
|
|
|
304
306
|
receiver = call_node.receiver
|
|
305
307
|
return scope if receiver.nil?
|
|
306
308
|
|
|
309
|
+
# Issue #2335 — a block parameter, an element read or any other receiver that may be an outer local's object.
|
|
310
|
+
if AliasMutationFloor.candidate?(call_node)
|
|
311
|
+
scope = AliasMutationFloor.floor_after_call(call_node, scope) { captured_element_root(receiver) }
|
|
312
|
+
end
|
|
307
313
|
widened = widen_outer_element_read(call_node, scope)
|
|
308
314
|
return widened if widened
|
|
309
315
|
|
|
@@ -321,6 +327,14 @@ module Rigor
|
|
|
321
327
|
end
|
|
322
328
|
end
|
|
323
329
|
|
|
330
|
+
# The outer root of an element read in a block body, whose element {#widen_outer_element_read} widens. (A captured
|
|
331
|
+
# local the receiver names takes the capture path's widening and the block content join after it, which a floor
|
|
332
|
+
# here would not change.)
|
|
333
|
+
def captured_element_root(receiver)
|
|
334
|
+
root = ElementReadWidening.element_read_path(receiver)&.first
|
|
335
|
+
root && !ReceiverAlias.block_local?(root) ? [root.name] : nil
|
|
336
|
+
end
|
|
337
|
+
|
|
324
338
|
# Issue #2287 — a mutator on an element read rooted at a captured local (`[0].each { |i| a[i] << 2 }`,
|
|
325
339
|
# `loop { a[0] << 2; break }`) mutates an element the outer binding still carries, and names no variable
|
|
326
340
|
# {ReceiverAlias.candidates} could widen. The element at that path widens in the outer scope as the
|
|
@@ -431,8 +445,7 @@ module Rigor
|
|
|
431
445
|
join_added_pairs(widen_hash_shape(type, values: values), method_name, arg_types,
|
|
432
446
|
ContentJoin.hash_shape_key_values(type))
|
|
433
447
|
when Type::Constant then StringMutation.widen_constant(type, method_name)
|
|
434
|
-
when Type::Difference
|
|
435
|
-
widen_difference(type, method_name, arg_types: arg_types)
|
|
448
|
+
when Type::Difference, Type::Refined, Type::Intersection then widen_refinement(type, method_name, arg_types)
|
|
436
449
|
when Type::Union
|
|
437
450
|
widen_union(type, method_name, values:, arg_types:, unread_arguments:, rewritten_element:, synthesised:)
|
|
438
451
|
end
|
|
@@ -477,7 +490,8 @@ module Rigor
|
|
|
477
490
|
(ARRAY_MUTATORS.include?(method_name) && ContentJoin.array_residue(member).empty?) ||
|
|
478
491
|
((HASH_MUTATORS.include?(method_name) || HashLookupMutation::MUTATORS.include?(method_name)) &&
|
|
479
492
|
ContentJoin.hash_residue(member).empty?) ||
|
|
480
|
-
(StringMutation::MUTATORS.include?(method_name) &&
|
|
493
|
+
(StringMutation::MUTATORS.include?(method_name) &&
|
|
494
|
+
(StringMutation.constant?(member) || StringMutation.refined?(member))) ||
|
|
481
495
|
(SetMutation::MUTATORS.include?(method_name) && SetMutation.carrier?(member))
|
|
482
496
|
end
|
|
483
497
|
|
|
@@ -660,6 +674,32 @@ module Rigor
|
|
|
660
674
|
end
|
|
661
675
|
end
|
|
662
676
|
|
|
677
|
+
# An intersection of String refinements carrying a predicate one (`non-empty-lowercase-string`) widens member
|
|
678
|
+
# by member: the `Refined` member to `String` ({StringMutation.widen_refined}), a `non-empty-string` member on
|
|
679
|
+
# {#widen_difference}'s witness terms. A member that widened to the bare `String` is implied by the others and
|
|
680
|
+
# drops out, so `s.upcase!` reads `non-empty-string` rather than `non-empty-string & String`. `nil` when the
|
|
681
|
+
# intersection carries no String predicate refinement, or nothing widened.
|
|
682
|
+
def widen_string_intersection(intersection, method_name, arg_types)
|
|
683
|
+
return nil unless intersection.members.any? { |m| StringMutation.refined?(m) }
|
|
684
|
+
|
|
685
|
+
members = intersection.members.map { |m| widen_refinement(m, method_name, arg_types) || m }
|
|
686
|
+
base = Type::Combinator.nominal_of("String")
|
|
687
|
+
kept = members.reject { |m| m == base }
|
|
688
|
+
widened = kept.empty? ? base : Type::Combinator.intersection(*kept)
|
|
689
|
+
widened == intersection ? nil : widened
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
# The refinement carriers {#widen_carrier} hands on: the empty witness ({#widen_difference}), a String predicate
|
|
693
|
+
# ({StringMutation.widen_refined}) and an intersection of them ({#widen_string_intersection}). `nil` when nothing
|
|
694
|
+
# widened.
|
|
695
|
+
def widen_refinement(type, method_name, arg_types)
|
|
696
|
+
case type
|
|
697
|
+
when Type::Difference then widen_difference(type, method_name, arg_types:)
|
|
698
|
+
when Type::Refined then StringMutation.widen_refined(type, method_name)
|
|
699
|
+
when Type::Intersection then widen_string_intersection(type, method_name, arg_types)
|
|
700
|
+
end
|
|
701
|
+
end
|
|
702
|
+
|
|
663
703
|
# `Tuple[A, B, C]` → `Nominal[Array, [union(A, B, C)]]`; under `values: :widen`, each element's
|
|
664
704
|
# VALUE pinning widens to its class nominal (`1 | 2` → `Integer`): a slot-rewriting mutator
|
|
665
705
|
# falsifies values along with the shape — `t = [1, 2]; t[0] += 5` holds `6` at slot 0, so a
|
|
@@ -13,6 +13,7 @@ require_relative "guard_rebinding"
|
|
|
13
13
|
require_relative "intervening_ivar_reset"
|
|
14
14
|
require_relative "key_presence_guard"
|
|
15
15
|
require_relative "last_line"
|
|
16
|
+
require_relative "match_rebinding"
|
|
16
17
|
require_relative "operand_effects"
|
|
17
18
|
require_relative "optimistic_origin"
|
|
18
19
|
require_relative "project_method_ownership"
|
|
@@ -532,7 +533,17 @@ module Rigor
|
|
|
532
533
|
lexical_class_name(condition, scope) || case_equality_target_class(condition)
|
|
533
534
|
end
|
|
534
535
|
body_scope = record_bot_guard(narrow_guarded_slot(body_scope, slot, truthy_type), slot, truthy_type, classes)
|
|
535
|
-
|
|
536
|
+
marked = unwrap_case_subject(subject).is_a?(Prism::LocalVariableReadNode) && scope.gradual_narrowing?(slot.name)
|
|
537
|
+
# A marked subject keeps its mark under any `when` (`when "r"` narrows it without writing it); an unmarked one
|
|
538
|
+
# is stamped only under class conditions.
|
|
539
|
+
if marked || classes.none?(&:nil?)
|
|
540
|
+
static = gradual_union_slot?(slot) ? gradual_static_part(slot.current) : nil
|
|
541
|
+
static &&= case_when_types(scope, static, conditions).first
|
|
542
|
+
body_scope = mark_gradual_narrowing(body_scope, slot, truthy_type, marked: marked, narrowed_static: static)
|
|
543
|
+
end
|
|
544
|
+
falsey_scope = narrow_guarded_slot(scope, slot, falsey_type)
|
|
545
|
+
falsey_scope = falsey_scope.with_gradual_narrowing_mark(slot.name) if marked
|
|
546
|
+
[body_scope, falsey_scope]
|
|
536
547
|
end
|
|
537
548
|
|
|
538
549
|
CASE_SUBJECT_KINDS = %i[local ivar global constant].freeze
|
|
@@ -588,12 +599,21 @@ module Rigor
|
|
|
588
599
|
end
|
|
589
600
|
end
|
|
590
601
|
|
|
591
|
-
# When
|
|
592
|
-
# match-data globals
|
|
593
|
-
#
|
|
594
|
-
#
|
|
595
|
-
#
|
|
596
|
-
#
|
|
602
|
+
# When every condition is a `RegularExpressionNode` literal, the body edge narrows the
|
|
603
|
+
# match-data globals as `analyse_regex_match_predicate`'s truthy edge does. With multiple
|
|
604
|
+
# regex conditions (`when /a/, /b/`) the body is reachable through any of them, so only
|
|
605
|
+
# `$~`/`$&` are safely non-nil; numbered groups whose presence differs per alternative stay
|
|
606
|
+
# `String | nil`. With no regex condition the entry scope passes through unchanged.
|
|
607
|
+
#
|
|
608
|
+
# Issue #1474 — a clause that mixes a regex literal with another condition (`when /(a)/,
|
|
609
|
+
# "lit"`) is also entered through the other conditions: each regex literal before them ran
|
|
610
|
+
# its `===` and failed, leaving the globals nil, and a condition before any regex leaves
|
|
611
|
+
# them as they were. The body scope then joins the match globals of every way in rather
|
|
612
|
+
# than claiming the regex matched: each condition contributes the scope its own `===`
|
|
613
|
+
# leaves — a regex literal's truthy edge, the falsey edge of the regex literals that
|
|
614
|
+
# failed before it, the entry state where none ran, and the unknown-match edges for a
|
|
615
|
+
# condition that may itself be or run a Regexp ({MatchRebinding::Operands.pattern_value?},
|
|
616
|
+
# {MatchRebinding.may_match?}).
|
|
597
617
|
def apply_when_regex_globals(conditions, scope)
|
|
598
618
|
regexes = conditions.grep(Prism::RegularExpressionNode)
|
|
599
619
|
return scope if regexes.empty?
|
|
@@ -604,22 +624,96 @@ module Rigor
|
|
|
604
624
|
else
|
|
605
625
|
Set.new
|
|
606
626
|
end
|
|
607
|
-
truthy, = regex_match_predicate_scopes(scope, unconditional)
|
|
608
|
-
truthy
|
|
627
|
+
truthy, falsey = regex_match_predicate_scopes(scope, unconditional)
|
|
628
|
+
return truthy if regexes.size == conditions.size
|
|
629
|
+
|
|
630
|
+
empty_truthy, empty_falsey = regex_match_predicate_scopes(scope, Set.new)
|
|
631
|
+
failed = scope
|
|
632
|
+
paths = conditions.map do |condition|
|
|
633
|
+
kind = when_regex_condition_kind(condition, scope)
|
|
634
|
+
path, failed = when_regex_condition_scopes(kind, failed, truthy, falsey, empty_truthy, empty_falsey)
|
|
635
|
+
path
|
|
636
|
+
end
|
|
637
|
+
paths.reduce(:join)
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
# How a `when` condition's `===` can touch the match globals, for {#apply_when_regex_globals}:
|
|
641
|
+
# `:literal` a `RegularExpressionNode`, whose match the clause's shared unconditional groups
|
|
642
|
+
# describe; `:interpolated` an interpolated regex literal, a definite match whose groups are
|
|
643
|
+
# unknown; `:may_match` any other condition that may be a Regexp or run one, whose `===`
|
|
644
|
+
# outcome for the globals is unknown; `:neutral` a condition whose `===` and evaluation run
|
|
645
|
+
# no match, leaving the globals as the earlier conditions left them.
|
|
646
|
+
def when_regex_condition_kind(condition, scope)
|
|
647
|
+
return :literal if condition.is_a?(Prism::RegularExpressionNode)
|
|
648
|
+
return :interpolated if condition.is_a?(Prism::InterpolatedRegularExpressionNode)
|
|
649
|
+
if MatchRebinding::Operands.pattern_value?(condition, scope) ||
|
|
650
|
+
MatchRebinding.may_match?(condition, scope)
|
|
651
|
+
return :may_match
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
:neutral
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
# `[body_path_scope, failed_scope]` for one `when` condition of a mixed clause, `failed`
|
|
658
|
+
# being the globals state the conditions before it left. On the path where this condition's
|
|
659
|
+
# `===` matched the body reads the truthy edge (`:literal`), the ungrouped truthy edge
|
|
660
|
+
# (`:interpolated`), the join of that edge with `failed` (`:may_match` — it may not have
|
|
661
|
+
# been a match at all), or `failed` itself (`:neutral`). On the path where it failed the
|
|
662
|
+
# next conditions read the matching falsey edge, the ungrouped one, the join of it with
|
|
663
|
+
# `failed`, or `failed`.
|
|
664
|
+
def when_regex_condition_scopes(kind, failed, truthy, falsey, empty_truthy, empty_falsey)
|
|
665
|
+
case kind
|
|
666
|
+
when :literal then [truthy, falsey]
|
|
667
|
+
when :interpolated then [empty_truthy, empty_falsey]
|
|
668
|
+
when :may_match then [empty_truthy.join(failed), failed.join(empty_falsey)]
|
|
669
|
+
else [failed, failed]
|
|
670
|
+
end
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
# Issue #2443, #1371 — the scope the block of a call that yields only after it has matched in the caller's frame
|
|
674
|
+
# enters with ({MatchRebinding::YieldingMatch}): the match edge of `pattern`, the call's pattern argument, over
|
|
675
|
+
# `scope` with the match globals forgotten. `$~` is a `MatchData`, `$&` and the pre- and post-match a `String`,
|
|
676
|
+
# and so is each numbered group {#yielded_capture_groups} vouches for; every other group is unbound, so it
|
|
677
|
+
# reads the RBS `String?` whatever an earlier match in the method bound it to.
|
|
678
|
+
def yielded_match_scope(scope, pattern)
|
|
679
|
+
regex_match_predicate_scopes(scope, yielded_capture_groups(pattern)).first
|
|
609
680
|
end
|
|
610
681
|
|
|
611
|
-
#
|
|
612
|
-
#
|
|
613
|
-
#
|
|
614
|
-
#
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
682
|
+
# The pattern sources {#yielded_capture_groups} reads groups from: literal characters, `.`, the anchors, the
|
|
683
|
+
# quantifiers, `|`, plain and `(?:…)` groups, the class escapes, `\n`, `\r` and `\t`, an escaped punctuation
|
|
684
|
+
# character, and a bracket class holding only those with no bracket inside it. The group walk
|
|
685
|
+
# ({#unconditional_capture_groups}) reads these right; it misreads sources outside them (#2472, #1476).
|
|
686
|
+
PLAIN_PATTERN_ATOM = %r{[A-Za-z0-9 _\-.,:;=<>!@%&~'"`/^$+*?|(){}#]|\\[wWdDsShHnrt]|\\[^A-Za-z0-9\s]}
|
|
687
|
+
PLAIN_PATTERN_CLASS =
|
|
688
|
+
%r{\[\^?(?:[A-Za-z0-9 _\-.,:;=<>!@%~'"`/^$+*?|(){}#]|\\[wWdDsShHnrt]|\\[^A-Za-z0-9\s])+\]}
|
|
689
|
+
PLAIN_PATTERN = /\A(?:#{PLAIN_PATTERN_CLASS}|#{PLAIN_PATTERN_ATOM}|\\[bBAzZG])*\z/
|
|
690
|
+
OTHER_GROUP_FORM = /\(\?(?!:)/
|
|
691
|
+
# A quantifier on a quantifier repeats the quantified atom, so `(a)+{0}` and `(a){2}?` may match without the
|
|
692
|
+
# group. `+?` and `++` are one quantifier.
|
|
693
|
+
STACKED_QUANTIFIER = /\+[?+]?[*{]|\+[?+][?+]|\}[*+?{]/
|
|
694
|
+
NO_GROUPS = Set.new.freeze
|
|
695
|
+
private_constant :PLAIN_PATTERN_ATOM, :PLAIN_PATTERN_CLASS, :PLAIN_PATTERN, :OTHER_GROUP_FORM,
|
|
696
|
+
:STACKED_QUANTIFIER, :NO_GROUPS
|
|
697
|
+
|
|
698
|
+
# Issue #1371 — the 1-based indices of the groups every match of `pattern` sets, read only from a regex
|
|
699
|
+
# literal written between slashes, without interpolation or the `x` flag, whose source is a {PLAIN_PATTERN}
|
|
700
|
+
# with no `(?` form but `(?:` and no {STACKED_QUANTIFIER}. Any other pattern, a String one included, vouches
|
|
701
|
+
# for no group: declining costs precision, while a misread group would read `String` where Ruby has nil. A
|
|
702
|
+
# `%r` literal is declined because its source escapes the delimiter, which hides a quantifier from the walk:
|
|
703
|
+
# `%r,(a){\,1}b,` is `/(a){,1}b/`.
|
|
704
|
+
def yielded_capture_groups(pattern)
|
|
705
|
+
return NO_GROUPS unless pattern.is_a?(Prism::RegularExpressionNode) && !pattern.extended?
|
|
706
|
+
return NO_GROUPS unless pattern.opening == "/"
|
|
707
|
+
|
|
708
|
+
plain_pattern_source?(pattern.content) ? unconditional_capture_groups(pattern.content) : NO_GROUPS
|
|
621
709
|
end
|
|
622
710
|
|
|
711
|
+
def plain_pattern_source?(source)
|
|
712
|
+
source.ascii_only? && PLAIN_PATTERN.match?(source) && !OTHER_GROUP_FORM.match?(source) &&
|
|
713
|
+
!STACKED_QUANTIFIER.match?(source)
|
|
714
|
+
end
|
|
715
|
+
private_class_method :yielded_capture_groups, :plain_pattern_source?
|
|
716
|
+
|
|
623
717
|
# Issue #2443 — true when every member of `type` is a String (a project subclass included) or a `Dynamic` that
|
|
624
718
|
# is untyped or may hold only Strings, Symbols and nil ({#match_member_kind}). A `nil` member never runs the
|
|
625
719
|
# block, so it counts neither way.
|
|
@@ -636,7 +730,30 @@ module Rigor
|
|
|
636
730
|
def analyse(node, scope)
|
|
637
731
|
edges = analyse_shape(node, scope)
|
|
638
732
|
reader = last_line_condition(node)
|
|
639
|
-
|
|
733
|
+
edges = LastLine.predicate_scopes(reader, scope, edges) if reader
|
|
734
|
+
keep_gradual_marks(node, scope, edges)
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
# Issue #2656 — a guard edge narrows a local; it does not write it. So a local a class guard marked as a gradual
|
|
738
|
+
# narrowing ({Scope#with_gradual_narrowing_mark}) keeps the mark on both edges of every later predicate
|
|
739
|
+
# (`return if x.nil?`, `x != ""`, a second class guard), which bind the narrowed type through `with_local` and
|
|
740
|
+
# drop it. A local the predicate itself writes (`if (x = v)`) loses it, as any write does.
|
|
741
|
+
def keep_gradual_marks(node, scope, edges)
|
|
742
|
+
return edges if edges.nil? || (edges[0].equal?(scope) && edges[1].equal?(scope))
|
|
743
|
+
|
|
744
|
+
names = scope.gradual_narrowing_names
|
|
745
|
+
return edges if names.empty?
|
|
746
|
+
|
|
747
|
+
# Defensive: the statement walk binds a predicate's own write (`if (x = v)`) before its edges are read, so the
|
|
748
|
+
# mark is already gone there; the exclusion keeps a write that reached here from carrying it.
|
|
749
|
+
names -= OperandEffects.written_variables(node).map(&:to_sym)
|
|
750
|
+
return edges if names.empty?
|
|
751
|
+
|
|
752
|
+
edges.map do |edge|
|
|
753
|
+
names.reduce(edge) do |acc, name|
|
|
754
|
+
acc.gradual_narrowing?(name) || acc.local(name).nil? ? acc : acc.with_gradual_narrowing_mark(name)
|
|
755
|
+
end
|
|
756
|
+
end
|
|
640
757
|
end
|
|
641
758
|
|
|
642
759
|
# The reader call a condition's value is, or nil.
|
|
@@ -1815,7 +1932,7 @@ module Rigor
|
|
|
1815
1932
|
|
|
1816
1933
|
def match_dynamic_kind(member, scope)
|
|
1817
1934
|
facet = member.static_facet
|
|
1818
|
-
return :dynamic if
|
|
1935
|
+
return :dynamic if Type::Combinator.untyped?(member)
|
|
1819
1936
|
|
|
1820
1937
|
facet_kinds = match_member_kinds(facet, scope)
|
|
1821
1938
|
:dynamic if facet_kinds.all? { |kind| (STRING_MATCH_KINDS + [:nil]).include?(kind) }
|
|
@@ -1977,7 +2094,7 @@ module Rigor
|
|
|
1977
2094
|
|
|
1978
2095
|
# Returns the Set of 1-based numbered-capture indices that are UNCONDITIONAL in
|
|
1979
2096
|
# `source`: present on every successful match because no optional quantifier (`?`,
|
|
1980
|
-
# `*`, `{0,…}`) applies to the group or any ancestor group, and the pattern contains no
|
|
2097
|
+
# `*`, `{0,…}`, `{,…}`) applies to the group or any ancestor group, and the pattern contains no
|
|
1981
2098
|
# alternation (`|`). Optional and alternation-reachable groups are excluded — at
|
|
1982
2099
|
# runtime `$N` is `nil` for them even when the overall match succeeds, so narrowing
|
|
1983
2100
|
# them to non-nil `String` would be unsound. The walker is intentionally light (char
|
|
@@ -2061,13 +2178,15 @@ module Rigor
|
|
|
2061
2178
|
end
|
|
2062
2179
|
|
|
2063
2180
|
# True when the group whose closing paren is at `source[pos]` is followed by a
|
|
2064
|
-
# quantifier that permits zero repetitions (`?`, `*`, `{0…}`). `+` and `{1,…}`
|
|
2065
|
-
# make a group optional. A lazy/possessive suffix (`*?`, `*+`) is still
|
|
2066
|
-
# on the base quantifier.
|
|
2181
|
+
# quantifier that permits zero repetitions (`?`, `*`, `{0…}`, `{,…}`). `+` and `{1,…}`
|
|
2182
|
+
# do NOT make a group optional. A lazy/possessive suffix (`*?`, `*+`) is still
|
|
2183
|
+
# zero-permitting on the base quantifier. `{,…}` counts only with a digit after the
|
|
2184
|
+
# comma — `{,}`, `{,x}` and `{, }` are literal braces, not quantifiers.
|
|
2067
2185
|
def next_quantifier_optional?(source, pos)
|
|
2068
2186
|
case source[pos]
|
|
2069
2187
|
when "?", "*" then true
|
|
2070
|
-
when "{" then source[pos + 1] == "0"
|
|
2188
|
+
when "{" then source[pos + 1] == "0" ||
|
|
2189
|
+
(source[pos + 1] == "," && source[pos + 2].to_s.match?(/\d/))
|
|
2071
2190
|
else false
|
|
2072
2191
|
end
|
|
2073
2192
|
end
|
|
@@ -2536,7 +2655,62 @@ module Rigor
|
|
|
2536
2655
|
truthy = narrow_class(slot.current, class_name, exact: exact, environment: environment, scope: scope)
|
|
2537
2656
|
falsey = narrow_not_class(slot.current, class_name, exact: exact, environment: environment, scope: scope)
|
|
2538
2657
|
truthy_scope = record_bot_guard(narrow_guarded_slot(scope, slot, truthy), slot, truthy, [class_name])
|
|
2539
|
-
|
|
2658
|
+
static = gradual_union_slot?(slot) ? gradual_static_part(slot.current) : nil
|
|
2659
|
+
static &&= narrow_class(static, class_name, exact: exact, environment: environment, scope: scope)
|
|
2660
|
+
[mark_gradual_narrowing(truthy_scope, slot, truthy, narrowed_static: static),
|
|
2661
|
+
narrow_guarded_slot(scope, slot, falsey)]
|
|
2662
|
+
end
|
|
2663
|
+
|
|
2664
|
+
# Issue #2656 — a class guard's truthy edge over a local that held a `Dynamic` value marks the narrowed binding
|
|
2665
|
+
# ({Scope#with_gradual_narrowing_mark}): the binding keeps the asked class's `Nominal`, which dispatch and the
|
|
2666
|
+
# method rules read, while `call.argument-type-mismatch` judges it as the gradual value it came from, which a
|
|
2667
|
+
# literal-typed parameter (`("r" | "w")`) accepts. Only a local is marked, and only when the edge left it
|
|
2668
|
+
# something other than `Dynamic` or `bot`.
|
|
2669
|
+
def mark_gradual_narrowing(scope, slot, narrowed, marked: false, narrowed_static: nil)
|
|
2670
|
+
return scope unless slot.kind == :local
|
|
2671
|
+
|
|
2672
|
+
mark_gradual_local(scope, slot.name, slot.current, narrowed, marked: marked, narrowed_static: narrowed_static)
|
|
2673
|
+
end
|
|
2674
|
+
|
|
2675
|
+
# {#mark_gradual_narrowing} for the local `name`, which held `current` before the guard and `narrowed` after.
|
|
2676
|
+
# `current` is gradual when it is `Dynamic` or a union with a `Dynamic` member (`h[:m] || "r"`, `c ? h[:m] :
|
|
2677
|
+
# nil`); `marked` says the local already carried the mark, so a `case` or a capture over it keeps it (a
|
|
2678
|
+
# predicate keeps it through {.keep_gradual_marks}).
|
|
2679
|
+
def mark_gradual_local(scope, name, current, narrowed, marked: false, narrowed_static: nil)
|
|
2680
|
+
return scope unless marked || gradual_bearing?(current)
|
|
2681
|
+
return scope if narrowed.nil? || narrowed.is_a?(Type::Dynamic) || narrowed.is_a?(Type::Bot)
|
|
2682
|
+
return scope unless marked || literal_or_empty?(narrowed_static || gradual_static_part(current))
|
|
2683
|
+
|
|
2684
|
+
scope.with_gradual_narrowing_mark(name)
|
|
2685
|
+
end
|
|
2686
|
+
public :mark_gradual_local
|
|
2687
|
+
|
|
2688
|
+
def gradual_bearing?(type)
|
|
2689
|
+
type.is_a?(Type::Dynamic) || (type.is_a?(Type::Union) && type.members.any?(Type::Dynamic))
|
|
2690
|
+
end
|
|
2691
|
+
|
|
2692
|
+
# Issue #2656 — the non-`Dynamic` members of a union `current` (nil for any other type). A union is stamped
|
|
2693
|
+
# only when that part, narrowed by the guard (`narrowed_static`, or the part itself where the caller cannot
|
|
2694
|
+
# narrow it), is literals or nothing: the rule judges a literal member as written, so the mark then withholds
|
|
2695
|
+
# only for what came from the gradual member. A static member that narrows to a class (`d = "a" + s.to_s;
|
|
2696
|
+
# x = h[:m] || d`) would be read past as if it were gradual, so such a union is not stamped.
|
|
2697
|
+
def gradual_static_part(current)
|
|
2698
|
+
return nil unless current.is_a?(Type::Union)
|
|
2699
|
+
|
|
2700
|
+
Type::Combinator.union(*current.members.grep_v(Type::Dynamic))
|
|
2701
|
+
end
|
|
2702
|
+
|
|
2703
|
+
# Whether `slot` is a local holding a union with a `Dynamic` member, the one case {.gradual_static_part}
|
|
2704
|
+
# answers for; checked first so a class guard on any other slot builds nothing.
|
|
2705
|
+
def gradual_union_slot?(slot)
|
|
2706
|
+
slot.kind == :local && slot.current.is_a?(Type::Union) && slot.current.members.any?(Type::Dynamic)
|
|
2707
|
+
end
|
|
2708
|
+
|
|
2709
|
+
def literal_or_empty?(type)
|
|
2710
|
+
return true if type.nil?
|
|
2711
|
+
|
|
2712
|
+
members = type.is_a?(Type::Union) ? type.members : [type]
|
|
2713
|
+
members.all? { |member| member.is_a?(Type::Bot) || member.is_a?(Type::Constant) }
|
|
2540
2714
|
end
|
|
2541
2715
|
|
|
2542
2716
|
# The class name a `is_a?` / `kind_of?` / `instance_of?` argument denotes: the top-level
|
|
@@ -31,6 +31,12 @@ module Rigor
|
|
|
31
31
|
# pure-value operand never reached, so a chained `b.push(2).size`, an argument `puts(b.push(2))` or
|
|
32
32
|
# `d.map!.with_index { … }` left `b` / `d` on the literal their assignment wrote.
|
|
33
33
|
#
|
|
34
|
+
# Issue #3163 — a `def` inside a block or lambda of the expression is looked into for one thing: an in-place
|
|
35
|
+
# mutation of an instance variable, class variable or global ({.method_mutation?}). The block may call the
|
|
36
|
+
# method it defines, and the evaluator floors such a variable after the block
|
|
37
|
+
# (`StatementEvaluator#floor_block_defined_method_mutations`), which it reaches only for an operand this module
|
|
38
|
+
# answers for: `puts([1].each { def h; $buf << 1; end; h })`.
|
|
39
|
+
#
|
|
34
40
|
# Short-circuiting and allocation-free outside a mutator-named call: the evaluator asks it of every call's
|
|
35
41
|
# operands, and asks it again of each operand it threads. A found effect stops the recursion without a
|
|
36
42
|
# `return` out of the child block, which would allocate once per frame it unwinds and make a deep literal
|
|
@@ -58,12 +64,15 @@ module Rigor
|
|
|
58
64
|
].freeze
|
|
59
65
|
JUMP_NODES = Set[Prism::NextNode, Prism::BreakNode].freeze
|
|
60
66
|
SCOPE_NODES = Set[Prism::BlockNode, Prism::LambdaNode].freeze
|
|
67
|
+
NON_LOCAL_READ_NODES = Set[
|
|
68
|
+
Prism::InstanceVariableReadNode, Prism::ClassVariableReadNode, Prism::GlobalVariableReadNode
|
|
69
|
+
].freeze
|
|
61
70
|
OPAQUE_NODES = Set[
|
|
62
71
|
Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode, Prism::DefinedNode
|
|
63
72
|
].freeze
|
|
64
73
|
private_constant :LOCAL_WRITE_NODES, :OUTLIVING_WRITE_NODES, :INSTANCE_WRITE_NODES, :GLOBAL_WRITE_NODES,
|
|
65
74
|
:COMPOUND_LOCAL_WRITES,
|
|
66
|
-
:COMPOUND_INSTANCE_WRITES, :JUMP_NODES, :SCOPE_NODES, :OPAQUE_NODES
|
|
75
|
+
:COMPOUND_INSTANCE_WRITES, :JUMP_NODES, :SCOPE_NODES, :NON_LOCAL_READ_NODES, :OPAQUE_NODES
|
|
67
76
|
|
|
68
77
|
module_function
|
|
69
78
|
|
|
@@ -79,7 +88,7 @@ module Rigor
|
|
|
79
88
|
return true if OUTLIVING_WRITE_NODES.include?(klass)
|
|
80
89
|
return true if jumps && JUMP_NODES.include?(klass)
|
|
81
90
|
return true if klass == Prism::CallNode && outliving_mutation?(node, nesting)
|
|
82
|
-
return
|
|
91
|
+
return block_defined_method_mutation?(node, nesting) if OPAQUE_NODES.include?(klass)
|
|
83
92
|
|
|
84
93
|
nesting += 1 if SCOPE_NODES.include?(klass)
|
|
85
94
|
jumps &&= !JumpTargets.boundary?(node)
|
|
@@ -89,6 +98,38 @@ module Rigor
|
|
|
89
98
|
end
|
|
90
99
|
private_class_method :effect?
|
|
91
100
|
|
|
101
|
+
# Issue #3163 — the one effect an opaque node has: a `def` under a block or lambda whose body mutates a
|
|
102
|
+
# non-local in place ({.method_mutation?}).
|
|
103
|
+
def block_defined_method_mutation?(node, nesting)
|
|
104
|
+
node.instance_of?(Prism::DefNode) && nesting.positive? && method_mutation?(node)
|
|
105
|
+
end
|
|
106
|
+
private_class_method :block_defined_method_mutation?
|
|
107
|
+
|
|
108
|
+
# True when `node`, a `def` or anything in its body, holds a call or index write
|
|
109
|
+
# that mutates an instance variable, class variable or global in place: a {MutationWidening::SHAPE_MUTATORS}
|
|
110
|
+
# name, or an index `||=` / `&&=` / `op=`, on a read of one. A class or module body inside the method is not
|
|
111
|
+
# looked into; a `def` nested in it is.
|
|
112
|
+
def method_mutation?(node)
|
|
113
|
+
klass = node.class
|
|
114
|
+
if klass == Prism::CallNode
|
|
115
|
+
return true if MutationWidening::SHAPE_MUTATORS.include?(node.name) && non_local_read?(node.receiver)
|
|
116
|
+
elsif OUTLIVING_WRITE_NODES.include?(klass)
|
|
117
|
+
return true if node.respond_to?(:receiver) && non_local_read?(node.receiver)
|
|
118
|
+
elsif klass != Prism::DefNode && OPAQUE_NODES.include?(klass)
|
|
119
|
+
return false
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
found = false
|
|
123
|
+
node.rigor_each_child { |child| found ||= method_mutation?(child) }
|
|
124
|
+
found
|
|
125
|
+
end
|
|
126
|
+
private_class_method :method_mutation?
|
|
127
|
+
|
|
128
|
+
def non_local_read?(node)
|
|
129
|
+
NON_LOCAL_READ_NODES.include?(node.class)
|
|
130
|
+
end
|
|
131
|
+
private_class_method :non_local_read?
|
|
132
|
+
|
|
92
133
|
# True when `node` is a call the straight-line widening would answer for a variable that outlives the
|
|
93
134
|
# expression: a {MutationWidening::SHAPE_MUTATORS} name on a receiver whose {ReceiverAlias.mutated_reads},
|
|
94
135
|
# or the local an element read is rooted at ({ElementReadWidening.element_read_path}, `a[0] << e`), reach
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
|
+
require_relative "../type"
|
|
6
|
+
|
|
5
7
|
module Rigor
|
|
6
8
|
module Inference
|
|
7
9
|
# Marks a value whose *nil-freeness* rests on Rigor's deliberate choice to ignore core RBS's
|
|
@@ -102,24 +104,147 @@ module Rigor
|
|
|
102
104
|
# `StatementEvaluator` and `AlwaysTruthyConditionCollector` all route here, so the three consumers the
|
|
103
105
|
# spec binds cannot drift apart.
|
|
104
106
|
#
|
|
105
|
-
# Resolution order — the mark recorded on the node itself, then the binding a bare local / ivar read
|
|
106
|
-
# a write in value position
|
|
107
|
-
#
|
|
107
|
+
# Resolution order — the mark recorded on the node itself, then the binding a bare local / ivar read resolves
|
|
108
|
+
# through, then a write in value position and a safe-navigation call and the predicate-fold derivation issue
|
|
109
|
+
# #313 added.
|
|
108
110
|
def resolve(node, scope)
|
|
109
111
|
return nil if node.nil? || scope.nil?
|
|
110
112
|
|
|
111
|
-
recorded = scope.
|
|
113
|
+
recorded = scope.optimistic_origin(node)
|
|
112
114
|
return recorded if recorded
|
|
113
115
|
|
|
116
|
+
resolve_node(node, scope)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# The shape dispatch {.resolve} applies once the node-level mark is ruled out.
|
|
120
|
+
def resolve_node(node, scope)
|
|
114
121
|
case node
|
|
115
|
-
when Prism::LocalVariableReadNode
|
|
116
|
-
when Prism::InstanceVariableReadNode
|
|
122
|
+
when Prism::LocalVariableReadNode then scope.optimistic_local(node.name)
|
|
123
|
+
when Prism::InstanceVariableReadNode then scope.optimistic_ivar(node.name)
|
|
124
|
+
when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode,
|
|
125
|
+
Prism::GlobalVariableWriteNode, Prism::ClassVariableWriteNode,
|
|
126
|
+
Prism::MultiWriteNode, Prism::LocalVariableOrWriteNode,
|
|
127
|
+
Prism::InstanceVariableOrWriteNode, Prism::ClassVariableOrWriteNode,
|
|
128
|
+
Prism::GlobalVariableOrWriteNode, Prism::LocalVariableAndWriteNode,
|
|
129
|
+
Prism::InstanceVariableAndWriteNode, Prism::ClassVariableAndWriteNode,
|
|
130
|
+
Prism::GlobalVariableAndWriteNode
|
|
131
|
+
resolve_write_form(node, scope)
|
|
117
132
|
when Prism::AndNode, Prism::OrNode then resolve(node.left, scope) || resolve(node.right, scope)
|
|
118
|
-
when Prism::CallNode
|
|
133
|
+
when Prism::CallNode
|
|
134
|
+
resolve_attribute_write(node, scope) || resolve_through_safe_navigation(node, scope) ||
|
|
135
|
+
resolve_through_predicate(node, scope)
|
|
119
136
|
when Prism::ParenthesesNode then resolve_through_parentheses(node, scope)
|
|
120
137
|
end
|
|
121
138
|
end
|
|
122
139
|
|
|
140
|
+
# The write forms in value position: the plain and multiple writes whose value is the
|
|
141
|
+
# right-hand side, and the `||=` / `&&=` spellings whose old binding participates.
|
|
142
|
+
def resolve_write_form(node, scope)
|
|
143
|
+
case node
|
|
144
|
+
when Prism::LocalVariableOrWriteNode, Prism::InstanceVariableOrWriteNode,
|
|
145
|
+
Prism::ClassVariableOrWriteNode, Prism::GlobalVariableOrWriteNode
|
|
146
|
+
resolve_or_write(node, scope)
|
|
147
|
+
when Prism::LocalVariableAndWriteNode, Prism::InstanceVariableAndWriteNode,
|
|
148
|
+
Prism::ClassVariableAndWriteNode, Prism::GlobalVariableAndWriteNode
|
|
149
|
+
resolve_and_write(node, scope)
|
|
150
|
+
else
|
|
151
|
+
resolve_write(node, scope)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Issue #2617 — a write in value position (`if (v = h[k])`) yields its right-hand side, so the write's own
|
|
156
|
+
# optimism is the RHS's. The binding lookup alone could not see it from the scope a certainty consumer
|
|
157
|
+
# reads: the `if`'s entry scope predates the predicate's evaluation, so `if (v = h[k])` found no mark for
|
|
158
|
+
# `v` there and `flow.always-truthy-condition` fired on the optimistic read. Resolving `node.value` reaches
|
|
159
|
+
# the mark recorded on the read itself, which lives in the side table every scope shares; the binding lookup
|
|
160
|
+
# stays as the fallback for the post-write scope `branch_certainty` passes.
|
|
161
|
+
def resolve_write(node, scope)
|
|
162
|
+
resolve(node.value, scope) || optimistic_write_binding(node, scope)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Only locals and ivars carry a binding mark; a global or class-variable write, and a
|
|
166
|
+
# multiple assignment's slots (marked through {.destructuring_marks} instead), have no
|
|
167
|
+
# `optimistic_*` table entry to fall back to, so they answer nil here.
|
|
168
|
+
def optimistic_write_binding(node, scope)
|
|
169
|
+
case node
|
|
170
|
+
when Prism::LocalVariableWriteNode then scope.optimistic_local(node.name)
|
|
171
|
+
when Prism::InstanceVariableWriteNode then scope.optimistic_ivar(node.name)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Issue #3054 — `v ||= rhs` yields the OLD binding whenever it is truthy, so the write's
|
|
176
|
+
# value is the right-hand side's only on the paths where the old value is falsey. When the
|
|
177
|
+
# old binding is provably always truthy — its type has no falsey inhabitant and it carries
|
|
178
|
+
# no optimistic mark — the right-hand side is never reached and the expression's verdict is
|
|
179
|
+
# the old value's own proof, so resolving through `node.value` would silence a correct
|
|
180
|
+
# always-truthy verdict (`v = "x"; if (v ||= h[k])` still folds to `"x"`). Otherwise the
|
|
181
|
+
# verdict, when one folds, rests on the right-hand side's bet.
|
|
182
|
+
def resolve_or_write(node, scope)
|
|
183
|
+
binding_type, binding_mark = or_write_binding(node, scope)
|
|
184
|
+
return nil if always_truthy_type?(binding_type) && binding_mark.nil?
|
|
185
|
+
|
|
186
|
+
resolve(node.value, scope)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Issue #3054 — `v &&= rhs` yields the right-hand side whenever the old binding is truthy;
|
|
190
|
+
# the old value itself wins only on its falsey edge, a result no always-truthy verdict can
|
|
191
|
+
# rest on. So the right-hand side's mark covers every constant verdict the expression folds
|
|
192
|
+
# to, joined by the old binding's own mark for the case where the miss — not the rvalue — is
|
|
193
|
+
# what makes the condition live (`v = h[k]; if (v &&= "lit")`).
|
|
194
|
+
def resolve_and_write(node, scope)
|
|
195
|
+
resolve(node.value, scope) || and_write_binding_mark(node, scope)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# The `[type, mark]` pair an `||=` reads its old value through: the scope binding and the
|
|
199
|
+
# optimistic mark it carries. Class- and global-variable or-writes have no mark table — the
|
|
200
|
+
# mark channel covers locals and ivars only — so their mark is always nil.
|
|
201
|
+
def or_write_binding(node, scope)
|
|
202
|
+
case node
|
|
203
|
+
when Prism::LocalVariableOrWriteNode
|
|
204
|
+
[scope.local(node.name), scope.optimistic_local(node.name)]
|
|
205
|
+
when Prism::InstanceVariableOrWriteNode
|
|
206
|
+
[scope.ivar(node.name), scope.optimistic_ivar(node.name)]
|
|
207
|
+
when Prism::ClassVariableOrWriteNode
|
|
208
|
+
[scope.cvar(node.name), nil]
|
|
209
|
+
when Prism::GlobalVariableOrWriteNode
|
|
210
|
+
[scope.global(node.name), nil]
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# The old binding's optimistic mark for an `&&=` — the same two tables {.or_write_binding}
|
|
215
|
+
# reads, with no type: `&&=` needs the mark alone.
|
|
216
|
+
def and_write_binding_mark(node, scope)
|
|
217
|
+
case node
|
|
218
|
+
when Prism::LocalVariableAndWriteNode then scope.optimistic_local(node.name)
|
|
219
|
+
when Prism::InstanceVariableAndWriteNode then scope.optimistic_ivar(node.name)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Issue #3054 — `recv.x = rhs` and `recv[i] = rhs` yield the assigned value, which Prism
|
|
224
|
+
# spells as the call's last argument (`x=` takes it alone, `[]=` trails the indices). The
|
|
225
|
+
# compound spellings never reach here: `+=` / `[]=` operators and `||=` / `&&=` on an
|
|
226
|
+
# attribute or index are `CallOperatorWriteNode` / `IndexOperatorWriteNode` /
|
|
227
|
+
# `CallOrWriteNode` / `CallAndWriteNode` / `IndexOrWriteNode` / `IndexAndWriteNode`, not
|
|
228
|
+
# `CallNode`, and their value is the old slot's, not the right-hand side's.
|
|
229
|
+
def resolve_attribute_write(node, scope)
|
|
230
|
+
resolve(node.arguments&.arguments&.last, scope) if node.attribute_write?
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Whether `type` proves every inhabitant truthy — the empty falsey fragment of
|
|
234
|
+
# `Narrowing.narrow_falsey`, mirrored carrier for carrier so this leaf module need not
|
|
235
|
+
# require `Narrowing` (which already requires this file). A `nil` binding (unbound) and
|
|
236
|
+
# every carrier the falsey fragment cannot empty (`Top`, `Dynamic`, ranges, refinements,
|
|
237
|
+
# differences) answer false: only a proof counts.
|
|
238
|
+
def always_truthy_type?(type)
|
|
239
|
+
case type
|
|
240
|
+
when Type::Constant then !(type.value.nil? || type.value == false)
|
|
241
|
+
when Type::Nominal then !%w[NilClass FalseClass].include?(type.class_name)
|
|
242
|
+
when Type::Union then type.members.all? { |member| always_truthy_type?(member) }
|
|
243
|
+
when Type::Singleton, Type::Tuple, Type::HashShape then true
|
|
244
|
+
else type.is_a?(Type::Bot)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
123
248
|
# `recv&.m` is `nil` exactly when `recv` is (or when `m` answers `nil`), so it restates `recv`'s presence
|
|
124
249
|
# and is as optimistic as `recv`. Only the call carrying the `&.` is derived: Ruby skips that one call and
|
|
125
250
|
# no more, so `recv&.m.n` sends `n` to the `nil` and raises (or answers `NilClass#n`), and neither it nor
|
|
@@ -169,7 +294,7 @@ module Rigor
|
|
|
169
294
|
# predicate's folded boolean only when the miss answers the other boolean (or cannot be told), so
|
|
170
295
|
# `!recv&.empty?` — `!false` on a hit, `!nil` on a miss — stays `true`, and so does `x = recv&.empty?; !x`.
|
|
171
296
|
def miss_answer(node, scope)
|
|
172
|
-
return nil if scope.
|
|
297
|
+
return nil if scope.optimistic_origin(node)
|
|
173
298
|
|
|
174
299
|
case node
|
|
175
300
|
when Prism::LocalVariableReadNode then scope.optimistic_local_miss(node.name)
|