rigortype 0.1.19 → 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/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -23
- data/lib/rigor/analysis/check_rules/rule_walk.rb +3 -21
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
- data/lib/rigor/analysis/check_rules.rb +492 -71
- 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/fact_store.rb +5 -4
- data/lib/rigor/analysis/rule_catalog.rb +153 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +17 -17
- data/lib/rigor/analysis/runner/project_pre_passes.rb +9 -8
- data/lib/rigor/analysis/runner.rb +17 -6
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
- data/lib/rigor/analysis/worker_session.rb +10 -14
- data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
- data/lib/rigor/cache/store.rb +5 -3
- data/lib/rigor/cli/annotate_command.rb +28 -7
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/check_command.rb +115 -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 +2 -1
- data/lib/rigor/cli/protection_renderer.rb +63 -0
- data/lib/rigor/cli/protection_report.rb +68 -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 +2 -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 +3 -2
- data/lib/rigor/configuration/dependencies.rb +2 -4
- data/lib/rigor/configuration.rb +45 -7
- 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 +49 -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/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 +20 -28
- 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/method_dispatcher/call_context.rb +1 -4
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +156 -21
- 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/shape_dispatch.rb +90 -15
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
- data/lib/rigor/inference/method_dispatcher.rb +40 -48
- data/lib/rigor/inference/mutation_widening.rb +5 -11
- data/lib/rigor/inference/narrowing.rb +14 -16
- 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 +129 -55
- data/lib/rigor/inference/statement_evaluator.rb +244 -114
- data/lib/rigor/inference/struct_fold_safety.rb +181 -0
- data/lib/rigor/inference/synthetic_method.rb +7 -7
- 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 +10 -8
- data/lib/rigor/plugin/macro/block_as_method.rb +3 -4
- data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
- data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
- data/lib/rigor/plugin/macro.rb +4 -5
- data/lib/rigor/plugin/manifest.rb +45 -66
- data/lib/rigor/plugin/registry.rb +6 -7
- 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 +14 -2
- data/lib/rigor/scope.rb +54 -11
- 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/type/bound_method.rb +2 -11
- data/lib/rigor/type/combinator.rb +16 -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 +3 -3
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +7 -10
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +6 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -2
- 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 +7 -9
- 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 +3 -3
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
- 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 +1 -1
- 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 +5 -5
- 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 +19 -14
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
- 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 +28 -41
- data/sig/rigor/scope.rbs +9 -1
- data/sig/rigor/type.rbs +36 -1
- metadata +19 -1
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
require_relative "singleton_folding"
|
|
5
|
+
require_relative "member_shape_projection"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Inference
|
|
9
|
+
module MethodDispatcher
|
|
10
|
+
# ADR-48 Struct follow-up — `Struct.new` value folding, the mutable
|
|
11
|
+
# sibling of {DataFolding}. Same fully-decidable-shape, degrade-on-any-
|
|
12
|
+
# uncertainty discipline, with one extra premise the immutable `Data`
|
|
13
|
+
# path does not need: **mutation soundness.**
|
|
14
|
+
#
|
|
15
|
+
# A `Struct` instance is mutable (`s.x = v`, `s[:x] = v`, escape), so a
|
|
16
|
+
# member map bound to a variable can be invalidated by a later write.
|
|
17
|
+
# This slice (ADR-48 slices 1+2 in the sound *transient* form) folds a
|
|
18
|
+
# member read ONLY off a **fresh** instance — the transient receiver of
|
|
19
|
+
# a `.new(...).x` / `.with(...).x` chain, which provably cannot have
|
|
20
|
+
# been mutated between materialisation and the read. A member read off
|
|
21
|
+
# a *stored* binding degrades to `Dynamic[top]` rather than fold a
|
|
22
|
+
# possibly-stale value. Promoting the fold to mutation-free bound
|
|
23
|
+
# locals is the deferred slice 3 (relax the fresh-receiver gate to a
|
|
24
|
+
# fold-safe-local scan); precise mutated-member re-typing is slice 4.
|
|
25
|
+
#
|
|
26
|
+
# Responsibilities:
|
|
27
|
+
#
|
|
28
|
+
# 1. `Struct.new(:x, :y [, keyword_init: <bool>])` on a
|
|
29
|
+
# `Singleton[Struct]` receiver, literal-Symbol members, NO block ->
|
|
30
|
+
# `StructClass{members:, keyword_init:}`. A block / non-literal
|
|
31
|
+
# members defer.
|
|
32
|
+
# 2. `.new` / `.[]` on a `StructClass` (or a `Singleton[Point]` with a
|
|
33
|
+
# recorded struct layout) -> a `StructInstance`, the member map built
|
|
34
|
+
# from the call's arguments (positional or keyword per the class's
|
|
35
|
+
# `keyword_init`). A form / arity mismatch degrades to `Dynamic[top]`
|
|
36
|
+
# rather than a wrong member map. `.members` on the class folds.
|
|
37
|
+
# 3. member reads + `[]` / `to_h` / `deconstruct` / `deconstruct_keys`
|
|
38
|
+
# / `members` / `with` on a *fresh* `StructInstance` -> the precise
|
|
39
|
+
# projected type; member *setters* (`s.x = v`) return the assigned
|
|
40
|
+
# value type. Unhandled / stored-receiver calls return nil so the
|
|
41
|
+
# pipeline projects the instance to its nominal through RbsDispatch.
|
|
42
|
+
#
|
|
43
|
+
# See docs/adr/48-data-struct-value-folding.md § "Struct follow-up".
|
|
44
|
+
module StructFolding
|
|
45
|
+
module_function
|
|
46
|
+
|
|
47
|
+
# The `[]` / `to_h` / `deconstruct` / `members` / `with` projections
|
|
48
|
+
# and the reader-redefinition guard are shared with {DataFolding}.
|
|
49
|
+
extend MemberShapeProjection
|
|
50
|
+
|
|
51
|
+
# @return [Rigor::Type, nil] the folded result, or nil to defer.
|
|
52
|
+
def try_dispatch(context)
|
|
53
|
+
receiver = context.receiver
|
|
54
|
+
|
|
55
|
+
return fold_struct_new(context) if SingletonFolding.receiver?(receiver, "Struct")
|
|
56
|
+
|
|
57
|
+
case receiver
|
|
58
|
+
when Type::StructClass
|
|
59
|
+
dispatch_struct_class(receiver, context)
|
|
60
|
+
when Type::StructInstance
|
|
61
|
+
fold_instance(receiver, context)
|
|
62
|
+
when Type::Singleton
|
|
63
|
+
fold_named_new(receiver, context)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# A `Struct.new`-defined class assigned to a constant (or a
|
|
68
|
+
# `class Point < Struct.new(...)` subclass) is canonicalised by the
|
|
69
|
+
# engine to `Singleton[Point]`, not a `StructClass` — so its member
|
|
70
|
+
# layout is read from the project side-table the scope indexer built
|
|
71
|
+
# (`Scope#struct_member_layout`) rather than from the receiver
|
|
72
|
+
# carrier.
|
|
73
|
+
def fold_named_new(singleton, context)
|
|
74
|
+
scope = context.scope
|
|
75
|
+
return nil if scope.nil?
|
|
76
|
+
|
|
77
|
+
layout = scope.struct_member_layout(singleton.class_name)
|
|
78
|
+
return nil if layout.nil?
|
|
79
|
+
|
|
80
|
+
materialize_instance(layout[:members], layout[:keyword_init], singleton.class_name, context)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# --- 1. Struct.new(:x, :y) --------------------------------------
|
|
84
|
+
|
|
85
|
+
def fold_struct_new(context)
|
|
86
|
+
return nil unless context.method_name == :new
|
|
87
|
+
# Block-form (`Struct.new(:x) do ... end`) defers — the named
|
|
88
|
+
# constant/subclass forms still fold via the layout side-table.
|
|
89
|
+
return nil unless context.block_type.nil?
|
|
90
|
+
|
|
91
|
+
parsed = parse_struct_new_args(context.args)
|
|
92
|
+
return nil if parsed.nil?
|
|
93
|
+
|
|
94
|
+
Type::Combinator.struct_class_of(members: parsed[:members], keyword_init: parsed[:keyword_init])
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Parses `Struct.new`'s arguments into `{ members:, keyword_init: }`,
|
|
98
|
+
# or nil when any argument is non-conforming (a dynamic member name,
|
|
99
|
+
# an unexpected trailing keyword). Handles the optional leading String
|
|
100
|
+
# class name and the trailing `keyword_init:` option hash.
|
|
101
|
+
def parse_struct_new_args(args)
|
|
102
|
+
rest = args.dup
|
|
103
|
+
keyword_init = false
|
|
104
|
+
|
|
105
|
+
if rest.last.is_a?(Type::HashShape)
|
|
106
|
+
options = rest.pop
|
|
107
|
+
return nil unless struct_options_hash?(options)
|
|
108
|
+
|
|
109
|
+
value = options.pairs[:keyword_init]
|
|
110
|
+
keyword_init = value.is_a?(Type::Constant) && value.value == true
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Optional leading String name: `Struct.new("Point", :x, :y)`.
|
|
114
|
+
rest = rest[1..] if rest.first.is_a?(Type::Constant) && rest.first.value.is_a?(String)
|
|
115
|
+
|
|
116
|
+
members = []
|
|
117
|
+
rest.each do |arg|
|
|
118
|
+
return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(Symbol)
|
|
119
|
+
|
|
120
|
+
members << arg.value
|
|
121
|
+
end
|
|
122
|
+
return nil if members.empty?
|
|
123
|
+
return nil unless members.uniq.size == members.size
|
|
124
|
+
|
|
125
|
+
{ members: members, keyword_init: keyword_init }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# A trailing hash is the `Struct.new` options hash only when it is a
|
|
129
|
+
# closed shape whose sole key is `:keyword_init`. Any other key means
|
|
130
|
+
# the call is not a recognisable member-list definition -> defer.
|
|
131
|
+
def struct_options_hash?(shape)
|
|
132
|
+
shape.closed? && shape.optional_keys.empty? &&
|
|
133
|
+
shape.pairs.keys.all? { |key| key == :keyword_init }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# --- 2. Point.new(...) / Point[...] / Point.members -------------
|
|
137
|
+
|
|
138
|
+
def dispatch_struct_class(struct_class, context)
|
|
139
|
+
case context.method_name
|
|
140
|
+
when :new, :[]
|
|
141
|
+
materialize_instance(struct_class.members, struct_class.keyword_init,
|
|
142
|
+
struct_class.class_name, context)
|
|
143
|
+
when :members
|
|
144
|
+
Type::Combinator.tuple_of(*struct_class.members.map { |name| Type::Combinator.constant_of(name) })
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def materialize_instance(members, keyword_init, class_name, context)
|
|
149
|
+
return nil unless %i[new []].include?(context.method_name)
|
|
150
|
+
|
|
151
|
+
map = member_map_for_new(members, keyword_init, context)
|
|
152
|
+
return degraded_instance if map.nil?
|
|
153
|
+
|
|
154
|
+
Type::Combinator.struct_instance_of(members: map, class_name: class_name)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Builds the member -> type map honouring the class's `keyword_init`
|
|
158
|
+
# flag: a `keyword_init: true` struct accepts only the keyword form,
|
|
159
|
+
# a positional struct only the positional form. The mismatched form
|
|
160
|
+
# is a different runtime shape, so it degrades rather than fold.
|
|
161
|
+
def member_map_for_new(members, keyword_init, context)
|
|
162
|
+
if keyword_new?(context)
|
|
163
|
+
keyword_init ? keyword_member_map(members, context.args) : nil
|
|
164
|
+
else
|
|
165
|
+
keyword_init ? nil : positional_member_map(members, context.args)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# `Point.new(x: 1)` arrives as a single trailing `HashShape` whose
|
|
170
|
+
# call node is a `KeywordHashNode`; distinguishing it from a
|
|
171
|
+
# positional hash needs the call node (both type to a `HashShape`).
|
|
172
|
+
def keyword_new?(context)
|
|
173
|
+
node = context.call_node
|
|
174
|
+
return false if node.nil?
|
|
175
|
+
|
|
176
|
+
arguments = node.arguments&.arguments
|
|
177
|
+
return false if arguments.nil? || arguments.empty?
|
|
178
|
+
|
|
179
|
+
arguments.last.is_a?(Prism::KeywordHashNode)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# `Struct.new(:a, :b).new(v)` is legal — trailing members default to
|
|
183
|
+
# `nil` — so fewer positionals than members pad with `Constant[nil]`;
|
|
184
|
+
# more positionals than members is an ArgumentError -> degrade.
|
|
185
|
+
def positional_member_map(members, args)
|
|
186
|
+
return nil if args.size > members.size
|
|
187
|
+
|
|
188
|
+
members.each_with_index.to_h do |name, index|
|
|
189
|
+
[name, index < args.size ? args[index] : Type::Combinator.constant_of(nil)]
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# `Struct.new(:a, :b).new(a: 1)` is legal — omitted members default
|
|
194
|
+
# to `nil` — so a keyword subset pads the rest; an unknown key is an
|
|
195
|
+
# ArgumentError -> degrade.
|
|
196
|
+
def keyword_member_map(members, args)
|
|
197
|
+
return nil unless args.size == 1
|
|
198
|
+
|
|
199
|
+
shape = args.first
|
|
200
|
+
return nil unless shape.is_a?(Type::HashShape) && shape.closed?
|
|
201
|
+
return nil unless shape.optional_keys.empty?
|
|
202
|
+
return nil unless shape.pairs.keys.all? { |key| members.include?(key) }
|
|
203
|
+
|
|
204
|
+
members.to_h { |name| [name, shape.pairs[name] || Type::Combinator.constant_of(nil)] }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# A `.new` whose arguments cannot soundly populate the member map
|
|
208
|
+
# degrades to `Dynamic[top]` (today's behaviour for `Struct.new(...)`
|
|
209
|
+
# instances), never a wrong map.
|
|
210
|
+
def degraded_instance
|
|
211
|
+
Type::Combinator.untyped
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# --- 3. inst.x / inst.x = v / inst[...] / inst.to_h / ... -------
|
|
215
|
+
|
|
216
|
+
def fold_instance(instance, context)
|
|
217
|
+
method_name = context.method_name
|
|
218
|
+
args = context.args
|
|
219
|
+
members = instance.members
|
|
220
|
+
|
|
221
|
+
# Member setter `s.x = v`: returns the assigned value type. Sound
|
|
222
|
+
# regardless of mutation state (it models the setter's own return),
|
|
223
|
+
# and avoids a fall-through undefined-method on a writer the
|
|
224
|
+
# existence table does not register.
|
|
225
|
+
setter = member_setter_target(method_name, members)
|
|
226
|
+
return args.first if setter && args.size == 1
|
|
227
|
+
|
|
228
|
+
foldable = foldable_receiver?(context)
|
|
229
|
+
|
|
230
|
+
if members.key?(method_name) && args.empty? && !reader_overridden?(instance, method_name, context.scope)
|
|
231
|
+
# A stored receiver folds only when the bound local is proven
|
|
232
|
+
# fold-safe (ADR-48 slice 3 — never mutated / aliased / escaped);
|
|
233
|
+
# otherwise the member value may be stale, so it degrades to
|
|
234
|
+
# `Dynamic[top]` (not nil -> no undefined-method fall-through).
|
|
235
|
+
return foldable ? members.fetch(method_name) : Type::Combinator.untyped
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Projections fold only off a foldable (fresh, or proven fold-safe)
|
|
239
|
+
# instance; off any other stored binding they defer to Struct's RBS
|
|
240
|
+
# (`to_h` / `[]` / `members` / `deconstruct*` all exist on `Struct`),
|
|
241
|
+
# which is sound and non-regressive.
|
|
242
|
+
return nil unless foldable
|
|
243
|
+
|
|
244
|
+
fold_fresh_projection(instance, method_name, args)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def fold_fresh_projection(instance, method_name, args)
|
|
248
|
+
case method_name
|
|
249
|
+
when :[] then instance_index(instance, args)
|
|
250
|
+
when :to_h, :to_hash then instance_to_h(instance)
|
|
251
|
+
when :deconstruct then instance_deconstruct(instance)
|
|
252
|
+
when :deconstruct_keys then instance_deconstruct_keys(instance, args)
|
|
253
|
+
when :members then instance_members(instance)
|
|
254
|
+
when :with
|
|
255
|
+
instance_with(instance, args) do |members, class_name|
|
|
256
|
+
Type::Combinator.struct_instance_of(members: members, class_name: class_name)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# The member name a `:<member>=` setter targets, or nil. Comparison
|
|
262
|
+
# operators (`==`, `>=`, ...) end with `=` too but never strip to a
|
|
263
|
+
# member symbol, so they fall through to the normal dispatch path.
|
|
264
|
+
def member_setter_target(method_name, members)
|
|
265
|
+
name = method_name.to_s
|
|
266
|
+
return nil unless name.end_with?("=")
|
|
267
|
+
|
|
268
|
+
base = name[0..-2].to_sym
|
|
269
|
+
members.key?(base) ? base : nil
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# A member read is foldable when the receiver is either FRESH (the
|
|
273
|
+
# transient result of a `.new(...)`/`.with(...)` chain, which cannot
|
|
274
|
+
# have been mutated between materialisation and this read) or a
|
|
275
|
+
# FOLD-SAFE stored local (ADR-48 slice 3 — `StructFoldSafety` proved
|
|
276
|
+
# the binding is never mutated / aliased / escaped in its scope).
|
|
277
|
+
def foldable_receiver?(context)
|
|
278
|
+
fresh_receiver?(context) || fold_safe_local_receiver?(context)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# A fresh receiver is the transient result of a chained call
|
|
282
|
+
# (`Point.new(1, 2).x`, `inst.with(x: 9).y`).
|
|
283
|
+
def fresh_receiver?(context)
|
|
284
|
+
node = context.call_node
|
|
285
|
+
return false if node.nil?
|
|
286
|
+
|
|
287
|
+
node.receiver.is_a?(Prism::CallNode)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# A fold-safe stored receiver is a local-variable read whose name the
|
|
291
|
+
# body's fold-safe set (on the scope) marks as never mutated.
|
|
292
|
+
def fold_safe_local_receiver?(context)
|
|
293
|
+
node = context.call_node
|
|
294
|
+
receiver = node&.receiver
|
|
295
|
+
scope = context.scope
|
|
296
|
+
return false unless receiver.is_a?(Prism::LocalVariableReadNode) && scope
|
|
297
|
+
|
|
298
|
+
scope.struct_fold_safe?(receiver.name)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
@@ -12,6 +12,7 @@ require_relative "method_dispatcher/constant_folding"
|
|
|
12
12
|
require_relative "method_dispatcher/literal_string_folding"
|
|
13
13
|
require_relative "method_dispatcher/shape_dispatch"
|
|
14
14
|
require_relative "method_dispatcher/data_folding"
|
|
15
|
+
require_relative "method_dispatcher/struct_folding"
|
|
15
16
|
require_relative "method_dispatcher/rbs_dispatch"
|
|
16
17
|
require_relative "method_dispatcher/iterator_dispatch"
|
|
17
18
|
require_relative "method_dispatcher/reduce_folding"
|
|
@@ -38,28 +39,15 @@ module Rigor
|
|
|
38
39
|
# callers (today only `ExpressionTyper`) own the fail-soft fallback
|
|
39
40
|
# and decide whether to record a `FallbackTracer` event.
|
|
40
41
|
#
|
|
41
|
-
#
|
|
42
|
+
# Tier order is documented inline in `resolve`; the precise-tier
|
|
43
|
+
# group is built from `PRECISE_TIERS_HEAD`, `STDLIB_SINGLETON_FOLDERS`,
|
|
44
|
+
# and `PRECISE_TIERS_TAIL`. `ShapeDispatch` runs above {RbsDispatch}
|
|
45
|
+
# so a precise per-position/per-key answer wins over the projected
|
|
46
|
+
# `Array#[]`/`Hash#fetch` RBS answer.
|
|
42
47
|
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
# 2. {ShapeDispatch}: returns the precise element/value type for a
|
|
47
|
-
# curated catalogue of `Tuple`/`HashShape` element-access
|
|
48
|
-
# methods (`first`, `last`, `[]` with a static integer/key,
|
|
49
|
-
# `fetch`, `dig`, `size`/`length`/`count`). Slice 5 phase 2.
|
|
50
|
-
# 3. {RbsDispatch}: looks up the receiver's class in the RBS
|
|
51
|
-
# environment carried by the scope and translates the method's
|
|
52
|
-
# return type into a Rigor::Type. Slice 4.
|
|
53
|
-
#
|
|
54
|
-
# `ShapeDispatch` deliberately runs *above* {RbsDispatch} so the
|
|
55
|
-
# precise per-position/per-key answer wins over the projected
|
|
56
|
-
# `Array#[]`/`Hash#fetch` answer; it falls through (`nil`) when
|
|
57
|
-
# the call cannot be proved against the static shape, in which
|
|
58
|
-
# case the projection answer from {RbsDispatch} applies.
|
|
59
|
-
#
|
|
60
|
-
# The dispatcher's public signature reserves space for `block_type:`
|
|
61
|
-
# and ADR-2 plugin extensions (later slices), so call sites added
|
|
62
|
-
# now do not have to be rewritten when those tiers arrive.
|
|
48
|
+
# The `block_type:` and plugin contribution (`dynamic_return`) tiers
|
|
49
|
+
# landed in Slice 6 phase C and v0.1.1 Track 2 respectively; all
|
|
50
|
+
# call sites pass through `dispatch`/`resolve` unchanged.
|
|
63
51
|
module MethodDispatcher # rubocop:disable Metrics/ModuleLength
|
|
64
52
|
module_function
|
|
65
53
|
|
|
@@ -326,19 +314,6 @@ module Rigor
|
|
|
326
314
|
Type::Combinator.untyped
|
|
327
315
|
end
|
|
328
316
|
|
|
329
|
-
# ADR-2 § "Flow Contribution Bundle" / v0.1.1 Track 2
|
|
330
|
-
# slice 7; ADR-52 WD3 — consults each loaded plugin's gated
|
|
331
|
-
# `dynamic_return` rules, wraps the contributed types as
|
|
332
|
-
# `FlowContribution` bundles, merges them through
|
|
333
|
-
# `FlowContribution::Merger`, and returns the merged
|
|
334
|
-
# `return_type` slot (or nil when no plugin contributed a
|
|
335
|
-
# return type).
|
|
336
|
-
#
|
|
337
|
-
# Plugins whose hook raises have their contribution
|
|
338
|
-
# silently dropped for this call so the dispatch chain
|
|
339
|
-
# keeps moving — the run-level diagnostic envelope (per
|
|
340
|
-
# ADR-2 § "Plugin Trust and I/O Policy") is owned by
|
|
341
|
-
# `Analysis::Runner#plugin_emitted_diagnostics`.
|
|
342
317
|
# ADR-20 slice 3 — looks up the receiver / method pair
|
|
343
318
|
# in {Rigor::Builtins::HktBuiltins::METHOD_RETURN_OVERRIDES}
|
|
344
319
|
# and returns the reduced HKT type. Only fires when the
|
|
@@ -413,6 +388,19 @@ module Rigor
|
|
|
413
388
|
end
|
|
414
389
|
end
|
|
415
390
|
|
|
391
|
+
# ADR-2 § "Flow Contribution Bundle" / v0.1.1 Track 2
|
|
392
|
+
# slice 7; ADR-52 WD3 — consults each loaded plugin's gated
|
|
393
|
+
# `dynamic_return` rules, wraps the contributed types as
|
|
394
|
+
# `FlowContribution` bundles, merges them through
|
|
395
|
+
# `FlowContribution::Merger`, and returns the merged
|
|
396
|
+
# `return_type` slot (or nil when no plugin contributed a
|
|
397
|
+
# return type).
|
|
398
|
+
#
|
|
399
|
+
# Plugins whose hook raises have their contribution
|
|
400
|
+
# silently dropped for this call so the dispatch chain
|
|
401
|
+
# keeps moving — the run-level diagnostic envelope (per
|
|
402
|
+
# ADR-2 § "Plugin Trust and I/O Policy") is owned by
|
|
403
|
+
# `Analysis::Runner#plugin_emitted_diagnostics`.
|
|
416
404
|
def try_plugin_contribution(call_node, scope, receiver_type)
|
|
417
405
|
return nil if call_node.nil? || scope.nil?
|
|
418
406
|
|
|
@@ -425,16 +413,6 @@ module Rigor
|
|
|
425
413
|
FlowContribution::Merger.merge(contributions).return_type
|
|
426
414
|
end
|
|
427
415
|
|
|
428
|
-
# ADR-10 slice 2b-ii. Consults the per-run
|
|
429
|
-
# `Analysis::DependencySourceInference::Index` carried by
|
|
430
|
-
# the environment for `(class_name, method_name)`
|
|
431
|
-
# observations harvested from opt-in gems' `roots:`. On a
|
|
432
|
-
# hit, returns `Combinator.untyped` so the call site
|
|
433
|
-
# carries the `Dynamic[top]` provenance (per ADR-10's
|
|
434
|
-
# "Inference contract": gem-source-inferred shapes never
|
|
435
|
-
# publish as ground-truth `T`). Returns `nil` when the
|
|
436
|
-
# environment carries no index, the index has no entry, or
|
|
437
|
-
# the receiver has no nominal class to look up.
|
|
438
416
|
# ADR-16 synthetic-method tier. Slice 2b shipped the floor —
|
|
439
417
|
# a match short-circuits at the right precedence (above
|
|
440
418
|
# dep-source / discovered / user-class-fallback; below RBS)
|
|
@@ -556,6 +534,16 @@ module Rigor
|
|
|
556
534
|
Type::Combinator.dynamic(entry.return_type)
|
|
557
535
|
end
|
|
558
536
|
|
|
537
|
+
# ADR-10 slice 2b-ii. Consults the per-run
|
|
538
|
+
# `Analysis::DependencySourceInference::Index` carried by
|
|
539
|
+
# the environment for `(class_name, method_name)`
|
|
540
|
+
# observations harvested from opt-in gems' `roots:`. On a
|
|
541
|
+
# hit, returns `Combinator.untyped` so the call site
|
|
542
|
+
# carries the `Dynamic[top]` provenance (per ADR-10's
|
|
543
|
+
# "Inference contract": gem-source-inferred shapes never
|
|
544
|
+
# publish as ground-truth `T`). Returns `nil` when the
|
|
545
|
+
# environment carries no index, the index has no entry, or
|
|
546
|
+
# the receiver has no nominal class to look up.
|
|
559
547
|
def try_dependency_source(receiver_type, method_name, environment)
|
|
560
548
|
index = environment&.dependency_source_index
|
|
561
549
|
return nil if index.nil? || index.empty?
|
|
@@ -759,10 +747,6 @@ module Rigor
|
|
|
759
747
|
# below this chain and is invoked by the outer `dispatch`
|
|
760
748
|
# method.
|
|
761
749
|
#
|
|
762
|
-
# `BlockFolding` runs last among the precision tiers because
|
|
763
|
-
# its rules apply only to block-taking calls, so the cheaper
|
|
764
|
-
# arity-based fold tiers above it filter out the common
|
|
765
|
-
# cases first. When `block_type` is nil the tier is a no-op.
|
|
766
750
|
# The precise-tier folders, consulted in order via the uniform
|
|
767
751
|
# `_DispatchTier` interface (`try_dispatch(CallContext) -> Type?`).
|
|
768
752
|
# Order is significant: ConstantFolding's exact-value folds win
|
|
@@ -815,6 +799,14 @@ module Rigor
|
|
|
815
799
|
data_result = DataFolding.try_dispatch(context)
|
|
816
800
|
return data_result if data_result
|
|
817
801
|
|
|
802
|
+
# ADR-48 Struct follow-up — runs in the same band and for the same
|
|
803
|
+
# reason as DataFolding: `meta_new` would otherwise intercept every
|
|
804
|
+
# `Singleton[*].new` (the old `struct_new_lift` produced a bare
|
|
805
|
+
# `Singleton[Struct]`), masking a Struct class's precise instance.
|
|
806
|
+
# Only fires on Struct receivers, so it never shadows meta's lifts.
|
|
807
|
+
struct_result = StructFolding.try_dispatch(context)
|
|
808
|
+
return struct_result if struct_result
|
|
809
|
+
|
|
818
810
|
meta_result = try_meta_introspection(context.receiver, context.method_name, context.args)
|
|
819
811
|
return meta_result if meta_result
|
|
820
812
|
|
|
@@ -67,18 +67,12 @@ module Rigor
|
|
|
67
67
|
# ...; @warning_issued = true`). Requires tracking the
|
|
68
68
|
# first-write position; flow-sensitive but orthogonal.
|
|
69
69
|
# - **Local-variable mutation inside a block body** (e.g.
|
|
70
|
-
# `arr = []; xs.each { |x| arr << x }`)
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
# an outer LOCAL does not yet flow back. **Ivar mutations
|
|
75
|
-
# inside a block ARE handled** (ivars live in the
|
|
76
|
-
# method-body scope, not the block-local scope) — the
|
|
77
|
-
# widening fires from inside the block and the new ivar
|
|
78
|
-
# binding is visible to the outer scope.
|
|
70
|
+
# `arr = []; xs.each { |x| arr << x }`) — landed as
|
|
71
|
+
# ADR-56 slice A (`widen_after_block`). **Ivar mutations
|
|
72
|
+
# inside a block ARE also handled** (ivars live in the
|
|
73
|
+
# method-body scope, not the block-local scope).
|
|
79
74
|
#
|
|
80
|
-
#
|
|
81
|
-
# `docs/CURRENT_WORK.md` and are intentionally deferred.
|
|
75
|
+
# The remaining three items above are demand-gated; see ADR-56.
|
|
82
76
|
module MutationWidening
|
|
83
77
|
# Array mutators that change either the size or the element
|
|
84
78
|
# set of a literal-shape carrier (Tuple). Receiver-mutating
|
|
@@ -11,7 +11,8 @@ require_relative "../builtins/regex_refinement"
|
|
|
11
11
|
|
|
12
12
|
module Rigor
|
|
13
13
|
module Inference
|
|
14
|
-
#
|
|
14
|
+
# Control-flow predicate narrowing and type-lattice narrowing
|
|
15
|
+
# primitives.
|
|
15
16
|
#
|
|
16
17
|
# `Rigor::Inference::Narrowing` answers two related questions:
|
|
17
18
|
#
|
|
@@ -19,22 +20,19 @@ module Rigor
|
|
|
19
20
|
# truthy fragment, its falsey fragment, its nil fragment, and its
|
|
20
21
|
# non-nil fragment? These primitives understand the value-lattice
|
|
21
22
|
# algebra (`Constant`, `Nominal`, `Singleton`, `Tuple`, `HashShape`,
|
|
22
|
-
# `Union`) and stay conservative on `Top` and `Dynamic[T]
|
|
23
|
-
# the analyzer cannot prove the boundary either way.
|
|
23
|
+
# `Union`) and stay conservative on `Top` and `Dynamic[T]`.
|
|
24
24
|
# 2. Predicate-level narrowing: given a Prism predicate node and an
|
|
25
25
|
# entry scope, what are the truthy-edge scope and the falsey-edge
|
|
26
|
-
# scope
|
|
27
|
-
#
|
|
28
|
-
# against
|
|
29
|
-
#
|
|
26
|
+
# scope? The catalogue covers truthiness, `nil?`, `!`, `&&`/`||`,
|
|
27
|
+
# class-membership (`is_a?`, `kind_of?`, `instance_of?`), trusted
|
|
28
|
+
# equality/inequality against static literals, `case`/`when`,
|
|
29
|
+
# regex match globals, string predicates (`start_with?` etc.),
|
|
30
|
+
# key-presence, array emptiness, numeric comparison, and
|
|
31
|
+
# `respond_to?`.
|
|
30
32
|
#
|
|
31
|
-
#
|
|
32
|
-
# `
|
|
33
|
-
# `
|
|
34
|
-
# bindings on truthiness and `nil?`; phase 2 extends the catalogue
|
|
35
|
-
# with class-membership predicates (`is_a?`, `kind_of?`,
|
|
36
|
-
# `instance_of?`) and trusted equality/inequality checks against
|
|
37
|
-
# static literals.
|
|
33
|
+
# Consumed by `Rigor::Inference::StatementEvaluator` to refine
|
|
34
|
+
# `then`/`else` scopes of `IfNode`/`UnlessNode` and
|
|
35
|
+
# `case`/`when` branches.
|
|
38
36
|
#
|
|
39
37
|
# The module is pure: every public function returns fresh values and
|
|
40
38
|
# MUST NOT mutate its inputs. Unrecognised predicate shapes degrade
|
|
@@ -43,8 +41,8 @@ module Rigor
|
|
|
43
41
|
# `[truthy_scope, falsey_scope]` pair (the entry scope twice when no
|
|
44
42
|
# rule matches).
|
|
45
43
|
#
|
|
46
|
-
# See docs/internal-spec/inference-engine.md (
|
|
47
|
-
#
|
|
44
|
+
# See docs/internal-spec/inference-engine.md (Narrowing) and
|
|
45
|
+
# docs/type-specification/control-flow-analysis.md for the
|
|
48
46
|
# binding contract.
|
|
49
47
|
# rubocop:disable Metrics/ModuleLength
|
|
50
48
|
module Narrowing
|