rigortype 0.1.18 → 0.2.0
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 +159 -224
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -3
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +32 -23
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +151 -23
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +9 -3
- data/lib/rigor/analysis/check_rules.rb +756 -132
- data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
- data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
- data/lib/rigor/analysis/diagnostic.rb +8 -0
- data/lib/rigor/analysis/fact_store.rb +5 -4
- data/lib/rigor/analysis/rule_catalog.rb +153 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +19 -18
- data/lib/rigor/analysis/runner/project_pre_passes.rb +13 -9
- data/lib/rigor/analysis/runner.rb +75 -27
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
- data/lib/rigor/analysis/worker_session.rb +31 -25
- data/lib/rigor/bleeding_edge.rb +123 -0
- data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
- data/lib/rigor/cache/descriptor.rb +86 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +5 -3
- data/lib/rigor/cli/annotate_command.rb +122 -16
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/check_command.rb +118 -16
- data/lib/rigor/cli/coverage_command.rb +148 -16
- data/lib/rigor/cli/coverage_scan.rb +57 -0
- data/lib/rigor/cli/explain_command.rb +2 -0
- data/lib/rigor/cli/lsp_command.rb +3 -7
- data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
- data/lib/rigor/cli/mutation_protection_report.rb +73 -0
- data/lib/rigor/cli/options.rb +9 -0
- data/lib/rigor/cli/plugins_command.rb +4 -5
- data/lib/rigor/cli/plugins_renderer.rb +0 -2
- data/lib/rigor/cli/protection_renderer.rb +63 -0
- data/lib/rigor/cli/protection_report.rb +68 -0
- data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
- data/lib/rigor/cli/sig_gen_command.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +2 -1
- data/lib/rigor/cli/triage_command.rb +8 -4
- data/lib/rigor/cli/triage_renderer.rb +15 -1
- data/lib/rigor/cli/type_of_command.rb +1 -1
- data/lib/rigor/cli/type_scan_command.rb +2 -1
- data/lib/rigor/cli.rb +12 -3
- data/lib/rigor/configuration/dependencies.rb +2 -4
- data/lib/rigor/configuration/severity_profile.rb +13 -1
- data/lib/rigor/configuration.rb +100 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
- data/lib/rigor/environment/class_registry.rb +4 -3
- data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
- data/lib/rigor/environment/lockfile_resolver.rb +1 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
- data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
- data/lib/rigor/environment/rbs_loader.rb +74 -5
- data/lib/rigor/environment.rb +17 -7
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/flow_contribution.rb +3 -5
- data/lib/rigor/inference/acceptance.rb +17 -9
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/body_fixpoint.rb +89 -0
- data/lib/rigor/inference/budget_trace.rb +29 -2
- data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
- data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
- data/lib/rigor/inference/expression_typer.rb +1072 -71
- data/lib/rigor/inference/hkt_body.rb +8 -11
- data/lib/rigor/inference/hkt_body_parser.rb +10 -12
- data/lib/rigor/inference/hkt_registry.rb +10 -11
- data/lib/rigor/inference/macro_block_self_type.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
- data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +210 -35
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +237 -24
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
- data/lib/rigor/inference/method_dispatcher.rb +112 -49
- data/lib/rigor/inference/method_parameter_binder.rb +56 -2
- data/lib/rigor/inference/multi_target_binder.rb +46 -3
- data/lib/rigor/inference/mutation_widening.rb +147 -11
- data/lib/rigor/inference/narrowing.rb +284 -53
- data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
- data/lib/rigor/inference/project_patched_methods.rb +4 -7
- data/lib/rigor/inference/project_patched_scanner.rb +2 -13
- data/lib/rigor/inference/protection_scanner.rb +86 -0
- data/lib/rigor/inference/scope_indexer.rb +821 -76
- data/lib/rigor/inference/statement_evaluator.rb +1179 -102
- data/lib/rigor/inference/struct_fold_safety.rb +181 -0
- data/lib/rigor/inference/synthetic_method.rb +7 -7
- data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
- data/lib/rigor/language_server/completion_provider.rb +6 -12
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
- data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
- data/lib/rigor/language_server/hover_provider.rb +2 -3
- data/lib/rigor/language_server/hover_renderer.rb +2 -11
- data/lib/rigor/language_server/server.rb +9 -17
- data/lib/rigor/language_server.rb +4 -5
- data/lib/rigor/plugin/base.rb +245 -87
- data/lib/rigor/plugin/macro/block_as_method.rb +25 -25
- data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
- data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
- data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
- data/lib/rigor/plugin/macro.rb +6 -8
- data/lib/rigor/plugin/manifest.rb +49 -90
- data/lib/rigor/plugin/node_rule_walk.rb +59 -14
- data/lib/rigor/plugin/registry.rb +18 -18
- data/lib/rigor/plugin/type_node_resolver.rb +6 -8
- data/lib/rigor/protection/mutation_scanner.rb +120 -0
- data/lib/rigor/protection/mutator.rb +246 -0
- data/lib/rigor/rbs_extended.rb +24 -36
- data/lib/rigor/reflection.rb +4 -7
- data/lib/rigor/scope/discovery_index.rb +16 -2
- data/lib/rigor/scope.rb +185 -16
- data/lib/rigor/sig_gen/generator.rb +8 -0
- data/lib/rigor/sig_gen/observed_call.rb +3 -3
- data/lib/rigor/sig_gen/writer.rb +40 -2
- data/lib/rigor/source/constant_path.rb +62 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/triage/catalogue.rb +4 -19
- data/lib/rigor/triage.rb +69 -1
- data/lib/rigor/type/bound_method.rb +2 -11
- data/lib/rigor/type/combinator.rb +45 -3
- data/lib/rigor/type/constant.rb +2 -11
- data/lib/rigor/type/data_class.rb +2 -11
- data/lib/rigor/type/data_instance.rb +2 -11
- data/lib/rigor/type/hash_shape.rb +2 -11
- data/lib/rigor/type/integer_range.rb +2 -11
- data/lib/rigor/type/intersection.rb +2 -11
- data/lib/rigor/type/nominal.rb +2 -11
- data/lib/rigor/type/plain_lattice.rb +37 -0
- data/lib/rigor/type/refined.rb +72 -13
- data/lib/rigor/type/singleton.rb +2 -11
- data/lib/rigor/type/struct_class.rb +75 -0
- data/lib/rigor/type/struct_instance.rb +93 -0
- data/lib/rigor/type/tuple.rb +5 -15
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +16 -32
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +34 -100
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +26 -27
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +9 -8
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +18 -49
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +4 -4
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +22 -35
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +16 -23
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -4
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +21 -27
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +52 -40
- data/sig/rigor/analysis/fact_store.rbs +3 -0
- data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
- data/sig/rigor/plugin/base.rbs +5 -2
- data/sig/rigor/plugin/manifest.rbs +1 -2
- data/sig/rigor/scope.rbs +18 -1
- data/sig/rigor/type.rbs +37 -1
- data/sig/rigor.rbs +1 -1
- data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
- data/skills/rigor-plugin-author/SKILL.md +6 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
- metadata +25 -2
- data/lib/rigor/plugin/macro/external_file.rb +0 -143
|
@@ -4,9 +4,11 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../scope"
|
|
6
6
|
require_relative "../type"
|
|
7
|
+
require_relative "../source/constant_path"
|
|
7
8
|
require_relative "mutation_widening"
|
|
8
9
|
require_relative "narrowing"
|
|
9
10
|
require_relative "statement_evaluator"
|
|
11
|
+
require_relative "struct_fold_safety"
|
|
10
12
|
|
|
11
13
|
module Rigor
|
|
12
14
|
module Inference
|
|
@@ -49,9 +51,14 @@ module Rigor
|
|
|
49
51
|
# @param default_scope [Rigor::Scope] the scope used for the root,
|
|
50
52
|
# and the fallback returned for any Prism node not contained in
|
|
51
53
|
# `root`'s subtree.
|
|
54
|
+
# @param converged_loop_recording [Boolean] display-path flag —
|
|
55
|
+
# when true the evaluator re-records fixpoint-tracked loop
|
|
56
|
+
# bodies from their CONVERGED bindings so per-line probes
|
|
57
|
+
# (`rigor annotate`) reflect the post-writeback state, not the
|
|
58
|
+
# cap-N intermediate constants. Off for the check path.
|
|
52
59
|
# @return [Hash{Prism::Node => Rigor::Scope}] identity-comparing
|
|
53
60
|
# table whose default value is `default_scope`.
|
|
54
|
-
def index(root, default_scope:) # rubocop:disable Metrics/AbcSize
|
|
61
|
+
def index(root, default_scope:, converged_loop_recording: false) # rubocop:disable Metrics/AbcSize
|
|
55
62
|
# Slice A-declarations. Build the declaration overrides
|
|
56
63
|
# first so every scope handed to the StatementEvaluator
|
|
57
64
|
# already carries the table; structural sharing through
|
|
@@ -109,12 +116,8 @@ module Rigor
|
|
|
109
116
|
# recognised `define_method` calls and records the
|
|
110
117
|
# introduced method names. `rigor check` consults the
|
|
111
118
|
# table to suppress false positives for methods the
|
|
112
|
-
# user has defined but no RBS sig describes.
|
|
113
|
-
#
|
|
114
|
-
# / include tables below) so a method `def`/`attr_reader`-
|
|
115
|
-
# declared in one file suppresses a false `undefined-method`
|
|
116
|
-
# for a call in another — `rigor check` seeds the project-wide
|
|
117
|
-
# table via `Runner#seed_project_scope`.
|
|
119
|
+
# user has defined but no RBS sig describes. Merged
|
|
120
|
+
# UNDER the cross-file pre-pass seed; details: merge_project_method_indexes.
|
|
118
121
|
discovered_methods = deep_merge_class_methods(
|
|
119
122
|
default_scope.discovered_methods, build_discovered_methods(root)
|
|
120
123
|
)
|
|
@@ -145,13 +148,29 @@ module Rigor
|
|
|
145
148
|
# `table[node]` to type predicates; the second pass's
|
|
146
149
|
# entry is the one that reflects all flow-derived
|
|
147
150
|
# rebinds, so it MUST overwrite the first.
|
|
151
|
+
# ADR-48 Struct slice 3 — install the top-level fold-safe-local set so
|
|
152
|
+
# a member read off a mutation-free top-level struct binding folds.
|
|
153
|
+
seeded_scope = seed_struct_fold_safe(seeded_scope, root)
|
|
154
|
+
|
|
148
155
|
on_enter = ->(node, scope) { table[node] = scope }
|
|
149
|
-
StatementEvaluator.new(scope: seeded_scope, on_enter: on_enter
|
|
156
|
+
StatementEvaluator.new(scope: seeded_scope, on_enter: on_enter,
|
|
157
|
+
converged_loop_recording: converged_loop_recording).evaluate(root)
|
|
150
158
|
|
|
151
159
|
propagate(root, table, seeded_scope)
|
|
152
160
|
table
|
|
153
161
|
end
|
|
154
162
|
|
|
163
|
+
# ADR-48 Struct slice 3 — installs the top-level fold-safe-local set
|
|
164
|
+
# ({Inference::StructFoldSafety}). Struct member layouts of constant
|
|
165
|
+
# receivers are resolved through the side-table the seeded scope carries.
|
|
166
|
+
def seed_struct_fold_safe(seeded_scope, root)
|
|
167
|
+
seeded_scope.with_struct_fold_safe(
|
|
168
|
+
StructFoldSafety.fold_safe_locals(
|
|
169
|
+
root, ->(name) { seeded_scope.struct_member_layout(name)&.[](:members) }
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
|
|
155
174
|
# v0.0.2 #5 + ADR-24 slice 2 — seeds the three
|
|
156
175
|
# project-method indexes onto `seeded_scope`: the
|
|
157
176
|
# per-instance-method def-node table, the class ->
|
|
@@ -164,6 +183,9 @@ module Rigor
|
|
|
164
183
|
def_nodes = default_scope.discovered_def_nodes.merge(
|
|
165
184
|
build_discovered_def_nodes(root)
|
|
166
185
|
) { |_class, cross_file, per_file| cross_file.merge(per_file) }
|
|
186
|
+
singleton_def_nodes = default_scope.discovered_singleton_def_nodes.merge(
|
|
187
|
+
build_discovered_singleton_def_nodes(root)
|
|
188
|
+
) { |_class, cross_file, per_file| cross_file.merge(per_file) }
|
|
167
189
|
superclasses = default_scope.discovered_superclasses.merge(
|
|
168
190
|
build_discovered_superclasses(root)
|
|
169
191
|
)
|
|
@@ -176,23 +198,34 @@ module Rigor
|
|
|
176
198
|
method_visibilities = default_scope.discovered_method_visibilities.merge(
|
|
177
199
|
build_discovered_method_visibilities(root)
|
|
178
200
|
) { |_class, cross_file, per_file| cross_file.merge(per_file) }
|
|
179
|
-
# ADR-48 — per-file Data member layouts merged OVER the
|
|
180
|
-
# seed (same-file declaration is authoritative
|
|
181
|
-
data_member_layouts = default_scope
|
|
182
|
-
build_data_member_layouts(root)
|
|
183
|
-
)
|
|
201
|
+
# ADR-48 — per-file Data + Struct member layouts merged OVER the
|
|
202
|
+
# cross-file seed (same-file declaration is authoritative).
|
|
203
|
+
data_member_layouts, struct_member_layouts = merge_member_layouts(default_scope, root)
|
|
184
204
|
|
|
185
205
|
seeded_scope.with_discovery(
|
|
186
206
|
seeded_scope.discovery.with(
|
|
187
207
|
discovered_def_nodes: def_nodes,
|
|
208
|
+
discovered_singleton_def_nodes: singleton_def_nodes,
|
|
188
209
|
discovered_superclasses: superclasses,
|
|
189
210
|
discovered_includes: includes,
|
|
190
211
|
discovered_method_visibilities: method_visibilities,
|
|
191
|
-
data_member_layouts: data_member_layouts
|
|
212
|
+
data_member_layouts: data_member_layouts,
|
|
213
|
+
struct_member_layouts: struct_member_layouts
|
|
192
214
|
)
|
|
193
215
|
)
|
|
194
216
|
end
|
|
195
217
|
|
|
218
|
+
# ADR-48 — the per-file Data + Struct member-layout tables, each merged
|
|
219
|
+
# OVER the cross-file seed so a same-file declaration wins for its own
|
|
220
|
+
# classes. Returned as a pair to keep {#merge_project_method_indexes}
|
|
221
|
+
# under the method-size budget.
|
|
222
|
+
def merge_member_layouts(default_scope, root)
|
|
223
|
+
[
|
|
224
|
+
default_scope.data_member_layouts.merge(build_data_member_layouts(root)),
|
|
225
|
+
default_scope.struct_member_layouts.merge(build_struct_member_layouts(root))
|
|
226
|
+
]
|
|
227
|
+
end
|
|
228
|
+
|
|
196
229
|
# Slice 7 phase 2. Builds the class-level ivar accumulator
|
|
197
230
|
# by walking every `Prism::ClassNode` / `Prism::ModuleNode`
|
|
198
231
|
# body, descending into each nested `Prism::DefNode`, and
|
|
@@ -210,8 +243,15 @@ module Rigor
|
|
|
210
243
|
mutated_ivars = {}
|
|
211
244
|
read_before_write = {}
|
|
212
245
|
init_writes = {}
|
|
246
|
+
# WD3 — per-class summary of `{class_name => {method_name =>
|
|
247
|
+
# Set<ivar names definitely assigned non-nil on every
|
|
248
|
+
# completing path>}}`, consulted by `dead_transient_nil_writes`
|
|
249
|
+
# so a ctor that reassigns `@x` indirectly through an
|
|
250
|
+
# unconditional same-class method call (`mask!`) credits the
|
|
251
|
+
# overwrite. Built once per program here, memoised by class.
|
|
252
|
+
method_assign_effects = build_method_assign_effects(root)
|
|
213
253
|
walk_class_ivars(root, [], default_scope, accumulator, mutated_ivars,
|
|
214
|
-
read_before_write, init_writes)
|
|
254
|
+
read_before_write, init_writes, method_assign_effects)
|
|
215
255
|
widen_mutated_ivar_entries!(accumulator, mutated_ivars)
|
|
216
256
|
contribute_read_before_write_nil!(accumulator, read_before_write, init_writes)
|
|
217
257
|
accumulator.transform_values(&:freeze).freeze
|
|
@@ -334,13 +374,13 @@ module Rigor
|
|
|
334
374
|
end
|
|
335
375
|
end
|
|
336
376
|
|
|
337
|
-
def walk_class_ivars(node, qualified_prefix, default_scope, accumulator, mutated_ivars, # rubocop:disable Metrics/CyclomaticComplexity
|
|
338
|
-
read_before_write = nil, init_writes = nil)
|
|
377
|
+
def walk_class_ivars(node, qualified_prefix, default_scope, accumulator, mutated_ivars, # rubocop:disable Metrics/CyclomaticComplexity,Metrics/ParameterLists
|
|
378
|
+
read_before_write = nil, init_writes = nil, method_assign_effects = nil)
|
|
339
379
|
return unless node.is_a?(Prism::Node)
|
|
340
380
|
|
|
341
381
|
case node
|
|
342
382
|
when Prism::ClassNode, Prism::ModuleNode
|
|
343
|
-
name =
|
|
383
|
+
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
344
384
|
if name
|
|
345
385
|
child_prefix = qualified_prefix + [name]
|
|
346
386
|
if node.body
|
|
@@ -361,13 +401,13 @@ module Rigor
|
|
|
361
401
|
# read.
|
|
362
402
|
collect_class_body_ivar_writes(node.body, child_prefix.join("::"), init_writes) if init_writes
|
|
363
403
|
walk_class_ivars(node.body, child_prefix, default_scope, accumulator,
|
|
364
|
-
mutated_ivars, read_before_write, init_writes)
|
|
404
|
+
mutated_ivars, read_before_write, init_writes, method_assign_effects)
|
|
365
405
|
end
|
|
366
406
|
return
|
|
367
407
|
end
|
|
368
408
|
when Prism::DefNode
|
|
369
409
|
collect_def_ivar_writes(node, qualified_prefix, default_scope, accumulator,
|
|
370
|
-
mutated_ivars, read_before_write, init_writes)
|
|
410
|
+
mutated_ivars, read_before_write, init_writes, method_assign_effects)
|
|
371
411
|
return
|
|
372
412
|
when Prism::CallNode
|
|
373
413
|
if init_writes && !qualified_prefix.empty? &&
|
|
@@ -380,12 +420,12 @@ module Rigor
|
|
|
380
420
|
|
|
381
421
|
node.compact_child_nodes.each do |child|
|
|
382
422
|
walk_class_ivars(child, qualified_prefix, default_scope, accumulator,
|
|
383
|
-
mutated_ivars, read_before_write, init_writes)
|
|
423
|
+
mutated_ivars, read_before_write, init_writes, method_assign_effects)
|
|
384
424
|
end
|
|
385
425
|
end
|
|
386
426
|
|
|
387
|
-
def collect_def_ivar_writes(def_node, qualified_prefix, default_scope, accumulator, mutated_ivars,
|
|
388
|
-
read_before_write = nil, init_writes = nil)
|
|
427
|
+
def collect_def_ivar_writes(def_node, qualified_prefix, default_scope, accumulator, mutated_ivars, # rubocop:disable Metrics/ParameterLists
|
|
428
|
+
read_before_write = nil, init_writes = nil, method_assign_effects = nil)
|
|
389
429
|
return if def_node.body.nil? || qualified_prefix.empty?
|
|
390
430
|
|
|
391
431
|
class_name = qualified_prefix.join("::")
|
|
@@ -399,7 +439,23 @@ module Rigor
|
|
|
399
439
|
end
|
|
400
440
|
body_scope = default_scope.with_self_type(self_type)
|
|
401
441
|
|
|
402
|
-
|
|
442
|
+
# C2 — transient `@x = nil` dead-write elimination. When a
|
|
443
|
+
# method body opens with an unconditional `@x = nil`
|
|
444
|
+
# (defensive init) and then *definitely* reassigns `@x` to a
|
|
445
|
+
# non-nil value on every completing path (a later
|
|
446
|
+
# unconditional statement-level write, OR an `if/else` whose
|
|
447
|
+
# both branches write `@x`), the opening nil is dead — it can
|
|
448
|
+
# never be observed at method exit. Recording it anyway folds
|
|
449
|
+
# a spurious `nil` constituent into the flow-insensitive
|
|
450
|
+
# class-ivar union, which then poisons reads in OTHER methods
|
|
451
|
+
# (e.g. ipaddr `IN4MASK ^ @mask_addr` rejects the resulting
|
|
452
|
+
# `Integer | nil`). The set holds the `object_id`s of the
|
|
453
|
+
# transient write nodes to skip; soundness is post-domination
|
|
454
|
+
# at the top statement level, so dropping the nil never hides
|
|
455
|
+
# a real runtime-nil read.
|
|
456
|
+
dead_writes = dead_transient_nil_writes(def_node.body, class_name, method_assign_effects)
|
|
457
|
+
gather_ivar_writes(def_node.body, body_scope, class_name, accumulator,
|
|
458
|
+
EMPTY_GUARDED_IVARS, mutated_ivars, dead_writes)
|
|
403
459
|
|
|
404
460
|
# B2.3 — collect per-method evidence for the read-before-
|
|
405
461
|
# write nil contribution. The accumulator-level decision
|
|
@@ -589,13 +645,29 @@ module Rigor
|
|
|
589
645
|
private_constant :EMPTY_GUARDED_IVARS
|
|
590
646
|
|
|
591
647
|
def gather_ivar_writes(node, scope, class_name, accumulator, guarded_ivars = EMPTY_GUARDED_IVARS,
|
|
592
|
-
mutated_ivars = nil)
|
|
648
|
+
mutated_ivars = nil, dead_writes = nil)
|
|
593
649
|
return unless node.is_a?(Prism::Node)
|
|
594
650
|
|
|
595
|
-
if node.is_a?(Prism::InstanceVariableWriteNode)
|
|
651
|
+
if node.is_a?(Prism::InstanceVariableWriteNode) &&
|
|
652
|
+
!(dead_writes && dead_writes.include?(node.object_id))
|
|
596
653
|
record_ivar_write(node, scope, class_name, accumulator,
|
|
597
654
|
guarded: guarded_ivars.include?(node.name))
|
|
598
655
|
end
|
|
656
|
+
|
|
657
|
+
# N1 — parallel / multiple assignment (`old, @cb = @cb, block`,
|
|
658
|
+
# `@i, @o, @e, @thr = Open3.popen3(cmd)`). A direct
|
|
659
|
+
# `InstanceVariableWriteNode` is the only write form this
|
|
660
|
+
# collector handled, so an ivar appearing as a `MultiWriteNode`
|
|
661
|
+
# target was silently dropped from the class-ivar union — leaving
|
|
662
|
+
# it to seed as pure `nil` (from a sibling `@cb = nil` ctor write,
|
|
663
|
+
# or absent entirely) and false-fire `if @cb` always-falsey /
|
|
664
|
+
# `@thr.alive?` undefined-for-nil. Record each ivar target with
|
|
665
|
+
# its tuple-position RHS type where the RHS is array/tuple-shaped,
|
|
666
|
+
# else the unanalyzable floor (the same `Dynamic[top]` a single
|
|
667
|
+
# write to an unknown RHS records — an unanalyzable multi-write
|
|
668
|
+
# means unknown, not nil).
|
|
669
|
+
record_multi_write_ivars(node, scope, class_name, accumulator)
|
|
670
|
+
|
|
599
671
|
record_ivar_mutator_call(node, class_name, mutated_ivars) if mutated_ivars && node.is_a?(Prism::CallNode)
|
|
600
672
|
|
|
601
673
|
# Don't recurse into nested defs, classes, or modules; their
|
|
@@ -603,12 +675,13 @@ module Rigor
|
|
|
603
675
|
return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
604
676
|
|
|
605
677
|
if node.is_a?(Prism::IfNode) || node.is_a?(Prism::UnlessNode)
|
|
606
|
-
walk_conditional_ivar_writes(node, scope, class_name, accumulator, guarded_ivars,
|
|
678
|
+
walk_conditional_ivar_writes(node, scope, class_name, accumulator, guarded_ivars,
|
|
679
|
+
mutated_ivars, dead_writes)
|
|
607
680
|
return
|
|
608
681
|
end
|
|
609
682
|
|
|
610
683
|
node.compact_child_nodes.each do |c|
|
|
611
|
-
gather_ivar_writes(c, scope, class_name, accumulator, guarded_ivars, mutated_ivars)
|
|
684
|
+
gather_ivar_writes(c, scope, class_name, accumulator, guarded_ivars, mutated_ivars, dead_writes)
|
|
612
685
|
end
|
|
613
686
|
end
|
|
614
687
|
|
|
@@ -646,16 +719,22 @@ module Rigor
|
|
|
646
719
|
# reads of `@x` would then surface a nil-receiver FP. The
|
|
647
720
|
# ELSE branch is left ungarded so those reads continue to type
|
|
648
721
|
# as they did before this fix.
|
|
649
|
-
def walk_conditional_ivar_writes(node, scope, class_name, accumulator, guarded_ivars,
|
|
722
|
+
def walk_conditional_ivar_writes(node, scope, class_name, accumulator, guarded_ivars,
|
|
723
|
+
mutated_ivars = nil, dead_writes = nil)
|
|
650
724
|
then_guards = then_body_guarded_ivars(node)
|
|
651
725
|
then_guarded = then_guards.empty? ? guarded_ivars : (guarded_ivars | then_guards)
|
|
652
726
|
|
|
653
|
-
gather_ivar_writes(node.predicate, scope, class_name, accumulator, guarded_ivars,
|
|
727
|
+
gather_ivar_writes(node.predicate, scope, class_name, accumulator, guarded_ivars,
|
|
728
|
+
mutated_ivars, dead_writes)
|
|
654
729
|
if node.statements
|
|
655
|
-
gather_ivar_writes(node.statements, scope, class_name, accumulator, then_guarded,
|
|
730
|
+
gather_ivar_writes(node.statements, scope, class_name, accumulator, then_guarded,
|
|
731
|
+
mutated_ivars, dead_writes)
|
|
656
732
|
end
|
|
657
733
|
branch = node.is_a?(Prism::IfNode) ? node.subsequent : node.else_clause
|
|
658
|
-
|
|
734
|
+
return unless branch
|
|
735
|
+
|
|
736
|
+
gather_ivar_writes(branch, scope, class_name, accumulator, guarded_ivars,
|
|
737
|
+
mutated_ivars, dead_writes)
|
|
659
738
|
end
|
|
660
739
|
|
|
661
740
|
# Returns the set of ivar names that, in the THEN body of this
|
|
@@ -723,6 +802,332 @@ module Rigor
|
|
|
723
802
|
end
|
|
724
803
|
end
|
|
725
804
|
|
|
805
|
+
# C2 — returns a Set of `object_id`s for transient `@x = nil`
|
|
806
|
+
# writes that a later statement in the same method body
|
|
807
|
+
# *definitely* overwrites with a non-nil value on every
|
|
808
|
+
# completing path. Such a nil can never be the ivar's value at
|
|
809
|
+
# method exit, so it must not contribute a `nil` constituent to
|
|
810
|
+
# the (flow-insensitive) class-ivar union.
|
|
811
|
+
#
|
|
812
|
+
# Scope is deliberately narrow and post-domination-sound:
|
|
813
|
+
# - only the top-level statement sequence of the body is
|
|
814
|
+
# considered (no writes hidden inside loops / rescue / nested
|
|
815
|
+
# conditionals count as the "definite" overwrite, except the
|
|
816
|
+
# one structured `if/else` form below);
|
|
817
|
+
# - the killing statement is either an unconditional
|
|
818
|
+
# statement-level `@x = <non-nil>`, OR an `if/else` (with a
|
|
819
|
+
# real `else`) where BOTH branches' final top-level write to
|
|
820
|
+
# `@x` is non-nil. Both shapes overwrite `@x` on every path;
|
|
821
|
+
# - only `@x = nil` literal writes are ever marked dead — a
|
|
822
|
+
# non-nil transient is left untouched (it is already
|
|
823
|
+
# precision-additive in the union).
|
|
824
|
+
# WD3 — ADR-41-style hard cap on how deep the same-class-call
|
|
825
|
+
# definite-assignment crediting recurses (the ctor calls
|
|
826
|
+
# `mask!`, which could itself call another same-class helper).
|
|
827
|
+
# Cycle-guarded independently; the cap bounds even acyclic
|
|
828
|
+
# chains.
|
|
829
|
+
SAME_CLASS_CALL_DEPTH_CAP = 3
|
|
830
|
+
private_constant :SAME_CLASS_CALL_DEPTH_CAP
|
|
831
|
+
|
|
832
|
+
# WD3 — builds the per-class definite-assignment summary
|
|
833
|
+
# `{class_name => {method_name => Set<ivar names assigned
|
|
834
|
+
# non-nil on every completing path>}}`. Used so a ctor's
|
|
835
|
+
# `dead_transient_nil_writes` can credit an indirect overwrite
|
|
836
|
+
# through an unconditionally-called same-class method (ipaddr's
|
|
837
|
+
# `initialize` reassigns `@mask_addr` via `mask!`).
|
|
838
|
+
#
|
|
839
|
+
# Each method's set is computed by the same suffix
|
|
840
|
+
# definite-assignment analysis used for the ctor seed, run from
|
|
841
|
+
# the method body's first statement for every ivar the method
|
|
842
|
+
# writes anywhere. Same-class calls inside a method are credited
|
|
843
|
+
# transitively (depth-capped, cycle-guarded) so the resulting
|
|
844
|
+
# FLAT table is correct at depth 0 for the ctor lookup.
|
|
845
|
+
def build_method_assign_effects(root)
|
|
846
|
+
defs = collect_class_method_defs(root)
|
|
847
|
+
effects = {}
|
|
848
|
+
memo = {}.compare_by_identity
|
|
849
|
+
defs.each do |class_name, methods|
|
|
850
|
+
methods.each do |method_name, def_node|
|
|
851
|
+
assigns = method_definite_assigns(class_name, method_name, def_node, defs, effects, memo, 0)
|
|
852
|
+
(effects[class_name] ||= {})[method_name] = assigns unless assigns.empty?
|
|
853
|
+
end
|
|
854
|
+
end
|
|
855
|
+
effects.freeze
|
|
856
|
+
end
|
|
857
|
+
|
|
858
|
+
# Collects `{class_name => {method_name => DefNode}}` for every
|
|
859
|
+
# instance-method def in the program. Singleton defs (`def
|
|
860
|
+
# self.x`) are excluded — the ctor-call crediting only follows
|
|
861
|
+
# instance-method calls on `self`. Last def wins on redefinition.
|
|
862
|
+
def collect_class_method_defs(root, prefix = [], acc = {})
|
|
863
|
+
return acc unless root.is_a?(Prism::Node)
|
|
864
|
+
|
|
865
|
+
case root
|
|
866
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
867
|
+
name = Source::ConstantPath.qualified_name(root.constant_path)
|
|
868
|
+
if name && root.body
|
|
869
|
+
child = prefix + [name]
|
|
870
|
+
collect_class_method_defs(root.body, child, acc)
|
|
871
|
+
end
|
|
872
|
+
return acc
|
|
873
|
+
when Prism::DefNode
|
|
874
|
+
(acc[prefix.join("::")] ||= {})[root.name] = root unless prefix.empty? || root.receiver
|
|
875
|
+
return acc
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
root.compact_child_nodes.each { |c| collect_class_method_defs(c, prefix, acc) }
|
|
879
|
+
acc
|
|
880
|
+
end
|
|
881
|
+
|
|
882
|
+
# Computes the definite-assignment set for one method, memoised
|
|
883
|
+
# per def node. The `memo` cycle-guards: a method re-entered
|
|
884
|
+
# while its own summary is in progress contributes nothing
|
|
885
|
+
# (sound under-approximation), so mutual recursion terminates.
|
|
886
|
+
def method_definite_assigns(class_name, _method_name, def_node, defs, effects, memo, depth)
|
|
887
|
+
return Set.new if def_node.body.nil?
|
|
888
|
+
return memo[def_node] if memo.key?(def_node)
|
|
889
|
+
return Set.new if depth >= SAME_CLASS_CALL_DEPTH_CAP
|
|
890
|
+
|
|
891
|
+
memo[def_node] = Set.new # in-progress sentinel (cycle guard)
|
|
892
|
+
statements = top_level_statements(def_node.body)
|
|
893
|
+
candidates = ivar_write_targets(def_node.body)
|
|
894
|
+
# A transient `@x = nil` opener whose own method reassigns it
|
|
895
|
+
# later must still count `@x` as assigned for callers, so the
|
|
896
|
+
# crediting is computed at the BUILD-time depth.
|
|
897
|
+
resolver = MethodEffectResolver.new(self, class_name, defs, effects, memo, depth)
|
|
898
|
+
assigns = Set.new
|
|
899
|
+
candidates.each do |ivar|
|
|
900
|
+
assigns << ivar if suffix_definitely_assigns_with_resolver?(statements, 0, ivar, class_name, resolver, depth)
|
|
901
|
+
end
|
|
902
|
+
memo[def_node] = assigns
|
|
903
|
+
end
|
|
904
|
+
|
|
905
|
+
# Every ivar this body assigns a non-nil value to ANYWHERE (the
|
|
906
|
+
# candidate set for the method's definite-assignment scan).
|
|
907
|
+
def ivar_write_targets(node, acc = Set.new)
|
|
908
|
+
return acc unless node.is_a?(Prism::Node)
|
|
909
|
+
|
|
910
|
+
acc << node.name if node.is_a?(Prism::InstanceVariableWriteNode) && !nil_literal_value?(node.value)
|
|
911
|
+
node.compact_child_nodes.each { |c| ivar_write_targets(c, acc) }
|
|
912
|
+
acc
|
|
913
|
+
end
|
|
914
|
+
|
|
915
|
+
# Build-time variant of `suffix_definitely_assigns?` that resolves
|
|
916
|
+
# same-class calls through the lazy `resolver` (which recurses
|
|
917
|
+
# into `method_definite_assigns` for not-yet-computed callees)
|
|
918
|
+
# rather than the finished flat table.
|
|
919
|
+
def suffix_definitely_assigns_with_resolver?(statements, from, target, class_name, resolver, depth)
|
|
920
|
+
statements[from..].each do |stmt|
|
|
921
|
+
outcome = statement_assignment_outcome(stmt, target, class_name, resolver, depth, nil)
|
|
922
|
+
return true if outcome == :assigned
|
|
923
|
+
return false if outcome == :terminates_unassigned
|
|
924
|
+
end
|
|
925
|
+
false
|
|
926
|
+
end
|
|
927
|
+
|
|
928
|
+
# Adapts `effects.dig(class, method)` for build-time crediting:
|
|
929
|
+
# when the callee summary is not yet in the flat table, compute
|
|
930
|
+
# it on demand (depth+1) via `method_definite_assigns`.
|
|
931
|
+
class MethodEffectResolver
|
|
932
|
+
def initialize(indexer, class_name, defs, effects, memo, depth)
|
|
933
|
+
@indexer = indexer
|
|
934
|
+
@class_name = class_name
|
|
935
|
+
@defs = defs
|
|
936
|
+
@effects = effects
|
|
937
|
+
@memo = memo
|
|
938
|
+
@depth = depth
|
|
939
|
+
end
|
|
940
|
+
|
|
941
|
+
def dig(class_name, method_name)
|
|
942
|
+
existing = @effects.dig(class_name, method_name)
|
|
943
|
+
return existing if existing
|
|
944
|
+
|
|
945
|
+
def_node = @defs.dig(class_name, method_name)
|
|
946
|
+
return nil if def_node.nil?
|
|
947
|
+
|
|
948
|
+
@indexer.send(:method_definite_assigns, class_name, method_name, def_node, @defs, @effects, @memo,
|
|
949
|
+
@depth + 1)
|
|
950
|
+
end
|
|
951
|
+
end
|
|
952
|
+
|
|
953
|
+
def dead_transient_nil_writes(body, class_name = nil, method_assign_effects = nil)
|
|
954
|
+
statements = top_level_statements(body)
|
|
955
|
+
return nil if statements.length < 2
|
|
956
|
+
|
|
957
|
+
dead = nil
|
|
958
|
+
|
|
959
|
+
statements.each_with_index do |stmt, i|
|
|
960
|
+
next unless stmt.is_a?(Prism::InstanceVariableWriteNode) && nil_literal_value?(stmt.value)
|
|
961
|
+
|
|
962
|
+
# The opening `@x = nil` is dead when every completing path
|
|
963
|
+
# of the SUFFIX after it (normal end OR early `return`,
|
|
964
|
+
# never a `raise`-terminated path) definitely reassigns
|
|
965
|
+
# `@x` non-nil. The suffix analysis credits an
|
|
966
|
+
# unconditionally-called same-class method's own definite
|
|
967
|
+
# assignments via `method_assign_effects`.
|
|
968
|
+
if suffix_definitely_assigns?(statements, i + 1, stmt.name, class_name, method_assign_effects)
|
|
969
|
+
(dead ||= Set.new) << stmt.object_id
|
|
970
|
+
end
|
|
971
|
+
end
|
|
972
|
+
|
|
973
|
+
dead
|
|
974
|
+
end
|
|
975
|
+
|
|
976
|
+
def top_level_statements(body)
|
|
977
|
+
return [] if body.nil?
|
|
978
|
+
return body.body if body.is_a?(Prism::StatementsNode)
|
|
979
|
+
|
|
980
|
+
[body]
|
|
981
|
+
end
|
|
982
|
+
|
|
983
|
+
def nil_literal_value?(node)
|
|
984
|
+
node.is_a?(Prism::NilNode)
|
|
985
|
+
end
|
|
986
|
+
|
|
987
|
+
# True when, starting from `statements[from]`, EVERY path that
|
|
988
|
+
# completes the method (falls off the end OR hits an early
|
|
989
|
+
# `return`) definitely assigns `target` a non-nil value first.
|
|
990
|
+
# Paths terminated by `raise` are not completing paths and are
|
|
991
|
+
# ignored (they never observe the ivar at method exit). A path
|
|
992
|
+
# that can fall through `statements` without assigning fails.
|
|
993
|
+
def suffix_definitely_assigns?(statements, from, target, class_name, effects)
|
|
994
|
+
statements[from..].each do |stmt|
|
|
995
|
+
outcome = statement_assignment_outcome(stmt, target, class_name, effects, 0, nil)
|
|
996
|
+
# The statement assigned on every continuing path -> the
|
|
997
|
+
# suffix is satisfied no matter what follows.
|
|
998
|
+
return true if outcome == :assigned
|
|
999
|
+
# The statement terminates control here (return/raise) and
|
|
1000
|
+
# the value it carried did not assign on every path -> some
|
|
1001
|
+
# completing path reached exit without the assignment.
|
|
1002
|
+
return false if outcome == :terminates_unassigned
|
|
1003
|
+
# Otherwise (:falls_through_unassigned) keep scanning the
|
|
1004
|
+
# remaining statements.
|
|
1005
|
+
end
|
|
1006
|
+
# Fell off the end with no definite assignment.
|
|
1007
|
+
false
|
|
1008
|
+
end
|
|
1009
|
+
|
|
1010
|
+
# Classifies a single statement's effect on `target`:
|
|
1011
|
+
# :assigned — every path through the statement
|
|
1012
|
+
# that continues OR returns assigns
|
|
1013
|
+
# `target` non-nil (suffix is done);
|
|
1014
|
+
# :terminates_unassigned — the statement ends the method
|
|
1015
|
+
# (return/raise) on some path
|
|
1016
|
+
# without a definite assignment, so
|
|
1017
|
+
# a completing path escaped;
|
|
1018
|
+
# :falls_through_unassigned — control may continue past it
|
|
1019
|
+
# without the assignment (keep
|
|
1020
|
+
# scanning the suffix).
|
|
1021
|
+
def statement_assignment_outcome(stmt, target, class_name, effects, depth, visiting)
|
|
1022
|
+
case stmt
|
|
1023
|
+
when Prism::InstanceVariableWriteNode
|
|
1024
|
+
return :falls_through_unassigned if stmt.name != target
|
|
1025
|
+
|
|
1026
|
+
nil_literal_value?(stmt.value) ? :falls_through_unassigned : :assigned
|
|
1027
|
+
when Prism::CallNode
|
|
1028
|
+
if unconditional_call_assigns?(stmt, target, class_name, effects, depth, visiting)
|
|
1029
|
+
:assigned
|
|
1030
|
+
else
|
|
1031
|
+
:falls_through_unassigned
|
|
1032
|
+
end
|
|
1033
|
+
when Prism::IfNode, Prism::UnlessNode
|
|
1034
|
+
conditional_assignment_outcome(stmt, target, class_name, effects, depth, visiting)
|
|
1035
|
+
when Prism::CaseNode
|
|
1036
|
+
case_assignment_outcome(stmt, target, class_name, effects, depth, visiting)
|
|
1037
|
+
when Prism::ReturnNode
|
|
1038
|
+
:terminates_unassigned
|
|
1039
|
+
else
|
|
1040
|
+
# Any other statement — including a bare `raise`/`fail`,
|
|
1041
|
+
# which terminates without a completing path that observes
|
|
1042
|
+
# the seed nil — is neutral: control either continues or the
|
|
1043
|
+
# path never reaches method exit. Keep scanning the suffix.
|
|
1044
|
+
:falls_through_unassigned
|
|
1045
|
+
end
|
|
1046
|
+
end
|
|
1047
|
+
|
|
1048
|
+
# True when a branch body (a StatementsNode / single node)
|
|
1049
|
+
# definitely assigns `target` non-nil on every path that
|
|
1050
|
+
# completes the method through it, OR terminates every path by
|
|
1051
|
+
# raise (vacuously safe — no completing path observes the seed
|
|
1052
|
+
# nil). Returns false if any path can complete/return without the
|
|
1053
|
+
# assignment.
|
|
1054
|
+
def branch_definitely_assigns?(branch, target, class_name, effects, depth, visiting)
|
|
1055
|
+
stmts = top_level_statements(branch)
|
|
1056
|
+
return false if stmts.empty?
|
|
1057
|
+
|
|
1058
|
+
stmts.each do |stmt|
|
|
1059
|
+
outcome = statement_assignment_outcome(stmt, target, class_name, effects, depth, visiting)
|
|
1060
|
+
return true if outcome == :assigned
|
|
1061
|
+
return false if outcome == :terminates_unassigned
|
|
1062
|
+
end
|
|
1063
|
+
# Reached the end of the branch without a definite assignment;
|
|
1064
|
+
# safe only if the branch's last statement always raises (no
|
|
1065
|
+
# completing path falls out of it).
|
|
1066
|
+
always_raises?(stmts.last)
|
|
1067
|
+
end
|
|
1068
|
+
|
|
1069
|
+
# `if`/`unless` is a definite assignment of `target` only when
|
|
1070
|
+
# BOTH the then and else arms definitely assign (or raise-out).
|
|
1071
|
+
# A missing else arm means the fall-through path skips the
|
|
1072
|
+
# assignment -> not definite. Modifier-form `if`/`unless` (no
|
|
1073
|
+
# else, single predicate'd statement) likewise.
|
|
1074
|
+
def conditional_assignment_outcome(node, target, class_name, effects, depth, visiting)
|
|
1075
|
+
else_branch = node.is_a?(Prism::IfNode) ? node.subsequent : node.else_clause
|
|
1076
|
+
return :falls_through_unassigned unless else_branch.is_a?(Prism::ElseNode)
|
|
1077
|
+
return :falls_through_unassigned unless node.statements
|
|
1078
|
+
|
|
1079
|
+
then_ok = branch_definitely_assigns?(node.statements, target, class_name, effects, depth, visiting)
|
|
1080
|
+
else_ok = branch_definitely_assigns?(else_branch.statements, target, class_name, effects, depth, visiting)
|
|
1081
|
+
then_ok && else_ok ? :assigned : :falls_through_unassigned
|
|
1082
|
+
end
|
|
1083
|
+
|
|
1084
|
+
# `case` is a definite assignment only when there is a real
|
|
1085
|
+
# `else` clause AND every `when`/`in` body plus the else body
|
|
1086
|
+
# definitely assigns (or raises-out). A missing else lets an
|
|
1087
|
+
# unmatched subject fall through unassigned.
|
|
1088
|
+
def case_assignment_outcome(node, target, class_name, effects, depth, visiting)
|
|
1089
|
+
else_clause = node.else_clause
|
|
1090
|
+
return :falls_through_unassigned unless else_clause.is_a?(Prism::ElseNode)
|
|
1091
|
+
|
|
1092
|
+
branches = node.conditions.map { |c| c.respond_to?(:statements) ? c.statements : nil }
|
|
1093
|
+
branches << else_clause.statements
|
|
1094
|
+
all_ok = branches.all? do |b|
|
|
1095
|
+
branch_definitely_assigns?(b, target, class_name, effects, depth, visiting)
|
|
1096
|
+
end
|
|
1097
|
+
all_ok ? :assigned : :falls_through_unassigned
|
|
1098
|
+
end
|
|
1099
|
+
|
|
1100
|
+
# True when `node` (a single statement or its last statement) is
|
|
1101
|
+
# an unconditional `raise`/`fail` call that always terminates the
|
|
1102
|
+
# path — used to treat raise-terminated branches as
|
|
1103
|
+
# non-completing (they never observe the seed nil).
|
|
1104
|
+
def always_raises?(node)
|
|
1105
|
+
node = top_level_statements(node).last if node.is_a?(Prism::StatementsNode)
|
|
1106
|
+
return false unless node.is_a?(Prism::CallNode)
|
|
1107
|
+
return false unless node.receiver.nil?
|
|
1108
|
+
|
|
1109
|
+
%i[raise fail].include?(node.name)
|
|
1110
|
+
end
|
|
1111
|
+
|
|
1112
|
+
# True when `call` is an unconditional, statement-level,
|
|
1113
|
+
# implicit-self (or `self.`) call to a SAME-CLASS method whose
|
|
1114
|
+
# definite-assignment summary includes `target`. Calls through a
|
|
1115
|
+
# block, on another receiver, or to an unresolved name contribute
|
|
1116
|
+
# nothing (the seed nil stays).
|
|
1117
|
+
def unconditional_call_assigns?(call, target, class_name, effects, depth, _visiting)
|
|
1118
|
+
return false if effects.nil? || class_name.nil?
|
|
1119
|
+
return false if depth >= SAME_CLASS_CALL_DEPTH_CAP
|
|
1120
|
+
return false unless call.is_a?(Prism::CallNode)
|
|
1121
|
+
return false unless call.block.nil?
|
|
1122
|
+
# Implicit self (`mask!(x)`) or explicit `self.mask!(x)` only.
|
|
1123
|
+
return false unless call.receiver.nil? || call.receiver.is_a?(Prism::SelfNode)
|
|
1124
|
+
|
|
1125
|
+
assigns = effects.dig(class_name, call.name)
|
|
1126
|
+
return false if assigns.nil?
|
|
1127
|
+
|
|
1128
|
+
assigns.include?(target)
|
|
1129
|
+
end
|
|
1130
|
+
|
|
726
1131
|
def record_ivar_write(node, scope, class_name, accumulator, guarded: false)
|
|
727
1132
|
rvalue_type = scope.type_of(node.value)
|
|
728
1133
|
|
|
@@ -747,10 +1152,104 @@ module Rigor
|
|
|
747
1152
|
return if guarded && falsey_constant?(rvalue_type)
|
|
748
1153
|
|
|
749
1154
|
rvalue_type = Type::Combinator.union(rvalue_type, Type::Combinator.constant_of(nil)) if guarded
|
|
1155
|
+
accumulate_ivar_type(accumulator, class_name, node.name, rvalue_type)
|
|
1156
|
+
end
|
|
1157
|
+
|
|
1158
|
+
# Unions `type` into the class-ivar accumulator for `(class_name,
|
|
1159
|
+
# ivar_name)`. Shared by the single-write and multi-write
|
|
1160
|
+
# (parallel-assignment) collectors.
|
|
1161
|
+
def accumulate_ivar_type(accumulator, class_name, ivar_name, type)
|
|
750
1162
|
accumulator[class_name] ||= {}
|
|
751
|
-
existing = accumulator[class_name][
|
|
752
|
-
accumulator[class_name][
|
|
753
|
-
existing ? Type::Combinator.union(existing,
|
|
1163
|
+
existing = accumulator[class_name][ivar_name]
|
|
1164
|
+
accumulator[class_name][ivar_name] =
|
|
1165
|
+
existing ? Type::Combinator.union(existing, type) : type
|
|
1166
|
+
end
|
|
1167
|
+
|
|
1168
|
+
# N1 — records each `InstanceVariableTargetNode` of a
|
|
1169
|
+
# `MultiWriteNode` (parallel / multiple assignment) into the
|
|
1170
|
+
# class-ivar union, with the best cheap per-slot type. When the RHS
|
|
1171
|
+
# is array/tuple-shaped (`Type::Tuple`) the ivar at position `i`
|
|
1172
|
+
# records the type of element `i`; otherwise — an unanalyzable RHS
|
|
1173
|
+
# such as `Open3.popen3(cmd)` typing to `Dynamic[top]` — every ivar
|
|
1174
|
+
# slot records that unanalyzable floor (NOT `nil`: a multi-write we
|
|
1175
|
+
# cannot decompose means the value is *unknown*, and `Dynamic[top]`
|
|
1176
|
+
# is the sound union constituent, mirroring what a single write to
|
|
1177
|
+
# an unknown RHS records). Nested targets (`(@a, @b), @c = …`)
|
|
1178
|
+
# recurse with the slot's type as the new RHS type.
|
|
1179
|
+
def record_multi_write_ivars(node, scope, class_name, accumulator)
|
|
1180
|
+
return unless node.is_a?(Prism::MultiWriteNode)
|
|
1181
|
+
|
|
1182
|
+
rhs_type = scope.type_of(node.value)
|
|
1183
|
+
record_multi_target_ivars(node, rhs_type, class_name, accumulator)
|
|
1184
|
+
end
|
|
1185
|
+
|
|
1186
|
+
# Walks a `MultiWriteNode` / `MultiTargetNode` target tree against
|
|
1187
|
+
# `rhs_type`, recording ivar targets per slot. Mirrors
|
|
1188
|
+
# `MultiTargetBinder`'s tuple decomposition but for ivar (rather
|
|
1189
|
+
# than local-variable) targets.
|
|
1190
|
+
def record_multi_target_ivars(node, rhs_type, class_name, accumulator)
|
|
1191
|
+
lefts = node.lefts || []
|
|
1192
|
+
rest = node.rest
|
|
1193
|
+
rights = node.rights || []
|
|
1194
|
+
fronts, rest_type, backs =
|
|
1195
|
+
decompose_multi_write_rhs(rhs_type, lefts.size, rights.size, rest_present: !rest.nil?)
|
|
1196
|
+
|
|
1197
|
+
lefts.each_with_index { |t, i| record_multi_ivar_target(t, fronts[i], class_name, accumulator) }
|
|
1198
|
+
record_multi_ivar_rest(rest, rest_type, class_name, accumulator) if rest
|
|
1199
|
+
rights.each_with_index { |t, i| record_multi_ivar_target(t, backs[i], class_name, accumulator) }
|
|
1200
|
+
end
|
|
1201
|
+
|
|
1202
|
+
def decompose_multi_write_rhs(rhs_type, front_count, back_count, rest_present:)
|
|
1203
|
+
if rhs_type.is_a?(Type::Tuple)
|
|
1204
|
+
elements = rhs_type.elements
|
|
1205
|
+
fronts = Array.new(front_count) { |i| multi_write_slot_type(elements, i) }
|
|
1206
|
+
if rest_present
|
|
1207
|
+
middle_end = [elements.size - back_count, front_count].max
|
|
1208
|
+
backs = Array.new(back_count) { |i| multi_write_slot_type(elements, middle_end + i) }
|
|
1209
|
+
[fronts, Type::Combinator.untyped, backs]
|
|
1210
|
+
else
|
|
1211
|
+
backs = Array.new(back_count) { |i| multi_write_slot_type(elements, front_count + i) }
|
|
1212
|
+
[fronts, nil, backs]
|
|
1213
|
+
end
|
|
1214
|
+
else
|
|
1215
|
+
# Unanalyzable / non-tuple RHS: every slot is the unknown floor.
|
|
1216
|
+
floor = Type::Combinator.untyped
|
|
1217
|
+
[Array.new(front_count) { floor }, rest_present ? floor : nil, Array.new(back_count) { floor }]
|
|
1218
|
+
end
|
|
1219
|
+
end
|
|
1220
|
+
|
|
1221
|
+
# The per-slot type for index `i` of a tuple RHS. A missing slot
|
|
1222
|
+
# (over-destructure) is `nil` at runtime; a present slot keeps its
|
|
1223
|
+
# type. Unlike the local-variable binder we do NOT soften an
|
|
1224
|
+
# optional slot here — a class-ivar seed deliberately preserves a
|
|
1225
|
+
# genuine `T | nil`, and any spurious nil is removed by the
|
|
1226
|
+
# flow-side narrowing, not by dropping it at collection time.
|
|
1227
|
+
def multi_write_slot_type(elements, index)
|
|
1228
|
+
element = elements[index]
|
|
1229
|
+
return Type::Combinator.constant_of(nil) if element.nil?
|
|
1230
|
+
|
|
1231
|
+
element
|
|
1232
|
+
end
|
|
1233
|
+
|
|
1234
|
+
def record_multi_ivar_target(target, type, class_name, accumulator)
|
|
1235
|
+
case target
|
|
1236
|
+
when Prism::InstanceVariableTargetNode
|
|
1237
|
+
accumulate_ivar_type(accumulator, class_name, target.name, type)
|
|
1238
|
+
when Prism::MultiTargetNode
|
|
1239
|
+
record_multi_target_ivars(target, type, class_name, accumulator)
|
|
1240
|
+
end
|
|
1241
|
+
end
|
|
1242
|
+
|
|
1243
|
+
def record_multi_ivar_rest(splat_node, _type, class_name, accumulator)
|
|
1244
|
+
return unless splat_node.is_a?(Prism::SplatNode)
|
|
1245
|
+
|
|
1246
|
+
expression = splat_node.expression
|
|
1247
|
+
return unless expression.is_a?(Prism::InstanceVariableTargetNode)
|
|
1248
|
+
|
|
1249
|
+
# A splat collects the middle slots into an Array; the precise
|
|
1250
|
+
# element type is not worth recovering here. Record the
|
|
1251
|
+
# unanalyzable floor (an Array of unknown), never nil.
|
|
1252
|
+
accumulate_ivar_type(accumulator, class_name, expression.name, Type::Combinator.untyped)
|
|
754
1253
|
end
|
|
755
1254
|
|
|
756
1255
|
def falsey_constant?(type)
|
|
@@ -775,7 +1274,7 @@ module Rigor
|
|
|
775
1274
|
|
|
776
1275
|
case node
|
|
777
1276
|
when Prism::ClassNode, Prism::ModuleNode
|
|
778
|
-
name =
|
|
1277
|
+
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
779
1278
|
if name
|
|
780
1279
|
child_prefix = qualified_prefix + [name]
|
|
781
1280
|
walk_class_cvars(node.body, child_prefix, default_scope, accumulator) if node.body
|
|
@@ -861,7 +1360,7 @@ module Rigor
|
|
|
861
1360
|
|
|
862
1361
|
case node
|
|
863
1362
|
when Prism::ClassNode, Prism::ModuleNode
|
|
864
|
-
name =
|
|
1363
|
+
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
865
1364
|
if name
|
|
866
1365
|
child_prefix = qualified_prefix + [name]
|
|
867
1366
|
walk_constant_writes(node.body, child_prefix, default_scope, accumulator) if node.body
|
|
@@ -871,7 +1370,7 @@ module Rigor
|
|
|
871
1370
|
record_constant_write(node, qualified_prefix, default_scope, accumulator, node.name.to_s)
|
|
872
1371
|
return
|
|
873
1372
|
when Prism::ConstantPathWriteNode
|
|
874
|
-
full =
|
|
1373
|
+
full = Source::ConstantPath.qualified_name(node.target)
|
|
875
1374
|
record_constant_write(node, [], default_scope, accumulator, full) if full
|
|
876
1375
|
return
|
|
877
1376
|
end
|
|
@@ -937,7 +1436,7 @@ module Rigor
|
|
|
937
1436
|
|
|
938
1437
|
case node
|
|
939
1438
|
when Prism::ClassNode, Prism::ModuleNode
|
|
940
|
-
name =
|
|
1439
|
+
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
941
1440
|
if name
|
|
942
1441
|
child_prefix = qualified_prefix + [name]
|
|
943
1442
|
record_meta_superclass_members(node, child_prefix, accumulator) if node.is_a?(Prism::ClassNode)
|
|
@@ -990,7 +1489,7 @@ module Rigor
|
|
|
990
1489
|
when Prism::SelfNode
|
|
991
1490
|
qualified_prefix
|
|
992
1491
|
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
993
|
-
rendered =
|
|
1492
|
+
rendered = Source::ConstantPath.qualified_name(node.expression)
|
|
994
1493
|
return nil unless rendered
|
|
995
1494
|
|
|
996
1495
|
if !qualified_prefix.empty? && qualified_prefix.last == rendered
|
|
@@ -1097,7 +1596,7 @@ module Rigor
|
|
|
1097
1596
|
when Prism::ConstantReadNode
|
|
1098
1597
|
receiver.name.to_s == qualified_prefix.last
|
|
1099
1598
|
when Prism::ConstantPathNode
|
|
1100
|
-
rendered =
|
|
1599
|
+
rendered = Source::ConstantPath.render(receiver)
|
|
1101
1600
|
return false unless rendered
|
|
1102
1601
|
|
|
1103
1602
|
path = rendered.split("::")
|
|
@@ -1129,7 +1628,7 @@ module Rigor
|
|
|
1129
1628
|
|
|
1130
1629
|
case node
|
|
1131
1630
|
when Prism::ClassNode, Prism::ModuleNode
|
|
1132
|
-
name =
|
|
1631
|
+
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
1133
1632
|
if name
|
|
1134
1633
|
child_prefix = qualified_prefix + [name]
|
|
1135
1634
|
walk_def_nodes(node.body, child_prefix, false, accumulator) if node.body
|
|
@@ -1175,6 +1674,146 @@ module Rigor
|
|
|
1175
1674
|
accumulator[class_name][def_node.name] = def_node
|
|
1176
1675
|
end
|
|
1177
1676
|
|
|
1677
|
+
# Module-singleton call resolution (ADR-57 follow-up) — the
|
|
1678
|
+
# SINGLETON-side mirror of `build_discovered_def_nodes`. Records the
|
|
1679
|
+
# `Prism::DefNode` for every singleton-side method (`def self.x`,
|
|
1680
|
+
# `def Foo.x`, a `class << self` body, and a `module_function`
|
|
1681
|
+
# method) keyed by qualified class/module name → method → node, so
|
|
1682
|
+
# `ExpressionTyper` can re-type the body when a `Singleton[Foo]`
|
|
1683
|
+
# receiver dispatches `Foo.x`. The instance-side table is kept
|
|
1684
|
+
# singleton-free on purpose (its ancestor walk binds `self` as
|
|
1685
|
+
# `Nominal`), so the two never overlap except for `module_function`
|
|
1686
|
+
# defs, which are genuinely callable on both sides and so appear in
|
|
1687
|
+
# both tables. Top-level singleton defs (`def self.x` outside any
|
|
1688
|
+
# class — `self` is `main`) are not recorded; they have no constant
|
|
1689
|
+
# receiver to dispatch through.
|
|
1690
|
+
def build_discovered_singleton_def_nodes(root)
|
|
1691
|
+
accumulator = {}
|
|
1692
|
+
walk_singleton_def_nodes(root, [], false, accumulator)
|
|
1693
|
+
accumulator.transform_values(&:freeze).freeze
|
|
1694
|
+
end
|
|
1695
|
+
|
|
1696
|
+
# Walks every node, entering class/module/singleton-class bodies via
|
|
1697
|
+
# {#walk_singleton_body} so a bare `module_function` toggle threads
|
|
1698
|
+
# correctly across the body's *sibling* statements (a child-by-child
|
|
1699
|
+
# recursion would reset it). At the top level / inside an arbitrary
|
|
1700
|
+
# node there is no `module_function` state to carry, so descent is a
|
|
1701
|
+
# plain per-child walk.
|
|
1702
|
+
def walk_singleton_def_nodes(node, qualified_prefix, in_singleton_class, accumulator)
|
|
1703
|
+
return unless node.is_a?(Prism::Node)
|
|
1704
|
+
|
|
1705
|
+
case node
|
|
1706
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
1707
|
+
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
1708
|
+
if name
|
|
1709
|
+
walk_singleton_body(node.body, qualified_prefix + [name], false, accumulator) if node.body
|
|
1710
|
+
return
|
|
1711
|
+
end
|
|
1712
|
+
when Prism::SingletonClassNode
|
|
1713
|
+
if node.body
|
|
1714
|
+
singleton_prefix = singleton_class_prefix(node, qualified_prefix)
|
|
1715
|
+
if singleton_prefix
|
|
1716
|
+
walk_singleton_body(node.body, singleton_prefix, true, accumulator)
|
|
1717
|
+
return
|
|
1718
|
+
end
|
|
1719
|
+
end
|
|
1720
|
+
when Prism::ConstantWriteNode
|
|
1721
|
+
if meta_new_block_body(node)
|
|
1722
|
+
child_prefix = qualified_prefix + [node.name.to_s]
|
|
1723
|
+
walk_singleton_body(meta_new_block_body(node), child_prefix, false, accumulator)
|
|
1724
|
+
return
|
|
1725
|
+
end
|
|
1726
|
+
when Prism::DefNode
|
|
1727
|
+
record_singleton_def_node(node, qualified_prefix, in_singleton_class, false, accumulator)
|
|
1728
|
+
return
|
|
1729
|
+
end
|
|
1730
|
+
|
|
1731
|
+
node.compact_child_nodes.each do |child|
|
|
1732
|
+
walk_singleton_def_nodes(child, qualified_prefix, in_singleton_class, accumulator)
|
|
1733
|
+
end
|
|
1734
|
+
end
|
|
1735
|
+
|
|
1736
|
+
# Walks a class/module/singleton-class body's direct statements in
|
|
1737
|
+
# source order, threading the bare-`module_function` toggle: once a
|
|
1738
|
+
# bare `module_function` is seen, every subsequent `def` in the body
|
|
1739
|
+
# registers as a singleton method. Nested classes/modules/defs and
|
|
1740
|
+
# `module_function :a, :b` named forms recurse / record through the
|
|
1741
|
+
# general walker so the toggle stays scoped to its own body.
|
|
1742
|
+
def walk_singleton_body(body, qualified_prefix, in_singleton_class, accumulator)
|
|
1743
|
+
module_function_on = false
|
|
1744
|
+
statements_of(body).each do |stmt|
|
|
1745
|
+
if stmt.is_a?(Prism::CallNode) && module_function_toggle?(stmt)
|
|
1746
|
+
if bare_module_function?(stmt)
|
|
1747
|
+
module_function_on = true
|
|
1748
|
+
else
|
|
1749
|
+
record_module_function_names(stmt, qualified_prefix, body, accumulator)
|
|
1750
|
+
end
|
|
1751
|
+
next
|
|
1752
|
+
end
|
|
1753
|
+
if stmt.is_a?(Prism::DefNode)
|
|
1754
|
+
record_singleton_def_node(stmt, qualified_prefix, in_singleton_class, module_function_on, accumulator)
|
|
1755
|
+
next
|
|
1756
|
+
end
|
|
1757
|
+
walk_singleton_def_nodes(stmt, qualified_prefix, in_singleton_class, accumulator)
|
|
1758
|
+
end
|
|
1759
|
+
end
|
|
1760
|
+
|
|
1761
|
+
# Direct statement children of a class/module body node (a
|
|
1762
|
+
# `Prism::StatementsNode`, a `Prism::BeginNode` wrapping one, or a
|
|
1763
|
+
# lone statement). Returns an empty list for an empty body.
|
|
1764
|
+
def statements_of(body)
|
|
1765
|
+
case body
|
|
1766
|
+
when Prism::StatementsNode then body.body
|
|
1767
|
+
when Prism::BeginNode then statements_of(body.statements)
|
|
1768
|
+
when nil then []
|
|
1769
|
+
else [body]
|
|
1770
|
+
end
|
|
1771
|
+
end
|
|
1772
|
+
|
|
1773
|
+
def record_singleton_def_node(def_node, qualified_prefix, in_singleton_class, module_function_on, accumulator)
|
|
1774
|
+
singleton = def_singleton?(def_node, qualified_prefix, in_singleton_class) || module_function_on
|
|
1775
|
+
return unless singleton
|
|
1776
|
+
return if qualified_prefix.empty?
|
|
1777
|
+
|
|
1778
|
+
class_name = qualified_prefix.join("::")
|
|
1779
|
+
(accumulator[class_name] ||= {})[def_node.name] = def_node
|
|
1780
|
+
end
|
|
1781
|
+
|
|
1782
|
+
# A bare `module_function` (no arguments) flips every following `def`
|
|
1783
|
+
# in the module body to module-function (instance + singleton) mode.
|
|
1784
|
+
def module_function_toggle?(node)
|
|
1785
|
+
node.name == :module_function && node.receiver.nil?
|
|
1786
|
+
end
|
|
1787
|
+
|
|
1788
|
+
def bare_module_function?(node)
|
|
1789
|
+
node.arguments.nil? || node.arguments.arguments.empty?
|
|
1790
|
+
end
|
|
1791
|
+
|
|
1792
|
+
# `module_function :a, :b` retro-marks named siblings (defined
|
|
1793
|
+
# earlier OR later in the same body) as module-functions. Resolves
|
|
1794
|
+
# each symbol-literal argument against the body's own `def`s and
|
|
1795
|
+
# registers the matching `DefNode` on the module's singleton side.
|
|
1796
|
+
# Non-symbol arguments and names with no matching `def` are skipped
|
|
1797
|
+
# (a miss degrades to today's `Dynamic`, never a false resolution).
|
|
1798
|
+
def record_module_function_names(node, qualified_prefix, body, accumulator)
|
|
1799
|
+
return if qualified_prefix.empty?
|
|
1800
|
+
|
|
1801
|
+
defs_by_name = statements_of(body).each_with_object({}) do |stmt, acc|
|
|
1802
|
+
acc[stmt.name] = stmt if stmt.is_a?(Prism::DefNode) && stmt.receiver.nil?
|
|
1803
|
+
end
|
|
1804
|
+
class_name = qualified_prefix.join("::")
|
|
1805
|
+
node.arguments&.arguments&.each do |arg|
|
|
1806
|
+
name = symbol_argument_name(arg)
|
|
1807
|
+
def_node = name && defs_by_name[name]
|
|
1808
|
+
(accumulator[class_name] ||= {})[name] = def_node if def_node
|
|
1809
|
+
end
|
|
1810
|
+
end
|
|
1811
|
+
|
|
1812
|
+
# The Symbol value of a `:name` / `"name"` literal argument, or nil.
|
|
1813
|
+
def symbol_argument_name(arg)
|
|
1814
|
+
arg.unescaped.to_sym if arg.is_a?(Prism::SymbolNode) || arg.is_a?(Prism::StringNode)
|
|
1815
|
+
end
|
|
1816
|
+
|
|
1178
1817
|
# ADR-24 slice 2 — per-class table mapping a fully
|
|
1179
1818
|
# qualified user class to its superclass name AS WRITTEN
|
|
1180
1819
|
# at the `class Foo < Bar` declaration. Only constant
|
|
@@ -1194,16 +1833,16 @@ module Rigor
|
|
|
1194
1833
|
|
|
1195
1834
|
case node
|
|
1196
1835
|
when Prism::ClassNode
|
|
1197
|
-
name =
|
|
1836
|
+
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
1198
1837
|
if name
|
|
1199
1838
|
full = (qualified_prefix + [name]).join("::")
|
|
1200
|
-
superclass = node.superclass &&
|
|
1839
|
+
superclass = node.superclass && Source::ConstantPath.qualified_name(node.superclass)
|
|
1201
1840
|
accumulator[full] = superclass if superclass
|
|
1202
1841
|
walk_class_superclasses(node.body, qualified_prefix + [name], accumulator) if node.body
|
|
1203
1842
|
return
|
|
1204
1843
|
end
|
|
1205
1844
|
when Prism::ModuleNode
|
|
1206
|
-
name =
|
|
1845
|
+
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
1207
1846
|
if name
|
|
1208
1847
|
walk_class_superclasses(node.body, qualified_prefix + [name], accumulator) if node.body
|
|
1209
1848
|
return
|
|
@@ -1235,14 +1874,14 @@ module Rigor
|
|
|
1235
1874
|
|
|
1236
1875
|
case node
|
|
1237
1876
|
when Prism::ClassNode
|
|
1238
|
-
name =
|
|
1877
|
+
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
1239
1878
|
if name
|
|
1240
1879
|
record_data_member_layout(accumulator, qualified_prefix + [name], node.superclass)
|
|
1241
1880
|
walk_data_member_layouts(node.body, qualified_prefix + [name], accumulator) if node.body
|
|
1242
1881
|
return
|
|
1243
1882
|
end
|
|
1244
1883
|
when Prism::ModuleNode
|
|
1245
|
-
name =
|
|
1884
|
+
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
1246
1885
|
if name
|
|
1247
1886
|
walk_data_member_layouts(node.body, qualified_prefix + [name], accumulator) if node.body
|
|
1248
1887
|
return
|
|
@@ -1267,6 +1906,74 @@ module Rigor
|
|
|
1267
1906
|
accumulator[qualified_parts.join("::")] = members.freeze
|
|
1268
1907
|
end
|
|
1269
1908
|
|
|
1909
|
+
# ADR-48 Struct follow-up — the `Struct.new(...)` sibling of
|
|
1910
|
+
# {#build_data_member_layouts}. A separate, additive table so the
|
|
1911
|
+
# existing `Data.define` value-shape contract (a bare `[Symbol]`) is
|
|
1912
|
+
# untouched: a Struct entry carries `{ members:, keyword_init: }`
|
|
1913
|
+
# because the dispatcher needs the flag to fold the matching `.new`
|
|
1914
|
+
# call form (positional vs keyword) without manufacturing a wrong map.
|
|
1915
|
+
def build_struct_member_layouts(root)
|
|
1916
|
+
accumulator = {}
|
|
1917
|
+
walk_struct_member_layouts(root, [], accumulator)
|
|
1918
|
+
accumulator.freeze
|
|
1919
|
+
end
|
|
1920
|
+
|
|
1921
|
+
def walk_struct_member_layouts(node, qualified_prefix, accumulator)
|
|
1922
|
+
return unless node.is_a?(Prism::Node)
|
|
1923
|
+
|
|
1924
|
+
case node
|
|
1925
|
+
when Prism::ClassNode
|
|
1926
|
+
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
1927
|
+
if name
|
|
1928
|
+
record_struct_member_layout(accumulator, qualified_prefix + [name], node.superclass)
|
|
1929
|
+
walk_struct_member_layouts(node.body, qualified_prefix + [name], accumulator) if node.body
|
|
1930
|
+
return
|
|
1931
|
+
end
|
|
1932
|
+
when Prism::ModuleNode
|
|
1933
|
+
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
1934
|
+
if name
|
|
1935
|
+
walk_struct_member_layouts(node.body, qualified_prefix + [name], accumulator) if node.body
|
|
1936
|
+
return
|
|
1937
|
+
end
|
|
1938
|
+
when Prism::ConstantWriteNode
|
|
1939
|
+
record_struct_member_layout(accumulator, qualified_prefix + [node.name.to_s], node.value)
|
|
1940
|
+
end
|
|
1941
|
+
|
|
1942
|
+
node.compact_child_nodes.each do |child|
|
|
1943
|
+
walk_struct_member_layouts(child, qualified_prefix, accumulator)
|
|
1944
|
+
end
|
|
1945
|
+
end
|
|
1946
|
+
|
|
1947
|
+
# Records `qualified -> { members:, keyword_init: }` when `expr` is a
|
|
1948
|
+
# `Struct.new(*Symbol [, keyword_init: <bool>])` call with at least one
|
|
1949
|
+
# literal-Symbol member.
|
|
1950
|
+
def record_struct_member_layout(accumulator, qualified_parts, expr)
|
|
1951
|
+
return unless struct_new_call?(expr)
|
|
1952
|
+
|
|
1953
|
+
members = meta_member_names(expr)
|
|
1954
|
+
return if members.empty?
|
|
1955
|
+
|
|
1956
|
+
accumulator[qualified_parts.join("::")] = {
|
|
1957
|
+
members: members.freeze,
|
|
1958
|
+
keyword_init: struct_new_keyword_init?(expr)
|
|
1959
|
+
}.freeze
|
|
1960
|
+
end
|
|
1961
|
+
|
|
1962
|
+
# True when a `Struct.new` call carries `keyword_init: true` as a
|
|
1963
|
+
# literal in its trailing keyword hash. A non-literal value (or its
|
|
1964
|
+
# absence) reads as `false` — the conservative positional default.
|
|
1965
|
+
def struct_new_keyword_init?(call_node)
|
|
1966
|
+
args = call_node.arguments&.arguments || []
|
|
1967
|
+
last = args.last
|
|
1968
|
+
return false unless last.is_a?(Prism::KeywordHashNode)
|
|
1969
|
+
|
|
1970
|
+
last.elements.any? do |assoc|
|
|
1971
|
+
assoc.is_a?(Prism::AssocNode) &&
|
|
1972
|
+
assoc.key.is_a?(Prism::SymbolNode) && assoc.key.unescaped == "keyword_init" &&
|
|
1973
|
+
assoc.value.is_a?(Prism::TrueNode)
|
|
1974
|
+
end
|
|
1975
|
+
end
|
|
1976
|
+
|
|
1270
1977
|
MIXIN_CALL_NAMES = %i[include prepend].freeze
|
|
1271
1978
|
|
|
1272
1979
|
# ADR-24 slice 2 — per-class/module table mapping a fully
|
|
@@ -1290,7 +1997,7 @@ module Rigor
|
|
|
1290
1997
|
|
|
1291
1998
|
case node
|
|
1292
1999
|
when Prism::ClassNode, Prism::ModuleNode
|
|
1293
|
-
name =
|
|
2000
|
+
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
1294
2001
|
if name
|
|
1295
2002
|
full = (qualified_prefix + [name]).join("::")
|
|
1296
2003
|
walk_class_includes(node.body, qualified_prefix + [name], full, accumulator) if node.body
|
|
@@ -1310,7 +2017,7 @@ module Rigor
|
|
|
1310
2017
|
return unless MIXIN_CALL_NAMES.include?(node.name)
|
|
1311
2018
|
|
|
1312
2019
|
node.arguments&.arguments&.each do |arg|
|
|
1313
|
-
mod =
|
|
2020
|
+
mod = Source::ConstantPath.qualified_name(arg)
|
|
1314
2021
|
(accumulator[current_class] ||= []) << mod if mod
|
|
1315
2022
|
end
|
|
1316
2023
|
end
|
|
@@ -1350,7 +2057,7 @@ module Rigor
|
|
|
1350
2057
|
|
|
1351
2058
|
case node
|
|
1352
2059
|
when Prism::ClassNode, Prism::ModuleNode
|
|
1353
|
-
name =
|
|
2060
|
+
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
1354
2061
|
if name
|
|
1355
2062
|
child_prefix = qualified_prefix + [name]
|
|
1356
2063
|
walk_method_visibilities(node.body, child_prefix, false, :public, accumulator) if node.body
|
|
@@ -1488,7 +2195,7 @@ module Rigor
|
|
|
1488
2195
|
|
|
1489
2196
|
case node
|
|
1490
2197
|
when Prism::ClassNode, Prism::ModuleNode
|
|
1491
|
-
name =
|
|
2198
|
+
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
1492
2199
|
if name
|
|
1493
2200
|
collect_class_alias_map(node.body, qualified_prefix + [name], accumulator) if node.body
|
|
1494
2201
|
return accumulator
|
|
@@ -1635,8 +2342,9 @@ module Rigor
|
|
|
1635
2342
|
# @return [Hash{Symbol => Hash}]
|
|
1636
2343
|
# `{ def_nodes:, def_sources:, superclasses:, includes:, class_sources: }`
|
|
1637
2344
|
def discovered_def_index_for_paths(paths, buffer: nil)
|
|
1638
|
-
acc = { def_nodes: {},
|
|
1639
|
-
class_sources: {}, data_member_layouts: {}
|
|
2345
|
+
acc = { def_nodes: {}, singleton_def_nodes: {}, def_sources: {}, superclasses: {}, includes: {},
|
|
2346
|
+
method_visibilities: {}, methods: {}, class_sources: {}, data_member_layouts: {},
|
|
2347
|
+
struct_member_layouts: {} }
|
|
1640
2348
|
paths.each do |path|
|
|
1641
2349
|
physical = buffer ? buffer.resolve(path) : path
|
|
1642
2350
|
root = Prism.parse(File.read(physical), filepath: path).value
|
|
@@ -1655,7 +2363,7 @@ module Rigor
|
|
|
1655
2363
|
# intact while still letting `attr_reader :x` in one file
|
|
1656
2364
|
# suppress a false undefined-method for `obj.x` in another.
|
|
1657
2365
|
acc[:methods] = subtract_def_methods(acc[:methods], acc[:def_nodes])
|
|
1658
|
-
%i[def_nodes def_sources includes method_visibilities methods class_sources].each do |key|
|
|
2366
|
+
%i[def_nodes singleton_def_nodes def_sources includes method_visibilities methods class_sources].each do |key|
|
|
1659
2367
|
acc[key].each_value(&:freeze)
|
|
1660
2368
|
end
|
|
1661
2369
|
acc.transform_values(&:freeze)
|
|
@@ -1678,6 +2386,9 @@ module Rigor
|
|
|
1678
2386
|
# visibility declared in a sibling file.
|
|
1679
2387
|
def accumulate_project_index(acc, path, root)
|
|
1680
2388
|
merge_discovered_defs(acc[:def_nodes], acc[:def_sources], path, root)
|
|
2389
|
+
build_discovered_singleton_def_nodes(root).each do |class_name, methods|
|
|
2390
|
+
(acc[:singleton_def_nodes][class_name] ||= {}).merge!(methods)
|
|
2391
|
+
end
|
|
1681
2392
|
superclasses = build_discovered_superclasses(root)
|
|
1682
2393
|
includes = build_discovered_includes(root)
|
|
1683
2394
|
acc[:superclasses].merge!(superclasses)
|
|
@@ -1687,6 +2398,7 @@ module Rigor
|
|
|
1687
2398
|
record_class_sources(acc[:class_sources], path, root, superclasses, includes)
|
|
1688
2399
|
merge_class_keyed_index_tables(acc, root)
|
|
1689
2400
|
acc[:data_member_layouts].merge!(build_data_member_layouts(root))
|
|
2401
|
+
acc[:struct_member_layouts].merge!(build_struct_member_layouts(root))
|
|
1690
2402
|
end
|
|
1691
2403
|
|
|
1692
2404
|
# Folds the per-class method-visibility and method-existence tables of
|
|
@@ -1745,20 +2457,72 @@ module Rigor
|
|
|
1745
2457
|
|
|
1746
2458
|
case node
|
|
1747
2459
|
when Prism::ClassNode
|
|
1748
|
-
name =
|
|
2460
|
+
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
1749
2461
|
if name
|
|
1750
2462
|
full = (qualified_prefix + [name]).join("::")
|
|
1751
2463
|
accumulator[full] = Type::Combinator.singleton_of(full)
|
|
1752
2464
|
return collect_class_decls(node.body, qualified_prefix + [name], accumulator) if node.body
|
|
1753
2465
|
end
|
|
1754
2466
|
when Prism::ModuleNode
|
|
1755
|
-
name =
|
|
2467
|
+
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
1756
2468
|
return collect_class_decls(node.body, qualified_prefix + [name], accumulator) if name && node.body
|
|
2469
|
+
when Prism::ConstantWriteNode
|
|
2470
|
+
record_class_new_constant_decl(node, qualified_prefix, accumulator)
|
|
1757
2471
|
end
|
|
1758
2472
|
|
|
1759
2473
|
node.compact_child_nodes.each { |child| collect_class_decls(child, qualified_prefix, accumulator) }
|
|
1760
2474
|
end
|
|
1761
2475
|
|
|
2476
|
+
# T1 (template-corpora survey) — record a `Const = Class.new(Super)`
|
|
2477
|
+
# (and the bare `Class.new` / `Module.new`) class-creating constant
|
|
2478
|
+
# in the cross-file discovery table so a reference to `Const` from
|
|
2479
|
+
# ANOTHER file under the same namespace resolves to the project
|
|
2480
|
+
# class instead of falling through to a core same-named class
|
|
2481
|
+
# (`Liquid::SyntaxError = Class.new(Error)` referenced in a sibling
|
|
2482
|
+
# file's `rescue SyntaxError => e`, which otherwise resolved to core
|
|
2483
|
+
# `::SyntaxError`). Mirrors the single-file `in_source_constants`
|
|
2484
|
+
# answer, which types `Class.new(Super)` as `Singleton[Super]` (the
|
|
2485
|
+
# constructed class answers method lookups through Super's chain).
|
|
2486
|
+
# The superclass name is resolved lexically against the enclosing
|
|
2487
|
+
# prefix; a bare `Class.new` with no superclass (or `Module.new`)
|
|
2488
|
+
# types as `Singleton[Const]` itself. The block form is left to the
|
|
2489
|
+
# existing `meta_new_block_body` machinery — only the plain
|
|
2490
|
+
# `Class.new(Super)` constant (the namespaced-sibling-error idiom)
|
|
2491
|
+
# is added here.
|
|
2492
|
+
def record_class_new_constant_decl(node, qualified_prefix, accumulator)
|
|
2493
|
+
rvalue = node.value
|
|
2494
|
+
return unless class_new_call?(rvalue) || module_new_call?(rvalue)
|
|
2495
|
+
return if rvalue.block # block form: handled by meta_new_block_body walks
|
|
2496
|
+
|
|
2497
|
+
full = (qualified_prefix + [node.name.to_s]).join("::")
|
|
2498
|
+
super_name = class_new_superclass_name(rvalue, qualified_prefix, accumulator)
|
|
2499
|
+
accumulator[full] = Type::Combinator.singleton_of(super_name || full)
|
|
2500
|
+
end
|
|
2501
|
+
|
|
2502
|
+
# Lexically-qualified name of a `Class.new(Super)` superclass
|
|
2503
|
+
# argument, or nil when there is no positional superclass (a bare
|
|
2504
|
+
# `Class.new` / `Module.new`). When the unqualified super name is a
|
|
2505
|
+
# class already discovered under an enclosing-prefix segment, the
|
|
2506
|
+
# qualified form is returned (so `Class.new(Error)` inside `module M`
|
|
2507
|
+
# resolves to `M::Error`); otherwise the literal name is returned
|
|
2508
|
+
# (covering a core / RBS-known superclass spelled bare).
|
|
2509
|
+
def class_new_superclass_name(call_node, qualified_prefix, accumulator)
|
|
2510
|
+
arg = call_node.arguments&.arguments&.first
|
|
2511
|
+
return nil if arg.nil?
|
|
2512
|
+
|
|
2513
|
+
raw = Source::ConstantPath.qualified_name(arg)
|
|
2514
|
+
return nil if raw.nil?
|
|
2515
|
+
|
|
2516
|
+
prefix = qualified_prefix.dup
|
|
2517
|
+
until prefix.empty?
|
|
2518
|
+
candidate = (prefix + [raw]).join("::")
|
|
2519
|
+
return candidate if accumulator.key?(candidate)
|
|
2520
|
+
|
|
2521
|
+
prefix.pop
|
|
2522
|
+
end
|
|
2523
|
+
raw
|
|
2524
|
+
end
|
|
2525
|
+
|
|
1762
2526
|
# Walks the program once for `Prism::ModuleNode` and
|
|
1763
2527
|
# `Prism::ClassNode`, recording the `Singleton[<qualified>]`
|
|
1764
2528
|
# type for the outermost `constant_path` node of each
|
|
@@ -1791,7 +2555,7 @@ module Rigor
|
|
|
1791
2555
|
end
|
|
1792
2556
|
|
|
1793
2557
|
def record_class_or_module?(node, qualified_prefix, identity_table, discovered)
|
|
1794
|
-
name =
|
|
2558
|
+
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
1795
2559
|
return false unless name
|
|
1796
2560
|
|
|
1797
2561
|
full = (qualified_prefix + [name]).join("::")
|
|
@@ -1903,25 +2667,6 @@ module Rigor
|
|
|
1903
2667
|
end
|
|
1904
2668
|
end
|
|
1905
2669
|
|
|
1906
|
-
def qualified_name_for(constant_path_node)
|
|
1907
|
-
case constant_path_node
|
|
1908
|
-
when Prism::ConstantReadNode
|
|
1909
|
-
constant_path_node.name.to_s
|
|
1910
|
-
when Prism::ConstantPathNode
|
|
1911
|
-
render_constant_path(constant_path_node)
|
|
1912
|
-
end
|
|
1913
|
-
end
|
|
1914
|
-
|
|
1915
|
-
def render_constant_path(node)
|
|
1916
|
-
prefix =
|
|
1917
|
-
case node.parent
|
|
1918
|
-
when Prism::ConstantReadNode then "#{node.parent.name}::"
|
|
1919
|
-
when Prism::ConstantPathNode then "#{render_constant_path(node.parent)}::"
|
|
1920
|
-
else ""
|
|
1921
|
-
end
|
|
1922
|
-
"#{prefix}#{node.name}"
|
|
1923
|
-
end
|
|
1924
|
-
|
|
1925
2670
|
# Walks `node`'s subtree DFS and fills in scope entries for every
|
|
1926
2671
|
# Prism node the StatementEvaluator did not visit (i.e. expression-
|
|
1927
2672
|
# interior nodes like the receiver/args of a CallNode). Those
|