rigortype 0.2.9 → 0.3.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 +2 -2
- data/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +18 -9
- data/docs/handbook/08-understanding-errors.md +1 -0
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +98 -4
- data/docs/manual/02-cli-reference.md +49 -12
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +23 -0
- data/docs/manual/07-plugins.md +4 -4
- data/docs/manual/09-editor-integration.md +4 -4
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +93 -0
- data/lib/rigor/analysis/check_rules.rb +557 -95
- data/lib/rigor/analysis/dependency_recorder.rb +93 -9
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +456 -51
- data/lib/rigor/analysis/path_expansion.rb +42 -0
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
- data/lib/rigor/analysis/rule_catalog.rb +226 -1
- data/lib/rigor/analysis/run_cache_key.rb +68 -0
- data/lib/rigor/analysis/run_cache_probe.rb +72 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
- data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
- data/lib/rigor/analysis/runner.rb +343 -68
- data/lib/rigor/analysis/severity_stamp.rb +43 -0
- data/lib/rigor/analysis/worker_session.rb +30 -2
- data/lib/rigor/bleeding_edge.rb +37 -8
- data/lib/rigor/builtins/regex_refinement.rb +36 -1
- data/lib/rigor/cache/descriptor.rb +88 -28
- data/lib/rigor/cache/file_digest.rb +158 -0
- data/lib/rigor/cache/incremental_snapshot.rb +41 -5
- data/lib/rigor/cache/rbs_descriptor.rb +56 -10
- data/lib/rigor/cache/store.rb +33 -3
- data/lib/rigor/ci_detector.rb +90 -0
- data/lib/rigor/cli/annotate_command.rb +15 -9
- data/lib/rigor/cli/baseline_command.rb +4 -1
- data/lib/rigor/cli/check_command.rb +212 -27
- data/lib/rigor/cli/coverage_command.rb +6 -1
- data/lib/rigor/cli/docs_command.rb +0 -30
- data/lib/rigor/cli/doctor_command.rb +163 -23
- data/lib/rigor/cli/lsp_command.rb +5 -0
- data/lib/rigor/cli/mcp_command.rb +5 -0
- data/lib/rigor/cli/plugins_command.rb +36 -6
- data/lib/rigor/cli/plugins_renderer.rb +44 -12
- data/lib/rigor/cli/probe_environment.rb +85 -0
- data/lib/rigor/cli/sig_gen_command.rb +36 -9
- data/lib/rigor/cli/skill_command.rb +1 -32
- data/lib/rigor/cli/trace_command.rb +5 -6
- data/lib/rigor/cli/type_of_command.rb +9 -8
- data/lib/rigor/cli/type_scan_command.rb +8 -8
- data/lib/rigor/cli.rb +15 -4
- data/lib/rigor/config_audit.rb +48 -2
- data/lib/rigor/configuration/severity_profile.rb +31 -0
- data/lib/rigor/configuration.rb +156 -8
- data/lib/rigor/environment/default_libraries.rb +35 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +19 -27
- data/lib/rigor/inference/budget_trace.rb +77 -2
- data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
- data/lib/rigor/inference/def_handle.rb +23 -0
- data/lib/rigor/inference/def_node_resolver.rb +90 -0
- data/lib/rigor/inference/def_return_typer.rb +2 -1
- data/lib/rigor/inference/expression_typer.rb +361 -113
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +11 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
- data/lib/rigor/inference/method_dispatcher.rb +57 -1
- data/lib/rigor/inference/mutation_widening.rb +42 -8
- data/lib/rigor/inference/narrowing.rb +127 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +15 -0
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +8 -1
- data/lib/rigor/inference/scope_indexer.rb +451 -36
- data/lib/rigor/inference/statement_evaluator.rb +64 -15
- data/lib/rigor/inference/struct_fold_safety.rb +48 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
- data/lib/rigor/inference/void_origin.rb +25 -0
- data/lib/rigor/inference/void_tail_summary.rb +220 -0
- data/lib/rigor/language_server/completion_provider.rb +2 -1
- data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
- data/lib/rigor/language_server/folding_range_provider.rb +2 -1
- data/lib/rigor/language_server/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/plugin/base.rb +9 -22
- data/lib/rigor/plugin/inflector.rb +12 -3
- data/lib/rigor/plugin/io_boundary.rb +5 -1
- data/lib/rigor/plugin/isolation.rb +81 -11
- data/lib/rigor/plugin/load_error.rb +10 -1
- data/lib/rigor/plugin/loader.rb +96 -14
- data/lib/rigor/plugin/node_rule_walk.rb +5 -3
- data/lib/rigor/plugin/registry.rb +32 -23
- data/lib/rigor/plugin.rb +26 -0
- data/lib/rigor/protection/mutator.rb +3 -2
- data/lib/rigor/reflection.rb +64 -0
- data/lib/rigor/runtime/jit.rb +128 -0
- data/lib/rigor/scope/discovery_index.rb +12 -2
- data/lib/rigor/scope.rb +122 -28
- data/lib/rigor/sig_gen/classification.rb +5 -1
- data/lib/rigor/sig_gen/generator.rb +38 -6
- data/lib/rigor/sig_gen/observation_collector.rb +4 -3
- data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
- data/lib/rigor/sig_gen/renderer.rb +10 -0
- data/lib/rigor/sig_gen/write_result.rb +9 -4
- data/lib/rigor/sig_gen/writer.rb +24 -2
- data/lib/rigor/source/node_children.rb +116 -0
- data/lib/rigor/source/node_locator.rb +3 -1
- data/lib/rigor/source/node_walker.rb +4 -2
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/difference.rb +28 -24
- data/lib/rigor/type/hash_shape.rb +34 -10
- data/lib/rigor/version.rb +1 -1
- data/lib/rigortype.rb +24 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +3 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +7 -5
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
- data/sig/prism_node_children.rbs +9 -0
- data/sig/rigor/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +2 -3
- data/sig/rigor/reflection.rbs +1 -0
- data/sig/rigor/scope.rbs +12 -1
- data/skills/rigor-ci-setup/SKILL.md +10 -0
- data/skills/rigor-editor-setup/SKILL.md +11 -0
- data/skills/rigor-next-steps/SKILL.md +23 -2
- data/skills/rigor-plugin-author/SKILL.md +3 -3
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
- data/skills/rigor-plugin-review/SKILL.md +2 -2
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
- data/skills/rigor-project-init/SKILL.md +29 -3
- data/skills/rigor-project-init/references/01-detect.md +1 -1
- data/skills/rigor-project-init/references/02-configure.md +39 -5
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
- data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
- metadata +42 -24
- data/lib/rigor/cli/ci_detector.rb +0 -89
|
@@ -6,10 +6,12 @@ require_relative "../reflection"
|
|
|
6
6
|
require_relative "../type"
|
|
7
7
|
require_relative "../analysis/fact_store"
|
|
8
8
|
require_relative "../source/node_walker"
|
|
9
|
+
require_relative "../source/node_children"
|
|
9
10
|
require_relative "../source/constant_path"
|
|
10
11
|
require_relative "block_parameter_binder"
|
|
11
12
|
require_relative "body_fixpoint"
|
|
12
13
|
require_relative "dynamic_origin"
|
|
14
|
+
require_relative "../analysis/check_rules/inferred_param_guard"
|
|
13
15
|
require_relative "struct_fold_safety"
|
|
14
16
|
require_relative "closure_escape_analyzer"
|
|
15
17
|
require_relative "indexed_narrowing"
|
|
@@ -187,6 +189,16 @@ module Rigor
|
|
|
187
189
|
[@scope.type_of(node, tracer: @tracer), @scope]
|
|
188
190
|
end
|
|
189
191
|
|
|
192
|
+
# ADR-89 WD2 — the sorted positions of the positional parameters whose CONTENT `def_node` mutates
|
|
193
|
+
# (`callee_content_mutated_parameters`, the ADR-56 arg-flooring surface a caller consumes). A per-def
|
|
194
|
+
# static property of the AST — this is a pure exposure of the existing private computation (it never
|
|
195
|
+
# reads the scope), so the incremental session can compute it on any `StatementEvaluator` (an empty
|
|
196
|
+
# scope suffices), persist it in a return summary, and re-check a callee's symbol dependents when it
|
|
197
|
+
# moves (a caller's arg flooring changes even if the callee's return does not).
|
|
198
|
+
def content_mutated_parameter_positions(def_node)
|
|
199
|
+
callee_content_mutated_parameters(def_node).values.uniq.sort
|
|
200
|
+
end
|
|
201
|
+
|
|
190
202
|
private
|
|
191
203
|
|
|
192
204
|
attr_reader :scope, :tracer
|
|
@@ -224,6 +236,16 @@ module Rigor
|
|
|
224
236
|
end
|
|
225
237
|
|
|
226
238
|
bound = post_rhs.with_local(node.name, rhs_type)
|
|
239
|
+
# ADR-67 WD6b — a local whose RHS is (transitively) rooted at an inferred parameter inherits the
|
|
240
|
+
# "inferred, not declared" mark, so a subsequent use of the local declines the negative rules for the
|
|
241
|
+
# same lower-bound reason (`vindex = codepoints[i] - x; vindex < y`). The mark is STICKY across
|
|
242
|
+
# narrowing/joins (see `Scope#without_inferred_param_mark`), so a genuine rewrite from a non-param RHS
|
|
243
|
+
# must clear it here. No-op unless the `parameter_inference:` gate seeded a parameter this RHS reaches.
|
|
244
|
+
if Analysis::CheckRules::InferredParamGuard.rooted?(node.value, scope)
|
|
245
|
+
return [rhs_type, bound.with_inferred_param_mark(node.name)]
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
bound = bound.without_inferred_param_mark(node.name)
|
|
227
249
|
bound = bound.with_local_origin(node.name, rhs_origin(node.value, post_rhs, rhs_type))
|
|
228
250
|
[rhs_type, bound]
|
|
229
251
|
end
|
|
@@ -748,7 +770,14 @@ module Rigor
|
|
|
748
770
|
node.is_a?(Prism::ModuleNode) ||
|
|
749
771
|
node.is_a?(Prism::BlockNode)
|
|
750
772
|
|
|
751
|
-
|
|
773
|
+
found = false
|
|
774
|
+
node.rigor_each_child do |c|
|
|
775
|
+
next unless arm_contains_retry?(c)
|
|
776
|
+
|
|
777
|
+
found = true
|
|
778
|
+
break
|
|
779
|
+
end
|
|
780
|
+
found
|
|
752
781
|
end
|
|
753
782
|
|
|
754
783
|
def absorb_retry_rebinds(accumulator, entry_scope, arm_post_scope)
|
|
@@ -936,7 +965,7 @@ module Rigor
|
|
|
936
965
|
return if node.nil?
|
|
937
966
|
|
|
938
967
|
found[node] = true if node.is_a?(Prism::BreakNode)
|
|
939
|
-
node.
|
|
968
|
+
node.rigor_each_child do |child|
|
|
940
969
|
next if BREAK_BOUNDARY_NODES.any? { |klass| child.is_a?(klass) }
|
|
941
970
|
|
|
942
971
|
collect_direct_breaks(child, found)
|
|
@@ -1322,6 +1351,13 @@ module Rigor
|
|
|
1322
1351
|
# (e.g. `arms << x`, `@tags << hashtag`). Stops a literal-shape carrier (`Tuple` / `HashShape`) from outliving
|
|
1323
1352
|
# its justification when the value is mutated. Always-safe (loses precision, never invents facts).
|
|
1324
1353
|
post_scope = MutationWidening.widen_after_call(call_node: node, current_scope: post_scope)
|
|
1354
|
+
# ADR-48 slice 4 — Struct member-setter re-typing. After `s.x = v` on a fold-safe StructInstance local, rebind
|
|
1355
|
+
# `s` to a StructInstance with member `:x` replaced by the assigned type, so a later `s.x` folds to `v` and a
|
|
1356
|
+
# sibling `s.y` stays precise. `call_type` is the setter's own result (the assigned value type). Sound only for
|
|
1357
|
+
# a fold-safe local (never aliased / escaped, straight-line setters) — the fold-safe scan is the gate.
|
|
1358
|
+
post_scope = MethodDispatcher::StructFolding.apply_setter_writeback(
|
|
1359
|
+
call_node: node, assigned_type: call_type, scope: post_scope
|
|
1360
|
+
)
|
|
1325
1361
|
# ADR-57 slice 3 work-item 1 (cross-method-boundary variant). When a self-call resolves to a user method that
|
|
1326
1362
|
# CONTENT-mutates one of its parameters inside an escaping block (the `build_option_parser(opts)` idiom — the
|
|
1327
1363
|
# callee returns an `OptionParser` whose `opts.on { o[:k] = v }` blocks close over the passed-in hash), floor
|
|
@@ -1624,9 +1660,9 @@ module Rigor
|
|
|
1624
1660
|
def collect_gated_statement_contributions(index, relevant, name, call_node, current_scope)
|
|
1625
1661
|
result = nil
|
|
1626
1662
|
relevant.each do |plugin|
|
|
1627
|
-
next unless index.
|
|
1663
|
+
next unless index.narrowing_facts_candidate_for?(plugin, name)
|
|
1628
1664
|
|
|
1629
|
-
facts = plugin.
|
|
1665
|
+
facts = plugin.narrowing_facts_for(call_node: call_node, scope: current_scope)
|
|
1630
1666
|
(result ||= []) << Rigor::FlowContribution.new(post_return_facts: facts) if facts && !facts.empty?
|
|
1631
1667
|
rescue StandardError
|
|
1632
1668
|
next
|
|
@@ -1910,7 +1946,7 @@ module Rigor
|
|
|
1910
1946
|
return if node.nil?
|
|
1911
1947
|
|
|
1912
1948
|
yield node
|
|
1913
|
-
node.
|
|
1949
|
+
node.rigor_each_child do |child|
|
|
1914
1950
|
next if child.is_a?(Prism::BlockNode) || child.is_a?(Prism::DefNode) || child.is_a?(Prism::LambdaNode)
|
|
1915
1951
|
|
|
1916
1952
|
each_node_outside_nested_scopes(child, &)
|
|
@@ -2480,7 +2516,7 @@ module Rigor
|
|
|
2480
2516
|
sub_eval(node.body, fresh, class_context: new_context)
|
|
2481
2517
|
end
|
|
2482
2518
|
|
|
2483
|
-
def build_method_entry_scope(def_node)
|
|
2519
|
+
def build_method_entry_scope(def_node) # rubocop:disable Metrics/AbcSize
|
|
2484
2520
|
singleton = singleton_def?(def_node)
|
|
2485
2521
|
binder = MethodParameterBinder.new(
|
|
2486
2522
|
environment: scope.environment,
|
|
@@ -2492,8 +2528,11 @@ module Rigor
|
|
|
2492
2528
|
# ADR-67 WD3 — override an undeclared parameter with its call-site inferred type (precision-additive; an
|
|
2493
2529
|
# RBS-declared parameter wins, the table is empty on a normal `check` run). The inferred type lives only as a
|
|
2494
2530
|
# body local, never as an RBS contract, so it cannot fire a parameter-boundary diagnostic (WD1, satisfied by
|
|
2495
|
-
# construction).
|
|
2496
|
-
|
|
2531
|
+
# construction). WD6b — the second element is the set of parameters this seed overrode, stamped with
|
|
2532
|
+
# the "inferred, not declared" provenance mark at bind time so the in-body rules can decline on them.
|
|
2533
|
+
seeded = seed_inferred_param_types(bindings, def_node, singleton)
|
|
2534
|
+
bindings = seeded.fetch(0)
|
|
2535
|
+
inferred_names = seeded.fetch(1)
|
|
2497
2536
|
|
|
2498
2537
|
# Method bodies do NOT see the outer scope's locals. They start from a fresh scope with the same environment,
|
|
2499
2538
|
# then receive the parameter bindings. Slice 7 phase 2: instance defs ALSO seed their `ivars` map from the
|
|
@@ -2511,7 +2550,7 @@ module Rigor
|
|
|
2511
2550
|
def_node.body, ->(name) { scope.struct_member_layout(name)&.[](:members) }
|
|
2512
2551
|
)
|
|
2513
2552
|
)
|
|
2514
|
-
bindings.reduce(fresh) { |acc, (name, type)| bind_param(acc, name, type) }
|
|
2553
|
+
bindings.reduce(fresh) { |acc, (name, type)| bind_param(acc, name, type, inferred_names) }
|
|
2515
2554
|
end
|
|
2516
2555
|
|
|
2517
2556
|
# ADR-82 root-enrichment — bind a method parameter, and for an *undeclared* (untyped) parameter seed its
|
|
@@ -2520,8 +2559,14 @@ module Rigor
|
|
|
2520
2559
|
# route to parameter inference rather than reporting no cause at all. Reuses the WD1 `local_origins`
|
|
2521
2560
|
# channel, so WD6 then carries the cause through any chain rooted at the parameter (`x.foo.bar`). An
|
|
2522
2561
|
# RBS-declared / call-site-inferred parameter (a non-untyped binding) keeps no origin — it is not a hole.
|
|
2523
|
-
|
|
2562
|
+
#
|
|
2563
|
+
# ADR-67 WD6b — a parameter the call-site inference seed overrode (`inferred_names`) is stamped with the
|
|
2564
|
+
# "inferred, not declared" provenance mark instead: its type is a concrete (non-untyped) lower bound, so it
|
|
2565
|
+
# is not an `inferred_return_untyped` hole, but the negative in-body rules must still decline on it (the
|
|
2566
|
+
# union is open, so firing is an FP by construction). The mark drops on any flow-live rewrite of the local.
|
|
2567
|
+
def bind_param(acc, name, type, inferred_names = nil)
|
|
2524
2568
|
bound = acc.with_local(name, type)
|
|
2569
|
+
return bound.with_inferred_param_mark(name) if inferred_names&.include?(name)
|
|
2525
2570
|
return bound unless untyped_binding?(type)
|
|
2526
2571
|
|
|
2527
2572
|
bound.with_local_origin(name, DynamicOrigin::INFERRED_RETURN_UNTYPED)
|
|
@@ -2534,19 +2579,23 @@ module Rigor
|
|
|
2534
2579
|
# seed is byte-identical there.
|
|
2535
2580
|
def seed_inferred_param_types(bindings, def_node, singleton)
|
|
2536
2581
|
inferred = scope.param_inferred_types
|
|
2537
|
-
return bindings if inferred.empty?
|
|
2582
|
+
return [bindings, nil] if inferred.empty?
|
|
2538
2583
|
|
|
2539
2584
|
path = current_class_path
|
|
2540
|
-
return bindings if path.nil?
|
|
2585
|
+
return [bindings, nil] if path.nil?
|
|
2541
2586
|
|
|
2542
2587
|
table = inferred[[path, def_node.name, singleton ? :singleton : :instance]]
|
|
2543
|
-
return bindings if table.nil? || table.empty?
|
|
2588
|
+
return [bindings, nil] if table.nil? || table.empty?
|
|
2544
2589
|
|
|
2545
2590
|
merged = bindings.dup
|
|
2591
|
+
overridden = nil
|
|
2546
2592
|
table.each do |name, type|
|
|
2547
|
-
|
|
2593
|
+
next unless merged.key?(name) && untyped_binding?(merged[name])
|
|
2594
|
+
|
|
2595
|
+
merged[name] = type
|
|
2596
|
+
(overridden ||= Set.new) << name
|
|
2548
2597
|
end
|
|
2549
|
-
merged
|
|
2598
|
+
[merged, overridden]
|
|
2550
2599
|
end
|
|
2551
2600
|
|
|
2552
2601
|
# True for the `Dynamic[Top]` carrier `MethodParameterBinder` leaves on an undeclared parameter — the only
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "../source/constant_path"
|
|
6
|
+
require_relative "../source/node_children"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
module Inference
|
|
@@ -59,11 +60,18 @@ module Rigor
|
|
|
59
60
|
return EMPTY if members.empty?
|
|
60
61
|
|
|
61
62
|
total = Hash.new(0)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
safe_uses = Hash.new(0)
|
|
64
|
+
deferred_setter = {}
|
|
65
|
+
count_uses(root, members, total, safe_uses, deferred_setter, false)
|
|
66
|
+
|
|
67
|
+
# A local is fold-safe iff every read of it is a safe use — a pure read OR (ADR-48 slice 4) a
|
|
68
|
+
# straight-line member setter (`n.x = v`) that the setter write-back re-types the binding for.
|
|
69
|
+
# `deferred_setter` disqualifies a local whose setter sits inside a loop / block / lambda, where a
|
|
70
|
+
# single static pass cannot model the setter's per-iteration effect (the write-back would leave a
|
|
71
|
+
# stale binding for the fold to read).
|
|
65
72
|
safe = members.each_key.select do |name|
|
|
66
|
-
writes[name] == 1 && total[name].positive? &&
|
|
73
|
+
writes[name] == 1 && total[name].positive? &&
|
|
74
|
+
total[name] == safe_uses[name] && !deferred_setter[name]
|
|
67
75
|
end
|
|
68
76
|
safe.empty? ? EMPTY : safe.to_set
|
|
69
77
|
end
|
|
@@ -84,22 +92,30 @@ module Rigor
|
|
|
84
92
|
end
|
|
85
93
|
end
|
|
86
94
|
|
|
87
|
-
# Pass 2 — count, per recorded struct local, total reads vs. reads
|
|
88
|
-
|
|
95
|
+
# Pass 2 — count, per recorded struct local, total reads vs. SAFE-use reads (the receiver of a pure-read call,
|
|
96
|
+
# or of a straight-line member setter). `deferred` is true inside a loop / block / lambda; a member setter seen
|
|
97
|
+
# there marks the local's `deferred_setter` so it is excluded (its write-back cannot be modelled statically).
|
|
98
|
+
def count_uses(node, members, total, safe_uses, deferred_setter, deferred)
|
|
89
99
|
return if node.nil?
|
|
90
100
|
|
|
91
101
|
total[node.name] += 1 if node.is_a?(Prism::LocalVariableReadNode) && members.key?(node.name)
|
|
92
102
|
|
|
93
103
|
if node.is_a?(Prism::CallNode)
|
|
94
104
|
receiver = node.receiver
|
|
95
|
-
if receiver.is_a?(Prism::LocalVariableReadNode) && members.key?(receiver.name)
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
if receiver.is_a?(Prism::LocalVariableReadNode) && members.key?(receiver.name)
|
|
106
|
+
member_set = members[receiver.name]
|
|
107
|
+
if pure_read_call?(node, member_set)
|
|
108
|
+
safe_uses[receiver.name] += 1
|
|
109
|
+
elsif member_setter_call?(node, member_set)
|
|
110
|
+
safe_uses[receiver.name] += 1
|
|
111
|
+
deferred_setter[receiver.name] = true if deferred
|
|
112
|
+
end
|
|
98
113
|
end
|
|
99
114
|
end
|
|
100
115
|
|
|
116
|
+
child_deferred = deferred || deferred_boundary?(node)
|
|
101
117
|
each_local_scope_child(node) do |child|
|
|
102
|
-
count_uses(child, members, total,
|
|
118
|
+
count_uses(child, members, total, safe_uses, deferred_setter, child_deferred)
|
|
103
119
|
end
|
|
104
120
|
end
|
|
105
121
|
|
|
@@ -110,6 +126,27 @@ module Rigor
|
|
|
110
126
|
FIXED_READS.include?(name) || member_set.include?(name)
|
|
111
127
|
end
|
|
112
128
|
|
|
129
|
+
# A `n.<member> = v` attribute setter on the receiver (ADR-48 slice 4): the selector strips its trailing `=` to
|
|
130
|
+
# a member reader and the call carries exactly one argument. Comparison operators (`==`, `>=`, `!=`) also end
|
|
131
|
+
# with `=` but never strip to a member, so they stay unknown (unsafe) calls. `:[]=` is likewise not a member.
|
|
132
|
+
def member_setter_call?(call_node, member_set)
|
|
133
|
+
name = call_node.name.to_s
|
|
134
|
+
return false unless name.length > 1 && name.end_with?("=")
|
|
135
|
+
return false unless member_set.include?(name[0..-2].to_sym)
|
|
136
|
+
|
|
137
|
+
(call_node.arguments&.arguments&.size || 0) == 1
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Constructs whose body runs zero-or-many times or is deferred (loops, blocks, lambdas): a single static pass
|
|
141
|
+
# over the body cannot model a member setter's effect across iterations, so a setter inside one disqualifies the
|
|
142
|
+
# local. Straight-line conditionals (`if` / `unless` / `case`) are NOT boundaries — their branch scopes join
|
|
143
|
+
# soundly, so a setter in one branch is fine.
|
|
144
|
+
def deferred_boundary?(node)
|
|
145
|
+
node.is_a?(Prism::WhileNode) || node.is_a?(Prism::UntilNode) ||
|
|
146
|
+
node.is_a?(Prism::ForNode) || node.is_a?(Prism::BlockNode) ||
|
|
147
|
+
node.is_a?(Prism::LambdaNode)
|
|
148
|
+
end
|
|
149
|
+
|
|
113
150
|
# The member set of a `<Struct chain>.new(...)` / `.[]` materialisation, or nil. Handles the inline
|
|
114
151
|
# `Struct.new(:a, :b).new(...)` form and the `Const.new(...)` form (resolved through the layout side-table).
|
|
115
152
|
def struct_materialization_members(value_node, layout_lookup)
|
|
@@ -156,7 +193,7 @@ module Rigor
|
|
|
156
193
|
# Yields each child to recurse into, skipping the subtree of a nested local-variable-scope boundary (a `def` /
|
|
157
194
|
# `class` / `module`).
|
|
158
195
|
def each_local_scope_child(node)
|
|
159
|
-
node.
|
|
196
|
+
node.rigor_each_child do |child|
|
|
160
197
|
next if scope_boundary?(child)
|
|
161
198
|
|
|
162
199
|
yield child
|
|
@@ -5,6 +5,7 @@ require "prism"
|
|
|
5
5
|
require_relative "../plugin/macro/heredoc_template"
|
|
6
6
|
require_relative "../plugin/macro/trait_registry"
|
|
7
7
|
require_relative "../source/literals"
|
|
8
|
+
require_relative "../source/node_children"
|
|
8
9
|
require_relative "synthetic_method"
|
|
9
10
|
require_relative "synthetic_method_index"
|
|
10
11
|
|
|
@@ -166,12 +167,12 @@ module Rigor
|
|
|
166
167
|
name = class_name_from(node, scope_stack)
|
|
167
168
|
yield name, node if name
|
|
168
169
|
new_stack = scope_stack + [node]
|
|
169
|
-
node.body&.
|
|
170
|
+
node.body&.rigor_each_child { |child| walk_classes(child, new_stack, &) }
|
|
170
171
|
when Prism::ModuleNode
|
|
171
172
|
new_stack = scope_stack + [node]
|
|
172
|
-
node.body&.
|
|
173
|
+
node.body&.rigor_each_child { |child| walk_classes(child, new_stack, &) }
|
|
173
174
|
else
|
|
174
|
-
node.
|
|
175
|
+
node.rigor_each_child { |child| walk_classes(child, scope_stack, &) }
|
|
175
176
|
end
|
|
176
177
|
end
|
|
177
178
|
|
|
@@ -303,12 +304,12 @@ module Rigor
|
|
|
303
304
|
name = class_name_from(node, scope_stack)
|
|
304
305
|
yield name, node.body
|
|
305
306
|
new_stack = scope_stack + [node]
|
|
306
|
-
node.body&.
|
|
307
|
+
node.body&.rigor_each_child { |child| walk_module_decls(child, new_stack, &) }
|
|
307
308
|
when Prism::ClassNode
|
|
308
309
|
new_stack = scope_stack + [node]
|
|
309
|
-
node.body&.
|
|
310
|
+
node.body&.rigor_each_child { |child| walk_module_decls(child, new_stack, &) }
|
|
310
311
|
else
|
|
311
|
-
node.
|
|
312
|
+
node.rigor_each_child { |child| walk_module_decls(child, scope_stack, &) }
|
|
312
313
|
end
|
|
313
314
|
end
|
|
314
315
|
|
|
@@ -372,7 +373,7 @@ module Rigor
|
|
|
372
373
|
hierarchy.freeze
|
|
373
374
|
end
|
|
374
375
|
|
|
375
|
-
def walk_class_decls(node, scope_stack, &)
|
|
376
|
+
def walk_class_decls(node, scope_stack, &)
|
|
376
377
|
return unless node.respond_to?(:compact_child_nodes)
|
|
377
378
|
|
|
378
379
|
if node.is_a?(Prism::ClassNode)
|
|
@@ -380,19 +381,19 @@ module Rigor
|
|
|
380
381
|
parent = parent_name_from(node, scope_stack)
|
|
381
382
|
yield name, parent if name
|
|
382
383
|
new_stack = scope_stack + [node]
|
|
383
|
-
node.body&.
|
|
384
|
+
node.body&.rigor_each_child { |child| walk_class_decls(child, new_stack, &) }
|
|
384
385
|
elsif node.is_a?(Prism::ModuleNode)
|
|
385
386
|
new_stack = scope_stack + [node]
|
|
386
|
-
node.body&.
|
|
387
|
+
node.body&.rigor_each_child { |child| walk_class_decls(child, new_stack, &) }
|
|
387
388
|
else
|
|
388
|
-
node.
|
|
389
|
+
node.rigor_each_child { |child| walk_class_decls(child, scope_stack, &) }
|
|
389
390
|
end
|
|
390
391
|
end
|
|
391
392
|
|
|
392
393
|
# Yields `(class_name, call_node)` for every Prism::CallNode at class-body top level (singleton-context calls).
|
|
393
394
|
# Nested method bodies, blocks, and conditionals are skipped — the Tier C call shapes the substrate targets all
|
|
394
395
|
# live at the class body's top level.
|
|
395
|
-
def walk_class_bodies(node, scope_stack = [], &)
|
|
396
|
+
def walk_class_bodies(node, scope_stack = [], &)
|
|
396
397
|
return unless node.respond_to?(:compact_child_nodes)
|
|
397
398
|
|
|
398
399
|
if node.is_a?(Prism::ClassNode)
|
|
@@ -403,12 +404,12 @@ module Rigor
|
|
|
403
404
|
yield name, stmt if stmt.is_a?(Prism::CallNode) && stmt.receiver.nil?
|
|
404
405
|
end
|
|
405
406
|
end
|
|
406
|
-
node.body&.
|
|
407
|
+
node.body&.rigor_each_child { |child| walk_class_bodies(child, new_stack, &) }
|
|
407
408
|
elsif node.is_a?(Prism::ModuleNode)
|
|
408
409
|
new_stack = scope_stack + [node]
|
|
409
|
-
node.body&.
|
|
410
|
+
node.body&.rigor_each_child { |child| walk_class_bodies(child, new_stack, &) }
|
|
410
411
|
else
|
|
411
|
-
node.
|
|
412
|
+
node.rigor_each_child { |child| walk_class_bodies(child, scope_stack, &) }
|
|
412
413
|
end
|
|
413
414
|
end
|
|
414
415
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Inference
|
|
5
|
+
# ADR-100 WD3 — the resolved `-> void` origin site recorded in {Rigor::Scope#void_origins}. A small cause
|
|
6
|
+
# record, never a carrier field: it says *which* author-declared `-> void` return was recovered to `top`
|
|
7
|
+
# at a use site, so the `static.value-use.void` diagnostic can name it. Mirrors the provenance-as-
|
|
8
|
+
# side-channel discipline {DynamicOrigin} established (ADR-75) — it never participates in subtyping,
|
|
9
|
+
# consistency, normalization, or erasure.
|
|
10
|
+
#
|
|
11
|
+
# @!attribute class_name
|
|
12
|
+
# @return [String] the receiver class whose method declared `-> void`.
|
|
13
|
+
# @!attribute method_name
|
|
14
|
+
# @return [Symbol] the method whose return was recovered.
|
|
15
|
+
# @!attribute kind
|
|
16
|
+
# @return [Symbol] `:instance` or `:singleton`.
|
|
17
|
+
VoidOrigin = Data.define(:class_name, :method_name, :kind) do
|
|
18
|
+
# A human-facing `Class#method` / `Class.method` label for the diagnostic message.
|
|
19
|
+
def label
|
|
20
|
+
separator = kind == :singleton ? "." : "#"
|
|
21
|
+
"#{class_name}#{separator}#{method_name}"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "void_origin"
|
|
6
|
+
require_relative "../reflection"
|
|
7
|
+
require_relative "../source/node_children"
|
|
8
|
+
|
|
9
|
+
module Rigor
|
|
10
|
+
module Inference
|
|
11
|
+
# ADR-100 WD4 — the lazy per-`def` "void tail" summary that carries an author-declared `-> void`
|
|
12
|
+
# provenance ACROSS an intermediate method whose own signature declares nothing. The direct case
|
|
13
|
+
# (`x = logger.log(...)`, an RBS `-> void` resolved on the receiver's own class) is recorded at the
|
|
14
|
+
# call site by {MethodDispatcher::RbsDispatch}; this summary answers the transitive case
|
|
15
|
+
# (`def bar; foo; end; b = bar`) where the `void` reaches the use through `bar`'s body, so no rule
|
|
16
|
+
# reading `bar`'s signature can see it.
|
|
17
|
+
#
|
|
18
|
+
# `VoidTail(def_node) -> VoidOrigin | nil`: computed on demand at the first consult, memoised per
|
|
19
|
+
# `def_node` (identity-keyed), cycle-guarded (a def already on the current chain rejects), and
|
|
20
|
+
# **pure** — AST shape + RBS reflection ({Reflection.instance_method_definition} /
|
|
21
|
+
# {Reflection.singleton_method_definition}) + discovery-index lookups ({Scope#user_def_for} /
|
|
22
|
+
# {Scope#singleton_def_for}) only, never expression evaluation. Because it never evaluates, it can
|
|
23
|
+
# neither re-enter the dispatcher nor depend on evaluation order or the fork-pool's file
|
|
24
|
+
# partitioning — the eager record-at-body-evaluation table WD4 rejected failed exactly there.
|
|
25
|
+
#
|
|
26
|
+
# A `def` (resolved on `owner` / `kind`) is admitted iff all four hold:
|
|
27
|
+
#
|
|
28
|
+
# 1. its OWN resolved RBS signature for `(owner, name, kind)` is absent, or every overload
|
|
29
|
+
# returns `untyped` (`RBS::Types::Bases::Any`). An author-declared concrete return — including
|
|
30
|
+
# `-> void` itself, which the direct rule already serves at the call site — keeps it out, so
|
|
31
|
+
# the direct rule and the summary are disjoint by construction (no double record).
|
|
32
|
+
# 2. its body is a plain statements body (no `rescue` / `else` / `ensure` on the `def` — those
|
|
33
|
+
# shapes make `def_node.body` a `Prism::BeginNode`) containing NO `Prism::ReturnNode` anywhere:
|
|
34
|
+
# a bare `return` yields `nil`, a non-void path, so the void tail must be the SOLE return path.
|
|
35
|
+
# The walk is deliberately over-conservative — a `return` inside a nested block also
|
|
36
|
+
# disqualifies.
|
|
37
|
+
# 3. its tail (last) expression is a `Prism::CallNode` with an implicit-`self` receiver (`nil`
|
|
38
|
+
# receiver or a literal `self`).
|
|
39
|
+
# 4. that tail resolves on the SAME owner (exact class, no ancestor walk on the discovery side) to
|
|
40
|
+
# either (a) an RBS definition whose EVERY overload returns `void` — the leaf, whose origin is
|
|
41
|
+
# `VoidOrigin(owner, tail_name, kind)` — or (b) another discovered `def` of the same kind, which
|
|
42
|
+
# recurses. Composition (`def baz; bar; end`) is the recursion, and the origin stays the leaf,
|
|
43
|
+
# so the message still names the author's `-> void` method. The RBS-void-leaf branch is checked
|
|
44
|
+
# FIRST because under ADR-93 auto-wire the leaf `def` is BOTH RBS-`void` and a discovered `def`;
|
|
45
|
+
# recursing into its body would miss the leaf.
|
|
46
|
+
#
|
|
47
|
+
# Everything else — explicit-receiver tails, conditional / boolean / `begin` tails, unresolvable
|
|
48
|
+
# names, non-void concrete RBS — rejects.
|
|
49
|
+
#
|
|
50
|
+
# Each tail chain is a functional graph (every `def` has exactly one tail, hence at most one
|
|
51
|
+
# successor), so a `def`'s result is fully determined by its own forward chain and is independent of
|
|
52
|
+
# the entry point: a chain either reaches a void leaf (`VoidOrigin`), a non-admitted node (nil), or a
|
|
53
|
+
# cycle (nil). Memoising is therefore always sound, and the cycle guard only ever rejects a node that
|
|
54
|
+
# is still mid-computation on the current chain (never yet memoised).
|
|
55
|
+
class VoidTailSummary
|
|
56
|
+
# Run-scoped memo, the ADR-84 `return_memo_bucket` house pattern: a single retained slot on
|
|
57
|
+
# `Thread.current` keyed by the run-generation token's identity, so a re-run in one process (LSP,
|
|
58
|
+
# ADR-62 warm loop) lands in a fresh bucket and can neither serve stale entries nor accumulate
|
|
59
|
+
# them. Thread-local ⇒ each fork-pool worker (a separate process) and each Ractor-pool worker (its
|
|
60
|
+
# own Ractor-local `Thread.current`) builds its own bucket; the bucket hash is created and mutated
|
|
61
|
+
# inside the worker and never shared, so no frozen / shared object is mutated and no
|
|
62
|
+
# graceful-degradation branch is needed. `def_node` identity is stable per run (the ADR-85
|
|
63
|
+
# {DefNodeResolver} yields one node object per `(path, node_id)`), the same identity the ADR-84
|
|
64
|
+
# return memo relies on.
|
|
65
|
+
MEMO_KEY = :__rigor_void_tail_summary_memo__
|
|
66
|
+
private_constant :MEMO_KEY
|
|
67
|
+
|
|
68
|
+
def initialize(scope)
|
|
69
|
+
@scope = scope
|
|
70
|
+
@environment = scope.environment
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The recovered `-> void` origin for `def_node` (whose owner / kind the caller resolved through
|
|
74
|
+
# the discovery index), or nil when the `def` is not admitted. A fresh visited set per top-level
|
|
75
|
+
# consult; the memo persists across consults for the run. The memo is probed before the visited
|
|
76
|
+
# set is allocated so the common post-warmup hit path allocates nothing.
|
|
77
|
+
#
|
|
78
|
+
# @param def_node [Prism::DefNode]
|
|
79
|
+
# @param owner [String] the qualified receiver class the `def` belongs to.
|
|
80
|
+
# @param kind [Symbol] `:instance` or `:singleton`.
|
|
81
|
+
# @return [Inference::VoidOrigin, nil]
|
|
82
|
+
def origin_for(def_node, owner, kind)
|
|
83
|
+
return nil if def_node.nil?
|
|
84
|
+
|
|
85
|
+
memo = memo_bucket
|
|
86
|
+
return memo[def_node] if memo.key?(def_node)
|
|
87
|
+
|
|
88
|
+
walk(def_node, owner, kind, {}.compare_by_identity)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
attr_reader :scope, :environment
|
|
94
|
+
|
|
95
|
+
# Memo + cycle-guarded computation. A def already on the current chain rejects WITHOUT memoising
|
|
96
|
+
# (it is mid-computation; its true result is memoised when its own frame completes).
|
|
97
|
+
def walk(def_node, owner, kind, visited)
|
|
98
|
+
return nil if def_node.nil?
|
|
99
|
+
return nil if visited.key?(def_node)
|
|
100
|
+
|
|
101
|
+
memo = memo_bucket
|
|
102
|
+
return memo[def_node] if memo.key?(def_node)
|
|
103
|
+
|
|
104
|
+
visited[def_node] = true
|
|
105
|
+
memo[def_node] = compute(def_node, owner, kind, visited)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def compute(def_node, owner, kind, visited)
|
|
109
|
+
return nil unless own_signature_admits?(owner, def_node.name, kind) # admission 1
|
|
110
|
+
|
|
111
|
+
tail = admissible_tail(def_node) # admissions 2 + 3
|
|
112
|
+
return nil if tail.nil?
|
|
113
|
+
|
|
114
|
+
resolve_tail(tail.name, owner, kind, visited) # admission 4
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Admission 1 — absent own signature, or every overload `untyped`. `untyped` is read as "no claim"
|
|
118
|
+
# per RBS's own semantics, so a hand-written `-> untyped` intermediate still admits: the provenance
|
|
119
|
+
# fact (this value was produced by an author-declared `-> void` return) remains true of it.
|
|
120
|
+
def own_signature_admits?(owner, method_name, kind)
|
|
121
|
+
definition = method_definition(owner, method_name, kind)
|
|
122
|
+
return true if definition.nil?
|
|
123
|
+
|
|
124
|
+
every_overload_return?(definition, RBS::Types::Bases::Any)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Admissions 2 + 3 — the tail expression when the body is a plain statements body with no
|
|
128
|
+
# `ReturnNode` anywhere and the tail is an implicit-self call; nil (reject) otherwise.
|
|
129
|
+
def admissible_tail(def_node)
|
|
130
|
+
body = def_node.body
|
|
131
|
+
return nil if body.nil?
|
|
132
|
+
return nil if body.is_a?(Prism::BeginNode) # def-level rescue / else / ensure
|
|
133
|
+
return nil if contains_return?(body)
|
|
134
|
+
|
|
135
|
+
tail = tail_expression(body)
|
|
136
|
+
return nil unless tail.is_a?(Prism::CallNode)
|
|
137
|
+
return nil unless implicit_self_receiver?(tail.receiver)
|
|
138
|
+
|
|
139
|
+
tail
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# A `Prism::StatementsNode` (the plain and endless-def shapes both parse to one) tails on its last
|
|
143
|
+
# statement; any other surviving shape is treated as the tail expression itself.
|
|
144
|
+
def tail_expression(body)
|
|
145
|
+
case body
|
|
146
|
+
when Prism::StatementsNode then body.body.last
|
|
147
|
+
else body
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def implicit_self_receiver?(receiver)
|
|
152
|
+
receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# True when a `Prism::ReturnNode` appears anywhere in the subtree — nested blocks and defs
|
|
156
|
+
# included (over-conservative is correct: a method that can return anything other than the void
|
|
157
|
+
# tail must never enter the table).
|
|
158
|
+
def contains_return?(node)
|
|
159
|
+
return false unless node.is_a?(Prism::Node)
|
|
160
|
+
return true if node.is_a?(Prism::ReturnNode)
|
|
161
|
+
|
|
162
|
+
node.rigor_each_child { |child| return true if contains_return?(child) }
|
|
163
|
+
false
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Admission 4 — the void leaf (RBS every-overload `void`) wins over the discovered-def recursion,
|
|
167
|
+
# because under ADR-93 auto-wire the annotated leaf is both.
|
|
168
|
+
def resolve_tail(tail_name, owner, kind, visited)
|
|
169
|
+
definition = method_definition(owner, tail_name, kind)
|
|
170
|
+
if definition && every_overload_return?(definition, RBS::Types::Bases::Void)
|
|
171
|
+
return VoidOrigin.new(class_name: owner, method_name: tail_name, kind: kind)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
next_def = discovered_def(owner, tail_name, kind)
|
|
175
|
+
return nil if next_def.nil?
|
|
176
|
+
|
|
177
|
+
walk(next_def, owner, kind, visited)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def method_definition(owner, method_name, kind)
|
|
181
|
+
if kind == :singleton
|
|
182
|
+
Reflection.singleton_method_definition(owner, method_name, environment: environment)
|
|
183
|
+
else
|
|
184
|
+
Reflection.instance_method_definition(owner, method_name, environment: environment)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def discovered_def(owner, method_name, kind)
|
|
189
|
+
if kind == :singleton
|
|
190
|
+
scope.singleton_def_for(owner, method_name)
|
|
191
|
+
else
|
|
192
|
+
scope.user_def_for(owner, method_name)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# True when the definition declares at least one overload and EVERY overload's return type is an
|
|
197
|
+
# instance of `base_klass` (`RBS::Types::Bases::Any` for admission 1, `...::Void` for the leaf).
|
|
198
|
+
# An `UntypedFunction` (`(?) -> T`) still responds to `return_type`, so it is handled the same way.
|
|
199
|
+
def every_overload_return?(definition, base_klass)
|
|
200
|
+
method_types = definition.method_types
|
|
201
|
+
return false if method_types.empty?
|
|
202
|
+
|
|
203
|
+
method_types.all? do |method_type|
|
|
204
|
+
fun = method_type.type
|
|
205
|
+
fun.respond_to?(:return_type) && fun.return_type.is_a?(base_klass)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def memo_bucket
|
|
210
|
+
generation = scope.run_generation || scope.discovered_def_nodes
|
|
211
|
+
slot = Thread.current[MEMO_KEY]
|
|
212
|
+
unless slot && slot[0].equal?(generation)
|
|
213
|
+
slot = [generation, {}.compare_by_identity]
|
|
214
|
+
Thread.current[MEMO_KEY] = slot
|
|
215
|
+
end
|
|
216
|
+
slot[1]
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
@@ -8,6 +8,7 @@ require_relative "../environment"
|
|
|
8
8
|
require_relative "../reflection"
|
|
9
9
|
require_relative "../scope"
|
|
10
10
|
require_relative "../source/node_locator"
|
|
11
|
+
require_relative "../source/node_children"
|
|
11
12
|
require_relative "../inference/scope_indexer"
|
|
12
13
|
require_relative "../type/nominal"
|
|
13
14
|
require_relative "../type/singleton"
|
|
@@ -187,7 +188,7 @@ module Rigor
|
|
|
187
188
|
n.location.start_offset <= symbol_offset && symbol_offset <= n.location.end_offset
|
|
188
189
|
result = n
|
|
189
190
|
end
|
|
190
|
-
n.
|
|
191
|
+
n.rigor_each_child(&walk)
|
|
191
192
|
end
|
|
192
193
|
walk.call(root)
|
|
193
194
|
result
|
|
@@ -4,6 +4,7 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "uri"
|
|
6
6
|
require_relative "buffer_resolution"
|
|
7
|
+
require_relative "../source/node_children"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module LanguageServer
|
|
@@ -67,7 +68,7 @@ module Rigor
|
|
|
67
68
|
when Prism::DefNode
|
|
68
69
|
block.call(def_symbol(node, in_namespace: in_namespace))
|
|
69
70
|
else
|
|
70
|
-
node.
|
|
71
|
+
node.rigor_each_child do |child|
|
|
71
72
|
each_decl(child, in_namespace: in_namespace, &block)
|
|
72
73
|
end
|
|
73
74
|
end
|