rigortype 0.4.2 → 0.4.4
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/data/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +28 -5
- data/docs/manual/02-cli-reference.md +23 -4
- data/docs/manual/03-configuration.md +21 -8
- data/docs/manual/04-diagnostics.md +44 -2
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/10-mcp-server.md +32 -3
- data/docs/manual/12-caching.md +18 -1
- data/docs/manual/14-rails-quickstart.md +2 -2
- data/docs/manual/18-removing-dead-code.md +26 -9
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- data/lib/rigor/analysis/check_rules/rule_ids.rb +64 -0
- data/lib/rigor/analysis/check_rules.rb +68 -57
- data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
- data/lib/rigor/analysis/incremental.rb +12 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -3
- data/lib/rigor/analysis/incremental_session.rb +207 -40
- data/lib/rigor/analysis/path_expansion.rb +62 -6
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
- data/lib/rigor/analysis/reachability/graph.rb +139 -15
- data/lib/rigor/analysis/reachability/scan.rb +374 -173
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- data/lib/rigor/analysis/rule_catalog.rb +6 -1
- data/lib/rigor/analysis/run_cache_key.rb +31 -7
- data/lib/rigor/analysis/run_cache_probe.rb +6 -4
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +74 -13
- data/lib/rigor/analysis/runner/pool_coordinator.rb +133 -19
- data/lib/rigor/analysis/runner/project_pre_passes.rb +46 -1
- data/lib/rigor/analysis/runner.rb +62 -18
- data/lib/rigor/analysis/severity_stamp.rb +34 -1
- data/lib/rigor/analysis/worker_session.rb +58 -2
- data/lib/rigor/cache/descriptor.rb +40 -6
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +56 -12
- data/lib/rigor/cli/check_command.rb +138 -39
- data/lib/rigor/cli/docs_command.rb +5 -4
- data/lib/rigor/cli/lens_command.rb +144 -0
- data/lib/rigor/cli/lsp_command.rb +5 -2
- data/lib/rigor/cli/mcp_command.rb +1 -1
- data/lib/rigor/cli/skill_command.rb +3 -3
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/unused_command.rb +157 -24
- data/lib/rigor/cli.rb +30 -4
- data/lib/rigor/config_audit.rb +111 -15
- data/lib/rigor/configuration/dependencies.rb +19 -11
- data/lib/rigor/configuration.rb +15 -0
- data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
- data/lib/rigor/environment/installed_gem_set.rb +38 -11
- data/lib/rigor/environment/lockfile_resolver.rb +37 -6
- data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
- data/lib/rigor/environment/rbs_loader.rb +79 -10
- data/lib/rigor/environment/required_features.rb +4 -2
- data/lib/rigor/environment.rb +12 -5
- data/lib/rigor/inference/block_call_timing.rb +5 -0
- data/lib/rigor/inference/builtins/cruby_definers.rb +22 -5
- data/lib/rigor/inference/captured_locals.rb +69 -6
- data/lib/rigor/inference/definer_resolution.rb +16 -7
- data/lib/rigor/inference/expression_typer.rb +117 -29
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +21 -17
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- data/lib/rigor/inference/match_rebinding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -16
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
- data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +52 -3
- data/lib/rigor/inference/module_function_state.rb +80 -13
- data/lib/rigor/inference/narrowing.rb +184 -68
- data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
- data/lib/rigor/inference/project_patched_methods.rb +2 -2
- data/lib/rigor/inference/project_patched_scanner.rb +38 -24
- data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +112 -40
- data/lib/rigor/inference/refined_dispatch.rb +62 -14
- data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +416 -1
- data/lib/rigor/inference/scope_indexer.rb +565 -61
- data/lib/rigor/inference/self_call_ivar_writes.rb +341 -0
- data/lib/rigor/inference/statement_evaluator.rb +307 -60
- data/lib/rigor/inference/untyped_rejoin.rb +75 -0
- data/lib/rigor/language_server/project_context.rb +56 -5
- data/lib/rigor/lens/anchor.rb +115 -0
- data/lib/rigor/lens/contract_reader.rb +133 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +802 -0
- data/lib/rigor/lens/members.rb +203 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +322 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +41 -0
- data/lib/rigor/mcp/server.rb +61 -20
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/project_environment.rb +23 -1
- data/lib/rigor/protection/mutation_cache.rb +2 -1
- data/lib/rigor/scope.rb +33 -17
- data/lib/rigor/sig_gen/generator.rb +45 -5
- data/lib/rigor/sig_gen/layout_index.rb +36 -0
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- data/lib/rigor/sig_gen/writer.rb +47 -34
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +28 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
- data/sig/rigor/scope.rbs +3 -0
- data/sig/rigor.rbs +6 -3
- data/skills/rigor-project-init/references/02-configure.md +2 -2
- metadata +21 -1
|
@@ -511,7 +511,8 @@ module Rigor
|
|
|
511
511
|
# none of the condition shapes are recognised, both returned scopes equal the input
|
|
512
512
|
# scope. The catalogue mirrors {.case_equality_target_class}: static class/module
|
|
513
513
|
# constants narrow as `is_a?`; integer/float-endpoint ranges narrow to `Numeric`;
|
|
514
|
-
# string-endpoint ranges and regexp literals narrow to `String
|
|
514
|
+
# string-endpoint ranges and regexp literals narrow to `String`; a `nil`, `true` or
|
|
515
|
+
# `false` literal narrows as its class does ({.when_condition_class}).
|
|
515
516
|
def case_when_scopes(subject, conditions, scope)
|
|
516
517
|
# C1 — `case x when /re/` runs `/re/ === x`, which sets the regex match-data globals
|
|
517
518
|
# exactly as a successful `=~` does. Narrow `$~`/`$&`/`$1..$N` on the clause body (the
|
|
@@ -535,6 +536,30 @@ module Rigor
|
|
|
535
536
|
CASE_SUBJECT_KINDS = %i[local ivar global constant].freeze
|
|
536
537
|
private_constant :CASE_SUBJECT_KINDS
|
|
537
538
|
|
|
539
|
+
# Returns `[body_scope, falsey_scope]` for one clause of a `case` with a subject: a `when` clause narrows
|
|
540
|
+
# through {.case_when_scopes}, and an `in` clause only for a bare class pattern ({.bare_class_pattern_node}),
|
|
541
|
+
# which matches exactly when `subject.is_a?(C)`. Every other pattern can fail to match where a class test
|
|
542
|
+
# passes (deconstruction arity, hash keys, a guard), so it narrows neither edge. The pattern's bindings are
|
|
543
|
+
# the caller's to apply.
|
|
544
|
+
def case_clause_scopes(subject, clause, scope)
|
|
545
|
+
return case_when_scopes(subject, clause.conditions, scope) if clause.is_a?(Prism::WhenNode)
|
|
546
|
+
|
|
547
|
+
class_node = bare_class_pattern_node(clause.pattern)
|
|
548
|
+
class_node ? case_when_scopes(subject, [class_node], scope) : [scope, scope]
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
# ADR-47 WD3a — the class-constant node of a `in C` / `in C => x` pattern (the only `in` shapes whose match is
|
|
552
|
+
# pure `is_a?`), or nil for any pattern that deconstructs, binds, or matches a value.
|
|
553
|
+
def bare_class_pattern_node(pattern)
|
|
554
|
+
case pattern
|
|
555
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
556
|
+
pattern
|
|
557
|
+
when Prism::CapturePatternNode
|
|
558
|
+
value = pattern.value
|
|
559
|
+
value if value.is_a?(Prism::ConstantReadNode) || value.is_a?(Prism::ConstantPathNode)
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
|
|
538
563
|
# When the clause has exactly one `RegularExpressionNode` literal condition, narrow the
|
|
539
564
|
# match-data globals on the body edge (same rule as `analyse_regex_match_predicate`'s
|
|
540
565
|
# truthy edge). With multiple regex conditions (`when /a/, /b/`) the body is reachable
|
|
@@ -1088,12 +1113,17 @@ module Rigor
|
|
|
1088
1113
|
# in {Rigor::Builtins::RegexRefinement::RULES}, the truthy branch narrows further than
|
|
1089
1114
|
# `String` to the matching imported refinement (e.g. `decimal-int-string` for `\d+`).
|
|
1090
1115
|
# Bodies outside the table fall back to the v0.1.0 baseline (plain `String`).
|
|
1116
|
+
#
|
|
1117
|
+
# The match rebinds `$~` and the other match globals on both edges, and this rule binds none of them (#1482),
|
|
1118
|
+
# so each edge forgets what an earlier match bound. The statement evaluator's walk already forgets them in the
|
|
1119
|
+
# scope the match leaves; the per-node scope walk of a conditional it did not enter narrows from the scope
|
|
1120
|
+
# before the match, where the falsey edge of an earlier `=~` had bound `$1` to `nil` (#1495).
|
|
1091
1121
|
def analyse_match_write(node, scope)
|
|
1092
1122
|
string_t = Type::Combinator.nominal_of("String")
|
|
1093
1123
|
nil_t = Type::Combinator.constant_of(nil)
|
|
1094
1124
|
refinements = match_write_capture_refinements(node)
|
|
1095
|
-
truthy = scope
|
|
1096
|
-
falsey =
|
|
1125
|
+
truthy = scope.forget_match_globals
|
|
1126
|
+
falsey = truthy
|
|
1097
1127
|
node.targets.each do |target|
|
|
1098
1128
|
next unless target.is_a?(Prism::LocalVariableTargetNode)
|
|
1099
1129
|
|
|
@@ -1575,23 +1605,59 @@ module Rigor
|
|
|
1575
1605
|
end
|
|
1576
1606
|
|
|
1577
1607
|
# Survey item (b): `/regex/ =~ str` and `str =~ /regex/` bind the regex match-data
|
|
1578
|
-
# globals on each edge
|
|
1608
|
+
# globals on each edge ({#regex_match_edges}), gated on the subject (#1494): an edge is
|
|
1609
|
+
# narrowed only when the receiver proves what the frame holds on it
|
|
1610
|
+
# ({#match_receiver_edges}), and an edge it cannot vouch for keeps the match globals
|
|
1611
|
+
# forgotten. A `String?` subject keeps the match edge only, since `NilClass#=~` answers
|
|
1612
|
+
# nil with the previous match still bound; an untyped one is read the same way.
|
|
1579
1613
|
#
|
|
1580
|
-
#
|
|
1614
|
+
# Returns nil (no narrowing) when the receiver / argument pair does not resolve to exactly one
|
|
1615
|
+
# regex pattern we can count (see {#regex_match_pattern}).
|
|
1616
|
+
def analyse_regex_match_predicate(node, scope)
|
|
1617
|
+
gated_regex_match_edges(node, scope)
|
|
1618
|
+
end
|
|
1619
|
+
|
|
1620
|
+
# Issue #1379 — `subject !~ pattern` is `Kernel#!~`, which returns `!(subject =~ pattern)`. It runs the same
|
|
1621
|
+
# match in the caller's frame, so its edges are {#analyse_regex_match_predicate}'s swapped: the falsey edge is
|
|
1622
|
+
# the match edge (`$~` a `MatchData`, an unconditional `$N` a `String`, and the whole-receiver refinement), the
|
|
1623
|
+
# truthy edge the no-match edge (`$~` and `$N` nil). The same patterns are recognised and the same ones
|
|
1624
|
+
# declined (a non-regex operand, an ambiguous constant, `//x`), and the receiver gates the edges alike.
|
|
1625
|
+
def analyse_regex_not_match_predicate(node, scope)
|
|
1626
|
+
gated_regex_match_edges(node, scope)&.reverse
|
|
1627
|
+
end
|
|
1628
|
+
|
|
1629
|
+
# `[match_edge, no_match_edge]` of a `=~` / `!~` predicate, each kept only where
|
|
1630
|
+
# {#match_receiver_edges} lets it be; an edge it cannot vouch for is the entry scope with the
|
|
1631
|
+
# match globals forgotten, since the call may still have rebound them. The whole-receiver
|
|
1632
|
+
# refinement is kept only when the local it would narrow is known to be a String on the
|
|
1633
|
+
# match edge ({#refinable_operand?}).
|
|
1634
|
+
def gated_regex_match_edges(node, scope)
|
|
1635
|
+
edges = regex_match_edges(node, scope, refine: refinable_operand?(node, scope))
|
|
1636
|
+
return nil if edges.nil?
|
|
1637
|
+
|
|
1638
|
+
match, no_match = edges
|
|
1639
|
+
forgotten = scope.forget_match_globals
|
|
1640
|
+
case match_receiver_edges(node, scope)
|
|
1641
|
+
when :both then [match, no_match]
|
|
1642
|
+
when :match then [match, forgotten]
|
|
1643
|
+
else [forgotten, forgotten]
|
|
1644
|
+
end
|
|
1645
|
+
end
|
|
1646
|
+
|
|
1647
|
+
# The ungated edges of a match:
|
|
1648
|
+
#
|
|
1649
|
+
# - Match edge (`=~` returned an Integer position — the match succeeded): `$~` to
|
|
1581
1650
|
# `Nominal[MatchData]`; `$&` and `$1..$N` (where N is the number of capture groups in
|
|
1582
1651
|
# the regex source) to `Nominal[String]`. This is the same optimistic-narrowing shape
|
|
1583
1652
|
# the existing `analyse_match_write` uses for named captures inside `if /(?<x>...)/
|
|
1584
1653
|
# =~ str` — optional groups in the regex source (`(\d+)?`) would bind `$N` to `nil`
|
|
1585
1654
|
# at runtime, but the floor here matches the common idiom (required captures) and
|
|
1586
1655
|
# lets `unless /(\d+)/ =~ s; raise; end; $1.to_i` resolve cleanly.
|
|
1587
|
-
# -
|
|
1656
|
+
# - No-match edge (`=~` returned nil): `$~` and every numbered / back-reference
|
|
1588
1657
|
# global bound to `Constant<nil>`.
|
|
1589
1658
|
#
|
|
1590
|
-
# Returns nil (no narrowing) when the receiver / argument pair does not resolve to exactly one
|
|
1591
|
-
# regex pattern we can count (see {#regex_match_pattern}).
|
|
1592
|
-
#
|
|
1593
1659
|
# `refine: false` leaves out the whole-receiver refinement, which names a String.
|
|
1594
|
-
def
|
|
1660
|
+
def regex_match_edges(node, scope, refine:)
|
|
1595
1661
|
return nil if node.arguments.nil?
|
|
1596
1662
|
return nil unless node.arguments.arguments.size == 1
|
|
1597
1663
|
|
|
@@ -1615,90 +1681,113 @@ module Rigor
|
|
|
1615
1681
|
[truthy, falsey]
|
|
1616
1682
|
end
|
|
1617
1683
|
|
|
1618
|
-
#
|
|
1619
|
-
# match
|
|
1620
|
-
# the match edge (`$~` a `MatchData`, an unconditional `$N` a `String`, and the whole-receiver refinement), the
|
|
1621
|
-
# truthy edge the no-match edge (`$~` and `$N` nil). The same patterns are recognised and the same ones
|
|
1622
|
-
# declined (a non-regex operand, an ambiguous constant, `//x`).
|
|
1623
|
-
#
|
|
1624
|
-
# Unlike `=~`'s, an edge here is narrowed only when the receiver proves what the frame holds on it
|
|
1625
|
-
# ({#not_match_receiver_edges}). An edge it cannot vouch for keeps the match globals forgotten, since the call
|
|
1626
|
-
# may still have rebound them. The refinement is kept only when the local it would narrow is known to be a
|
|
1627
|
-
# String on the match edge ({#refinable_operand?}).
|
|
1628
|
-
def analyse_regex_not_match_predicate(node, scope)
|
|
1629
|
-
edges = analyse_regex_match_predicate(node, scope, refine: refinable_operand?(node, scope))
|
|
1630
|
-
return nil if edges.nil?
|
|
1631
|
-
|
|
1632
|
-
match, no_match = edges
|
|
1633
|
-
forgotten = scope.forget_match_globals
|
|
1634
|
-
case not_match_receiver_edges(node, scope)
|
|
1635
|
-
when :both then [no_match, match]
|
|
1636
|
-
when :match then [forgotten, match]
|
|
1637
|
-
else [forgotten, forgotten]
|
|
1638
|
-
end
|
|
1639
|
-
end
|
|
1640
|
-
|
|
1641
|
-
# Which edges of `recv !~ pattern` the receiver lets {#analyse_regex_not_match_predicate} narrow. `Kernel#!~`
|
|
1642
|
-
# dispatches `=~` on the receiver:
|
|
1684
|
+
# Which edges of `recv =~ pattern` / `recv !~ pattern` the receiver lets {#gated_regex_match_edges} narrow:
|
|
1685
|
+
# `:both`, `:match` (the match edge only) or nil. `Kernel#!~` dispatches `=~` on the receiver:
|
|
1643
1686
|
#
|
|
1644
1687
|
# - A Regexp receiver (the pattern on the left) runs `Regexp#=~`, which sets `$~` on both outcomes, a `nil`
|
|
1645
1688
|
# argument included: `:both`.
|
|
1646
|
-
# - `String#=~` and `Symbol#=~` do the same: `:both` when every member of the receiver's type is a String
|
|
1647
|
-
# Symbol.
|
|
1689
|
+
# - `String#=~` and `Symbol#=~` do the same: `:both` when every member of the receiver's type is a String
|
|
1690
|
+
# (a project subclass of String included) or a Symbol.
|
|
1648
1691
|
# - `NilClass#=~` answers nil and leaves `$~` alone, so a nil receiver reaches the no-match edge with the
|
|
1649
1692
|
# previous match still bound; it never reaches the match edge. A `Dynamic` receiver may be nil at run time
|
|
1650
|
-
# whatever its static facet says
|
|
1651
|
-
#
|
|
1652
|
-
# - Any other receiver may define `=~` in Ruby, which binds `$~` in its own frame, not the caller's: nil.
|
|
1693
|
+
# whatever its static facet says. So does a safe-navigation call, which answers nil without matching on a
|
|
1694
|
+
# nil receiver.
|
|
1653
1695
|
#
|
|
1654
|
-
#
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
return
|
|
1696
|
+
# The two operators part over a receiver that is none of those ({#match_operator_edges},
|
|
1697
|
+
# {#not_match_operator_edges}).
|
|
1698
|
+
def match_receiver_edges(node, scope)
|
|
1699
|
+
return nil if node.receiver.nil?
|
|
1700
|
+
|
|
1701
|
+
edges =
|
|
1702
|
+
if regex_operand_pattern(node.receiver, scope).is_a?(RegexMatchPattern)
|
|
1703
|
+
:both
|
|
1704
|
+
else
|
|
1705
|
+
kinds = match_member_kinds(scope.type_of(node.receiver), scope)
|
|
1706
|
+
node.name == :=~ ? match_operator_edges(kinds) : not_match_operator_edges(kinds)
|
|
1707
|
+
end
|
|
1708
|
+
return edges unless node.safe_navigation?
|
|
1658
1709
|
|
|
1659
|
-
|
|
1660
|
-
|
|
1710
|
+
:match if edges && node.name == :=~
|
|
1711
|
+
end
|
|
1712
|
+
|
|
1713
|
+
STRING_MATCH_KINDS = %i[string string_subclass symbol].freeze
|
|
1714
|
+
private_constant :STRING_MATCH_KINDS
|
|
1661
1715
|
|
|
1662
|
-
|
|
1716
|
+
# `recv =~ pattern`'s match edge is the truthy one. Only a member known to define its own `=~` in the
|
|
1717
|
+
# project keeps it out (#1494): that method binds `$~` in its own frame, not the caller's. Any other member
|
|
1718
|
+
# either matches in the caller's frame, answers nil (`NilClass`), or has no `=~` at all (`Integer` since
|
|
1719
|
+
# Ruby 3.2 drops `Object#=~`) and so never reaches the edge; a receiver the analyzer cannot classify keeps the
|
|
1720
|
+
# edge, as every `=~` did before #1494.
|
|
1721
|
+
def match_operator_edges(kinds)
|
|
1722
|
+
return nil if kinds.include?(:custom)
|
|
1723
|
+
return :both if kinds.all? { |kind| STRING_MATCH_KINDS.include?(kind) }
|
|
1724
|
+
|
|
1725
|
+
:match
|
|
1726
|
+
end
|
|
1727
|
+
|
|
1728
|
+
# `recv !~ pattern`'s match edge is the falsey one, which a receiver whose `=~` is not the caller's match
|
|
1729
|
+
# also reaches, so it is kept only when every member is a String, a Symbol, nil, or a `Dynamic` that is
|
|
1730
|
+
# untyped or whose facet holds only those (#1379).
|
|
1731
|
+
def not_match_operator_edges(kinds)
|
|
1732
|
+
return :both if kinds.all? { |kind| STRING_MATCH_KINDS.include?(kind) }
|
|
1733
|
+
|
|
1734
|
+
:match if kinds.all? { |kind| (STRING_MATCH_KINDS + %i[nil dynamic]).include?(kind) }
|
|
1663
1735
|
end
|
|
1664
1736
|
|
|
1665
1737
|
# Whether the local operand {#apply_whole_receiver_refinement} would narrow holds only Strings and nil, which
|
|
1666
1738
|
# the match edge excludes. The refinement names a String, so a member that may be a Symbol — a Symbol, or an
|
|
1667
|
-
# untyped `Dynamic` that
|
|
1668
|
-
# `
|
|
1739
|
+
# untyped `Dynamic` that may be one at run time — keeps it out: `return unless raw =~ /\A\d+\z/` on an untyped
|
|
1740
|
+
# `raw` must not make `raw.to_proc` an undefined method (#1379, #1494).
|
|
1669
1741
|
def refinable_operand?(node, scope)
|
|
1670
1742
|
operand = [node.receiver, *node.arguments&.arguments].find { |o| o.is_a?(Prism::LocalVariableReadNode) }
|
|
1671
1743
|
type = operand && scope.local(operand.name)
|
|
1672
1744
|
return true if type.nil?
|
|
1673
1745
|
|
|
1674
|
-
|
|
1746
|
+
match_member_kinds(type, scope).all? { |kind| %i[string nil].include?(kind) }
|
|
1675
1747
|
end
|
|
1676
1748
|
|
|
1677
|
-
def
|
|
1678
|
-
(type.is_a?(Type::Union) ? type.members : [type]).map { |member|
|
|
1749
|
+
def match_member_kinds(type, scope)
|
|
1750
|
+
(type.is_a?(Type::Union) ? type.members : [type]).map { |member| match_member_kind(member, scope) }
|
|
1679
1751
|
end
|
|
1680
1752
|
|
|
1681
|
-
# `:
|
|
1682
|
-
#
|
|
1683
|
-
#
|
|
1684
|
-
#
|
|
1685
|
-
|
|
1686
|
-
|
|
1753
|
+
# `:custom` for a member whose class the project gives a `=~` of its own (in its ancestry, a reopened
|
|
1754
|
+
# `String` included); `:string` or `:symbol` for a member {.narrow_not_class} proves a String or a Symbol (a
|
|
1755
|
+
# value or refinement of either, or a class the environment orders below it); `:string_subclass` for a
|
|
1756
|
+
# project class whose superclass chain reaches `String`, which the environment cannot order; `:nil`;
|
|
1757
|
+
# `:dynamic` for a `Dynamic` whose facet is untyped or holds only Strings, Symbols and nil; or nil for anything
|
|
1758
|
+
# else.
|
|
1759
|
+
def match_member_kind(member, scope)
|
|
1760
|
+
return match_dynamic_kind(member, scope) if member.is_a?(Type::Dynamic)
|
|
1687
1761
|
return :nil if narrow_nil(member) == member && !member.is_a?(Type::Bot)
|
|
1762
|
+
return :custom if custom_match_operator?(member, scope)
|
|
1688
1763
|
|
|
1689
1764
|
environment = scope.environment
|
|
1690
1765
|
{ "String" => :string, "Symbol" => :symbol }.each do |class_name, kind|
|
|
1691
1766
|
return kind if narrow_not_class(member, class_name, environment: environment, scope: scope).is_a?(Type::Bot)
|
|
1692
1767
|
end
|
|
1693
|
-
|
|
1768
|
+
:string_subclass if project_string_subclass?(member, scope)
|
|
1694
1769
|
end
|
|
1695
1770
|
|
|
1696
|
-
def
|
|
1771
|
+
def match_dynamic_kind(member, scope)
|
|
1697
1772
|
facet = member.static_facet
|
|
1698
1773
|
return :dynamic if facet.is_a?(Type::Top)
|
|
1699
1774
|
|
|
1700
|
-
facet_kinds =
|
|
1701
|
-
:dynamic if facet_kinds.all? { |kind|
|
|
1775
|
+
facet_kinds = match_member_kinds(facet, scope)
|
|
1776
|
+
:dynamic if facet_kinds.all? { |kind| (STRING_MATCH_KINDS + [:nil]).include?(kind) }
|
|
1777
|
+
end
|
|
1778
|
+
|
|
1779
|
+
def custom_match_operator?(member, scope)
|
|
1780
|
+
targets = ProjectMethodOwnership.targets(member)
|
|
1781
|
+
return false if targets.nil?
|
|
1782
|
+
|
|
1783
|
+
targets.any? { |class_name, kind| ProjectMethodOwnership.defines?(class_name, :=~, kind, scope) }
|
|
1784
|
+
end
|
|
1785
|
+
|
|
1786
|
+
def project_string_subclass?(member, scope)
|
|
1787
|
+
return false unless member.is_a?(Type::Nominal) && scope.known_user_class?(member.class_name)
|
|
1788
|
+
|
|
1789
|
+
scope.external_ancestor_name_candidates(member.class_name, mixins: false)
|
|
1790
|
+
.flatten.any? { |name| name.to_s.delete_prefix("::") == "String" }
|
|
1702
1791
|
end
|
|
1703
1792
|
|
|
1704
1793
|
# `str.match?(/\A\d+\z/)` — `String#match?` is a pure boolean predicate whose truthy edge
|
|
@@ -2626,6 +2715,21 @@ module Rigor
|
|
|
2626
2715
|
]
|
|
2627
2716
|
end
|
|
2628
2717
|
|
|
2718
|
+
# The class a `when` condition narrows the subject to, as `is_a?` would, or nil when the condition is no class
|
|
2719
|
+
# test Rigor reads: a class or module constant, a range or regexp literal ({.case_equality_target_class}),
|
|
2720
|
+
# or (#1925) a `nil` / `true` / `false` literal. `nil === x` runs `NilClass#===`, which holds only for `nil`
|
|
2721
|
+
# itself whatever the subject's class defines, and `nil` is `NilClass`'s only instance, so `when nil` is
|
|
2722
|
+
# `when NilClass`; `true` and `false` likewise.
|
|
2723
|
+
def when_condition_class(condition, scope)
|
|
2724
|
+
lexical_class_name(condition, scope) || case_equality_target_class(condition) ||
|
|
2725
|
+
SINGLETON_WHEN_LITERAL_CLASSES[unwrap_parens(condition).class]
|
|
2726
|
+
end
|
|
2727
|
+
|
|
2728
|
+
SINGLETON_WHEN_LITERAL_CLASSES = {
|
|
2729
|
+
Prism::NilNode => "NilClass", Prism::TrueNode => "TrueClass", Prism::FalseNode => "FalseClass"
|
|
2730
|
+
}.freeze
|
|
2731
|
+
private_constant :SINGLETON_WHEN_LITERAL_CLASSES
|
|
2732
|
+
|
|
2629
2733
|
# Maps a case-equality literal receiver to the class whose membership is implied by the
|
|
2630
2734
|
# truthy edge. `Prism::ParenthesesNode` wrappers are transparently unwrapped (`(1..10)
|
|
2631
2735
|
# === x` is parsed with the range inside parentheses). Range literals: integer
|
|
@@ -2818,15 +2922,15 @@ module Rigor
|
|
|
2818
2922
|
# narrowed type for the subject as if `condition === subject`, and accumulates them.
|
|
2819
2923
|
# The body's narrowed type is the union across all conditions; the falsey type is the
|
|
2820
2924
|
# running result after subtracting every condition's class. Conditions whose shape we
|
|
2821
|
-
# cannot statically classify
|
|
2822
|
-
#
|
|
2823
|
-
# edge is the entry type (because we cannot prove the unknown condition didn't match).
|
|
2925
|
+
# cannot statically classify narrow neither edge: the body reads as
|
|
2926
|
+
# {.case_when_body_type} says, and the falsey edge is the entry type.
|
|
2824
2927
|
#
|
|
2825
2928
|
# `[body type, next-clause type]` of a `when` clause's conditions over the subject type `current`.
|
|
2826
2929
|
def case_when_types(scope, current, conditions)
|
|
2827
2930
|
truthy_members = []
|
|
2828
2931
|
falsey_type = current
|
|
2829
2932
|
fully_narrowable = true
|
|
2933
|
+
unread = false
|
|
2830
2934
|
|
|
2831
2935
|
conditions.each do |condition|
|
|
2832
2936
|
applied = apply_case_when_condition(scope, current, condition, falsey_type)
|
|
@@ -2835,12 +2939,24 @@ module Rigor
|
|
|
2835
2939
|
falsey_type = applied[:falsey]
|
|
2836
2940
|
fully_narrowable &&= applied[:fully_narrowable]
|
|
2837
2941
|
else
|
|
2942
|
+
unread = true
|
|
2838
2943
|
fully_narrowable = false
|
|
2839
2944
|
end
|
|
2840
2945
|
end
|
|
2841
2946
|
|
|
2842
|
-
|
|
2843
|
-
|
|
2947
|
+
[case_when_body_type(current, truthy_members, unread), fully_narrowable ? falsey_type : current]
|
|
2948
|
+
end
|
|
2949
|
+
|
|
2950
|
+
# #1925 — the body type of a `when` clause. A clause whose conditions are all unread keeps the entry type. A
|
|
2951
|
+
# clause mixing read conditions with an unread one reads `Dynamic[top]`: the body may run because the unread
|
|
2952
|
+
# condition matched, so the read conditions' union is unsound (`when nil, ""` is not `nil`), and the entry
|
|
2953
|
+
# type is wider than what reaches the body when the unread condition is itself a test
|
|
2954
|
+
# (`when /a/, ->(v) { !v.nil? }` never enters with `nil`), so reading it would report a call no `nil` reaches.
|
|
2955
|
+
def case_when_body_type(current, truthy_members, unread)
|
|
2956
|
+
return current if truthy_members.empty?
|
|
2957
|
+
return Type::Combinator.untyped if unread
|
|
2958
|
+
|
|
2959
|
+
Type::Combinator.union(*truthy_members)
|
|
2844
2960
|
end
|
|
2845
2961
|
|
|
2846
2962
|
# Per-condition rule. Returns `nil` when the condition shape is not recognised (caller
|
|
@@ -2855,7 +2971,7 @@ module Rigor
|
|
|
2855
2971
|
return integer_literal_when_result(current, int_literal, falsey_acc)
|
|
2856
2972
|
end
|
|
2857
2973
|
|
|
2858
|
-
target =
|
|
2974
|
+
target = when_condition_class(condition, scope)
|
|
2859
2975
|
return class_when_result(scope, current, target, falsey_acc) if target
|
|
2860
2976
|
|
|
2861
2977
|
nil
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../source/node_children"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Inference
|
|
9
|
+
module ProjectPatchedScanner
|
|
10
|
+
# Finds the `def`s of a `pre_eval:` file written under a definedness guard naming the same method
|
|
11
|
+
# (`def foo = 1 unless method_defined?(:foo)`): polyfills that run only when nothing bound the name before.
|
|
12
|
+
module DefinednessGuard
|
|
13
|
+
DEFINEDNESS_PREDICATES = %i[
|
|
14
|
+
method_defined? private_method_defined? public_method_defined? protected_method_defined? respond_to?
|
|
15
|
+
].freeze
|
|
16
|
+
INSTANCE_METHOD_LISTS = %i[
|
|
17
|
+
instance_methods public_instance_methods private_instance_methods protected_instance_methods
|
|
18
|
+
].freeze
|
|
19
|
+
private_constant :DEFINEDNESS_PREDICATES, :INSTANCE_METHOD_LISTS
|
|
20
|
+
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
# The `def` nodes (by identity) that sit on the "name not yet defined" side of a definedness guard naming the
|
|
24
|
+
# same method: `def foo = 1 unless method_defined?(:foo)`, or the same inside an `unless` / `if !` block.
|
|
25
|
+
def defs(root)
|
|
26
|
+
found = {}.compare_by_identity
|
|
27
|
+
collect_guarded(root, found)
|
|
28
|
+
found
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def collect_guarded(node, found)
|
|
32
|
+
return unless node.is_a?(Prism::Node)
|
|
33
|
+
|
|
34
|
+
if node.is_a?(Prism::IfNode) || node.is_a?(Prism::UnlessNode)
|
|
35
|
+
name, negated = definedness_guard(node.predicate)
|
|
36
|
+
mark_guarded_defs(undefined_branch(node, negated), name, found) if name
|
|
37
|
+
end
|
|
38
|
+
node.rigor_each_child { |child| collect_guarded(child, found) }
|
|
39
|
+
end
|
|
40
|
+
private_class_method :collect_guarded
|
|
41
|
+
|
|
42
|
+
# `[method_name, negated]` for a predicate like `method_defined?(:m)` / `!respond_to?(:m)`, else nil.
|
|
43
|
+
def definedness_guard(predicate)
|
|
44
|
+
predicate = predicate.body if predicate.is_a?(Prism::ParenthesesNode) && predicate.body
|
|
45
|
+
predicate = predicate.body.first if predicate.is_a?(Prism::StatementsNode) && predicate.body.size == 1
|
|
46
|
+
if predicate.is_a?(Prism::CallNode) && predicate.name == :! && predicate.receiver
|
|
47
|
+
name, negated = definedness_guard(predicate.receiver)
|
|
48
|
+
return name && [name, !negated]
|
|
49
|
+
end
|
|
50
|
+
name = guard_method_name(predicate)
|
|
51
|
+
name && [name, false]
|
|
52
|
+
end
|
|
53
|
+
private_class_method :definedness_guard
|
|
54
|
+
|
|
55
|
+
def guard_method_name(call)
|
|
56
|
+
return unless call.is_a?(Prism::CallNode)
|
|
57
|
+
|
|
58
|
+
listed = %i[include? member?].include?(call.name)
|
|
59
|
+
listed &&= call.receiver.is_a?(Prism::CallNode) && INSTANCE_METHOD_LISTS.include?(call.receiver.name)
|
|
60
|
+
return unless listed || DEFINEDNESS_PREDICATES.include?(call.name)
|
|
61
|
+
|
|
62
|
+
arg = call.arguments&.arguments&.first
|
|
63
|
+
arg.unescaped.to_sym if arg.is_a?(Prism::SymbolNode) || arg.is_a?(Prism::StringNode)
|
|
64
|
+
end
|
|
65
|
+
private_class_method :guard_method_name
|
|
66
|
+
|
|
67
|
+
# The branch that runs when the guarded name is NOT defined yet.
|
|
68
|
+
def undefined_branch(node, negated)
|
|
69
|
+
branch_when_true = node.is_a?(Prism::IfNode) ? node.statements : node.else_clause
|
|
70
|
+
branch_when_false = node.is_a?(Prism::IfNode) ? node.subsequent : node.statements
|
|
71
|
+
# The predicate is true when the name is defined (unless negated).
|
|
72
|
+
negated ? branch_when_true : branch_when_false
|
|
73
|
+
end
|
|
74
|
+
private_class_method :undefined_branch
|
|
75
|
+
|
|
76
|
+
def mark_guarded_defs(node, name, found)
|
|
77
|
+
return unless node.is_a?(Prism::Node)
|
|
78
|
+
|
|
79
|
+
found[node] = true if node.is_a?(Prism::DefNode) && node.name == name
|
|
80
|
+
node.rigor_each_child { |child| mark_guarded_defs(child, name, found) } unless node.is_a?(Prism::DefNode)
|
|
81
|
+
end
|
|
82
|
+
private_class_method :mark_guarded_defs
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -36,14 +36,14 @@ module Rigor
|
|
|
36
36
|
attr_reader :by_key, :write_census
|
|
37
37
|
|
|
38
38
|
# @param entries — flat list of declarations observed during the pre-pass.
|
|
39
|
-
#
|
|
39
|
+
# Last-write-wins on `(class_name, method_name, kind)` duplicates, as at runtime (ADR-17); the
|
|
40
40
|
# `pre-eval.duplicate-declaration` diagnostic emission stays decoupled from registry behaviour.
|
|
41
41
|
# @param write_census — issue #1367: the `pre_eval:` files' {GlobalWriteCensus}, which the `global.*` write
|
|
42
42
|
# rules join with the project's, since a patch file is loaded ahead of the code it patches.
|
|
43
43
|
def initialize(entries: [], write_census: GlobalWriteCensus::EMPTY)
|
|
44
44
|
@by_key = entries.each_with_object({}) do |entry, acc|
|
|
45
45
|
key = [entry.class_name, entry.method_name, entry.kind]
|
|
46
|
-
acc[key]
|
|
46
|
+
acc[key] = entry
|
|
47
47
|
end.freeze
|
|
48
48
|
@write_census = write_census.frozen? ? write_census : write_census.dup.freeze
|
|
49
49
|
freeze
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "project_patched_methods"
|
|
6
|
+
require_relative "project_patched_definedness_guard"
|
|
6
7
|
require_relative "../analysis/dependency_source_inference/return_type_heuristic"
|
|
7
8
|
require_relative "../source/alias_names"
|
|
8
9
|
require_relative "../source/constant_path"
|
|
@@ -35,6 +36,13 @@ module Rigor
|
|
|
35
36
|
PendingAlias = Data.define(:class_name, :new_name, :old_name, :kind, :source_path, :source_line)
|
|
36
37
|
private_constant :PendingAlias
|
|
37
38
|
|
|
39
|
+
# A `def` written under a definedness guard (`def foo ... unless method_defined?(:foo)`): a polyfill that runs
|
|
40
|
+
# only when nothing bound the name before it, so it never replaces an earlier binding.
|
|
41
|
+
GuardedDef = Data.define(:entry) do
|
|
42
|
+
def source_path = entry.source_path
|
|
43
|
+
end
|
|
44
|
+
private_constant :GuardedDef
|
|
45
|
+
|
|
38
46
|
module_function
|
|
39
47
|
|
|
40
48
|
# @param paths — absolute paths to the pre-eval files. The runner has already
|
|
@@ -58,29 +66,28 @@ module Rigor
|
|
|
58
66
|
end
|
|
59
67
|
|
|
60
68
|
# ADR-17 § "Failure modes" — when two pre-eval entries declare the same `(class_name, method_name,
|
|
61
|
-
# kind)` triple,
|
|
62
|
-
#
|
|
63
|
-
#
|
|
69
|
+
# kind)` triple, the last-listed one wins, as at runtime where the later `def` replaces the earlier. Emit
|
|
70
|
+
# one `:info` `pre-eval.duplicate-declaration` diagnostic per SHADOWED entry, reported on that earlier
|
|
71
|
+
# declaration (the declaration that never takes effect) and naming the
|
|
72
|
+
# winning declaration it yields to.
|
|
64
73
|
def duplicate_declaration_diagnostics(entries)
|
|
65
|
-
|
|
66
|
-
entries.
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
seen[key] = entry
|
|
83
|
-
end
|
|
74
|
+
winners = entries.to_h { |entry| [[entry.class_name, entry.method_name, entry.kind], entry] }
|
|
75
|
+
entries.filter_map do |entry|
|
|
76
|
+
winner = winners[[entry.class_name, entry.method_name, entry.kind]]
|
|
77
|
+
next if winner.equal?(entry)
|
|
78
|
+
|
|
79
|
+
build_diagnostic(
|
|
80
|
+
path: entry.source_path,
|
|
81
|
+
line: entry.source_line,
|
|
82
|
+
column: 1,
|
|
83
|
+
severity: :info,
|
|
84
|
+
rule: "pre-eval.duplicate-declaration",
|
|
85
|
+
message: "pre-eval duplicate declaration: " \
|
|
86
|
+
"#{entry.class_name}##{entry.method_name} " \
|
|
87
|
+
"(#{entry.kind}) is declared again at " \
|
|
88
|
+
"#{winner.source_path}:#{winner.source_line}. " \
|
|
89
|
+
"The last-listed declaration wins; this entry is shadowed."
|
|
90
|
+
)
|
|
84
91
|
end
|
|
85
92
|
end
|
|
86
93
|
private_class_method :duplicate_declaration_diagnostics
|
|
@@ -97,14 +104,17 @@ module Rigor
|
|
|
97
104
|
# Within one file the later binding of a name replaces the earlier, as at runtime: the alias-method-chain
|
|
98
105
|
# idiom (`def foo`, `alias_method :foo_without_x, :foo`, `alias_method :foo, :foo_with_x`) leaves `foo` the
|
|
99
106
|
# alias. Bindings of one name in DIFFERENT files are what `pre-eval.duplicate-declaration` reports, and the
|
|
100
|
-
# registry keeps the
|
|
107
|
+
# registry keeps the last of those, as Ruby does.
|
|
101
108
|
def resolve_aliases(collected)
|
|
102
109
|
bound = {}
|
|
103
110
|
per_file = collected.group_by(&:source_path).values.map do |items|
|
|
104
111
|
final = {}
|
|
105
112
|
items.each do |item|
|
|
106
113
|
entry = item.is_a?(PendingAlias) ? alias_entry(item, bound) : item
|
|
114
|
+
entry = entry.entry if entry.is_a?(GuardedDef)
|
|
107
115
|
key = [entry.class_name, entry.method_name, entry.kind]
|
|
116
|
+
next if item.is_a?(GuardedDef) && bound.key?(key)
|
|
117
|
+
|
|
108
118
|
bound[key] = entry
|
|
109
119
|
final.delete(key)
|
|
110
120
|
final[key] = entry
|
|
@@ -138,6 +148,7 @@ module Rigor
|
|
|
138
148
|
return
|
|
139
149
|
end
|
|
140
150
|
|
|
151
|
+
Thread.current[:rigor_guarded_defs] = DefinednessGuard.defs(parse_result.value)
|
|
141
152
|
walk_node(parse_result.value, [], false, path, entries)
|
|
142
153
|
# Issue #1367 — the file's `global.*` write facts: a patch file may alias a special or give a class the
|
|
143
154
|
# method a setter asks for, as any project file may.
|
|
@@ -151,6 +162,8 @@ module Rigor
|
|
|
151
162
|
"#{e.class}: #{e.message}. Pre-evaluation skipped for this file; " \
|
|
152
163
|
"the rest of the run proceeds."
|
|
153
164
|
)
|
|
165
|
+
ensure
|
|
166
|
+
Thread.current[:rigor_guarded_defs] = nil
|
|
154
167
|
end
|
|
155
168
|
private_class_method :scan_file
|
|
156
169
|
|
|
@@ -233,11 +246,12 @@ module Rigor
|
|
|
233
246
|
kind = node.receiver.is_a?(Prism::SelfNode) || in_singleton_class ? :singleton : :instance
|
|
234
247
|
line = node.location&.start_line || 1
|
|
235
248
|
return_type = Analysis::DependencySourceInference::ReturnTypeHeuristic.extract(node)
|
|
236
|
-
|
|
249
|
+
entry = ProjectPatchedMethods::Entry.new(
|
|
237
250
|
class_name: class_name, method_name: node.name, kind: kind,
|
|
238
251
|
source_path: source_path, source_line: line,
|
|
239
252
|
return_type: return_type
|
|
240
253
|
)
|
|
254
|
+
entries << (Thread.current[:rigor_guarded_defs]&.key?(node) ? GuardedDef.new(entry: entry) : entry)
|
|
241
255
|
end
|
|
242
256
|
private_class_method :record_def_node
|
|
243
257
|
|