rigortype 0.1.16 → 0.1.18
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 -2
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +18 -1
- data/lib/rigor/analysis/check_rules/rule_walk.rb +67 -0
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +100 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +226 -0
- data/lib/rigor/analysis/check_rules.rb +180 -73
- data/lib/rigor/analysis/dependency_recorder.rb +122 -0
- data/lib/rigor/analysis/diagnostic.rb +18 -0
- data/lib/rigor/analysis/incremental.rb +162 -0
- data/lib/rigor/analysis/incremental_session.rb +337 -0
- data/lib/rigor/analysis/rule_catalog.rb +48 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +580 -0
- data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +318 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
- data/lib/rigor/analysis/runner.rb +477 -1110
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +121 -0
- data/lib/rigor/analysis/worker_session.rb +47 -8
- data/lib/rigor/builtins/static_return_refinements.rb +7 -1
- data/lib/rigor/cache/descriptor.rb +50 -49
- data/lib/rigor/cache/incremental_snapshot.rb +153 -0
- data/lib/rigor/cache/rbs_cache_producer.rb +34 -0
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +2 -8
- data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -8
- data/lib/rigor/cache/rbs_constant_table.rb +2 -8
- data/lib/rigor/cache/rbs_environment.rb +2 -8
- data/lib/rigor/cache/rbs_known_class_names.rb +2 -8
- data/lib/rigor/cache/store.rb +145 -14
- data/lib/rigor/cli/annotate_command.rb +2 -7
- data/lib/rigor/cli/baseline_command.rb +2 -7
- data/lib/rigor/cli/check_command.rb +705 -0
- data/lib/rigor/cli/ci_detector.rb +94 -0
- data/lib/rigor/cli/command.rb +47 -0
- data/lib/rigor/cli/coverage_command.rb +3 -23
- data/lib/rigor/cli/coverage_renderer.rb +3 -8
- data/lib/rigor/cli/diagnostic_formats.rb +345 -0
- data/lib/rigor/cli/diff_command.rb +3 -7
- data/lib/rigor/cli/explain_command.rb +2 -7
- data/lib/rigor/cli/lsp_command.rb +3 -7
- data/lib/rigor/cli/mcp_command.rb +3 -7
- data/lib/rigor/cli/options.rb +57 -0
- data/lib/rigor/cli/plugin_command.rb +3 -7
- data/lib/rigor/cli/plugins_command.rb +2 -7
- data/lib/rigor/cli/prism_colorizer.rb +10 -3
- data/lib/rigor/cli/renderable.rb +26 -0
- data/lib/rigor/cli/sig_gen_command.rb +2 -7
- data/lib/rigor/cli/skill_command.rb +3 -7
- data/lib/rigor/cli/trace_command.rb +143 -0
- data/lib/rigor/cli/trace_renderer.rb +310 -0
- data/lib/rigor/cli/triage_command.rb +2 -7
- data/lib/rigor/cli/type_of_command.rb +5 -38
- data/lib/rigor/cli/type_of_renderer.rb +4 -9
- data/lib/rigor/cli/type_scan_command.rb +3 -23
- data/lib/rigor/cli/type_scan_renderer.rb +4 -9
- data/lib/rigor/cli.rb +15 -532
- data/lib/rigor/configuration/dependencies.rb +18 -1
- data/lib/rigor/configuration/severity_profile.rb +22 -3
- data/lib/rigor/configuration.rb +16 -3
- data/lib/rigor/environment/rbs_loader.rb +129 -71
- data/lib/rigor/environment.rb +1 -1
- data/lib/rigor/inference/acceptance.rb +10 -0
- data/lib/rigor/inference/block_parameter_binder.rb +1 -2
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/complex_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/date_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/encoding_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/exception_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/hash_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/method_catalog.rb +15 -0
- data/lib/rigor/inference/builtins/numeric_catalog.rb +21 -93
- data/lib/rigor/inference/builtins/pathname_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/proc_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/random_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/range_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/rational_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/re_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/set_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/string_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/struct_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/time_catalog.rb +2 -5
- data/lib/rigor/inference/expression_typer.rb +149 -63
- data/lib/rigor/inference/flow_tracer.rb +180 -0
- data/lib/rigor/inference/macro_block_self_type.rb +10 -11
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +5 -1
- data/lib/rigor/inference/method_dispatcher/call_context.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +11 -10
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +12 -6
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +246 -0
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -1
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +4 -1
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +12 -7
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +23 -13
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +9 -9
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +120 -9
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +12 -12
- data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +49 -0
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -9
- data/lib/rigor/inference/method_dispatcher.rb +185 -84
- data/lib/rigor/inference/narrowing.rb +262 -5
- data/lib/rigor/inference/scope_indexer.rb +208 -21
- data/lib/rigor/inference/statement_evaluator.rb +110 -48
- data/lib/rigor/language_server/buffer_resolution.rb +33 -0
- data/lib/rigor/language_server/completion_provider.rb +4 -4
- data/lib/rigor/language_server/document_symbol_provider.rb +4 -4
- data/lib/rigor/language_server/folding_range_provider.rb +4 -4
- data/lib/rigor/language_server/hover_provider.rb +4 -4
- data/lib/rigor/language_server/selection_range_provider.rb +4 -4
- data/lib/rigor/language_server/signature_help_provider.rb +4 -4
- data/lib/rigor/plugin/additional_initializer.rb +61 -38
- data/lib/rigor/plugin/base.rb +302 -45
- data/lib/rigor/plugin/node_rule_walk.rb +147 -0
- data/lib/rigor/plugin/registry.rb +281 -15
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/conformance_checker.rb +293 -0
- data/lib/rigor/rbs_extended.rb +39 -0
- data/lib/rigor/scope/discovery_index.rb +58 -0
- data/lib/rigor/scope.rb +150 -167
- data/lib/rigor/sig_gen/observation_collector.rb +6 -6
- data/lib/rigor/source/literals.rb +14 -0
- data/lib/rigor/type/acceptance_router.rb +19 -0
- data/lib/rigor/type/accepts_result.rb +3 -10
- data/lib/rigor/type/app.rb +3 -7
- data/lib/rigor/type/bot.rb +2 -3
- data/lib/rigor/type/bound_method.rb +5 -12
- data/lib/rigor/type/combinator.rb +22 -0
- data/lib/rigor/type/constant.rb +2 -3
- data/lib/rigor/type/data_class.rb +80 -0
- data/lib/rigor/type/data_instance.rb +100 -0
- data/lib/rigor/type/difference.rb +5 -10
- data/lib/rigor/type/dynamic.rb +5 -10
- data/lib/rigor/type/hash_shape.rb +5 -15
- data/lib/rigor/type/integer_range.rb +5 -10
- data/lib/rigor/type/intersection.rb +5 -10
- data/lib/rigor/type/nominal.rb +5 -10
- data/lib/rigor/type/refined.rb +5 -10
- data/lib/rigor/type/singleton.rb +5 -10
- data/lib/rigor/type/top.rb +2 -3
- data/lib/rigor/type/tuple.rb +5 -10
- data/lib/rigor/type/union.rb +5 -10
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/value_semantics.rb +77 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +70 -32
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +15 -21
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +12 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +35 -18
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +83 -36
- data/sig/rigor/cache.rbs +19 -0
- data/sig/rigor/environment.rbs +0 -2
- data/sig/rigor/inference.rbs +27 -0
- data/sig/rigor/plugin/base.rbs +1 -2
- data/sig/rigor/rbs_extended.rbs +2 -0
- data/sig/rigor/scope.rbs +42 -25
- data/sig/rigor/source.rbs +1 -0
- data/sig/rigor/type.rbs +58 -1
- data/sig/rigor.rbs +6 -1
- data/skills/rigor-ci-setup/SKILL.md +319 -0
- metadata +36 -2
- data/lib/rigor/cache/rbs_instance_definitions.rb +0 -79
|
@@ -123,6 +123,66 @@ module Rigor
|
|
|
123
123
|
end
|
|
124
124
|
end
|
|
125
125
|
|
|
126
|
+
# Three-valued truthiness certainty of a predicate's type,
|
|
127
|
+
# derived from the truthy / falsey fragments above: `:truthy`
|
|
128
|
+
# when no inhabitant is falsey (the falsey fragment is `Bot`),
|
|
129
|
+
# `:falsey` when no inhabitant is truthy, nil when both
|
|
130
|
+
# fragments are inhabited — or when the type itself is nil /
|
|
131
|
+
# `Bot` (dead code is not a certainty claim). This is the single
|
|
132
|
+
# owner of the judgment both branch-elision consumers read
|
|
133
|
+
# (`ExpressionTyper#elide_or_union` on the value side,
|
|
134
|
+
# `StatementEvaluator#live_branch_for_if` on the scope side), so
|
|
135
|
+
# the type a dead branch is elided from and the scope that stops
|
|
136
|
+
# flowing through it can never disagree.
|
|
137
|
+
def predicate_certainty(type)
|
|
138
|
+
return nil if type.nil? || type.is_a?(Type::Bot)
|
|
139
|
+
|
|
140
|
+
truthy_bot = narrow_truthy(type).is_a?(Type::Bot)
|
|
141
|
+
falsey_bot = narrow_falsey(type).is_a?(Type::Bot)
|
|
142
|
+
|
|
143
|
+
return :falsey if truthy_bot && !falsey_bot
|
|
144
|
+
return :truthy if !truthy_bot && falsey_bot
|
|
145
|
+
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Three-valued certainty of `C === subject` for a class / module
|
|
150
|
+
# `when` pattern, derived from {.narrow_class} /
|
|
151
|
+
# {.narrow_not_class}: `:no` when no inhabitant of the subject
|
|
152
|
+
# matches, `:yes` when every inhabitant matches, `:maybe`
|
|
153
|
+
# otherwise. The value-side counterpart of the scope narrowing
|
|
154
|
+
# {.case_when_scopes} performs for the same condition shape, kept
|
|
155
|
+
# here so the branch a `case` expression's type drops and the
|
|
156
|
+
# clause whose body scope goes dead derive from one judgment.
|
|
157
|
+
def class_pattern_certainty(subject_type, class_name, environment:)
|
|
158
|
+
truthy_bot = narrow_class(subject_type, class_name, environment: environment).is_a?(Type::Bot)
|
|
159
|
+
falsey_bot = narrow_not_class(subject_type, class_name, environment: environment).is_a?(Type::Bot)
|
|
160
|
+
|
|
161
|
+
return :no if truthy_bot && !falsey_bot
|
|
162
|
+
return :yes if !truthy_bot && falsey_bot
|
|
163
|
+
|
|
164
|
+
:maybe
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Classes whose `===` is plain value equality, so a literal
|
|
168
|
+
# `when` pattern against a pinned `Constant` subject is exact in
|
|
169
|
+
# both directions. Anything else keeps custom-`===` semantics
|
|
170
|
+
# and stays `:maybe` in {.value_pattern_certainty}.
|
|
171
|
+
VALUE_EQUALITY_CLASSES = [Integer, Float, Rational, Complex, String, Symbol,
|
|
172
|
+
TrueClass, FalseClass, NilClass].freeze
|
|
173
|
+
|
|
174
|
+
# Three-valued certainty of `<literal> === subject` for a
|
|
175
|
+
# value-equality literal pattern: exact (`:yes` / `:no`) only
|
|
176
|
+
# when the subject is itself a pinned `Constant` of a
|
|
177
|
+
# value-equality class; `:maybe` otherwise (the runtime value
|
|
178
|
+
# isn't pinned, or `===` may be user-defined).
|
|
179
|
+
def value_pattern_certainty(subject_type, pattern_value)
|
|
180
|
+
return :maybe unless subject_type.is_a?(Type::Constant)
|
|
181
|
+
return :maybe unless VALUE_EQUALITY_CLASSES.any? { |klass| subject_type.value.is_a?(klass) }
|
|
182
|
+
|
|
183
|
+
pattern_value == subject_type.value ? :yes : :no
|
|
184
|
+
end
|
|
185
|
+
|
|
126
186
|
# Equality fragment of `type` against a trusted literal.
|
|
127
187
|
#
|
|
128
188
|
# String/Symbol/Integer equality narrows only when the current
|
|
@@ -960,7 +1020,7 @@ module Rigor
|
|
|
960
1020
|
end
|
|
961
1021
|
|
|
962
1022
|
def simple_dispatch_name?(name)
|
|
963
|
-
%i[nil? ! is_a? kind_of? instance_of? == != === =~].include?(name)
|
|
1023
|
+
%i[nil? ! is_a? kind_of? instance_of? == != === =~ key? has_key? empty? any? none?].include?(name)
|
|
964
1024
|
end
|
|
965
1025
|
|
|
966
1026
|
def dispatch_call_simple(node, scope, name)
|
|
@@ -971,9 +1031,177 @@ module Rigor
|
|
|
971
1031
|
when :==, :!= then analyse_equality_predicate(node, scope, equality: name)
|
|
972
1032
|
when :=== then analyse_case_equality_predicate(node, scope)
|
|
973
1033
|
when :=~ then analyse_regex_match_predicate(node, scope)
|
|
1034
|
+
when :key?, :has_key? then analyse_key_presence_predicate(node, scope)
|
|
1035
|
+
when :empty?, :any?, :none? then analyse_array_emptiness_predicate(node, scope, name)
|
|
1036
|
+
end
|
|
1037
|
+
end
|
|
1038
|
+
|
|
1039
|
+
# ADR-47 §4-4 (Elixir `tuple_size`/non-empty analogue) — a bare
|
|
1040
|
+
# `arr.empty?` / `arr.any?` / `arr.none?` (no block, no args)
|
|
1041
|
+
# narrows an Array-typed receiver to `non-empty-array[T]` on the
|
|
1042
|
+
# edge that implies "at least one element":
|
|
1043
|
+
#
|
|
1044
|
+
# - `empty?` → false edge (the array is NOT empty)
|
|
1045
|
+
# - `any?` → true edge (a truthy element exists ⇒ non-empty)
|
|
1046
|
+
# - `none?` → false edge (a truthy element exists ⇒ non-empty)
|
|
1047
|
+
#
|
|
1048
|
+
# The opposite edge is the conservative no-op (`any?`/`none?`
|
|
1049
|
+
# falseness does not imply emptiness — the array may hold only
|
|
1050
|
+
# falsey elements; `empty?` truth could narrow to an empty array
|
|
1051
|
+
# but that carrier move is deferred). Only `Nominal[Array, [T]]`
|
|
1052
|
+
# receivers narrow — `Tuple` is already known-length, `Dynamic`
|
|
1053
|
+
# is left alone (gradual guarantee), and a non-Array receiver
|
|
1054
|
+
# (`String#empty?`, `Range#any?`, …) bails so the existing string
|
|
1055
|
+
# / predicate paths still run.
|
|
1056
|
+
def analyse_array_emptiness_predicate(node, scope, name)
|
|
1057
|
+
return nil if node.block
|
|
1058
|
+
return nil unless node.arguments.nil? || node.arguments.arguments.empty?
|
|
1059
|
+
|
|
1060
|
+
reader, writer = emptiness_receiver_accessors(node.receiver)
|
|
1061
|
+
return nil if reader.nil?
|
|
1062
|
+
|
|
1063
|
+
current = scope.public_send(reader, node.receiver.name)
|
|
1064
|
+
return nil if current.nil?
|
|
1065
|
+
|
|
1066
|
+
non_empty = narrow_to_non_empty_array(current)
|
|
1067
|
+
return nil if non_empty.equal?(current) # receiver is not an Array shape → opaque
|
|
1068
|
+
|
|
1069
|
+
non_empty_scope = scope.public_send(writer, node.receiver.name, non_empty)
|
|
1070
|
+
name == :any? ? [non_empty_scope, scope] : [scope, non_empty_scope]
|
|
1071
|
+
end
|
|
1072
|
+
|
|
1073
|
+
def emptiness_receiver_accessors(receiver)
|
|
1074
|
+
case receiver
|
|
1075
|
+
when Prism::LocalVariableReadNode then %i[local with_local]
|
|
1076
|
+
when Prism::InstanceVariableReadNode then %i[ivar with_ivar]
|
|
1077
|
+
else [nil, nil]
|
|
974
1078
|
end
|
|
975
1079
|
end
|
|
976
1080
|
|
|
1081
|
+
# Refines `Array[T]` (and every Array member of a Union) to
|
|
1082
|
+
# `non-empty-array[T]`; returns the input unchanged when nothing
|
|
1083
|
+
# applies, so the caller can detect "no narrowing".
|
|
1084
|
+
def narrow_to_non_empty_array(type)
|
|
1085
|
+
case type
|
|
1086
|
+
when Type::Nominal
|
|
1087
|
+
return type unless type.class_name == "Array"
|
|
1088
|
+
|
|
1089
|
+
Type::Combinator.non_empty_array(type.type_args.first || Type::Combinator.top)
|
|
1090
|
+
when Type::Union
|
|
1091
|
+
Type::Combinator.union(*type.members.map { |member| narrow_to_non_empty_array(member) })
|
|
1092
|
+
else
|
|
1093
|
+
type
|
|
1094
|
+
end
|
|
1095
|
+
end
|
|
1096
|
+
|
|
1097
|
+
# ADR-47 §4-3 (Elixir `is_map_key/2` analogue) — `h.key?(:foo)`
|
|
1098
|
+
# narrows the receiver on the truthy edge: a `HashShape` whose
|
|
1099
|
+
# `:foo` is an OPTIONAL key has it promoted to REQUIRED, so a
|
|
1100
|
+
# subsequent `h[:foo]` reads the declared value type instead of
|
|
1101
|
+
# `value | nil` (the optionality nil is gone). Sound because key
|
|
1102
|
+
# presence removes only the optionality-injected nil, never the
|
|
1103
|
+
# value's own intrinsic nil (`h = {foo: nil}` keeps `h[:foo]`
|
|
1104
|
+
# nil-typed). The falsey edge is left unchanged — "key absent"
|
|
1105
|
+
# is the conservative no-op. Only literal `Symbol`/`String`
|
|
1106
|
+
# arguments and `LocalVariableReadNode`/`InstanceVariableReadNode`
|
|
1107
|
+
# receivers narrow; everything else (Dynamic, `Nominal[Hash]`,
|
|
1108
|
+
# method-chain receivers, dynamic keys) bails to no narrowing.
|
|
1109
|
+
def analyse_key_presence_predicate(node, scope)
|
|
1110
|
+
return nil if node.arguments.nil?
|
|
1111
|
+
return nil unless node.arguments.arguments.size == 1
|
|
1112
|
+
|
|
1113
|
+
key = static_hash_key(node.arguments.arguments.first)
|
|
1114
|
+
return nil if key.nil?
|
|
1115
|
+
|
|
1116
|
+
case node.receiver
|
|
1117
|
+
when Prism::LocalVariableReadNode
|
|
1118
|
+
key_presence_scopes(node.receiver.name, key, scope, reader: :local, writer: :with_local)
|
|
1119
|
+
when Prism::InstanceVariableReadNode
|
|
1120
|
+
key_presence_scopes(node.receiver.name, key, scope, reader: :ivar, writer: :with_ivar)
|
|
1121
|
+
end
|
|
1122
|
+
end
|
|
1123
|
+
|
|
1124
|
+
def key_presence_scopes(name, key, scope, reader:, writer:)
|
|
1125
|
+
current = scope.public_send(reader, name)
|
|
1126
|
+
return nil if current.nil?
|
|
1127
|
+
|
|
1128
|
+
truthy = narrow_hash_key_present(current, key)
|
|
1129
|
+
# §4-3 false edge — `h.key?(:foo)` being false proves `:foo` is
|
|
1130
|
+
# absent, so on that edge `h[:foo]` reads `nil`. Remove the
|
|
1131
|
+
# (optional) key from the shape; a required key makes the false
|
|
1132
|
+
# edge dead and an unknown key is already nil, both no-ops.
|
|
1133
|
+
falsey = narrow_hash_key_absent(current, key)
|
|
1134
|
+
return nil if truthy.equal?(current) && falsey.equal?(current) # predicate is opaque
|
|
1135
|
+
|
|
1136
|
+
truthy_scope = truthy.equal?(current) ? scope : scope.public_send(writer, name, truthy)
|
|
1137
|
+
falsey_scope = falsey.equal?(current) ? scope : scope.public_send(writer, name, falsey)
|
|
1138
|
+
[truthy_scope, falsey_scope]
|
|
1139
|
+
end
|
|
1140
|
+
|
|
1141
|
+
def static_hash_key(node)
|
|
1142
|
+
case node
|
|
1143
|
+
when Prism::SymbolNode then node.unescaped.to_sym
|
|
1144
|
+
when Prism::StringNode then node.unescaped
|
|
1145
|
+
end
|
|
1146
|
+
end
|
|
1147
|
+
|
|
1148
|
+
# Promotes `key` from optional to required across a HashShape (or
|
|
1149
|
+
# every HashShape member of a Union). Returns the input unchanged
|
|
1150
|
+
# when nothing applies, so the caller can detect "no narrowing".
|
|
1151
|
+
def narrow_hash_key_present(type, key)
|
|
1152
|
+
case type
|
|
1153
|
+
when Type::HashShape
|
|
1154
|
+
promote_hash_key(type, key)
|
|
1155
|
+
when Type::Union
|
|
1156
|
+
Type::Combinator.union(*type.members.map { |member| narrow_hash_key_present(member, key) })
|
|
1157
|
+
else
|
|
1158
|
+
type
|
|
1159
|
+
end
|
|
1160
|
+
end
|
|
1161
|
+
|
|
1162
|
+
def promote_hash_key(shape, key)
|
|
1163
|
+
return shape unless shape.pairs.key?(key) # unknown key → no value type to assert
|
|
1164
|
+
return shape unless shape.optional_key?(key) # already required → nothing to promote
|
|
1165
|
+
|
|
1166
|
+
Type::HashShape.new(
|
|
1167
|
+
shape.pairs,
|
|
1168
|
+
required_keys: shape.required_keys + [key],
|
|
1169
|
+
optional_keys: shape.optional_keys - [key],
|
|
1170
|
+
read_only_keys: shape.read_only_keys,
|
|
1171
|
+
extra_keys: shape.extra_keys
|
|
1172
|
+
)
|
|
1173
|
+
end
|
|
1174
|
+
|
|
1175
|
+
# §4-3 false edge — drops `key` from a HashShape (or every HashShape
|
|
1176
|
+
# member of a Union) so the proven-absent key reads `nil`. Returns
|
|
1177
|
+
# the input unchanged when nothing applies (caller detects "no
|
|
1178
|
+
# narrowing"). Only an *optional* present key is removed: a required
|
|
1179
|
+
# key makes `key?` always true (the false edge is dead, leave the
|
|
1180
|
+
# shape opaque) and a key absent from `pairs` already reads `nil`.
|
|
1181
|
+
def narrow_hash_key_absent(type, key)
|
|
1182
|
+
case type
|
|
1183
|
+
when Type::HashShape
|
|
1184
|
+
remove_hash_key(type, key)
|
|
1185
|
+
when Type::Union
|
|
1186
|
+
Type::Combinator.union(*type.members.map { |member| narrow_hash_key_absent(member, key) })
|
|
1187
|
+
else
|
|
1188
|
+
type
|
|
1189
|
+
end
|
|
1190
|
+
end
|
|
1191
|
+
|
|
1192
|
+
def remove_hash_key(shape, key)
|
|
1193
|
+
return shape unless shape.pairs.key?(key)
|
|
1194
|
+
return shape unless shape.optional_key?(key)
|
|
1195
|
+
|
|
1196
|
+
Type::HashShape.new(
|
|
1197
|
+
shape.pairs.except(key),
|
|
1198
|
+
required_keys: shape.required_keys,
|
|
1199
|
+
optional_keys: shape.optional_keys - [key],
|
|
1200
|
+
read_only_keys: shape.read_only_keys - [key],
|
|
1201
|
+
extra_keys: shape.extra_keys
|
|
1202
|
+
)
|
|
1203
|
+
end
|
|
1204
|
+
|
|
977
1205
|
# Survey item (b): `/regex/ =~ str` and `str =~ /regex/`
|
|
978
1206
|
# bind the regex match-data globals on each edge.
|
|
979
1207
|
#
|
|
@@ -1554,13 +1782,42 @@ module Rigor
|
|
|
1554
1782
|
return nil if node.arguments.nil?
|
|
1555
1783
|
return nil unless node.arguments.arguments.size == 1
|
|
1556
1784
|
|
|
1557
|
-
|
|
1558
|
-
|
|
1785
|
+
arg = node.arguments.arguments.first
|
|
1786
|
+
case arg
|
|
1787
|
+
when Prism::LocalVariableReadNode
|
|
1788
|
+
current = scope.local(arg.name)
|
|
1789
|
+
return nil if current.nil?
|
|
1559
1790
|
|
|
1560
|
-
|
|
1791
|
+
analyse_case_equality_receiver(node.receiver, scope, arg.name, current)
|
|
1792
|
+
when Prism::CallNode
|
|
1793
|
+
analyse_case_equality_on_chain(node.receiver, arg, scope)
|
|
1794
|
+
end
|
|
1795
|
+
end
|
|
1796
|
+
|
|
1797
|
+
# `Class === <local/ivar>.<method>` — the case-equality counterpart
|
|
1798
|
+
# of {.analyse_class_predicate_on_chain}. The `open3` idiom
|
|
1799
|
+
# `if Hash === cmd.last` narrows `cmd.last` to the class inside the
|
|
1800
|
+
# branch, recorded as a single-hop method-chain narrowing keyed on
|
|
1801
|
+
# the chain address (same stability rules as `is_a?` on a chain).
|
|
1802
|
+
# Only static class/module receivers narrow here — the Range /
|
|
1803
|
+
# Regexp literal receivers (`(1..10) === x.foo`) are not a common
|
|
1804
|
+
# method-chain shape and stay deferred.
|
|
1805
|
+
def analyse_case_equality_on_chain(receiver, chain_arg, scope)
|
|
1806
|
+
class_name = static_class_name(receiver)
|
|
1807
|
+
return nil if class_name.nil?
|
|
1808
|
+
|
|
1809
|
+
address = stable_chain_address(chain_arg)
|
|
1810
|
+
return nil if address.nil?
|
|
1811
|
+
|
|
1812
|
+
current = scope.type_of(chain_arg)
|
|
1561
1813
|
return nil if current.nil?
|
|
1562
1814
|
|
|
1563
|
-
|
|
1815
|
+
truthy_type = narrow_class(current, class_name, exact: false, environment: scope.environment)
|
|
1816
|
+
falsey_type = narrow_not_class(current, class_name, exact: false, environment: scope.environment)
|
|
1817
|
+
[
|
|
1818
|
+
scope.with_method_chain_narrowing(*address, truthy_type),
|
|
1819
|
+
scope.with_method_chain_narrowing(*address, falsey_type)
|
|
1820
|
+
]
|
|
1564
1821
|
end
|
|
1565
1822
|
|
|
1566
1823
|
def analyse_case_equality_receiver(receiver, scope, local_name, current)
|
|
@@ -68,16 +68,17 @@ module Rigor
|
|
|
68
68
|
# collision — same-file declarations are the most
|
|
69
69
|
# specific authority.
|
|
70
70
|
merged_classes = default_scope.discovered_classes.merge(discovered_classes)
|
|
71
|
-
seeded_scope = default_scope
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
seeded_scope = default_scope.with_discovery(
|
|
72
|
+
default_scope.discovery.with(declared_types: declared_types,
|
|
73
|
+
discovered_classes: merged_classes)
|
|
74
|
+
)
|
|
74
75
|
|
|
75
76
|
# Slice 7 phase 2. Pre-pass over every class/module body
|
|
76
77
|
# to collect the per-class ivar accumulator. Seeded after
|
|
77
78
|
# declared_types so the rvalue typer in the pre-pass can
|
|
78
79
|
# see declaration overrides.
|
|
79
80
|
class_ivars = build_class_ivar_index(root, seeded_scope)
|
|
80
|
-
seeded_scope = seeded_scope.
|
|
81
|
+
seeded_scope = seeded_scope.with_discovery(seeded_scope.discovery.with(class_ivars: class_ivars))
|
|
81
82
|
|
|
82
83
|
# Slice 7 phase 6. Same pre-pass shape for cvars (per
|
|
83
84
|
# class) and globals (program-wide). Globals are also
|
|
@@ -86,9 +87,9 @@ module Rigor
|
|
|
86
87
|
# not enter a method body) observe the precise type
|
|
87
88
|
# without consulting the accumulator on every lookup.
|
|
88
89
|
class_cvars = build_class_cvar_index(root, seeded_scope)
|
|
89
|
-
seeded_scope = seeded_scope.
|
|
90
|
+
seeded_scope = seeded_scope.with_discovery(seeded_scope.discovery.with(class_cvars: class_cvars))
|
|
90
91
|
program_globals = build_program_global_index(root, seeded_scope)
|
|
91
|
-
seeded_scope = seeded_scope.
|
|
92
|
+
seeded_scope = seeded_scope.with_discovery(seeded_scope.discovery.with(program_globals: program_globals))
|
|
92
93
|
program_globals.each { |name, type| seeded_scope = seeded_scope.with_global(name, type) }
|
|
93
94
|
|
|
94
95
|
# Slice 7 phase 9. In-source constant value tracking.
|
|
@@ -99,7 +100,9 @@ module Rigor
|
|
|
99
100
|
# references resolve correctly. Multiple writes to the
|
|
100
101
|
# same qualified name union via `Type::Combinator.union`.
|
|
101
102
|
in_source_constants = build_in_source_constants(root, seeded_scope)
|
|
102
|
-
seeded_scope = seeded_scope.
|
|
103
|
+
seeded_scope = seeded_scope.with_discovery(
|
|
104
|
+
seeded_scope.discovery.with(in_source_constants: in_source_constants)
|
|
105
|
+
)
|
|
103
106
|
|
|
104
107
|
# Slice 7 phase 12. In-source method discovery. Walks
|
|
105
108
|
# every class/module body for `Prism::DefNode` and
|
|
@@ -115,7 +118,7 @@ module Rigor
|
|
|
115
118
|
discovered_methods = deep_merge_class_methods(
|
|
116
119
|
default_scope.discovered_methods, build_discovered_methods(root)
|
|
117
120
|
)
|
|
118
|
-
seeded_scope = seeded_scope.
|
|
121
|
+
seeded_scope = seeded_scope.with_discovery(seeded_scope.discovery.with(discovered_methods: discovered_methods))
|
|
119
122
|
|
|
120
123
|
# v0.0.2 #5 + ADR-24 slice 2 — record per-instance-method
|
|
121
124
|
# def nodes, the class -> superclass map, and the
|
|
@@ -173,12 +176,21 @@ module Rigor
|
|
|
173
176
|
method_visibilities = default_scope.discovered_method_visibilities.merge(
|
|
174
177
|
build_discovered_method_visibilities(root)
|
|
175
178
|
) { |_class, cross_file, per_file| cross_file.merge(per_file) }
|
|
179
|
+
# ADR-48 — per-file Data member layouts merged OVER the cross-file
|
|
180
|
+
# seed (same-file declaration is authoritative for its own classes).
|
|
181
|
+
data_member_layouts = default_scope.data_member_layouts.merge(
|
|
182
|
+
build_data_member_layouts(root)
|
|
183
|
+
)
|
|
176
184
|
|
|
177
|
-
seeded_scope
|
|
178
|
-
.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
185
|
+
seeded_scope.with_discovery(
|
|
186
|
+
seeded_scope.discovery.with(
|
|
187
|
+
discovered_def_nodes: def_nodes,
|
|
188
|
+
discovered_superclasses: superclasses,
|
|
189
|
+
discovered_includes: includes,
|
|
190
|
+
discovered_method_visibilities: method_visibilities,
|
|
191
|
+
data_member_layouts: data_member_layouts
|
|
192
|
+
)
|
|
193
|
+
)
|
|
182
194
|
end
|
|
183
195
|
|
|
184
196
|
# Slice 7 phase 2. Builds the class-level ivar accumulator
|
|
@@ -322,7 +334,7 @@ module Rigor
|
|
|
322
334
|
end
|
|
323
335
|
end
|
|
324
336
|
|
|
325
|
-
def walk_class_ivars(node, qualified_prefix, default_scope, accumulator, mutated_ivars,
|
|
337
|
+
def walk_class_ivars(node, qualified_prefix, default_scope, accumulator, mutated_ivars, # rubocop:disable Metrics/CyclomaticComplexity
|
|
326
338
|
read_before_write = nil, init_writes = nil)
|
|
327
339
|
return unless node.is_a?(Prism::Node)
|
|
328
340
|
|
|
@@ -357,6 +369,13 @@ module Rigor
|
|
|
357
369
|
collect_def_ivar_writes(node, qualified_prefix, default_scope, accumulator,
|
|
358
370
|
mutated_ivars, read_before_write, init_writes)
|
|
359
371
|
return
|
|
372
|
+
when Prism::CallNode
|
|
373
|
+
if init_writes && !qualified_prefix.empty? &&
|
|
374
|
+
node.block.is_a?(Prism::BlockNode) &&
|
|
375
|
+
block_initializer?(qualified_prefix.join("::"), node.name, default_scope)
|
|
376
|
+
collect_block_ivar_writes(node.block, qualified_prefix, default_scope,
|
|
377
|
+
accumulator, mutated_ivars, init_writes)
|
|
378
|
+
end
|
|
360
379
|
end
|
|
361
380
|
|
|
362
381
|
node.compact_child_nodes.each do |child|
|
|
@@ -393,6 +412,53 @@ module Rigor
|
|
|
393
412
|
collect_read_before_write_evidence(def_node, class_name, read_before_write, init_writes, default_scope)
|
|
394
413
|
end
|
|
395
414
|
|
|
415
|
+
# ADR-38 block-form: collects ivar writes from a CallNode's
|
|
416
|
+
# block body (e.g. RSpec `before { @x = … }` / `let(:x) { … }`)
|
|
417
|
+
# and folds them into `init_writes`, suppressing the
|
|
418
|
+
# read-before-write nil contribution the same way a def-form
|
|
419
|
+
# initializer does. The block body is always treated as an
|
|
420
|
+
# initializer (the caller has already verified the method name
|
|
421
|
+
# is declared as a block_method initializer), so there is no
|
|
422
|
+
# read-before-write evidence collection step here.
|
|
423
|
+
def collect_block_ivar_writes(block_node, qualified_prefix, default_scope, accumulator,
|
|
424
|
+
mutated_ivars, init_writes)
|
|
425
|
+
return if block_node.body.nil? || qualified_prefix.empty?
|
|
426
|
+
|
|
427
|
+
class_name = qualified_prefix.join("::")
|
|
428
|
+
self_type = Type::Combinator.nominal_of(class_name)
|
|
429
|
+
body_scope = default_scope.with_self_type(self_type)
|
|
430
|
+
|
|
431
|
+
gather_ivar_writes(block_node.body, body_scope, class_name, accumulator,
|
|
432
|
+
EMPTY_GUARDED_IVARS, mutated_ivars)
|
|
433
|
+
|
|
434
|
+
seen_writes = Set.new
|
|
435
|
+
read_first = Set.new
|
|
436
|
+
detect_read_before_write(block_node.body, seen_writes, read_first)
|
|
437
|
+
init_set = (init_writes[class_name] ||= Set.new)
|
|
438
|
+
seen_writes.each { |name| init_set << name }
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# ADR-38 block-form gate: true when a loaded plugin declares
|
|
442
|
+
# `method_name` a block-form initializer for `class_name` (or
|
|
443
|
+
# an ancestor). Mirrors `additional_initializer?` but queries
|
|
444
|
+
# `covers_block_method?` instead of `covers_method?`.
|
|
445
|
+
def block_initializer?(class_name, method_name, default_scope)
|
|
446
|
+
return false if class_name.nil? || default_scope.nil?
|
|
447
|
+
|
|
448
|
+
environment = default_scope.environment
|
|
449
|
+
registry = environment&.plugin_registry
|
|
450
|
+
return false if registry.nil?
|
|
451
|
+
return false if registry.respond_to?(:empty?) && registry.empty?
|
|
452
|
+
return false unless registry.respond_to?(:additional_initializers)
|
|
453
|
+
|
|
454
|
+
registry.additional_initializers.any? do |entry|
|
|
455
|
+
entry.covers_block_method?(method_name) &&
|
|
456
|
+
class_matches_constraint?(class_name, entry.receiver_constraint, environment)
|
|
457
|
+
end
|
|
458
|
+
rescue StandardError
|
|
459
|
+
false
|
|
460
|
+
end
|
|
461
|
+
|
|
396
462
|
# Walks the method body in AST (== execution) order
|
|
397
463
|
# tracking ivar names whose first reference is a read.
|
|
398
464
|
# The set is unioned into the class-wide
|
|
@@ -865,7 +931,7 @@ module Rigor
|
|
|
865
931
|
end
|
|
866
932
|
end
|
|
867
933
|
|
|
868
|
-
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
934
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
|
|
869
935
|
def walk_methods(node, qualified_prefix, in_singleton_class, accumulator)
|
|
870
936
|
return unless node.is_a?(Prism::Node)
|
|
871
937
|
|
|
@@ -874,6 +940,7 @@ module Rigor
|
|
|
874
940
|
name = qualified_name_for(node.constant_path)
|
|
875
941
|
if name
|
|
876
942
|
child_prefix = qualified_prefix + [name]
|
|
943
|
+
record_meta_superclass_members(node, child_prefix, accumulator) if node.is_a?(Prism::ClassNode)
|
|
877
944
|
walk_methods(node.body, child_prefix, false, accumulator) if node.body
|
|
878
945
|
return
|
|
879
946
|
end
|
|
@@ -933,7 +1000,7 @@ module Rigor
|
|
|
933
1000
|
end
|
|
934
1001
|
end
|
|
935
1002
|
end
|
|
936
|
-
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
1003
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
|
|
937
1004
|
|
|
938
1005
|
# v0.1.2 — when a `Const = Data.define(*sym) do ... end`
|
|
939
1006
|
# / `Const = Struct.new(*sym) do ... end` constant write
|
|
@@ -958,6 +1025,40 @@ module Rigor
|
|
|
958
1025
|
rvalue.block&.body
|
|
959
1026
|
end
|
|
960
1027
|
|
|
1028
|
+
# `class Foo < Data.define(:a, :b)` / `class Bar < Struct.new(:x)`
|
|
1029
|
+
# synthesizes reader methods (`a`, `b`, `x`) on the subclass that no
|
|
1030
|
+
# `def` / `attr_*` declares. Register them in the discovered-methods
|
|
1031
|
+
# existence table so an implicit-self read of a member inside the
|
|
1032
|
+
# class body is known to exist — both for the existing
|
|
1033
|
+
# undefined-method suppression and for the ADR-24 slice-4 self-call
|
|
1034
|
+
# recorder, which must treat a synthesized member as an existing
|
|
1035
|
+
# method, not an unresolved call. The block-form
|
|
1036
|
+
# (`Const = Data.define(:a) do ... end`) is handled by the
|
|
1037
|
+
# `ConstantWriteNode` branch's block recursion; its members type
|
|
1038
|
+
# `self` as `Object`, out of scope here.
|
|
1039
|
+
def record_meta_superclass_members(class_node, qualified_prefix, accumulator)
|
|
1040
|
+
superclass = class_node.superclass
|
|
1041
|
+
return unless data_define_call?(superclass) || struct_new_call?(superclass)
|
|
1042
|
+
|
|
1043
|
+
members = meta_member_names(superclass)
|
|
1044
|
+
return if members.empty?
|
|
1045
|
+
|
|
1046
|
+
class_name = qualified_prefix.join("::")
|
|
1047
|
+
table = (accumulator[class_name] ||= {})
|
|
1048
|
+
members.each { |member| table[member] ||= :instance }
|
|
1049
|
+
end
|
|
1050
|
+
|
|
1051
|
+
# The Symbol member names of a `Data.define(*Symbol)` /
|
|
1052
|
+
# `Struct.new(*Symbol [, keyword_init:])` call. For `Struct.new` the
|
|
1053
|
+
# optional leading String name and trailing `keyword_init:` hash are
|
|
1054
|
+
# stripped by {#struct_new_positionals}; `Data.define` args are all
|
|
1055
|
+
# Symbols already.
|
|
1056
|
+
def meta_member_names(call_node)
|
|
1057
|
+
raw = call_node.arguments&.arguments || []
|
|
1058
|
+
symbols = struct_new_call?(call_node) ? (struct_new_positionals(raw) || []) : raw
|
|
1059
|
+
symbols.filter_map { |arg| arg.unescaped.to_sym if arg.is_a?(Prism::SymbolNode) }
|
|
1060
|
+
end
|
|
1061
|
+
|
|
961
1062
|
def record_def_method(def_node, qualified_prefix, in_singleton_class, accumulator)
|
|
962
1063
|
return if qualified_prefix.empty?
|
|
963
1064
|
|
|
@@ -1114,6 +1215,58 @@ module Rigor
|
|
|
1114
1215
|
end
|
|
1115
1216
|
end
|
|
1116
1217
|
|
|
1218
|
+
# ADR-48 — per qualified class name -> ordered `Data.define`
|
|
1219
|
+
# member-name list, for both the named-subclass form
|
|
1220
|
+
# (`class Point < Data.define(:x, :y)`) and the constant-assigned
|
|
1221
|
+
# form (`Point = Data.define(:x, :y)`). Only `Data.define` is
|
|
1222
|
+
# recorded: `Struct.new` instances are mutable, so member-value
|
|
1223
|
+
# folding would be unsound (the Struct follow-up is deferred — see
|
|
1224
|
+
# ADR-48 § "Struct follow-up"). Consumed by
|
|
1225
|
+
# {Inference::MethodDispatcher::DataFolding} via
|
|
1226
|
+
# {Scope#data_member_layout}.
|
|
1227
|
+
def build_data_member_layouts(root)
|
|
1228
|
+
accumulator = {}
|
|
1229
|
+
walk_data_member_layouts(root, [], accumulator)
|
|
1230
|
+
accumulator.freeze
|
|
1231
|
+
end
|
|
1232
|
+
|
|
1233
|
+
def walk_data_member_layouts(node, qualified_prefix, accumulator)
|
|
1234
|
+
return unless node.is_a?(Prism::Node)
|
|
1235
|
+
|
|
1236
|
+
case node
|
|
1237
|
+
when Prism::ClassNode
|
|
1238
|
+
name = qualified_name_for(node.constant_path)
|
|
1239
|
+
if name
|
|
1240
|
+
record_data_member_layout(accumulator, qualified_prefix + [name], node.superclass)
|
|
1241
|
+
walk_data_member_layouts(node.body, qualified_prefix + [name], accumulator) if node.body
|
|
1242
|
+
return
|
|
1243
|
+
end
|
|
1244
|
+
when Prism::ModuleNode
|
|
1245
|
+
name = qualified_name_for(node.constant_path)
|
|
1246
|
+
if name
|
|
1247
|
+
walk_data_member_layouts(node.body, qualified_prefix + [name], accumulator) if node.body
|
|
1248
|
+
return
|
|
1249
|
+
end
|
|
1250
|
+
when Prism::ConstantWriteNode
|
|
1251
|
+
record_data_member_layout(accumulator, qualified_prefix + [node.name.to_s], node.value)
|
|
1252
|
+
end
|
|
1253
|
+
|
|
1254
|
+
node.compact_child_nodes.each do |child|
|
|
1255
|
+
walk_data_member_layouts(child, qualified_prefix, accumulator)
|
|
1256
|
+
end
|
|
1257
|
+
end
|
|
1258
|
+
|
|
1259
|
+
# Records `qualified -> [members]` when `expr` is a
|
|
1260
|
+
# `Data.define(*Symbol)` call with at least one literal-Symbol member.
|
|
1261
|
+
def record_data_member_layout(accumulator, qualified_parts, expr)
|
|
1262
|
+
return unless data_define_call?(expr)
|
|
1263
|
+
|
|
1264
|
+
members = meta_member_names(expr)
|
|
1265
|
+
return if members.empty?
|
|
1266
|
+
|
|
1267
|
+
accumulator[qualified_parts.join("::")] = members.freeze
|
|
1268
|
+
end
|
|
1269
|
+
|
|
1117
1270
|
MIXIN_CALL_NAMES = %i[include prepend].freeze
|
|
1118
1271
|
|
|
1119
1272
|
# ADR-24 slice 2 — per-class/module table mapping a fully
|
|
@@ -1480,9 +1633,10 @@ module Rigor
|
|
|
1480
1633
|
# @param paths [Array<String>] project file paths.
|
|
1481
1634
|
# @param buffer [Rigor::Analysis::BufferBinding, nil]
|
|
1482
1635
|
# @return [Hash{Symbol => Hash}]
|
|
1483
|
-
# `{ def_nodes:, def_sources:, superclasses:, includes: }`
|
|
1636
|
+
# `{ def_nodes:, def_sources:, superclasses:, includes:, class_sources: }`
|
|
1484
1637
|
def discovered_def_index_for_paths(paths, buffer: nil)
|
|
1485
|
-
acc = { def_nodes: {}, def_sources: {}, superclasses: {}, includes: {}, method_visibilities: {}, methods: {}
|
|
1638
|
+
acc = { def_nodes: {}, def_sources: {}, superclasses: {}, includes: {}, method_visibilities: {}, methods: {},
|
|
1639
|
+
class_sources: {}, data_member_layouts: {} }
|
|
1486
1640
|
paths.each do |path|
|
|
1487
1641
|
physical = buffer ? buffer.resolve(path) : path
|
|
1488
1642
|
root = Prism.parse(File.read(physical), filepath: path).value
|
|
@@ -1501,7 +1655,9 @@ module Rigor
|
|
|
1501
1655
|
# intact while still letting `attr_reader :x` in one file
|
|
1502
1656
|
# suppress a false undefined-method for `obj.x` in another.
|
|
1503
1657
|
acc[:methods] = subtract_def_methods(acc[:methods], acc[:def_nodes])
|
|
1504
|
-
%i[def_nodes def_sources includes method_visibilities methods].each
|
|
1658
|
+
%i[def_nodes def_sources includes method_visibilities methods class_sources].each do |key|
|
|
1659
|
+
acc[key].each_value(&:freeze)
|
|
1660
|
+
end
|
|
1505
1661
|
acc.transform_values(&:freeze)
|
|
1506
1662
|
end
|
|
1507
1663
|
|
|
@@ -1522,10 +1678,21 @@ module Rigor
|
|
|
1522
1678
|
# visibility declared in a sibling file.
|
|
1523
1679
|
def accumulate_project_index(acc, path, root)
|
|
1524
1680
|
merge_discovered_defs(acc[:def_nodes], acc[:def_sources], path, root)
|
|
1525
|
-
|
|
1526
|
-
build_discovered_includes(root)
|
|
1681
|
+
superclasses = build_discovered_superclasses(root)
|
|
1682
|
+
includes = build_discovered_includes(root)
|
|
1683
|
+
acc[:superclasses].merge!(superclasses)
|
|
1684
|
+
includes.each do |class_name, mods|
|
|
1527
1685
|
acc[:includes][class_name] = ((acc[:includes][class_name] || []) + mods).uniq
|
|
1528
1686
|
end
|
|
1687
|
+
record_class_sources(acc[:class_sources], path, root, superclasses, includes)
|
|
1688
|
+
merge_class_keyed_index_tables(acc, root)
|
|
1689
|
+
acc[:data_member_layouts].merge!(build_data_member_layouts(root))
|
|
1690
|
+
end
|
|
1691
|
+
|
|
1692
|
+
# Folds the per-class method-visibility and method-existence tables of
|
|
1693
|
+
# one file into the cross-file accumulator (kept out of
|
|
1694
|
+
# {#accumulate_project_index} to hold its ABC budget).
|
|
1695
|
+
def merge_class_keyed_index_tables(acc, root)
|
|
1529
1696
|
build_discovered_method_visibilities(root).each do |class_name, table|
|
|
1530
1697
|
(acc[:method_visibilities][class_name] ||= {}).merge!(table)
|
|
1531
1698
|
end
|
|
@@ -1534,6 +1701,26 @@ module Rigor
|
|
|
1534
1701
|
end
|
|
1535
1702
|
end
|
|
1536
1703
|
|
|
1704
|
+
# ADR-46 slice 1 — accumulates, per qualified user class/module
|
|
1705
|
+
# name, the set of files that declare it. A class's declaration
|
|
1706
|
+
# shape (its body `def`s, its `class Foo < Bar` superclass, its
|
|
1707
|
+
# `include`s) lives wherever the class is opened, so every file that
|
|
1708
|
+
# contributes a def / superclass / include for a name is a source of
|
|
1709
|
+
# that name's ancestry edges. {Scope#superclass_of} /
|
|
1710
|
+
# {Scope#includes_of} record this set when resolving the edge during
|
|
1711
|
+
# dependency recording (ADR-46). The class-declaration walk
|
|
1712
|
+
# (`collect_class_decls`) catches bodyless / def-less reopenings the
|
|
1713
|
+
# other three builders miss.
|
|
1714
|
+
def record_class_sources(class_sources, path, root, superclasses, includes)
|
|
1715
|
+
names = Set.new
|
|
1716
|
+
collect_class_decls(root, [], decls = {})
|
|
1717
|
+
names.merge(decls.keys)
|
|
1718
|
+
names.merge(superclasses.keys)
|
|
1719
|
+
names.merge(includes.keys)
|
|
1720
|
+
names.merge(build_discovered_def_nodes(root).keys)
|
|
1721
|
+
names.each { |name| (class_sources[name] ||= Set.new) << path }
|
|
1722
|
+
end
|
|
1723
|
+
|
|
1537
1724
|
# Merges one file's `class → method → DefNode` map into the
|
|
1538
1725
|
# cross-file `def_nodes` index and records each method's first-
|
|
1539
1726
|
# seen `"path:line"` definition site in `def_sources` (ADR-17 —
|