rigortype 0.2.8 → 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/data/core_overlay/csv.rbs +28 -0
- data/data/core_overlay/psych.rbs +22 -0
- data/docs/handbook/01-getting-started.md +9 -1
- data/docs/handbook/02-everyday-types.md +4 -1
- 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 +4 -3
- 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 +99 -4
- data/docs/manual/02-cli-reference.md +65 -19
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +23 -0
- data/docs/manual/07-plugins.md +5 -5
- 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/docs/manual/14-rails-quickstart.md +4 -2
- data/docs/manual/15-type-protection-coverage.md +21 -0
- data/docs/manual/plugins/rigor-actionpack.md +1 -1
- data/docs/manual/plugins/rigor-activerecord.md +12 -5
- data/docs/manual/plugins/rigor-rails-routes.md +11 -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 +207 -60
- 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 +56 -30
- data/lib/rigor/cli/diagnostic_formats.rb +4 -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/protection_fork_scan.rb +55 -0
- data/lib/rigor/cli/protection_report.rb +7 -1
- 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/missing_gem_constant_index.rb +128 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +71 -43
- 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 +379 -113
- data/lib/rigor/inference/fork_map.rb +87 -0
- 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 +245 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +70 -10
- 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 +460 -49
- data/lib/rigor/inference/statement_evaluator.rb +111 -17
- 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/analyzer.rb +23 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +21 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +13 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +65 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +20 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +172 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +22 -8
- 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-activesupport-core-ext/sig/active_support/core_ext.rbs +32 -0
- 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 +191 -0
- 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/helper_table.rb +19 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +48 -15
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +42 -12
- 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/environment.rbs +1 -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 +49 -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
|
|
@@ -1825,10 +1861,19 @@ module Rigor
|
|
|
1825
1861
|
def_node = resolve_self_callee_def(node)
|
|
1826
1862
|
return base_scope if def_node.nil?
|
|
1827
1863
|
|
|
1828
|
-
|
|
1829
|
-
return base_scope if
|
|
1864
|
+
mutated = callee_content_mutated_parameters(def_node)
|
|
1865
|
+
return base_scope if mutated.empty?
|
|
1866
|
+
|
|
1867
|
+
floor_arguments_at_positions(node, mutated, base_scope)
|
|
1868
|
+
end
|
|
1830
1869
|
|
|
1831
|
-
|
|
1870
|
+
# The `{ name => position }` positional parameters whose content the callee mutates, from either channel: those
|
|
1871
|
+
# escape-mutated inside a block (which may run later / repeatedly) AND those mutated directly in the method body
|
|
1872
|
+
# during the call itself (`declaration[:prefix] = v`). Both leave the caller's argument binding stale after the
|
|
1873
|
+
# call, so both floor it. Memoised per def node (the merge is otherwise recomputed at every call site).
|
|
1874
|
+
def callee_content_mutated_parameters(def_node)
|
|
1875
|
+
cache = (@callee_mutated_param_cache ||= {}.compare_by_identity)
|
|
1876
|
+
cache[def_node] ||= escaped_content_parameters(def_node).merge(direct_content_parameters(def_node))
|
|
1832
1877
|
end
|
|
1833
1878
|
|
|
1834
1879
|
# The user def a self-dispatch `node` resolves to in the enclosing class, or nil. Reuses the discovery index
|
|
@@ -1872,6 +1917,42 @@ module Rigor
|
|
|
1872
1917
|
positions.slice(*mutated)
|
|
1873
1918
|
end
|
|
1874
1919
|
|
|
1920
|
+
# The `{ name => position }` positional parameters whose CONTENT the callee mutates directly in its method body —
|
|
1921
|
+
# a top-level `param[k] = v` / `param << x`, outside a nested block. Memoised per def node. The block-nested case
|
|
1922
|
+
# is `escaped_content_parameters`'s job; walking only outside nested blocks / defs / lambdas here means a matching
|
|
1923
|
+
# parameter-name read is genuinely at method scope (depth 0) rather than a block-local of the same name that
|
|
1924
|
+
# merely shadows the parameter — so we never floor a caller argument the callee did not actually mutate.
|
|
1925
|
+
def direct_content_parameters(def_node)
|
|
1926
|
+
cache = (@direct_param_cache ||= {}.compare_by_identity)
|
|
1927
|
+
cache[def_node] ||= compute_direct_content_parameters(def_node)
|
|
1928
|
+
end
|
|
1929
|
+
|
|
1930
|
+
def compute_direct_content_parameters(def_node)
|
|
1931
|
+
positions = positional_parameter_positions(def_node)
|
|
1932
|
+
return {} if positions.empty?
|
|
1933
|
+
|
|
1934
|
+
mutated = Set.new
|
|
1935
|
+
each_node_outside_nested_scopes(def_node.body) do |descendant|
|
|
1936
|
+
name, = content_mutation_target(descendant) { |r| r.is_a?(Prism::LocalVariableReadNode) && r.depth.zero? }
|
|
1937
|
+
mutated << name if !name.nil? && positions.key?(name)
|
|
1938
|
+
end
|
|
1939
|
+
positions.slice(*mutated)
|
|
1940
|
+
end
|
|
1941
|
+
|
|
1942
|
+
# Yields every node reachable from `body` without crossing into a nested block / def / lambda — i.e. the nodes
|
|
1943
|
+
# that execute in the method's own scope. A local-variable read found here has depth 0 relative to the method, so
|
|
1944
|
+
# a content mutation whose receiver is a positional-parameter name is a genuine mutation of that parameter.
|
|
1945
|
+
def each_node_outside_nested_scopes(node, &)
|
|
1946
|
+
return if node.nil?
|
|
1947
|
+
|
|
1948
|
+
yield node
|
|
1949
|
+
node.rigor_each_child do |child|
|
|
1950
|
+
next if child.is_a?(Prism::BlockNode) || child.is_a?(Prism::DefNode) || child.is_a?(Prism::LambdaNode)
|
|
1951
|
+
|
|
1952
|
+
each_node_outside_nested_scopes(child, &)
|
|
1953
|
+
end
|
|
1954
|
+
end
|
|
1955
|
+
|
|
1875
1956
|
# A receiver-independent over-approximation of `ClosureEscapeAnalyzer`'s non-escaping verdict, used when scanning
|
|
1876
1957
|
# a callee body where the block- owning call's receiver TYPE is not available. A call whose method name is a known
|
|
1877
1958
|
# structural iterator (`each` / `map` / `tap` / …) runs its block synchronously and does not retain it, so its
|
|
@@ -2435,7 +2516,7 @@ module Rigor
|
|
|
2435
2516
|
sub_eval(node.body, fresh, class_context: new_context)
|
|
2436
2517
|
end
|
|
2437
2518
|
|
|
2438
|
-
def build_method_entry_scope(def_node)
|
|
2519
|
+
def build_method_entry_scope(def_node) # rubocop:disable Metrics/AbcSize
|
|
2439
2520
|
singleton = singleton_def?(def_node)
|
|
2440
2521
|
binder = MethodParameterBinder.new(
|
|
2441
2522
|
environment: scope.environment,
|
|
@@ -2447,8 +2528,11 @@ module Rigor
|
|
|
2447
2528
|
# ADR-67 WD3 — override an undeclared parameter with its call-site inferred type (precision-additive; an
|
|
2448
2529
|
# RBS-declared parameter wins, the table is empty on a normal `check` run). The inferred type lives only as a
|
|
2449
2530
|
# body local, never as an RBS contract, so it cannot fire a parameter-boundary diagnostic (WD1, satisfied by
|
|
2450
|
-
# construction).
|
|
2451
|
-
|
|
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)
|
|
2452
2536
|
|
|
2453
2537
|
# Method bodies do NOT see the outer scope's locals. They start from a fresh scope with the same environment,
|
|
2454
2538
|
# then receive the parameter bindings. Slice 7 phase 2: instance defs ALSO seed their `ivars` map from the
|
|
@@ -2466,7 +2550,7 @@ module Rigor
|
|
|
2466
2550
|
def_node.body, ->(name) { scope.struct_member_layout(name)&.[](:members) }
|
|
2467
2551
|
)
|
|
2468
2552
|
)
|
|
2469
|
-
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) }
|
|
2470
2554
|
end
|
|
2471
2555
|
|
|
2472
2556
|
# ADR-82 root-enrichment — bind a method parameter, and for an *undeclared* (untyped) parameter seed its
|
|
@@ -2475,8 +2559,14 @@ module Rigor
|
|
|
2475
2559
|
# route to parameter inference rather than reporting no cause at all. Reuses the WD1 `local_origins`
|
|
2476
2560
|
# channel, so WD6 then carries the cause through any chain rooted at the parameter (`x.foo.bar`). An
|
|
2477
2561
|
# RBS-declared / call-site-inferred parameter (a non-untyped binding) keeps no origin — it is not a hole.
|
|
2478
|
-
|
|
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)
|
|
2479
2568
|
bound = acc.with_local(name, type)
|
|
2569
|
+
return bound.with_inferred_param_mark(name) if inferred_names&.include?(name)
|
|
2480
2570
|
return bound unless untyped_binding?(type)
|
|
2481
2571
|
|
|
2482
2572
|
bound.with_local_origin(name, DynamicOrigin::INFERRED_RETURN_UNTYPED)
|
|
@@ -2489,19 +2579,23 @@ module Rigor
|
|
|
2489
2579
|
# seed is byte-identical there.
|
|
2490
2580
|
def seed_inferred_param_types(bindings, def_node, singleton)
|
|
2491
2581
|
inferred = scope.param_inferred_types
|
|
2492
|
-
return bindings if inferred.empty?
|
|
2582
|
+
return [bindings, nil] if inferred.empty?
|
|
2493
2583
|
|
|
2494
2584
|
path = current_class_path
|
|
2495
|
-
return bindings if path.nil?
|
|
2585
|
+
return [bindings, nil] if path.nil?
|
|
2496
2586
|
|
|
2497
2587
|
table = inferred[[path, def_node.name, singleton ? :singleton : :instance]]
|
|
2498
|
-
return bindings if table.nil? || table.empty?
|
|
2588
|
+
return [bindings, nil] if table.nil? || table.empty?
|
|
2499
2589
|
|
|
2500
2590
|
merged = bindings.dup
|
|
2591
|
+
overridden = nil
|
|
2501
2592
|
table.each do |name, type|
|
|
2502
|
-
|
|
2593
|
+
next unless merged.key?(name) && untyped_binding?(merged[name])
|
|
2594
|
+
|
|
2595
|
+
merged[name] = type
|
|
2596
|
+
(overridden ||= Set.new) << name
|
|
2503
2597
|
end
|
|
2504
|
-
merged
|
|
2598
|
+
[merged, overridden]
|
|
2505
2599
|
end
|
|
2506
2600
|
|
|
2507
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
|