rigortype 0.4.0 → 0.4.1
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 +4 -3
- data/docs/handbook/08-understanding-errors.md +3 -2
- data/docs/install.md +87 -15
- data/docs/manual/02-cli-reference.md +2 -2
- data/docs/manual/03-configuration.md +5 -0
- data/docs/manual/08-skills.md +4 -3
- data/docs/manual/12-caching.md +43 -4
- data/docs/manual/14-rails-quickstart.md +19 -11
- data/lib/rigor/analysis/check_rules/source_arity.rb +163 -59
- data/lib/rigor/analysis/check_rules.rb +118 -87
- data/lib/rigor/analysis/dependency_recorder.rb +200 -18
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
- data/lib/rigor/analysis/incremental_session.rb +273 -49
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
- data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
- data/lib/rigor/analysis/run_cache_key.rb +27 -8
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +1 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +2 -2
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
- data/lib/rigor/analysis/runner.rb +337 -46
- data/lib/rigor/analysis/shadow_harness.rb +27 -0
- data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
- data/lib/rigor/analysis/template_unit_paths.rb +6 -4
- data/lib/rigor/analysis/template_units.rb +15 -5
- data/lib/rigor/analysis/worker_session.rb +4 -3
- data/lib/rigor/cache/descriptor.rb +40 -1
- data/lib/rigor/cache/file_digest.rb +28 -0
- data/lib/rigor/cache/incremental_snapshot.rb +70 -8
- data/lib/rigor/cache/store.rb +63 -3
- data/lib/rigor/cache/value_digest.rb +325 -0
- data/lib/rigor/cli/check_command.rb +83 -13
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/effects_report.rb +2 -2
- data/lib/rigor/cli/unused_command.rb +3 -3
- data/lib/rigor/cli.rb +1 -1
- data/lib/rigor/config_audit.rb +3 -2
- data/lib/rigor/config_path.rb +29 -0
- data/lib/rigor/configuration/severity_profile.rb +5 -0
- data/lib/rigor/configuration.rb +6 -5
- data/lib/rigor/effects/plugin_facts.rb +6 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
- data/lib/rigor/environment/lockfile_resolver.rb +10 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
- data/lib/rigor/environment/rbs_loader.rb +1 -1
- data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
- data/lib/rigor/environment.rb +13 -70
- data/lib/rigor/inference/block_parameter_binder.rb +29 -13
- data/lib/rigor/inference/captured_locals.rb +7 -1
- data/lib/rigor/inference/declaration_walk/context.rb +195 -0
- data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
- data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
- data/lib/rigor/inference/declaration_walk.rb +7 -0
- data/lib/rigor/inference/def_node_resolver.rb +3 -2
- data/lib/rigor/inference/definer_resolution.rb +266 -0
- data/lib/rigor/inference/expression_typer.rb +136 -45
- data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
- data/lib/rigor/inference/guard_rebinding.rb +53 -12
- data/lib/rigor/inference/macro_block_self_type.rb +14 -2
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +46 -26
- data/lib/rigor/inference/module_function_state.rb +213 -0
- data/lib/rigor/inference/multi_target_binder.rb +21 -16
- data/lib/rigor/inference/narrowing.rb +147 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
- data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
- data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
- data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
- data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
- data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
- data/lib/rigor/inference/scope_indexer.rb +1443 -295
- data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
- data/lib/rigor/inference/statement_evaluator.rb +80 -14
- data/lib/rigor/plugin/base.rb +28 -8
- data/lib/rigor/plugin/io_boundary.rb +82 -4
- data/lib/rigor/plugin/isolation.rb +1 -1
- data/lib/rigor/plugin/trust_policy.rb +3 -3
- data/lib/rigor/protection/discovery_seed.rb +18 -9
- data/lib/rigor/protection/mutation_cache.rb +3 -3
- data/lib/rigor/reflection/constant_ancestors.rb +96 -47
- data/lib/rigor/reflection/constant_path.rb +22 -7
- data/lib/rigor/reflection.rb +2 -3
- data/lib/rigor/scope/discovery_index.rb +218 -2
- data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
- data/lib/rigor/scope/resolution_chain.rb +884 -0
- data/lib/rigor/scope.rb +295 -322
- data/lib/rigor/sig_gen/alias_index.rb +1 -1
- data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
- data/lib/rigor/sig_gen/generator.rb +22 -16
- data/lib/rigor/type/combinator.rb +25 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +10 -0
- data/sig/rigor/environment.rbs +2 -0
- data/sig/rigor/plugin/io_boundary.rbs +1 -0
- data/sig/rigor/scope.rbs +43 -1
- data/skills/rigor-project-init/SKILL.md +37 -11
- data/skills/rigor-project-init/references/02-configure.md +7 -2
- metadata +25 -1
|
@@ -440,6 +440,20 @@ module Rigor
|
|
|
440
440
|
Source::ConstantPath.rooted?(node) ? "::#{name}" : name
|
|
441
441
|
end
|
|
442
442
|
|
|
443
|
+
# Issue #1446 — the classes a class guard narrowed the receiver `node` to where the guard left it `bot`
|
|
444
|
+
# ({Scope#bot_guard_classes_for}), or nil: a node that is not a local, instance-variable, global or constant
|
|
445
|
+
# read, or one no such guard narrowed.
|
|
446
|
+
def bot_guard_classes_of(node, scope)
|
|
447
|
+
case node
|
|
448
|
+
when Prism::LocalVariableReadNode then scope.bot_guard_classes_for(:local, node.name)
|
|
449
|
+
when Prism::InstanceVariableReadNode then scope.bot_guard_classes_for(:ivar, node.name)
|
|
450
|
+
when Prism::GlobalVariableReadNode then scope.bot_guard_classes_for(:global, node.name)
|
|
451
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
452
|
+
key = constant_key(node)
|
|
453
|
+
key && scope.bot_guard_classes_for(:constant, key)
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
443
457
|
# Public predicate analyser. Returns `[truthy_scope, falsey_scope]`, always; when no
|
|
444
458
|
# narrowing rule matches the predicate node both entries are the receiver scope unchanged.
|
|
445
459
|
def predicate_scopes(node, scope)
|
|
@@ -504,15 +518,20 @@ module Rigor
|
|
|
504
518
|
# a different regex. Applied even when the subject is not a narrowable local read.
|
|
505
519
|
body_scope = apply_when_regex_globals(conditions, scope)
|
|
506
520
|
|
|
507
|
-
# Issue #1429 — a global or constant subject (`case $stdout when StringIO`) narrows as a local does
|
|
521
|
+
# Issue #1429 — a global or constant subject (`case $stdout when StringIO`) narrows as a local does, and so
|
|
522
|
+
# does an instance variable (#1446).
|
|
508
523
|
slot = receiver_slot(subject, scope, kinds: CASE_SUBJECT_KINDS)
|
|
509
524
|
return [body_scope, scope] if slot.nil?
|
|
510
525
|
|
|
511
526
|
truthy_type, falsey_type = case_when_types(scope, slot.current, conditions)
|
|
512
|
-
|
|
527
|
+
classes = conditions.map do |condition|
|
|
528
|
+
lexical_class_name(condition, scope) || case_equality_target_class(condition)
|
|
529
|
+
end
|
|
530
|
+
body_scope = record_bot_guard(narrow_guarded_slot(body_scope, slot, truthy_type), slot, truthy_type, classes)
|
|
531
|
+
[body_scope, narrow_guarded_slot(scope, slot, falsey_type)]
|
|
513
532
|
end
|
|
514
533
|
|
|
515
|
-
CASE_SUBJECT_KINDS = %i[local global constant].freeze
|
|
534
|
+
CASE_SUBJECT_KINDS = %i[local ivar global constant].freeze
|
|
516
535
|
private_constant :CASE_SUBJECT_KINDS
|
|
517
536
|
|
|
518
537
|
# When the clause has exactly one `RegularExpressionNode` literal condition, narrow the
|
|
@@ -979,8 +998,9 @@ module Rigor
|
|
|
979
998
|
end
|
|
980
999
|
|
|
981
1000
|
GLOBAL_AND_CONSTANT = %i[global constant].freeze
|
|
1001
|
+
CLASS_GUARD_SLOT_KINDS = %i[ivar global constant].freeze
|
|
982
1002
|
ALL_SLOT_KINDS = %i[local ivar global constant].freeze
|
|
983
|
-
private_constant :GLOBAL_AND_CONSTANT, :ALL_SLOT_KINDS
|
|
1003
|
+
private_constant :GLOBAL_AND_CONSTANT, :CLASS_GUARD_SLOT_KINDS, :ALL_SLOT_KINDS
|
|
984
1004
|
|
|
985
1005
|
# The {ReceiverSlot} `node` reads, among `kinds`, or nil. A local or instance variable needs a binding. A
|
|
986
1006
|
# global the scope does not bind is read as it reads unbound, and a constant reference as it resolves
|
|
@@ -1029,6 +1049,25 @@ module Rigor
|
|
|
1029
1049
|
end
|
|
1030
1050
|
end
|
|
1031
1051
|
|
|
1052
|
+
# Issue #1446 — {#narrow_slot} for a class guard: an instance variable narrows with its pre-guard type
|
|
1053
|
+
# recorded, as a global does, since code that may reach `self` may rebind it. The other guards on an instance
|
|
1054
|
+
# variable keep {#narrow_slot}'s unrecorded binding.
|
|
1055
|
+
def narrow_guarded_slot(scope, slot, type)
|
|
1056
|
+
return narrow_slot(scope, slot, type) unless slot.kind == :ivar
|
|
1057
|
+
return scope if type == slot.current
|
|
1058
|
+
|
|
1059
|
+
scope.with_guarded_ivar(slot.name, type, slot.current)
|
|
1060
|
+
end
|
|
1061
|
+
|
|
1062
|
+
# Issue #1446 — `scope`, the guard's truthy edge, with `class_names` recorded for `slot` when the guard left
|
|
1063
|
+
# it `bot` ({Scope#with_bot_guard_classes}): the value that passes it is one of those classes. Nothing is
|
|
1064
|
+
# recorded when a condition names no class.
|
|
1065
|
+
def record_bot_guard(scope, slot, type, class_names)
|
|
1066
|
+
return scope unless type.is_a?(Type::Bot) && !class_names.empty? && class_names.none?(&:nil?)
|
|
1067
|
+
|
|
1068
|
+
scope.with_bot_guard_classes(slot.kind, slot.name, class_names)
|
|
1069
|
+
end
|
|
1070
|
+
|
|
1032
1071
|
def narrow_global_slot(scope, slot, type)
|
|
1033
1072
|
return scope if type == slot.current
|
|
1034
1073
|
|
|
@@ -1234,7 +1273,7 @@ module Rigor
|
|
|
1234
1273
|
end
|
|
1235
1274
|
|
|
1236
1275
|
def simple_dispatch_name?(name)
|
|
1237
|
-
%i[nil? ! is_a? kind_of? instance_of? == != === =~ match? key? has_key? empty? any?
|
|
1276
|
+
%i[nil? ! is_a? kind_of? instance_of? == != === =~ !~ match? key? has_key? empty? any?
|
|
1238
1277
|
none? respond_to? nan? finite?].include?(name)
|
|
1239
1278
|
end
|
|
1240
1279
|
|
|
@@ -1245,7 +1284,7 @@ module Rigor
|
|
|
1245
1284
|
when :instance_of? then analyse_class_predicate(node, scope, exact: true)
|
|
1246
1285
|
when :==, :!= then analyse_equality_predicate(node, scope, equality: name)
|
|
1247
1286
|
when :=== then analyse_case_equality_predicate(node, scope)
|
|
1248
|
-
when
|
|
1287
|
+
when :=~, :!~ then dispatch_regex_match_predicate(node, scope, name)
|
|
1249
1288
|
when :match? then analyse_whole_regex_match_predicate(node, scope)
|
|
1250
1289
|
when :key?, :has_key? then analyse_key_presence_predicate(node, scope)
|
|
1251
1290
|
when :empty?, :any?, :none? then analyse_array_emptiness_predicate(node, scope, name)
|
|
@@ -1254,6 +1293,10 @@ module Rigor
|
|
|
1254
1293
|
end
|
|
1255
1294
|
end
|
|
1256
1295
|
|
|
1296
|
+
def dispatch_regex_match_predicate(node, scope, name)
|
|
1297
|
+
name == :=~ ? analyse_regex_match_predicate(node, scope) : analyse_regex_not_match_predicate(node, scope)
|
|
1298
|
+
end
|
|
1299
|
+
|
|
1257
1300
|
# T3 (template-corpora survey) — `recv.respond_to?(sym)` truthy edge narrows `recv`
|
|
1258
1301
|
# non-nil. `nil.respond_to?(m)` is `false` for every method `m` that `NilClass` does not
|
|
1259
1302
|
# define, so a truthy `respond_to?` proves the receiver was not `nil` UNLESS the queried
|
|
@@ -1532,7 +1575,9 @@ module Rigor
|
|
|
1532
1575
|
#
|
|
1533
1576
|
# Returns nil (no narrowing) when the receiver / argument pair does not resolve to exactly one
|
|
1534
1577
|
# regex pattern we can count (see {#regex_match_pattern}).
|
|
1535
|
-
|
|
1578
|
+
#
|
|
1579
|
+
# `refine: false` leaves out the whole-receiver refinement, which names a String.
|
|
1580
|
+
def analyse_regex_match_predicate(node, scope, refine: true)
|
|
1536
1581
|
return nil if node.arguments.nil?
|
|
1537
1582
|
return nil unless node.arguments.arguments.size == 1
|
|
1538
1583
|
|
|
@@ -1552,10 +1597,96 @@ module Rigor
|
|
|
1552
1597
|
# match-global logic above. A successful `str =~ /\A\d+\z/` proves the WHOLE string
|
|
1553
1598
|
# matched, so the string operand narrows to the imported refinement the anchored pattern
|
|
1554
1599
|
# names. The falsey edge is untouched: a failed match proves nothing about the shape.
|
|
1555
|
-
truthy = apply_whole_receiver_refinement(truthy, node.receiver, arg, pattern.source, scope)
|
|
1600
|
+
truthy = apply_whole_receiver_refinement(truthy, node.receiver, arg, pattern.source, scope) if refine
|
|
1556
1601
|
[truthy, falsey]
|
|
1557
1602
|
end
|
|
1558
1603
|
|
|
1604
|
+
# Issue #1379 — `subject !~ pattern` is `Kernel#!~`, which returns `!(subject =~ pattern)`. It runs the same
|
|
1605
|
+
# match in the caller's frame, so its edges are {#analyse_regex_match_predicate}'s swapped: the falsey edge is
|
|
1606
|
+
# the match edge (`$~` a `MatchData`, an unconditional `$N` a `String`, and the whole-receiver refinement), the
|
|
1607
|
+
# truthy edge the no-match edge (`$~` and `$N` nil). The same patterns are recognised and the same ones
|
|
1608
|
+
# declined (a non-regex operand, an ambiguous constant, `//x`).
|
|
1609
|
+
#
|
|
1610
|
+
# Unlike `=~`'s, an edge here is narrowed only when the receiver proves what the frame holds on it
|
|
1611
|
+
# ({#not_match_receiver_edges}). An edge it cannot vouch for keeps the match globals forgotten, since the call
|
|
1612
|
+
# may still have rebound them. The refinement is kept only when the local it would narrow is known to be a
|
|
1613
|
+
# String on the match edge ({#refinable_operand?}).
|
|
1614
|
+
def analyse_regex_not_match_predicate(node, scope)
|
|
1615
|
+
edges = analyse_regex_match_predicate(node, scope, refine: refinable_operand?(node, scope))
|
|
1616
|
+
return nil if edges.nil?
|
|
1617
|
+
|
|
1618
|
+
match, no_match = edges
|
|
1619
|
+
forgotten = scope.forget_match_globals
|
|
1620
|
+
case not_match_receiver_edges(node, scope)
|
|
1621
|
+
when :both then [no_match, match]
|
|
1622
|
+
when :match then [forgotten, match]
|
|
1623
|
+
else [forgotten, forgotten]
|
|
1624
|
+
end
|
|
1625
|
+
end
|
|
1626
|
+
|
|
1627
|
+
# Which edges of `recv !~ pattern` the receiver lets {#analyse_regex_not_match_predicate} narrow. `Kernel#!~`
|
|
1628
|
+
# dispatches `=~` on the receiver:
|
|
1629
|
+
#
|
|
1630
|
+
# - A Regexp receiver (the pattern on the left) runs `Regexp#=~`, which sets `$~` on both outcomes, a `nil`
|
|
1631
|
+
# argument included: `:both`.
|
|
1632
|
+
# - `String#=~` and `Symbol#=~` do the same: `:both` when every member of the receiver's type is a String or a
|
|
1633
|
+
# Symbol.
|
|
1634
|
+
# - `NilClass#=~` answers nil and leaves `$~` alone, so a nil receiver reaches the no-match edge with the
|
|
1635
|
+
# previous match still bound; it never reaches the match edge. A `Dynamic` receiver may be nil at run time
|
|
1636
|
+
# whatever its static facet says: `:match` when every member is a String, a Symbol, nil, or a `Dynamic`
|
|
1637
|
+
# that is untyped or whose facet holds only those.
|
|
1638
|
+
# - Any other receiver may define `=~` in Ruby, which binds `$~` in its own frame, not the caller's: nil.
|
|
1639
|
+
#
|
|
1640
|
+
# A safe-navigation `recv&.!~(pattern)` answers nil without matching on a nil receiver, so it declines too.
|
|
1641
|
+
def not_match_receiver_edges(node, scope)
|
|
1642
|
+
return nil if node.receiver.nil? || node.safe_navigation?
|
|
1643
|
+
return :both if regex_operand_pattern(node.receiver, scope).is_a?(RegexMatchPattern)
|
|
1644
|
+
|
|
1645
|
+
kinds = not_match_member_kinds(scope.type_of(node.receiver), scope)
|
|
1646
|
+
return :both if kinds.all? { |kind| %i[string symbol].include?(kind) }
|
|
1647
|
+
|
|
1648
|
+
:match if kinds.all? { |kind| %i[string symbol nil dynamic].include?(kind) }
|
|
1649
|
+
end
|
|
1650
|
+
|
|
1651
|
+
# Whether the local operand {#apply_whole_receiver_refinement} would narrow holds only Strings and nil, which
|
|
1652
|
+
# the match edge excludes. The refinement names a String, so a member that may be a Symbol — a Symbol, or an
|
|
1653
|
+
# untyped `Dynamic` that `Kernel#!~` accepts as the receiver or `Regexp#=~` as the argument — keeps it out:
|
|
1654
|
+
# `return if raw !~ /\A\d+\z/` on an untyped `raw` must not make `raw.to_proc` an undefined method.
|
|
1655
|
+
def refinable_operand?(node, scope)
|
|
1656
|
+
operand = [node.receiver, *node.arguments&.arguments].find { |o| o.is_a?(Prism::LocalVariableReadNode) }
|
|
1657
|
+
type = operand && scope.local(operand.name)
|
|
1658
|
+
return true if type.nil?
|
|
1659
|
+
|
|
1660
|
+
not_match_member_kinds(type, scope).all? { |kind| %i[string nil].include?(kind) }
|
|
1661
|
+
end
|
|
1662
|
+
|
|
1663
|
+
def not_match_member_kinds(type, scope)
|
|
1664
|
+
(type.is_a?(Type::Union) ? type.members : [type]).map { |member| not_match_member_kind(member, scope) }
|
|
1665
|
+
end
|
|
1666
|
+
|
|
1667
|
+
# `:string` or `:symbol` for a member {.narrow_not_class} proves a String or a Symbol (a value or refinement of
|
|
1668
|
+
# either, or a class the environment orders below it; a project subclass it cannot order reads as neither),
|
|
1669
|
+
# `:nil`, `:dynamic` for a `Dynamic` whose facet is untyped or holds only Strings, Symbols and nil, or nil for
|
|
1670
|
+
# anything else.
|
|
1671
|
+
def not_match_member_kind(member, scope)
|
|
1672
|
+
return not_match_dynamic_kind(member, scope) if member.is_a?(Type::Dynamic)
|
|
1673
|
+
return :nil if narrow_nil(member) == member && !member.is_a?(Type::Bot)
|
|
1674
|
+
|
|
1675
|
+
environment = scope.environment
|
|
1676
|
+
{ "String" => :string, "Symbol" => :symbol }.each do |class_name, kind|
|
|
1677
|
+
return kind if narrow_not_class(member, class_name, environment: environment, scope: scope).is_a?(Type::Bot)
|
|
1678
|
+
end
|
|
1679
|
+
nil
|
|
1680
|
+
end
|
|
1681
|
+
|
|
1682
|
+
def not_match_dynamic_kind(member, scope)
|
|
1683
|
+
facet = member.static_facet
|
|
1684
|
+
return :dynamic if facet.is_a?(Type::Top)
|
|
1685
|
+
|
|
1686
|
+
facet_kinds = not_match_member_kinds(facet, scope)
|
|
1687
|
+
:dynamic if facet_kinds.all? { |kind| %i[string symbol nil].include?(kind) }
|
|
1688
|
+
end
|
|
1689
|
+
|
|
1559
1690
|
# `str.match?(/\A\d+\z/)` — `String#match?` is a pure boolean predicate whose truthy edge
|
|
1560
1691
|
# proves a property of the ENTIRE receiver. When the argument is a fully `\A…\z`-anchored
|
|
1561
1692
|
# single-char-class pattern recognised by {RegexRefinement.for_whole_pattern}, narrow the
|
|
@@ -2239,9 +2370,10 @@ module Rigor
|
|
|
2239
2370
|
end
|
|
2240
2371
|
|
|
2241
2372
|
# Issue #1429 — `$stdout.is_a?(StringIO)`, `STDOUT.kind_of?(StringIO)`: a global or constant receiver
|
|
2242
|
-
# narrows as a local does ({#class_predicate_scopes}), with the pre-guard type recorded ({#narrow_slot}).
|
|
2373
|
+
# narrows as a local does ({#class_predicate_scopes}), with the pre-guard type recorded ({#narrow_slot}). So
|
|
2374
|
+
# does an instance variable (#1446, {#narrow_guarded_slot}).
|
|
2243
2375
|
def analyse_class_predicate_on_slot(receiver, scope, class_name, exact)
|
|
2244
|
-
slot = receiver_slot(receiver, scope, kinds:
|
|
2376
|
+
slot = receiver_slot(receiver, scope, kinds: CLASS_GUARD_SLOT_KINDS)
|
|
2245
2377
|
return nil if slot.nil?
|
|
2246
2378
|
|
|
2247
2379
|
class_slot_scopes(scope, slot, class_name, exact: exact)
|
|
@@ -2250,11 +2382,9 @@ module Rigor
|
|
|
2250
2382
|
def class_slot_scopes(scope, slot, class_name, exact:)
|
|
2251
2383
|
environment = scope.environment
|
|
2252
2384
|
truthy = narrow_class(slot.current, class_name, exact: exact, environment: environment, scope: scope)
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
environment: environment, scope: scope))
|
|
2257
|
-
]
|
|
2385
|
+
falsey = narrow_not_class(slot.current, class_name, exact: exact, environment: environment, scope: scope)
|
|
2386
|
+
truthy_scope = record_bot_guard(narrow_guarded_slot(scope, slot, truthy), slot, truthy, [class_name])
|
|
2387
|
+
[truthy_scope, narrow_guarded_slot(scope, slot, falsey)]
|
|
2258
2388
|
end
|
|
2259
2389
|
|
|
2260
2390
|
# The class name a `is_a?` / `kind_of?` / `instance_of?` argument denotes: the top-level
|
|
@@ -2429,12 +2559,12 @@ module Rigor
|
|
|
2429
2559
|
end
|
|
2430
2560
|
|
|
2431
2561
|
# Issue #1429 — `StringIO === $stdout`, `StringIO === STDOUT`: the class-constant receiver form on a global or
|
|
2432
|
-
# constant argument. The Range / Regexp receivers stay local-only.
|
|
2562
|
+
# constant argument, and on an instance variable (#1446). The Range / Regexp receivers stay local-only.
|
|
2433
2563
|
def analyse_case_equality_on_slot(receiver, arg, scope)
|
|
2434
2564
|
class_name = lexical_class_name(receiver, scope)
|
|
2435
2565
|
return nil if class_name.nil?
|
|
2436
2566
|
|
|
2437
|
-
slot = receiver_slot(arg, scope, kinds:
|
|
2567
|
+
slot = receiver_slot(arg, scope, kinds: CLASS_GUARD_SLOT_KINDS)
|
|
2438
2568
|
return nil if slot.nil?
|
|
2439
2569
|
|
|
2440
2570
|
class_slot_scopes(scope, slot, class_name, exact: false)
|
|
@@ -245,13 +245,16 @@ module Rigor
|
|
|
245
245
|
tables[:discovered_classes] = classes unless classes.empty?
|
|
246
246
|
DISCOVERY_FIELD.each do |index_key, field|
|
|
247
247
|
table = def_index.fetch(index_key)
|
|
248
|
-
tables[field] = table unless table.empty?
|
|
248
|
+
tables[field] = table unless table.empty? && !Scope::DiscoveryIndex.paired?(field)
|
|
249
249
|
end
|
|
250
|
-
|
|
251
|
-
|
|
250
|
+
# ADR-119 WD1 — the pairs travel together: a pair is dropped only when both halves are empty. A sibling whose
|
|
251
|
+
# member this collector does not seed completes to that member's empty value, which the base scope holds.
|
|
252
|
+
Scope::DiscoveryIndex.compact_pairs(tables.merge(def_index.fetch(:siblings)))
|
|
253
|
+
rescue StandardError => e
|
|
252
254
|
# Discovery is best-effort; a malformed corner of the project must not crash the
|
|
253
|
-
# protection scan. Without discovery the collector simply resolves fewer call sites.
|
|
254
|
-
|
|
255
|
+
# protection scan. Without discovery the collector simply resolves fewer call sites. A broken
|
|
256
|
+
# walk contract is not such a corner, and an empty seed would hide it.
|
|
257
|
+
e.is_a?(DeclarationWalk::ContractError) ? raise : {}
|
|
255
258
|
end
|
|
256
259
|
|
|
257
260
|
DISCOVERY_FIELD = {
|
|
@@ -260,7 +263,7 @@ module Rigor
|
|
|
260
263
|
def_sources: :discovered_def_sources,
|
|
261
264
|
superclasses: :discovered_superclasses,
|
|
262
265
|
header_nestings: :discovered_header_nestings,
|
|
263
|
-
includes: :discovered_includes,
|
|
266
|
+
includes: :discovered_includes, unpositioned_mixins: :unpositioned_mixins,
|
|
264
267
|
class_sources: :discovered_class_sources,
|
|
265
268
|
method_visibilities: :discovered_method_visibilities,
|
|
266
269
|
methods: :discovered_methods,
|
|
@@ -359,9 +362,7 @@ module Rigor
|
|
|
359
362
|
end
|
|
360
363
|
|
|
361
364
|
def lookup_def(scope, class_name, method, kind)
|
|
362
|
-
|
|
363
|
-
per_class = table[class_name]
|
|
364
|
-
per_class && per_class[method]
|
|
365
|
+
scope.def_node_slot(class_name, method, kind)
|
|
365
366
|
end
|
|
366
367
|
|
|
367
368
|
# The leading required-positional parameters — the ones a call's leading positional arguments
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module ScopeIndexer
|
|
8
|
+
# ADR-119 WD3 — which of a file's method contributions are `possible`: the classifier the def-contribution
|
|
9
|
+
# producers ({ScopeIndexer#build_methods_and_def_nodes}, {ScopeIndexer#build_discovered_singleton_def_nodes})
|
|
10
|
+
# consult to fill `possible_discovered_methods` and the `contested_*` siblings.
|
|
11
|
+
#
|
|
12
|
+
# A contribution is `certain` when its statement executes whenever the file's top level executes. The certain
|
|
13
|
+
# regions are:
|
|
14
|
+
#
|
|
15
|
+
# - the program's statements, and the bodies of `class`, `module` and `class << self` declarations that are
|
|
16
|
+
# themselves certain;
|
|
17
|
+
# - the block of a meta-new constant write (`K = Class.new do … end`, `Module.new`, `Struct.new`,
|
|
18
|
+
# `Data.define`, {ScopeIndexer#meta_new_block_call}) and of a bare factory call
|
|
19
|
+
# ({AnonymousMetaClass.block_form_receiver}), when the write or the call is certain: Ruby runs the block once,
|
|
20
|
+
# as the new class's body (ADR-119 WD2, "Conditional definers");
|
|
21
|
+
# - a `begin` WITHOUT a `rescue` clause: its main statements and its `ensure` clause;
|
|
22
|
+
# - the receiver and arguments of a certain call, so `private def x` and `memoize def x` stay certain.
|
|
23
|
+
#
|
|
24
|
+
# Everything else is `possible`: the branches of `if` / `unless` / `case` and the modifiers, loop bodies, the
|
|
25
|
+
# right operand of `&&` / `||`, every other block and every lambda, `BEGIN` / `END`, a `class X … end if Y`
|
|
26
|
+
# reopening, and the WHOLE of a `begin` that has a `rescue` clause, its main statements included. That last
|
|
27
|
+
# rule refines WD3's wording: a rescued raise skips every statement after the raising one, so the main body of
|
|
28
|
+
# a rescuing `begin` is not certain either, while a `begin` with no rescue either runs its main statements
|
|
29
|
+
# and `ensure` or propagates the raise out of the file's top level. A `def` inside a method body is never
|
|
30
|
+
# recorded by the walks (they return at a `DefNode`), so the classifier does not descend there either.
|
|
31
|
+
#
|
|
32
|
+
# The answer is an identity Set of the POSSIBLE contribution nodes — the nodes a producer records from — and
|
|
33
|
+
# {EMPTY} when the file has none, so a file written entirely in certain regions allocates nothing. It is
|
|
34
|
+
# separate from {MixinAccumulator}, which answers a different question (whether an edge's POSITION is a fact).
|
|
35
|
+
module Certainty
|
|
36
|
+
# The answer for a file with no possible contribution.
|
|
37
|
+
EMPTY = Set.new.compare_by_identity.freeze
|
|
38
|
+
|
|
39
|
+
# The nodes a def-contribution producer records from: a `def`, an `alias` / `undef`, a call (`attr_*`,
|
|
40
|
+
# `define_method`, `alias_method`, `module_function :x`, a factory's members), and the declarations whose
|
|
41
|
+
# header records members (`class X < Struct.new(:a)`, `X = Struct.new(:a) do … end`).
|
|
42
|
+
CONTRIBUTIONS = Set[
|
|
43
|
+
Prism::DefNode, Prism::AliasMethodNode, Prism::UndefNode, Prism::CallNode, Prism::ClassNode,
|
|
44
|
+
Prism::ConstantWriteNode, Prism::ConstantPathWriteNode, Prism::ConstantOrWriteNode,
|
|
45
|
+
Prism::ConstantPathOrWriteNode
|
|
46
|
+
].freeze
|
|
47
|
+
|
|
48
|
+
CONSTANT_WRITES = Set[
|
|
49
|
+
Prism::ConstantWriteNode, Prism::ConstantPathWriteNode, Prism::ConstantOrWriteNode,
|
|
50
|
+
Prism::ConstantPathOrWriteNode
|
|
51
|
+
].freeze
|
|
52
|
+
|
|
53
|
+
# The nodes whose whole subtree is possible.
|
|
54
|
+
POSSIBLE_ROOTS = Set[
|
|
55
|
+
Prism::BlockNode, Prism::LambdaNode, Prism::PreExecutionNode, Prism::PostExecutionNode,
|
|
56
|
+
Prism::RescueModifierNode
|
|
57
|
+
].freeze
|
|
58
|
+
private_constant :CONSTANT_WRITES, :POSSIBLE_ROOTS
|
|
59
|
+
|
|
60
|
+
module_function
|
|
61
|
+
|
|
62
|
+
# The possible contribution nodes of the tree under `root`, an identity Set ({EMPTY} when none).
|
|
63
|
+
def possible_nodes(root)
|
|
64
|
+
classifier = Classifier.new
|
|
65
|
+
classifier.certain(root)
|
|
66
|
+
classifier.result
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Whether `node` was classified possible in `possible` (a {possible_nodes} answer).
|
|
70
|
+
def possible?(possible, node)
|
|
71
|
+
!possible.empty? && possible.include?(node)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# One classification walk. `certain` descends a certain region; `possible` marks every contribution in a
|
|
75
|
+
# subtree, since nothing below a possible node is certain.
|
|
76
|
+
class Classifier
|
|
77
|
+
def initialize
|
|
78
|
+
@possible = nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def result
|
|
82
|
+
@possible ? @possible.freeze : EMPTY
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def certain(node) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
86
|
+
return unless node.is_a?(Prism::Node)
|
|
87
|
+
return possible(node) if POSSIBLE_ROOTS.include?(node.class)
|
|
88
|
+
|
|
89
|
+
case node
|
|
90
|
+
when Prism::DefNode then return
|
|
91
|
+
when Prism::IfNode then return branch(node.predicate, node.statements, node.subsequent)
|
|
92
|
+
when Prism::UnlessNode then return branch(node.predicate, node.statements, node.else_clause)
|
|
93
|
+
when Prism::CaseNode, Prism::CaseMatchNode then return case_branches(node)
|
|
94
|
+
when Prism::WhileNode, Prism::UntilNode then return branch(node.predicate, node.statements)
|
|
95
|
+
when Prism::ForNode then return branch(node.collection, node.index, node.statements)
|
|
96
|
+
when Prism::AndNode, Prism::OrNode then return branch(node.left, node.right)
|
|
97
|
+
when Prism::BeginNode then return begin_block(node)
|
|
98
|
+
when Prism::CallNode then return call(node)
|
|
99
|
+
end
|
|
100
|
+
return if CONSTANT_WRITES.include?(node.class) && meta_new_write?(node)
|
|
101
|
+
|
|
102
|
+
node.rigor_each_child { |child| certain(child) }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Marks every contribution of the subtree.
|
|
106
|
+
def possible(node)
|
|
107
|
+
return unless node.is_a?(Prism::Node)
|
|
108
|
+
|
|
109
|
+
(@possible ||= Set.new.compare_by_identity) << node if CONTRIBUTIONS.include?(node.class)
|
|
110
|
+
return if node.is_a?(Prism::DefNode)
|
|
111
|
+
|
|
112
|
+
node.rigor_each_child { |child| possible(child) }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
# The first node runs whenever the construct does; the rest may not.
|
|
118
|
+
def branch(first, *rest)
|
|
119
|
+
certain(first)
|
|
120
|
+
rest.each { |node| possible(node) }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def case_branches(node)
|
|
124
|
+
certain(node.predicate)
|
|
125
|
+
node.conditions.each { |clause| possible(clause) }
|
|
126
|
+
possible(node.else_clause)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Without a rescue the main statements run (or the raise leaves the file) and `ensure` runs; with one, a
|
|
130
|
+
# rescued raise skips every statement after the raising one, so all of it is possible.
|
|
131
|
+
def begin_block(node)
|
|
132
|
+
return possible_children(node) if node.rescue_clause
|
|
133
|
+
|
|
134
|
+
certain(node.statements)
|
|
135
|
+
possible(node.else_clause)
|
|
136
|
+
certain(node.ensure_clause)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def possible_children(node)
|
|
140
|
+
node.rigor_each_child { |child| possible(child) }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# The receiver and arguments of a certain call run; its block runs as a class body only for a bare
|
|
144
|
+
# factory call. A safe-navigation call may skip its arguments.
|
|
145
|
+
def call(node)
|
|
146
|
+
certain(node.receiver)
|
|
147
|
+
if node.safe_navigation?
|
|
148
|
+
possible(node.arguments)
|
|
149
|
+
possible(node.block)
|
|
150
|
+
return
|
|
151
|
+
end
|
|
152
|
+
certain(node.arguments)
|
|
153
|
+
block = node.block
|
|
154
|
+
if block.is_a?(Prism::BlockNode) && AnonymousMetaClass.block_form_receiver(node)
|
|
155
|
+
certain(block.body)
|
|
156
|
+
else
|
|
157
|
+
certain(block)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# A meta-new constant write: the factory's receiver and arguments, and its block as the class body.
|
|
162
|
+
def meta_new_write?(node)
|
|
163
|
+
call = ScopeIndexer.meta_new_block_call(node)
|
|
164
|
+
return false unless call
|
|
165
|
+
|
|
166
|
+
certain(call.receiver)
|
|
167
|
+
certain(call.arguments)
|
|
168
|
+
block = call.block
|
|
169
|
+
certain(block.is_a?(Prism::BlockNode) ? block.body : block)
|
|
170
|
+
true
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../declaration_walk/traversal"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module ScopeIndexer
|
|
8
|
+
# The `class_cvars` discovery table (slice 7 phase 6) as a {DeclarationWalk} collector: every
|
|
9
|
+
# `@@x = …` a `def` body writes, typed under the census scope and unioned per class. Ruby cvars are
|
|
10
|
+
# shared by the instance and singleton facets, so every `def` counts, `def self.x` included.
|
|
11
|
+
class ClassCvarsCollector
|
|
12
|
+
include DeclarationWalk::Collector
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@table = {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# `@@x` in a `def` resolves through the LEXICAL cref: `Module.nesting` is the same inside a meta-new or
|
|
19
|
+
# eval block, so the context's `prefix` keys the write and its rebound `self` never does —
|
|
20
|
+
# `K = Class.new { def m = @@x }` inside `class C` writes `C::@@x`. The walk goes no further in: the
|
|
21
|
+
# gather reads the body with `def`, `class` and `module` as barriers, and nothing below a `def` reaches
|
|
22
|
+
# this table.
|
|
23
|
+
def on_def(node, context)
|
|
24
|
+
ScopeIndexer.collect_def_cvar_writes(node, context.prefix, context.scope, @table)
|
|
25
|
+
DeclarationWalk::DECLINE
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The table as the discovery index holds it: each class's entry frozen, then the whole.
|
|
29
|
+
def table
|
|
30
|
+
@table.transform_values(&:freeze).freeze
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../declaration_walk/traversal"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module ScopeIndexer
|
|
8
|
+
# Issue #681's `{Prism::DefNode => Module.nesting}` table as a {DeclarationWalk} collector: the chain every
|
|
9
|
+
# `def` is declared under, keyed by node identity. A top-level `def` records the empty chain (#716); a `def`
|
|
10
|
+
# below a header that renders no name records nothing, because no chain survives it.
|
|
11
|
+
class DefNestingsCollector
|
|
12
|
+
include DeclarationWalk::Collector
|
|
13
|
+
|
|
14
|
+
# The rules `walk_def_nestings` answered differently from the walk, kept so the table stays
|
|
15
|
+
# byte-identical (ADR-116 WD5). Each is a legacy answer, and #1521 tracks converging it:
|
|
16
|
+
#
|
|
17
|
+
# - `factory_block: :ordinary_call` — no bare-factory arm, so a `self::` header in a `Class.new { … }`
|
|
18
|
+
# block anchors on the enclosing `self` and the block's parameters are walked (#1521 item 8).
|
|
19
|
+
# - `lexical_prefix: :nesting_head` — the walker threads no qualified prefix; its meta-new and eval
|
|
20
|
+
# splits resolve against `nesting.first` split into segments, which differs from the walk's prefix
|
|
21
|
+
# below a compact header and below an unnameable cref (#1521 item 1).
|
|
22
|
+
# - `unrendered_header: :body_with_lost_nesting` — a header that renders no name (a parse error) walks
|
|
23
|
+
# its body alone, with the chain lost unless an unnameable cref encloses it (#1521 item 3).
|
|
24
|
+
VARIANTS = {
|
|
25
|
+
factory_block: :ordinary_call,
|
|
26
|
+
lexical_prefix: :nesting_head,
|
|
27
|
+
unrendered_header: :body_with_lost_nesting
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
def initialize
|
|
31
|
+
@table = {}.compare_by_identity
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Nothing below a `def` declares a class or module, so the walk goes no further in for this table.
|
|
35
|
+
def on_def(node, context)
|
|
36
|
+
nesting = context.nesting
|
|
37
|
+
@table[node] = nesting unless nesting.nil?
|
|
38
|
+
DeclarationWalk::DECLINE
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def table
|
|
42
|
+
@table.freeze
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Inference
|
|
5
|
+
module ScopeIndexer
|
|
6
|
+
# Issue #1507 — a file's issue #681 def-nesting table read over the cross-file seed rather than merged into a
|
|
7
|
+
# copy of it. The copy inserted every `def` in the PROJECT once per analysed file (263 ms of an 18.95 s cold
|
|
8
|
+
# Mastodon run); a lookup probes at most both layers, and only for a `def` whose body is re-walked.
|
|
9
|
+
#
|
|
10
|
+
# Both layers are keyed by node identity, so a same-file declaration and its cross-file twin are distinct keys
|
|
11
|
+
# and the order cannot change an answer. The file layer is read first all the same, which is what the merge it
|
|
12
|
+
# replaces did (`merge!(seed)`, then `merge!(file)`), so the answer holds even if a shared parse ever made the
|
|
13
|
+
# keys overlap.
|
|
14
|
+
#
|
|
15
|
+
# It answers what the table's readers ask — `[]` ({ExpressionTyper#recorded_def_nesting}) and `empty?` — and
|
|
16
|
+
# is deliberately not a Hash: `each`, `key?`, `size` or `==` would have to see both layers, and a Hash holding
|
|
17
|
+
# one of them would answer those wrong instead of raising.
|
|
18
|
+
class LayeredDefNestings
|
|
19
|
+
def initialize(file, seed)
|
|
20
|
+
@file = file
|
|
21
|
+
@seed = seed
|
|
22
|
+
freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# `fetch` rather than `||`, so presence in the file layer decides, as the later `merge!` did.
|
|
26
|
+
def [](def_node)
|
|
27
|
+
@file.fetch(def_node) { @seed[def_node] }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def empty?
|
|
31
|
+
@file.empty? && @seed.empty?
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../declaration_walk/traversal"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module ScopeIndexer
|
|
8
|
+
# ADR-48's two member-layout tables as one {DeclarationWalk} collector: the ordered members of each
|
|
9
|
+
# `Data.define` class and of each `Struct.new` class (with its `keyword_init:` flag), in both the subclass
|
|
10
|
+
# form (`class Point < Data.define(:x, :y)`) and the constant form (`Point = Data.define(:x, :y)`). The
|
|
11
|
+
# legacy walkers built the two tables in two walks with the same rules; one collector builds both.
|
|
12
|
+
class MemberLayoutsCollector
|
|
13
|
+
include DeclarationWalk::Collector
|
|
14
|
+
|
|
15
|
+
# The rules the member-layout walkers answered differently from the walk, kept so both tables stay
|
|
16
|
+
# byte-identical (ADR-116 WD5). Each is a legacy answer, and #1521 tracks converging it:
|
|
17
|
+
#
|
|
18
|
+
# - `factory_block: :ordinary_call` — no bare-factory arm, so a `self::` header in a `Class.new { … }`
|
|
19
|
+
# block anchors on the enclosing `self` and the block's parameters are walked (#1521 item 8).
|
|
20
|
+
# - `unrendered_header: :skip` — a header that renders no name (a parse error) stops the walk there:
|
|
21
|
+
# neither its parts nor its body are walked (#1521 item 3).
|
|
22
|
+
VARIANTS = { factory_block: :ordinary_call, unrendered_header: :skip }.freeze
|
|
23
|
+
|
|
24
|
+
def initialize
|
|
25
|
+
@data = {}
|
|
26
|
+
@struct = {}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# A `class Point < Data.define(:x, :y)` header, recorded under the class it names; an unnameable
|
|
30
|
+
# header (`class D` below `class <<` opens `#<Class:C>::D`, which no name spells) records nothing.
|
|
31
|
+
def on_declaration(node, _context, body)
|
|
32
|
+
if node.is_a?(Prism::ClassNode) && !body.singleton_cref
|
|
33
|
+
ScopeIndexer.record_data_member_layout(@data, body.prefix, node.superclass, allow_outer_block: false)
|
|
34
|
+
ScopeIndexer.record_struct_member_layout(@struct, body.prefix, node.superclass, allow_outer_block: false)
|
|
35
|
+
end
|
|
36
|
+
DeclarationWalk::DESCEND
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# A `Point = Data.define(:x, :y)` write, recorded under the class the write names. Below an unnameable
|
|
40
|
+
# cref only a path write still names one.
|
|
41
|
+
def on_constant_write(node, context)
|
|
42
|
+
self_owner = context.self_owner
|
|
43
|
+
unless context.singleton_cref && !ScopeIndexer.meta_new_path_target_nameable?(node, self_owner)
|
|
44
|
+
child_prefix = ScopeIndexer.meta_new_child_prefix(node, context.prefix, self_owner)
|
|
45
|
+
end
|
|
46
|
+
if child_prefix
|
|
47
|
+
rvalue = ScopeIndexer.meta_new_rvalue(node)
|
|
48
|
+
ScopeIndexer.record_data_member_layout(@data, child_prefix, rvalue)
|
|
49
|
+
ScopeIndexer.record_struct_member_layout(@struct, child_prefix, rvalue)
|
|
50
|
+
end
|
|
51
|
+
DeclarationWalk::DESCEND
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# `[data_member_layouts, struct_member_layouts]`, each frozen.
|
|
55
|
+
def tables
|
|
56
|
+
[@data.freeze, @struct.freeze]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|