rigortype 0.4.5 → 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 +1 -1
- data/docs/handbook/11-sig-gen.md +11 -2
- data/docs/manual/02-cli-reference.md +46 -7
- data/docs/manual/03-configuration.md +2 -1
- data/docs/manual/04-diagnostics.md +1 -1
- data/docs/manual/10-mcp-server.md +9 -1
- data/docs/manual/12-caching.md +8 -1
- 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 +2 -1
- data/lib/rigor/analysis/buffer_binding.rb +14 -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 +4 -0
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
- 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/unreachable_clause_collector.rb +51 -0
- data/lib/rigor/analysis/check_rules.rb +93 -8
- data/lib/rigor/analysis/dependency_recorder.rb +11 -0
- data/lib/rigor/analysis/edit_replay.rb +67 -0
- data/lib/rigor/analysis/incremental.rb +37 -0
- data/lib/rigor/analysis/incremental_session.rb +175 -27
- data/lib/rigor/analysis/path_expansion.rb +41 -0
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +5 -1
- 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 +17 -4
- 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 +156 -49
- data/lib/rigor/analysis/worker_session.rb +14 -2
- data/lib/rigor/cache/incremental_snapshot.rb +30 -7
- data/lib/rigor/cli/check_command.rb +93 -4
- data/lib/rigor/cli/diff_command.rb +2 -1
- 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/plugins_command.rb +5 -0
- 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 +46 -7
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/type_of_project_scope.rb +27 -0
- data/lib/rigor/config_audit.rb +16 -5
- data/lib/rigor/configuration.rb +14 -4
- data/lib/rigor/environment/rbs_loader.rb +115 -6
- 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/callee_mutation_chain.rb +115 -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 +17 -3
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +62 -13
- data/lib/rigor/inference/global_write_census.rb +36 -1
- data/lib/rigor/inference/ivar_writer_census.rb +195 -0
- data/lib/rigor/inference/loop_value_record.rb +53 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +110 -9
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
- data/lib/rigor/inference/method_dispatcher.rb +46 -16
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +12 -0
- data/lib/rigor/inference/mutation_widening.rb +5 -2
- data/lib/rigor/inference/narrowing.rb +66 -5
- data/lib/rigor/inference/object_mixins.rb +13 -1
- data/lib/rigor/inference/project_seed_tables.rb +2 -1
- data/lib/rigor/inference/project_yielder.rb +128 -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 +480 -103
- data/lib/rigor/inference/self_call_ivar_writes.rb +18 -6
- data/lib/rigor/inference/statement_evaluator.rb +658 -90
- 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/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/mcp/server.rb +9 -1
- data/lib/rigor/plugin/base.rb +26 -11
- data/lib/rigor/plugin/manifest.rb +29 -3
- data/lib/rigor/plugin/registry.rb +38 -0
- data/lib/rigor/protection/discovery_seed.rb +57 -2
- data/lib/rigor/rooted_glob.rb +10 -1
- data/lib/rigor/scope.rb +14 -5
- data/lib/rigor/sig_gen/generator.rb +308 -37
- data/lib/rigor/sig_gen/observation_collector.rb +4 -1
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +6 -3
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- data/lib/rigor/type/combinator.rb +4 -3
- data/lib/rigor/type/constant.rb +45 -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-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +60 -43
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +6 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -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 +4 -4
- 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/scope.rbs +31 -50
- data/sig/rigor/type.rbs +0 -6
- metadata +20 -1
|
@@ -535,10 +535,9 @@ module Rigor
|
|
|
535
535
|
# The question is asked HERE, of the same two tiers `resolve` consults and in the same order, rather
|
|
536
536
|
# than reconstructed in the veto: a name this reports as answered is a name the dispatch really would
|
|
537
537
|
# resolve, and no plugin knowledge moves into the engine's veto. The tiers are the gated
|
|
538
|
-
# `dynamic_return` walk and the ADR-16 synthetic-method index (Tier C `heredoc_templates` emissions
|
|
539
|
-
#
|
|
540
|
-
#
|
|
541
|
-
# this predicate in production yet — it will when #476 lands, with no change here).
|
|
538
|
+
# `dynamic_return` walk and the ADR-16 synthetic-method index (Tier C `heredoc_templates` emissions and
|
|
539
|
+
# the Tier B `Plugin::Macro::TraitRegistry` explosions, which share that table; since #476 the pre-pass
|
|
540
|
+
# explodes a trait module whenever the project's RBS declares it).
|
|
542
541
|
#
|
|
543
542
|
# It is a predicate, not a type: the veto's job is to stop the top-level bind, after which the call
|
|
544
543
|
# falls through to the ordinary dispatch chain and the tier that answered here answers there too. The
|
|
@@ -1031,6 +1030,7 @@ module Rigor
|
|
|
1031
1030
|
|
|
1032
1031
|
fallback_receiver = user_class_fallback_receiver(receiver_type, environment)
|
|
1033
1032
|
return nil if fallback_receiver.nil?
|
|
1033
|
+
return nil if declared_by_external_ancestor?(receiver_type, environment, context)
|
|
1034
1034
|
|
|
1035
1035
|
# Preserve the ORIGINAL receiver type as the `self` substitution so `Kernel#dup: () ->
|
|
1036
1036
|
# self` and other `self`-returning methods route through Object's RBS while still
|
|
@@ -1055,6 +1055,45 @@ module Rigor
|
|
|
1055
1055
|
)
|
|
1056
1056
|
end
|
|
1057
1057
|
|
|
1058
|
+
# Issue #2196 — a project class whose ancestry reaches a core or stdlib class before `Object` (`class FL <
|
|
1059
|
+
# Array`) answers a name that class declares from that class, not from `Object` / `Kernel`: Ruby dispatches
|
|
1060
|
+
# `select { … }` on an `FL` to `Array#select`, never to the private `Kernel#select` (`-> Array[String]`). The
|
|
1061
|
+
# ancestor tiers decline such a name where they cannot type it (`RbsDispatch#returns_the_walked_ancestry?`),
|
|
1062
|
+
# and the `Object` fallback then fabricated `Kernel#select`'s answer, so `fields.detect { |f| f.errors }` read
|
|
1063
|
+
# `f` as `String`. The fallback declines the name instead, which leaves the call `Dynamic[top]`.
|
|
1064
|
+
# {ExternalAncestorResolution.resolve} answers nil for a name only `Object`, `Kernel` or `BasicObject` declares,
|
|
1065
|
+
# so `puts` and `raise` keep resolving.
|
|
1066
|
+
def declared_by_external_ancestor?(receiver_type, environment, context)
|
|
1067
|
+
return false unless receiver_type.is_a?(Type::Nominal)
|
|
1068
|
+
|
|
1069
|
+
scope = context.scope
|
|
1070
|
+
return false if scope.nil?
|
|
1071
|
+
|
|
1072
|
+
memo = external_ancestor_memo(scope, environment)
|
|
1073
|
+
key = [receiver_type.class_name, context.method_name]
|
|
1074
|
+
return memo[key] if memo.key?(key)
|
|
1075
|
+
|
|
1076
|
+
memo[key] = !ExternalAncestorResolution.resolve(receiver_type.class_name, context.method_name, :instance,
|
|
1077
|
+
scope: scope, environment: environment,
|
|
1078
|
+
record_dependencies: false).nil?
|
|
1079
|
+
end
|
|
1080
|
+
|
|
1081
|
+
EXTERNAL_ANCESTOR_MEMO_KEY = :__rigor_user_class_fallback_external_ancestor__
|
|
1082
|
+
private_constant :EXTERNAL_ANCESTOR_MEMO_KEY
|
|
1083
|
+
|
|
1084
|
+
# One slot per discovery index and environment, as `RbsDispatch#ancestor_memo_slot` keeps its own: the answer
|
|
1085
|
+
# reads only the frozen discovery tables and the RBS environment, and its walk is withheld from dependency
|
|
1086
|
+
# recording, so it may be served again for the same pair.
|
|
1087
|
+
def external_ancestor_memo(scope, environment)
|
|
1088
|
+
discovery = scope.discovery
|
|
1089
|
+
slot = Thread.current[EXTERNAL_ANCESTOR_MEMO_KEY]
|
|
1090
|
+
unless slot && slot[0].equal?(discovery) && slot[1].equal?(environment)
|
|
1091
|
+
slot = [discovery, environment, {}]
|
|
1092
|
+
Thread.current[EXTERNAL_ANCESTOR_MEMO_KEY] = slot
|
|
1093
|
+
end
|
|
1094
|
+
slot[2]
|
|
1095
|
+
end
|
|
1096
|
+
|
|
1058
1097
|
# True when the call node carries an explicit receiver that is not the literal `self`.
|
|
1059
1098
|
# Such a call cannot legally dispatch to a private method, so the user-class fallback must
|
|
1060
1099
|
# skip private signatures rather than return a confidently-wrong type. Returns false for
|
|
@@ -1257,7 +1296,7 @@ module Rigor
|
|
|
1257
1296
|
range_lift = range_new_lift(receiver_type.class_name, arg_types)
|
|
1258
1297
|
return range_lift if range_lift
|
|
1259
1298
|
|
|
1260
|
-
set_lift = set_new_lift(receiver_type.class_name, arg_types)
|
|
1299
|
+
set_lift = set_new_lift(receiver_type.class_name, arg_types, context)
|
|
1261
1300
|
return set_lift if set_lift
|
|
1262
1301
|
|
|
1263
1302
|
hash_lift = hash_new_lift(receiver_type.class_name, arg_types, context&.call_node)
|
|
@@ -1613,19 +1652,10 @@ module Rigor
|
|
|
1613
1652
|
# are given or the single argument is a `Tuple` whose every element is a `Constant[T]`.
|
|
1614
1653
|
# Mirrors `SetFolding#fold_new` but lives here so the `:new` path in
|
|
1615
1654
|
# `try_meta_introspection` can reach it before the RBS tier answers `Nominal[Set]`.
|
|
1616
|
-
def set_new_lift(class_name, arg_types)
|
|
1655
|
+
def set_new_lift(class_name, arg_types, context = nil)
|
|
1617
1656
|
return nil unless class_name == "Set"
|
|
1618
|
-
return Type::Combinator.constant_of(::Set.new) if arg_types.empty?
|
|
1619
|
-
return nil if arg_types.size > 1
|
|
1620
1657
|
|
|
1621
|
-
|
|
1622
|
-
return nil unless arg.is_a?(Type::Tuple)
|
|
1623
|
-
return nil unless arg.elements.all?(Type::Constant)
|
|
1624
|
-
|
|
1625
|
-
values = arg.elements.map(&:value)
|
|
1626
|
-
Type::Combinator.constant_of(::Set.new(values))
|
|
1627
|
-
rescue StandardError
|
|
1628
|
-
nil
|
|
1658
|
+
SetFolding.fold_new(arg_types, context&.call_node, context&.block_type)
|
|
1629
1659
|
end
|
|
1630
1660
|
|
|
1631
1661
|
# `Regexp.new(pattern)` / `Regexp.new(pattern, opts)` — folds to `Constant[Regexp]` when
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "../../source/literals"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Inference
|
|
8
|
+
module ModuleFunctionState
|
|
9
|
+
# Issue #2209 — the `module_function` spellings other than the receiverless call that the deferred-ranges
|
|
10
|
+
# prescan ({ModuleFunctionState.prescan_deferred}) reads: `self.module_function(...)`, and
|
|
11
|
+
# `send(:module_function, ...)` or `__send__(:module_function, ...)` on no receiver or `self`, which Ruby runs
|
|
12
|
+
# as the receiverless call. `public_send` raises on a private method such as `module_function`, so it is not
|
|
13
|
+
# one.
|
|
14
|
+
#
|
|
15
|
+
# A literal name gives the `:singleton` row and a `def` argument the `:both` or `:singleton` row a receiverless
|
|
16
|
+
# call gives. The bare form, and an argument that is neither (`*names`, a local), gives an {ANY_ROW_NAME} row
|
|
17
|
+
# over the call: the prescan reads neither the toggle these spellings set nor such a name, so every singleton
|
|
18
|
+
# copy of the owner is uncertain (`DefinerResolution.module_function_row?`).
|
|
19
|
+
module Spellings
|
|
20
|
+
SEND_CALLS = %i[send __send__].freeze
|
|
21
|
+
private_constant :SEND_CALLS
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# The arguments the spelling `node` passes `module_function`, or nil for any other call.
|
|
26
|
+
def arguments(node)
|
|
27
|
+
arguments = node.arguments&.arguments || []
|
|
28
|
+
return arguments if node.name == :module_function && node.receiver.is_a?(Prism::SelfNode)
|
|
29
|
+
return nil unless SEND_CALLS.include?(node.name)
|
|
30
|
+
return nil unless node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
31
|
+
return nil unless Source::Literals.symbol_or_string(arguments.first) == :module_function
|
|
32
|
+
|
|
33
|
+
arguments.drop(1)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Appends the rows of the spelling `node` with `arguments` ({.arguments}) to `ranges`, and prescans the rest
|
|
37
|
+
# as {ModuleFunctionState.prescan_deferred} prescans a receiverless call's.
|
|
38
|
+
def prescan(node, arguments, qualified_prefix, in_singleton_class, offsets, ranges)
|
|
39
|
+
owner = qualified_prefix.empty? ? nil : qualified_prefix.join("::")
|
|
40
|
+
call_span = [node.location.start_offset, node.location.end_offset]
|
|
41
|
+
any = arguments.empty?
|
|
42
|
+
arguments.each do |arg|
|
|
43
|
+
if arg.is_a?(Prism::DefNode)
|
|
44
|
+
kind = ScopeIndexer.def_singleton?(arg, qualified_prefix, in_singleton_class) ? :singleton : :both
|
|
45
|
+
ranges << [arg.location.start_offset, arg.location.end_offset, arg.name, kind, owner]
|
|
46
|
+
elsif (name = Source::Literals.symbol_or_string(arg))
|
|
47
|
+
ranges << [*call_span, name, :singleton, owner]
|
|
48
|
+
else
|
|
49
|
+
any = true
|
|
50
|
+
nested_prescan(arg, qualified_prefix, in_singleton_class, offsets, ranges)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
ranges << [*call_span, ANY_ROW_NAME, :singleton, owner] if any
|
|
54
|
+
nested_prescan(node.block, qualified_prefix, in_singleton_class, offsets, ranges) if node.block
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def nested_prescan(node, qualified_prefix, in_singleton_class, offsets, ranges)
|
|
58
|
+
ModuleFunctionState.prescan_deferred(node, qualified_prefix, in_singleton_class, offsets, ranges,
|
|
59
|
+
nested: true)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -318,10 +318,20 @@ module Rigor
|
|
|
318
318
|
end
|
|
319
319
|
end
|
|
320
320
|
|
|
321
|
+
# Issue #2209 — the row name of a deferred-ranges row that stands for every name of its owner
|
|
322
|
+
# ({Spellings}).
|
|
323
|
+
ANY_ROW_NAME = :*
|
|
324
|
+
|
|
325
|
+
# Issue #2209 — the deferred-ranges row names that make `method_name` a module function on their owner: its
|
|
326
|
+
# own and {ANY_ROW_NAME}.
|
|
327
|
+
def row_names(method_name) = [method_name.to_sym, ANY_ROW_NAME]
|
|
328
|
+
|
|
321
329
|
# The prescan's arms for a call; true when the arm took the call and its subtree.
|
|
322
330
|
def prescanned_call?(node, qualified_prefix, in_singleton_class, offsets, ranges, nested)
|
|
323
331
|
if call?(node)
|
|
324
332
|
prescan_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
|
|
333
|
+
elsif (arguments = Spellings.arguments(node))
|
|
334
|
+
Spellings.prescan(node, arguments, qualified_prefix, in_singleton_class, offsets, ranges)
|
|
325
335
|
elsif visibility_reset?(node)
|
|
326
336
|
offsets << [node.location.start_offset, nested ? :maybe_off : :off]
|
|
327
337
|
elsif ScopeIndexer.receiver_eval_call?(node) || (node.block && ScopeIndexer.meta_new_constant_rvalue?(node))
|
|
@@ -374,3 +384,5 @@ module Rigor
|
|
|
374
384
|
end
|
|
375
385
|
end
|
|
376
386
|
end
|
|
387
|
+
|
|
388
|
+
require_relative "module_function_state/spellings"
|
|
@@ -7,6 +7,7 @@ require_relative "../source/node_children"
|
|
|
7
7
|
require_relative "content_join"
|
|
8
8
|
require_relative "hash_lookup_mutation"
|
|
9
9
|
require_relative "mutation_rejoin"
|
|
10
|
+
require_relative "deep_erasure"
|
|
10
11
|
require_relative "receiver_alias"
|
|
11
12
|
require_relative "refinement_mutation"
|
|
12
13
|
require_relative "rewrite_mutation"
|
|
@@ -200,12 +201,15 @@ module Rigor
|
|
|
200
201
|
# `Prism::BlockNode`s — each block is processed by its own `eval_call`.
|
|
201
202
|
def widen_after_block(call_node:, outer_scope:)
|
|
202
203
|
block = call_node.block
|
|
204
|
+
return DeepErasure.widen_symbol_proc(call_node, outer_scope) if DeepErasure.symbol_proc_mutation?(block)
|
|
203
205
|
return outer_scope unless block.is_a?(Prism::BlockNode)
|
|
204
206
|
|
|
205
207
|
body = block.body
|
|
206
208
|
return outer_scope if body.nil?
|
|
207
209
|
|
|
208
|
-
walk_for_outer_mutations(body, outer_scope)
|
|
210
|
+
scope = walk_for_outer_mutations(body, outer_scope)
|
|
211
|
+
names = DeepErasure.parameter_names(block.parameters)
|
|
212
|
+
DeepErasure.widen_element_aliases(call_node.receiver, block.body, names, scope)
|
|
209
213
|
end
|
|
210
214
|
|
|
211
215
|
def walk_for_outer_mutations(node, scope)
|
|
@@ -257,7 +261,6 @@ module Rigor
|
|
|
257
261
|
Prism::ClassVariableReadNode => %i[cvar with_cvar],
|
|
258
262
|
Prism::GlobalVariableReadNode => %i[global with_global]
|
|
259
263
|
})
|
|
260
|
-
private_constant :ALIAS_ACCESSORS
|
|
261
264
|
|
|
262
265
|
def widen_alias_read(method_name, read, scope, values: :widen, arg_types: NO_ARG_TYPES)
|
|
263
266
|
getter, builder = ALIAS_ACCESSORS[read.class]
|
|
@@ -524,7 +524,7 @@ module Rigor
|
|
|
524
524
|
|
|
525
525
|
# Issue #1429 — a global or constant subject (`case $stdout when StringIO`) narrows as a local does, and so
|
|
526
526
|
# does an instance variable (#1446).
|
|
527
|
-
slot =
|
|
527
|
+
slot = case_subject_slot(subject, scope)
|
|
528
528
|
return [body_scope, scope] if slot.nil?
|
|
529
529
|
|
|
530
530
|
truthy_type, falsey_type = case_when_types(scope, slot.current, conditions)
|
|
@@ -538,6 +538,32 @@ module Rigor
|
|
|
538
538
|
CASE_SUBJECT_KINDS = %i[local ivar global constant].freeze
|
|
539
539
|
private_constant :CASE_SUBJECT_KINDS
|
|
540
540
|
|
|
541
|
+
# The slot a `case` subject narrows. Issue #2211 — a subject that writes a local or instance variable
|
|
542
|
+
# (`case (x = v)`, `case x = v`) tests the value it stores, which the variable holds in every clause, so the
|
|
543
|
+
# clauses narrow the variable as they narrow a bare read of it: `case (x = s ? String(s) : nil) when String then
|
|
544
|
+
# x.upcase end` reads `x` as `String`. A single-statement `(…)` is unwrapped.
|
|
545
|
+
def case_subject_slot(subject, scope)
|
|
546
|
+
subject = unwrap_case_subject(subject)
|
|
547
|
+
case subject
|
|
548
|
+
when Prism::LocalVariableWriteNode
|
|
549
|
+
variable_slot(:local, subject.name, scope.local(subject.name), CASE_SUBJECT_KINDS)
|
|
550
|
+
when Prism::InstanceVariableWriteNode
|
|
551
|
+
variable_slot(:ivar, subject.name, scope.ivar(subject.name), CASE_SUBJECT_KINDS)
|
|
552
|
+
else receiver_slot(subject, scope, kinds: CASE_SUBJECT_KINDS)
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
private_class_method :case_subject_slot
|
|
556
|
+
|
|
557
|
+
# `subject` with each single-statement `(…)` around it removed: the node {.case_subject_slot} narrows through,
|
|
558
|
+
# which `StatementEvaluator#when_test_rebinds_subject?` reads the same way.
|
|
559
|
+
def unwrap_case_subject(subject)
|
|
560
|
+
while subject.is_a?(Prism::ParenthesesNode) && subject.body.is_a?(Prism::StatementsNode) &&
|
|
561
|
+
subject.body.body.size == 1
|
|
562
|
+
subject = subject.body.body.first
|
|
563
|
+
end
|
|
564
|
+
subject
|
|
565
|
+
end
|
|
566
|
+
|
|
541
567
|
# Returns `[body_scope, falsey_scope]` for one clause of a `case` with a subject: a `when` clause narrows
|
|
542
568
|
# through {.case_when_scopes}, and an `in` clause only for a bare class pattern ({.bare_class_pattern_node}),
|
|
543
569
|
# which matches exactly when `subject.is_a?(C)`. Every other pattern can fail to match where a class test
|
|
@@ -2762,10 +2788,14 @@ module Rigor
|
|
|
2762
2788
|
|
|
2763
2789
|
# Issue #1955 — the subject on the match edge of `/re/ === x` (and of `case x when /re/`). `Regexp#===`
|
|
2764
2790
|
# matches a String, a Symbol by its name, and an object that converts with `to_str`, and answers false for
|
|
2765
|
-
# anything else.
|
|
2766
|
-
#
|
|
2767
|
-
#
|
|
2768
|
-
#
|
|
2791
|
+
# anything else. A member is kept whole when its class defines `to_str` (#2002) — a String and its
|
|
2792
|
+
# subclasses, a class whose signature, ancestry or project source declares it — or when no signature
|
|
2793
|
+
# declares the class, and a `Dynamic` member is kept, since it may also be a Symbol at run time. Any other
|
|
2794
|
+
# member narrows to its String and Symbol parts, which are only what can match: a `String | nil` subject
|
|
2795
|
+
# reads `String`, a Symbol one stays `Symbol`, an Integer one drops out. An `Object`, `BasicObject` or
|
|
2796
|
+
# module member narrows too (to `String | Symbol`): its instances may be anything, and reading the edge as
|
|
2797
|
+
# the String it most often is keeps `s.length` on it from reporting. The no-match edge keeps the entry type:
|
|
2798
|
+
# a String that does not match reaches it too.
|
|
2769
2799
|
def regexp_case_equality_type(current, scope)
|
|
2770
2800
|
members = current.is_a?(Type::Union) ? current.members : [current]
|
|
2771
2801
|
Type::Combinator.union(*members.map { |member| regexp_case_equality_member(member, scope) })
|
|
@@ -2773,6 +2803,7 @@ module Rigor
|
|
|
2773
2803
|
|
|
2774
2804
|
def regexp_case_equality_member(member, scope)
|
|
2775
2805
|
return member if member.is_a?(Type::Dynamic)
|
|
2806
|
+
return member if converts_with_to_str?(member, scope)
|
|
2776
2807
|
|
|
2777
2808
|
parts = %w[String Symbol].map do |class_name|
|
|
2778
2809
|
narrow_class(member, class_name, exact: false, environment: scope.environment, scope: scope)
|
|
@@ -2780,6 +2811,36 @@ module Rigor
|
|
|
2780
2811
|
Type::Combinator.union(*parts)
|
|
2781
2812
|
end
|
|
2782
2813
|
|
|
2814
|
+
# True when a class `member` dispatches through declares `to_str`, or is declared by no signature
|
|
2815
|
+
# ({#class_lacks_method?} answers false for it). `Object`, `BasicObject` and a module String or Symbol
|
|
2816
|
+
# includes (`Kernel`, `Comparable`) are read as their String and Symbol parts instead: their instances may be
|
|
2817
|
+
# anything, and the String they most often are keeps a String method on the arm from reporting. Any other
|
|
2818
|
+
# module is kept whole, since a matching value may include it; so is one of those that declares `to_str`.
|
|
2819
|
+
def converts_with_to_str?(member, scope)
|
|
2820
|
+
targets = ProjectMethodOwnership.targets(member)
|
|
2821
|
+
return false if targets.nil? || targets.empty?
|
|
2822
|
+
|
|
2823
|
+
targets.any? { |class_name, kind| class_converts_with_to_str?(class_name, kind, scope) }
|
|
2824
|
+
end
|
|
2825
|
+
|
|
2826
|
+
# The modules String's or Symbol's own ancestry includes, read from the running Ruby.
|
|
2827
|
+
STRING_SYMBOL_MODULES = (String.ancestors + Symbol.ancestors).grep_v(Class).filter_map(&:name).uniq.freeze
|
|
2828
|
+
private_constant :STRING_SYMBOL_MODULES
|
|
2829
|
+
|
|
2830
|
+
def class_converts_with_to_str?(class_name, kind, scope)
|
|
2831
|
+
return false if kind == :instance && OPEN_SURFACE_CLASSES.include?(class_name)
|
|
2832
|
+
if kind == :instance && scope.environment.rbs_module?(class_name)
|
|
2833
|
+
return module_declares_to_str?(class_name, scope) || !STRING_SYMBOL_MODULES.include?(class_name)
|
|
2834
|
+
end
|
|
2835
|
+
|
|
2836
|
+
!class_lacks_method?(class_name, :to_str, kind, scope)
|
|
2837
|
+
end
|
|
2838
|
+
|
|
2839
|
+
def module_declares_to_str?(module_name, scope)
|
|
2840
|
+
ProjectMethodOwnership.defines?(module_name, :to_str, :instance, scope) ||
|
|
2841
|
+
!ExternalAncestorResolution.method_definition(module_name, :to_str, :instance, scope: scope).nil?
|
|
2842
|
+
end
|
|
2843
|
+
|
|
2783
2844
|
def unwrap_parens(node)
|
|
2784
2845
|
while node.is_a?(Prism::ParenthesesNode) && node.body.is_a?(Prism::StatementsNode) &&
|
|
2785
2846
|
node.body.body.size == 1
|
|
@@ -123,9 +123,14 @@ module Rigor
|
|
|
123
123
|
# all.
|
|
124
124
|
def answer_for_entry(scope, entry, method_name)
|
|
125
125
|
unless entry.external?
|
|
126
|
-
|
|
126
|
+
defines = source_defines?(scope, entry.name, method_name) || macro_named?(scope, entry.name, method_name)
|
|
127
|
+
return defines ? [:source, entry.name] : nil
|
|
127
128
|
end
|
|
128
129
|
|
|
130
|
+
# Issue #2094 — a project module with no `def` is held as an external; a macro of its body may name it.
|
|
131
|
+
named = entry.candidates.find { |candidate| macro_named?(scope, candidate, method_name) }
|
|
132
|
+
return [:source, named] if named
|
|
133
|
+
|
|
129
134
|
entry.candidates.each do |candidate|
|
|
130
135
|
definition = Rigor::Reflection.instance_method_definition(candidate, method_name, scope: scope)
|
|
131
136
|
return [private_definition?(definition) ? :private_rbs : :rbs, candidate, definition] if definition
|
|
@@ -137,6 +142,13 @@ module Rigor
|
|
|
137
142
|
end
|
|
138
143
|
private_class_method :answer_for_entry
|
|
139
144
|
|
|
145
|
+
# Issue #2094 — whether a class-body macro of `owner` was handed `method_name` as a literal
|
|
146
|
+
# (`dsl_method :xdyn`, `def_delegators :@target, :xdyn`). The indexer files every such name as an opaque
|
|
147
|
+
# envelope, since what the macro defines under it is unknown, so the module may define the name.
|
|
148
|
+
def macro_named?(scope, owner, method_name)
|
|
149
|
+
scope.parameter_envelopes_of(owner).key?([:instance, method_name.to_sym])
|
|
150
|
+
end
|
|
151
|
+
|
|
140
152
|
def private_definition?(definition)
|
|
141
153
|
definition.respond_to?(:accessibility) && definition.accessibility == :private
|
|
142
154
|
end
|
|
@@ -39,7 +39,8 @@ module Rigor
|
|
|
39
39
|
# `dependencies.source_inference:`), and no `discovery_seed:` base is merged in (the runner can start from one).
|
|
40
40
|
NOT_SEEDED = {
|
|
41
41
|
run_generation: "the identity token of a check run's return memo; these paths run no check",
|
|
42
|
-
constant_sources: "read
|
|
42
|
+
constant_sources: "read by dependency recording and the #663 pre_eval retraction; a seed carries it only " \
|
|
43
|
+
"when asked (DiscoverySeed.tables_for_buffer(constant_sources: true), the editor publish)",
|
|
43
44
|
param_inferred_types: "the parameter-inference output itself; each caller adds its own " \
|
|
44
45
|
"(DiscoverySeed.build, coverage under parameter_inference:, the collector's last round)"
|
|
45
46
|
}.freeze
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../type"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Inference
|
|
9
|
+
# Issue #2127 — whether a call to a PROJECT method may run the block it passes more than once, read off the
|
|
10
|
+
# callee's body. {BlockRepetition.may_repeat?} answers only for the core iterators {ClosureEscapeAnalyzer}
|
|
11
|
+
# catalogues, so `def yielder = (yield; yield)` or the idiomatic wrapper `def each = @items.each { |i| yield i }`
|
|
12
|
+
# counted as running the block once, and a `return` in the block that only a later run reaches (`each { return
|
|
13
|
+
# false if (seen += 1) > 3 }`) never joined the method's return type.
|
|
14
|
+
#
|
|
15
|
+
# Only the later-run `return` pass asks this (`StatementEvaluator#collect_repeated_block_returns`); the block's
|
|
16
|
+
# entry scopes and the ADR-56 write-back keep {BlockRepetition}'s rule. That pass only widens the method's
|
|
17
|
+
# return type, so the rule here errs toward "repeats". The callee repeats when its body, outside a nested `def`
|
|
18
|
+
# (a `yield` in a class body is a syntax error inside a method):
|
|
19
|
+
#
|
|
20
|
+
# - holds two or more sites that run the block (`yield`, or `call` / `yield` / `===` / `[]` on the `&blk`
|
|
21
|
+
# parameter), or one inside a loop, a block or a lambda, which may run it many times; or
|
|
22
|
+
# - hands the block on (`each(&blk)`, `each(&)`, `...` forwarding) or reads the `&blk` parameter any other way
|
|
23
|
+
# (stores it, passes it as an argument), so code the walk does not see may run it any number of times.
|
|
24
|
+
#
|
|
25
|
+
# A callee the analysis cannot resolve to a project `def` (an unknown receiver, a method defined by
|
|
26
|
+
# metaprogramming) answers false, as before.
|
|
27
|
+
module ProjectYielder
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
BLOCK_RUN_CALLS = Set[:call, :yield, :===, :[]].freeze
|
|
31
|
+
REPEATING_NODES = Set[Prism::WhileNode, Prism::UntilNode, Prism::ForNode, Prism::BlockNode,
|
|
32
|
+
Prism::LambdaNode].freeze
|
|
33
|
+
private_constant :BLOCK_RUN_CALLS, :REPEATING_NODES
|
|
34
|
+
|
|
35
|
+
def may_run_block_repeatedly?(call_node, receiver_type, scope)
|
|
36
|
+
def_node = callee_def(call_node, receiver_type, scope)
|
|
37
|
+
return false unless def_node.is_a?(Prism::DefNode) && def_node.body
|
|
38
|
+
|
|
39
|
+
repeats?(def_node)
|
|
40
|
+
rescue StandardError
|
|
41
|
+
false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Issue #2301 — whether the callee hands the block it is called with on to another call (`g(x, &)`,
|
|
45
|
+
# `g(x, &blk)`), outside a nested `def`. The block-return pass types such a forward's target `yield` as the
|
|
46
|
+
# caller's block ({ExpressionTyper#forwarded_block_return_type}), and the target may run it any number of
|
|
47
|
+
# times, so the caller's block is typed as one that may repeat. Answers false where the callee does not resolve.
|
|
48
|
+
def hands_block_on?(call_node, receiver_type, scope)
|
|
49
|
+
def_node = callee_def(call_node, receiver_type, scope)
|
|
50
|
+
return false unless def_node.is_a?(Prism::DefNode) && def_node.body
|
|
51
|
+
|
|
52
|
+
hand_off_in?(def_node.body, def_node.parameters&.block&.name)
|
|
53
|
+
rescue StandardError
|
|
54
|
+
false
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def hand_off_in?(node, block_name)
|
|
58
|
+
return false if node.is_a?(Prism::DefNode)
|
|
59
|
+
return true if node.is_a?(Prism::BlockArgumentNode) && forwards_block?(node, block_name)
|
|
60
|
+
|
|
61
|
+
node.compact_child_nodes.any? { |child| hand_off_in?(child, block_name) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The `def` an instance call on a project class, a singleton call on one, or an implicit-self call to a
|
|
65
|
+
# top-level method resolves to.
|
|
66
|
+
def callee_def(call_node, receiver_type, scope)
|
|
67
|
+
name = call_node.name
|
|
68
|
+
found =
|
|
69
|
+
case receiver_type
|
|
70
|
+
when Type::Nominal then scope.user_def_through_ancestors(receiver_type.class_name, name).first
|
|
71
|
+
when Type::Singleton then scope.singleton_def_for(receiver_type.class_name, name)
|
|
72
|
+
end
|
|
73
|
+
found || (call_node.receiver.nil? ? scope.top_level_def_for(name) : nil)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def repeats?(def_node)
|
|
77
|
+
block_param = def_node.parameters&.block
|
|
78
|
+
counter = { sites: 0, repeats: false }
|
|
79
|
+
walk(def_node.body, block_param&.name, false, counter)
|
|
80
|
+
counter[:repeats] || counter[:sites] >= 2
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def walk(node, block_name, repeating, counter)
|
|
84
|
+
return if counter[:repeats] || node.is_a?(Prism::DefNode)
|
|
85
|
+
|
|
86
|
+
skipped = visit(node, block_name, repeating, counter)
|
|
87
|
+
inner = repeating || REPEATING_NODES.include?(node.class)
|
|
88
|
+
node.compact_child_nodes.each do |child|
|
|
89
|
+
walk(child, block_name, inner, counter) unless child.equal?(skipped)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Counts `node` and answers the child the walk must not visit: the `blk` receiver of a `blk.call`, which is a
|
|
94
|
+
# run, not a hand-off.
|
|
95
|
+
def visit(node, block_name, repeating, counter)
|
|
96
|
+
case node
|
|
97
|
+
when Prism::YieldNode then run_site(repeating, counter)
|
|
98
|
+
when Prism::ForwardingArgumentsNode then counter[:repeats] = true
|
|
99
|
+
when Prism::BlockArgumentNode then counter[:repeats] = true if forwards_block?(node, block_name)
|
|
100
|
+
when Prism::CallNode
|
|
101
|
+
return unless block_run_call?(node, block_name)
|
|
102
|
+
|
|
103
|
+
run_site(repeating, counter)
|
|
104
|
+
node.receiver
|
|
105
|
+
when Prism::LocalVariableReadNode then counter[:repeats] = true if block_name && node.name == block_name
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def run_site(repeating, counter)
|
|
110
|
+
counter[:sites] += 1
|
|
111
|
+
counter[:repeats] = true if repeating
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# `blk.call` and its spellings.
|
|
115
|
+
def block_run_call?(node, block_name)
|
|
116
|
+
receiver = node.receiver
|
|
117
|
+
!block_name.nil? && receiver.is_a?(Prism::LocalVariableReadNode) && receiver.name == block_name &&
|
|
118
|
+
BLOCK_RUN_CALLS.include?(node.name)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# `&blk` or an anonymous `&` handed to another call.
|
|
122
|
+
def forwards_block?(node, block_name)
|
|
123
|
+
expression = node.expression
|
|
124
|
+
expression.nil? || (expression.is_a?(Prism::LocalVariableReadNode) && expression.name == block_name)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -66,6 +66,15 @@ module Rigor
|
|
|
66
66
|
classifier.result
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
+
# Issue #2021 — every contribution of the tree under `root`, for a file discovery reached only through a
|
|
70
|
+
# `require_relative` walk: another file the walk does not see may redefine what it defines, so none of its
|
|
71
|
+
# contributions is certain.
|
|
72
|
+
def every_contribution(root)
|
|
73
|
+
classifier = Classifier.new
|
|
74
|
+
classifier.possible(root)
|
|
75
|
+
classifier.result
|
|
76
|
+
end
|
|
77
|
+
|
|
69
78
|
# Whether `node` was classified possible in `possible` (a {possible_nodes} answer).
|
|
70
79
|
def possible?(possible, node)
|
|
71
80
|
!possible.empty? && possible.include?(node)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../reflection"
|
|
4
|
+
require_relative "../rbs_type_translator"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Inference
|
|
8
|
+
module ScopeIndexer
|
|
9
|
+
# Issue #1406 — an ivar the class's RBS declares, as `@x: T` or through an `attr_reader x: T` /
|
|
10
|
+
# `attr_writer` / `attr_accessor` member (in `sig/`, or inline `attr_reader :x #: T`), reads the declared type
|
|
11
|
+
# in place of the census's join of the writes. The declaration is the author's contract for the field, so it
|
|
12
|
+
# wins over every write the census saw, #541's untyped `attr_writer` write included; whether the writes
|
|
13
|
+
# honour it is ADR-112 WD5's question (#1075), not this one's. It serves
|
|
14
|
+
# checking only: `rigor sig-gen` indexes with `declared_ivar_types: false` and keeps the census's answer.
|
|
15
|
+
#
|
|
16
|
+
# Only an ivar the census already holds is replaced, so a declaration adds no entry the file's writes did not.
|
|
17
|
+
# A declaration is not taken when it is `untyped` (what rbs-inline supplies for a bare `attr_reader :x`, which
|
|
18
|
+
# the type specification reads as a placeholder) or mentions a type variable (`@x: T` in `class Box[T]` is
|
|
19
|
+
# unsolved at a read), and the census entry stands.
|
|
20
|
+
module DeclaredIvarTypes
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
def apply!(accumulator, scope)
|
|
24
|
+
environment = scope&.environment
|
|
25
|
+
return if environment.nil?
|
|
26
|
+
|
|
27
|
+
accumulator.each do |class_name, entries|
|
|
28
|
+
next unless class_name.is_a?(String) # a guard: the side-table keys are deleted before this runs
|
|
29
|
+
|
|
30
|
+
declared = declared_ivars(class_name, environment)
|
|
31
|
+
next if declared.empty?
|
|
32
|
+
|
|
33
|
+
accumulator[class_name] = entries.to_h do |ivar, type|
|
|
34
|
+
[ivar, declared.fetch(ivar, type)]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# `{ivar name => Rigor::Type}` for the ivars `class_name`'s RBS instance definition declares with a usable
|
|
40
|
+
# type, or `{}`.
|
|
41
|
+
def declared_ivars(class_name, environment)
|
|
42
|
+
definition = Reflection.instance_definition(class_name, environment: environment)
|
|
43
|
+
return {} if definition.nil?
|
|
44
|
+
|
|
45
|
+
instance = Type::Combinator.nominal_of(class_name)
|
|
46
|
+
definition.instance_variables.each_with_object({}) do |(name, variable), acc|
|
|
47
|
+
type = translate(variable.type, instance, environment)
|
|
48
|
+
acc[name] = type if type
|
|
49
|
+
end
|
|
50
|
+
rescue StandardError # a guard: a definition that fails to build declares nothing
|
|
51
|
+
{}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def translate(rbs_type, instance, environment)
|
|
55
|
+
return nil unless rbs_type.free_variables.empty?
|
|
56
|
+
|
|
57
|
+
type = RbsTypeTranslator.translate(rbs_type, self_type: instance, instance_type: instance,
|
|
58
|
+
alias_expander: environment.rbs_loader)
|
|
59
|
+
type.is_a?(Type::Dynamic) ? nil : type
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|