rigortype 0.3.1 → 0.3.2
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 +10 -10
- data/data/core_overlay/pathname.rbs +5 -0
- data/data/core_overlay/resolv.rbs +31 -0
- data/data/core_overlay/string_scanner.rbs +5 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +10 -6
- data/data/vendored_gem_sigs/bundler/bundler.rbs +39 -27
- data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +6 -0
- data/data/vendored_gem_sigs/prism/prism_supplement.rbs +7 -0
- data/data/vendored_gem_sigs/racc/racc.rbs +67 -0
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +20 -9
- data/docs/manual/02-cli-reference.md +71 -11
- data/docs/manual/03-configuration.md +16 -1
- data/docs/manual/12-caching.md +17 -5
- data/docs/manual/15-type-protection-coverage.md +122 -1
- data/docs/manual/plugins/rigor-rbs-inline.md +22 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -0
- data/lib/rigor/analysis/check_rules.rb +45 -6
- data/lib/rigor/analysis/incremental_session.rb +156 -11
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +14 -0
- data/lib/rigor/analysis/run_cache_key.rb +17 -1
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +201 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +28 -10
- data/lib/rigor/analysis/runner/pool_coordinator.rb +7 -10
- data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -1
- data/lib/rigor/analysis/runner.rb +78 -7
- data/lib/rigor/bleeding_edge.rb +132 -7
- data/lib/rigor/cache/descriptor.rb +6 -1
- data/lib/rigor/cache/engine_source.rb +162 -0
- data/lib/rigor/cache/incremental_snapshot.rb +87 -18
- data/lib/rigor/cli/check_command.rb +49 -28
- data/lib/rigor/cli/coverage_command.rb +15 -2
- data/lib/rigor/cli/coverage_mutation.rb +242 -11
- data/lib/rigor/cli/fused_protection_renderer.rb +10 -0
- data/lib/rigor/cli/fused_protection_report.rb +12 -3
- data/lib/rigor/cli/mutation_fork_scan.rb +64 -0
- data/lib/rigor/cli/mutation_protection_renderer.rb +12 -0
- data/lib/rigor/cli/mutation_protection_report.rb +26 -5
- data/lib/rigor/cli/protection_renderer.rb +13 -0
- data/lib/rigor/cli/protection_report.rb +11 -3
- data/lib/rigor/cli/show_bleedingedge_command.rb +17 -4
- data/lib/rigor/cli/sig_gen_command.rb +5 -4
- data/lib/rigor/configuration.rb +45 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +2 -1
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +434 -75
- data/lib/rigor/environment.rb +28 -13
- data/lib/rigor/inference/expression_typer.rb +15 -0
- data/lib/rigor/inference/fork_map.rb +9 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +50 -2
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +35 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +16 -0
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +64 -5
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +137 -9
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +124 -1
- data/lib/rigor/inference/mutation_widening.rb +30 -25
- data/lib/rigor/inference/narrowing.rb +3 -1
- data/lib/rigor/inference/optimistic_origin.rb +58 -0
- data/lib/rigor/inference/protection_scanner.rb +18 -2
- data/lib/rigor/inference/receiver_alias.rb +57 -0
- data/lib/rigor/inference/scope_indexer.rb +78 -27
- data/lib/rigor/inference/statement_evaluator.rb +40 -2
- data/lib/rigor/language_server/buffer_table.rb +22 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +203 -4
- data/lib/rigor/language_server/project_context.rb +45 -0
- data/lib/rigor/language_server/publish_batcher.rb +82 -0
- data/lib/rigor/language_server/server.rb +20 -1
- data/lib/rigor/language_server.rb +1 -0
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
- data/lib/rigor/protection/closure_kill_oracle.rb +171 -0
- data/lib/rigor/protection/dependency_closure.rb +59 -0
- data/lib/rigor/protection/diagnostic_oracle.rb +12 -8
- data/lib/rigor/protection/discovery_seed.rb +116 -0
- data/lib/rigor/protection/kill_signature.rb +31 -0
- data/lib/rigor/protection/mutation_cache.rb +355 -0
- data/lib/rigor/protection/mutation_scanner.rb +55 -11
- data/lib/rigor/protection/mutator.rb +26 -4
- data/lib/rigor/runtime/jit.rb +63 -1
- data/lib/rigor/scope/discovery_index.rb +9 -0
- data/lib/rigor/scope.rb +52 -8
- data/lib/rigor/sig_gen/generator.rb +130 -31
- data/lib/rigor/sig_gen/layout_index.rb +6 -0
- data/lib/rigor/sig_gen/meta_class_shape.rb +84 -0
- data/lib/rigor/sig_gen/renderer.rb +20 -1
- data/lib/rigor/sig_gen/write_result.rb +6 -4
- data/lib/rigor/sig_gen/writer.rb +12 -0
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +6 -6
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/result_shape.rb +146 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +177 -12
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +74 -7
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +257 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +69 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +87 -11
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +34 -4
- data/sig/rigor/inference/void_origin.rbs +6 -3
- data/sig/rigor/scope.rbs +8 -0
- metadata +17 -1
|
@@ -20,12 +20,31 @@ module Rigor
|
|
|
20
20
|
# decode. Returns `Constant[String]`.
|
|
21
21
|
# * `encode_uri_component(str)` / `decode_uri_component(str)` — Same encoding but may preserve
|
|
22
22
|
# additional reserved chars (Ruby 3.2+). Returns `Constant[String]`.
|
|
23
|
+
# * `encode_www_form(enum)` — the whole-form encoder, over a `Tuple` of `[key, value]` `Tuple`s (an
|
|
24
|
+
# array literal) or a `HashShape` (a hash literal) whose keys and values are all `Constant`.
|
|
25
|
+
# Returns `Constant[String]`.
|
|
26
|
+
# * `decode_www_form(str)` — the inverse, over a `Constant[String]`. Returns a `Tuple` of two-element
|
|
27
|
+
# `Tuple[Constant[String], Constant[String]]` pairs rather than the RBS tier's
|
|
28
|
+
# `Array[[String, String]]`, so a destructuring read of a known form gets the concrete strings.
|
|
29
|
+
# * `extract(str)` — the URIs a constant string contains, as a `Tuple` of `Constant[String]` rather
|
|
30
|
+
# than `Array[String]`. Single-argument form only (see {.fold_extract}).
|
|
31
|
+
#
|
|
32
|
+
# === Deliberately NOT folded
|
|
33
|
+
#
|
|
34
|
+
# `URI.parse` and `URI.join` are the two remaining gaps in this module's coverage note, and both are
|
|
35
|
+
# declined on the repo's own existing rule rather than on judgement: they return `URI::Generic` and
|
|
36
|
+
# friends, which are absent from `ConstantFolding::FOLDABLE_CONSTANT_CLASSES`, so there is no
|
|
37
|
+
# `Constant[…]` for this tier to produce. Narrowing `URI.parse`'s ten-arm return union to the one
|
|
38
|
+
# scheme class a constant string selects WOULD be a real precision win, but it makes a gradually-valid
|
|
39
|
+
# dispatch precise and can therefore surface a diagnostic that does not fire today — which puts it in
|
|
40
|
+
# the bucket-3 / P0 category, not in this FP-safe fold category ([#121](
|
|
41
|
+
# https://github.com/rigortype/rigor/issues/121)).
|
|
23
42
|
#
|
|
24
43
|
# === Non-constant / unsupported cases
|
|
25
44
|
#
|
|
26
45
|
# Returns `nil` (deferring to the next dispatcher tier) when:
|
|
27
46
|
# - the receiver is not `Singleton[URI]`,
|
|
28
|
-
# -
|
|
47
|
+
# - an argument is not the constant shape the method's fold requires,
|
|
29
48
|
# - the method is not in the supported set.
|
|
30
49
|
module URIFolding
|
|
31
50
|
URI_COMPONENT_METHODS = Set[
|
|
@@ -34,6 +53,12 @@ module Rigor
|
|
|
34
53
|
].freeze
|
|
35
54
|
private_constant :URI_COMPONENT_METHODS
|
|
36
55
|
|
|
56
|
+
# A form long enough to suggest generated or accumulated data rather than a literal a reader wrote;
|
|
57
|
+
# past it the folded `Constant[String]` stops being useful in a diagnostic and only costs memory.
|
|
58
|
+
# Mirrors the byte ceiling `ShapeDispatch`'s own `tuple_join` fold applies for the same reason.
|
|
59
|
+
FORM_PAIR_LIMIT = 64
|
|
60
|
+
private_constant :FORM_PAIR_LIMIT
|
|
61
|
+
|
|
37
62
|
module_function
|
|
38
63
|
|
|
39
64
|
# @return [Rigor::Type, nil] folded result, or nil to defer.
|
|
@@ -42,11 +67,109 @@ module Rigor
|
|
|
42
67
|
method_name = context.method_name
|
|
43
68
|
args = context.args
|
|
44
69
|
return nil unless SingletonFolding.receiver?(receiver, "URI")
|
|
70
|
+
return fold_encode_www_form(args) if method_name == :encode_www_form
|
|
71
|
+
return fold_decode_www_form(args) if method_name == :decode_www_form
|
|
72
|
+
return fold_extract(args) if method_name == :extract
|
|
45
73
|
return nil unless URI_COMPONENT_METHODS.include?(method_name)
|
|
46
74
|
|
|
47
75
|
fold_uri_call(method_name, args)
|
|
48
76
|
end
|
|
49
77
|
|
|
78
|
+
# `URI.encode_www_form(enum)` — deterministic over its whole input, so it folds only when EVERY key
|
|
79
|
+
# and value is already a `Constant`. One non-constant pair declines the whole call: a partially
|
|
80
|
+
# known form has no `Constant[String]` to stand for it.
|
|
81
|
+
def fold_encode_www_form(args)
|
|
82
|
+
return nil unless args.size == 1
|
|
83
|
+
|
|
84
|
+
pairs = form_pairs(args.first)
|
|
85
|
+
return nil if pairs.nil? || pairs.size > FORM_PAIR_LIMIT
|
|
86
|
+
|
|
87
|
+
Type::Combinator.constant_of(URI.encode_www_form(pairs))
|
|
88
|
+
rescue StandardError
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The `[key, value]` pairs of a literal form argument, or nil when the shape is not fully known:
|
|
93
|
+
# a `Tuple` of two-element `Tuple`s (the array-of-pairs spelling) or a `HashShape` (the hash
|
|
94
|
+
# spelling). `HashShape` pairs are read only when the shape is CLOSED and carries no optional key —
|
|
95
|
+
# an open or partially-optional shape describes a hash whose real contents this fold cannot see.
|
|
96
|
+
def form_pairs(arg)
|
|
97
|
+
case arg
|
|
98
|
+
when Type::Tuple then tuple_form_pairs(arg)
|
|
99
|
+
when Type::HashShape then hash_form_pairs(arg)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def tuple_form_pairs(tuple)
|
|
104
|
+
tuple.elements.map do |element|
|
|
105
|
+
return nil unless element.is_a?(Type::Tuple) && element.elements.size == 2
|
|
106
|
+
return nil unless element.elements.all?(Type::Constant)
|
|
107
|
+
|
|
108
|
+
element.elements.map(&:value)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def hash_form_pairs(shape)
|
|
113
|
+
return nil unless shape.closed? && shape.optional_keys.empty?
|
|
114
|
+
|
|
115
|
+
shape.pairs.map do |key, value|
|
|
116
|
+
return nil unless value.is_a?(Type::Constant)
|
|
117
|
+
|
|
118
|
+
[key, value.value]
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# `URI.decode_www_form(str)` — the pair list of a constant form, as a `Tuple` of two-element
|
|
123
|
+
# `Tuple`s so a destructuring read (`k, v = URI.decode_www_form(s).first`) reaches the concrete
|
|
124
|
+
# strings instead of the RBS tier's `Array[[String, String]]`.
|
|
125
|
+
def fold_decode_www_form(args)
|
|
126
|
+
return nil unless args.size == 1
|
|
127
|
+
|
|
128
|
+
str = SingletonFolding.constant_string(args.first)
|
|
129
|
+
return nil if str.nil?
|
|
130
|
+
|
|
131
|
+
pairs = URI.decode_www_form(str)
|
|
132
|
+
return nil if pairs.size > FORM_PAIR_LIMIT
|
|
133
|
+
|
|
134
|
+
Type::Combinator.tuple_of(*pairs.map { |key, value| constant_pair(key, value) })
|
|
135
|
+
rescue StandardError
|
|
136
|
+
nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def constant_pair(key, value)
|
|
140
|
+
Type::Combinator.tuple_of(Type::Combinator.constant_of(key), Type::Combinator.constant_of(value))
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# `URI.extract(str)` — the URIs a constant string contains, as a `Tuple` of `Constant[String]`
|
|
144
|
+
# instead of the RBS tier's `Array[String]`. Only the single-argument form folds: the optional
|
|
145
|
+
# second argument is a schema FILTER, and honouring it means reproducing `URI.extract`'s own
|
|
146
|
+
# argument handling rather than the one call this fold makes, so it declines instead.
|
|
147
|
+
#
|
|
148
|
+
# Ruby emits an obsolescence warning for this method under `$VERBOSE`; the fold calls it with
|
|
149
|
+
# warnings silenced, because a warning about the ANALYZED program's API choice must not appear on
|
|
150
|
+
# the analyzer's own stderr, where a reader would attribute it to Rigor.
|
|
151
|
+
def fold_extract(args)
|
|
152
|
+
return nil unless args.size == 1
|
|
153
|
+
|
|
154
|
+
str = SingletonFolding.constant_string(args.first)
|
|
155
|
+
return nil if str.nil?
|
|
156
|
+
|
|
157
|
+
found = silence_warnings { URI.extract(str) }
|
|
158
|
+
return nil if found.nil? || found.size > FORM_PAIR_LIMIT
|
|
159
|
+
|
|
160
|
+
Type::Combinator.tuple_of(*found.map { |uri| Type::Combinator.constant_of(uri) })
|
|
161
|
+
rescue StandardError
|
|
162
|
+
nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def silence_warnings
|
|
166
|
+
previous = $VERBOSE
|
|
167
|
+
$VERBOSE = nil
|
|
168
|
+
yield
|
|
169
|
+
ensure
|
|
170
|
+
$VERBOSE = previous
|
|
171
|
+
end
|
|
172
|
+
|
|
50
173
|
def fold_uri_call(method_name, args)
|
|
51
174
|
return nil unless args.size == 1
|
|
52
175
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../type"
|
|
4
4
|
require_relative "../source/node_children"
|
|
5
|
+
require_relative "receiver_alias"
|
|
5
6
|
|
|
6
7
|
module Rigor
|
|
7
8
|
module Inference
|
|
@@ -45,6 +46,10 @@ module Rigor
|
|
|
45
46
|
#
|
|
46
47
|
# - `arr.<mutator>(...)` where `arr` is a local variable.
|
|
47
48
|
# - `@arr.<mutator>(...)` where `@arr` is an instance variable.
|
|
49
|
+
# - a receiver that *selects* among such variables rather than naming one —
|
|
50
|
+
# `(kind == :required ? required : optional)[key] = info`, an `if`/`else`, `||` / `&&`. Every
|
|
51
|
+
# variable the expression can evaluate to is a possible mutation target, so every one widens
|
|
52
|
+
# (issue #277). See {ReceiverAlias}.
|
|
48
53
|
#
|
|
49
54
|
# Out of scope (left for a separate cycle):
|
|
50
55
|
#
|
|
@@ -100,10 +105,10 @@ module Rigor
|
|
|
100
105
|
PURE_SELF_RETURNERS.include?(method_name)
|
|
101
106
|
end
|
|
102
107
|
|
|
103
|
-
# Returns a scope with the call's receiver widened,
|
|
104
|
-
#
|
|
108
|
+
# Returns a scope with the call's receiver widened, for every variable the receiver expression
|
|
109
|
+
# can evaluate to ({ReceiverAlias.candidates}) whose current binding is a literal-shape carrier
|
|
105
110
|
# (`Tuple` / `HashShape`) or an empty-witness refinement (`non-empty-array` /
|
|
106
|
-
# `non-empty-hash`) AND
|
|
111
|
+
# `non-empty-hash`) AND whose call name is a known in-place mutator for that shape.
|
|
107
112
|
# Returns `current_scope` unchanged otherwise.
|
|
108
113
|
#
|
|
109
114
|
# @param call_node [Prism::CallNode]
|
|
@@ -115,13 +120,8 @@ module Rigor
|
|
|
115
120
|
receiver = call_node.receiver
|
|
116
121
|
return current_scope if receiver.nil?
|
|
117
122
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
widen_local(call_node.name, receiver.name, current_scope)
|
|
121
|
-
when Prism::InstanceVariableReadNode
|
|
122
|
-
widen_ivar(call_node.name, receiver.name, current_scope)
|
|
123
|
-
else
|
|
124
|
-
current_scope
|
|
123
|
+
ReceiverAlias.candidates(receiver).reduce(current_scope) do |acc, read|
|
|
124
|
+
widen_alias_read(call_node.name, read, acc)
|
|
125
125
|
end
|
|
126
126
|
end
|
|
127
127
|
|
|
@@ -130,13 +130,13 @@ module Rigor
|
|
|
130
130
|
# LOCALS are otherwise invisible to the post-call outer scope (ivars are handled correctly
|
|
131
131
|
# already because they live in the method-body scope, not the block-local scope).
|
|
132
132
|
#
|
|
133
|
-
# Walks the block AST for `<receiver>.<method>(...)` calls
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
# `
|
|
137
|
-
#
|
|
138
|
-
# precision — so blindly propagating is sound regardless of
|
|
139
|
-
# runs.
|
|
133
|
+
# Walks the block AST for `<receiver>.<method>(...)` calls, resolves the receiver expression
|
|
134
|
+
# to the variables it can evaluate to ({ReceiverAlias.candidates}), and widens each one
|
|
135
|
+
# against the outer scope. A `LocalVariableReadNode` with `depth == 0` is skipped — Prism's
|
|
136
|
+
# `depth` counts scope hops outward, so `0` means a block-local, not a capture; an
|
|
137
|
+
# `InstanceVariableReadNode` is always method-scope and always applies. The widening is
|
|
138
|
+
# always safe — it can only LOSE precision — so blindly propagating is sound regardless of
|
|
139
|
+
# whether the block actually runs.
|
|
140
140
|
#
|
|
141
141
|
# Recurses into nested expression nodes so chained / nested forms (`arr << f(x); arr <<
|
|
142
142
|
# g(y)`, `arr.push(x) if cond`) are all caught. Does NOT recurse into nested
|
|
@@ -174,15 +174,20 @@ module Rigor
|
|
|
174
174
|
receiver = call_node.receiver
|
|
175
175
|
return scope if receiver.nil?
|
|
176
176
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
ReceiverAlias.candidates(receiver).reduce(scope) do |acc, read|
|
|
178
|
+
# A block-local read (`depth == 0`) is not a capture of the outer scope, so widening its
|
|
179
|
+
# name against the OUTER scope would hit an unrelated same-named binding.
|
|
180
|
+
next acc if read.is_a?(Prism::LocalVariableReadNode) && read.depth.zero?
|
|
180
181
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
182
|
+
widen_alias_read(call_node.name, read, acc)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def widen_alias_read(method_name, read, scope)
|
|
187
|
+
case read
|
|
188
|
+
when Prism::LocalVariableReadNode then widen_local(method_name, read.name, scope)
|
|
189
|
+
when Prism::InstanceVariableReadNode then widen_ivar(method_name, read.name, scope)
|
|
190
|
+
else scope
|
|
186
191
|
end
|
|
187
192
|
end
|
|
188
193
|
|
|
@@ -1169,7 +1169,9 @@ module Rigor
|
|
|
1169
1169
|
# so the proven-absent key reads `nil`. Returns the input unchanged when nothing
|
|
1170
1170
|
# applies (caller detects "no narrowing"). Only an *optional* present key is removed: a
|
|
1171
1171
|
# required key makes `key?` always true (the false edge is dead, leave the shape opaque)
|
|
1172
|
-
# and a key absent from `pairs` already reads `nil
|
|
1172
|
+
# and a key absent from `pairs` already reads `nil` on a closed shape. On an open shape an
|
|
1173
|
+
# undeclared key keeps reading `untyped` on this edge — sound but imprecise, since the shape
|
|
1174
|
+
# carries no per-key exclusion to record what the guard just proved absent.
|
|
1173
1175
|
def narrow_hash_key_absent(type, key)
|
|
1174
1176
|
case type
|
|
1175
1177
|
when Type::HashShape
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Inference
|
|
5
|
+
# Marks a value whose *nil-freeness* rests on Rigor's deliberate choice to ignore core RBS's
|
|
6
|
+
# `%a{implicitly-returns-nil}` annotation rather than on the value's class. See
|
|
7
|
+
# docs/internal-spec/inference-engine.md § "That deferred-to answer is nil-free for a lookup that can
|
|
8
|
+
# miss": `Hash#[]` reads as `V` and `Array#first` as `E` because pessimising them to `V?` costs 25
|
|
9
|
+
# measured false positives on Rigor's own `lib`. The consequence the spec draws is that such a value is
|
|
10
|
+
# **optimistic, not proof** — `MAP[key]` reading as `"x" | "y"` asserts nothing about whether the key was
|
|
11
|
+
# present.
|
|
12
|
+
#
|
|
13
|
+
# A side channel in the ADR-75 / ADR-82 sense: it never participates in subtyping, consistency,
|
|
14
|
+
# normalization or erasure, and no diagnostic fires from it. It differs from {DynamicOrigin} in what it
|
|
15
|
+
# attaches to — the value here is *not* `Dynamic`; it is an ordinary `Union` / `Constant` / `Nominal`
|
|
16
|
+
# that happens to have been produced optimistically.
|
|
17
|
+
#
|
|
18
|
+
# Issue #286: the `if` / `unless` branch elision is a third consumer of `Narrowing.predicate_certainty`,
|
|
19
|
+
# and unlike `flow.always-truthy-condition` and the `&&` / `||` gate it is not constrained by the spec
|
|
20
|
+
# passage above. This channel is what lets a certainty judgment tell the two apart.
|
|
21
|
+
module OptimisticOrigin
|
|
22
|
+
# The core-RBS annotation `RbsDispatch` reads the return type past.
|
|
23
|
+
ANNOTATION = "implicitly-returns-nil"
|
|
24
|
+
|
|
25
|
+
# The single cause carried today. Kept as a symbol (rather than a bare `true`) so a later slice can
|
|
26
|
+
# distinguish further optimistic families without changing the table's shape.
|
|
27
|
+
IMPLICITLY_RETURNS_NIL = :implicitly_returns_nil
|
|
28
|
+
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
# Whether the overload the selector actually picked carries the ignored annotation. The judgment is
|
|
32
|
+
# per-overload, which is what makes it precise: `Array#first` is optimistic while `Array#first(3)` is
|
|
33
|
+
# not, and `String#[]` / `Enumerable#find` are honest because they already spell the miss as `?`.
|
|
34
|
+
#
|
|
35
|
+
# @param method_definition [RBS::Definition::Method]
|
|
36
|
+
# @param method_type [RBS::MethodType] the overload {OverloadSelector.select} returned
|
|
37
|
+
# @return [Boolean]
|
|
38
|
+
def optimistic_overload?(method_definition, method_type)
|
|
39
|
+
type_def = matching_type_def(method_definition, method_type)
|
|
40
|
+
return false unless type_def.respond_to?(:overload_annotations)
|
|
41
|
+
|
|
42
|
+
type_def.overload_annotations.any? { |annotation| annotation.string == ANNOTATION }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# `RBS::Definition::Method#defs` runs parallel to `#method_types`, and the selector returns one of the
|
|
46
|
+
# latter's elements verbatim (`ReceiverAffinity.reorder` permutes the array without copying its
|
|
47
|
+
# members), so identity resolves the overload exactly. Equality is a fallback for any future path that
|
|
48
|
+
# rebuilds the method type.
|
|
49
|
+
def matching_type_def(method_definition, method_type)
|
|
50
|
+
return nil unless method_definition.respond_to?(:defs)
|
|
51
|
+
|
|
52
|
+
defs = method_definition.defs
|
|
53
|
+
defs.find { |type_def| type_def.type.equal?(method_type) } ||
|
|
54
|
+
defs.find { |type_def| type_def.type == method_type }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "scope_indexer"
|
|
4
4
|
require_relative "origin_lookup"
|
|
5
5
|
require_relative "../source/node_walker"
|
|
6
|
+
require_relative "../analysis/check_rules/inferred_param_guard"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
module Inference
|
|
@@ -20,7 +21,11 @@ module Rigor
|
|
|
20
21
|
# A single unprotected call site.
|
|
21
22
|
Site = Data.define(:line, :receiver, :method_name, :dynamic_origin)
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
# `lower_bound_typed` is a split WITHIN `protected_count`, never a move out of it (issue #263):
|
|
25
|
+
# a site whose receiver is concrete but rooted at a WD6b-marked inferred parameter (
|
|
26
|
+
# {Analysis::CheckRules::InferredParamGuard.rooted?}) is a lower-bound type — `protected_count`
|
|
27
|
+
# and `ratio` stay exactly as before, so `--threshold` never moves on unchanged code.
|
|
28
|
+
FileResult = Data.define(:protected_count, :unprotected_count, :lower_bound_typed, :sites) do
|
|
24
29
|
def total = protected_count + unprotected_count
|
|
25
30
|
|
|
26
31
|
# Protected ratio; a file with no dispatch sites is vacuously fully protected (nothing to get
|
|
@@ -37,6 +42,7 @@ module Rigor
|
|
|
37
42
|
def scan(root)
|
|
38
43
|
index = ScopeIndexer.index(root, default_scope: @scope)
|
|
39
44
|
protected_count = 0
|
|
45
|
+
lower_bound_typed = 0
|
|
40
46
|
sites = []
|
|
41
47
|
|
|
42
48
|
Source::NodeWalker.each(root) do |node|
|
|
@@ -46,6 +52,7 @@ module Rigor
|
|
|
46
52
|
receiver_type = scope.type_of(node.receiver)
|
|
47
53
|
if concrete_receiver?(receiver_type)
|
|
48
54
|
protected_count += 1
|
|
55
|
+
lower_bound_typed += 1 if lower_bound_typed?(node.receiver, scope)
|
|
49
56
|
else
|
|
50
57
|
origin = OriginLookup.origin_for(scope, node.receiver)
|
|
51
58
|
sites << Site.new(
|
|
@@ -57,7 +64,8 @@ module Rigor
|
|
|
57
64
|
end
|
|
58
65
|
end
|
|
59
66
|
|
|
60
|
-
FileResult.new(protected_count: protected_count, unprotected_count: sites.size,
|
|
67
|
+
FileResult.new(protected_count: protected_count, unprotected_count: sites.size,
|
|
68
|
+
lower_bound_typed: lower_bound_typed, sites: sites)
|
|
61
69
|
end
|
|
62
70
|
|
|
63
71
|
private
|
|
@@ -77,6 +85,14 @@ module Rigor
|
|
|
77
85
|
end
|
|
78
86
|
end
|
|
79
87
|
|
|
88
|
+
# ADR-67 WD6b (issue #263) — true when a concrete receiver is rooted at an inferred-but-undeclared
|
|
89
|
+
# parameter: the type is a call-site lower bound, so the negative in-body rules decline on it (no
|
|
90
|
+
# diagnostic can actually fire here yet). Delegates to the single-homed shared predicate every
|
|
91
|
+
# negative rule already consults — nothing is re-derived.
|
|
92
|
+
def lower_bound_typed?(receiver, scope)
|
|
93
|
+
Analysis::CheckRules::InferredParamGuard.rooted?(receiver, scope)
|
|
94
|
+
end
|
|
95
|
+
|
|
80
96
|
def safe_describe(type)
|
|
81
97
|
type.respond_to?(:describe) ? type.describe(:short) : type.to_s
|
|
82
98
|
rescue StandardError
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
# Which variables can a receiver EXPRESSION evaluate to?
|
|
8
|
+
#
|
|
9
|
+
# A receiver-fact invalidation (see {MutationWidening}) has to name the binding it invalidates,
|
|
10
|
+
# and the overwhelmingly common receiver — a bare `arr` / `@arr` read — names exactly one. But a
|
|
11
|
+
# receiver may also *select* among variables without naming any of them:
|
|
12
|
+
#
|
|
13
|
+
# (kind == :required ? required : optional)[key] = info
|
|
14
|
+
#
|
|
15
|
+
# The mutation lands on whichever of `required` / `optional` the ternary picked, so BOTH are
|
|
16
|
+
# possible targets and both must forget their literal shape. Reading only the syntactic head
|
|
17
|
+
# left both hashes carrying the empty `HashShape` the literal `{}` wrote, and a downstream
|
|
18
|
+
# `.empty?` then constant-folded into a false `flow.always-truthy-condition`
|
|
19
|
+
# ([#277](https://github.com/rigortype/rigor/issues/277)).
|
|
20
|
+
#
|
|
21
|
+
# The recursion covers only the forms whose value IS one of the sub-expressions: `if` / `unless`
|
|
22
|
+
# (including the ternary spelling), the short-circuit operators, and the transparent wrappers.
|
|
23
|
+
# Anything else — an index read (`declared[kind] << key`), a call result, a literal — names an
|
|
24
|
+
# object no binding can be attributed to and yields `[]`, which is what every receiver form
|
|
25
|
+
# outside the single-read case already contributed. The walk is depth-capped so a pathological
|
|
26
|
+
# nest cannot make receiver classification unbounded.
|
|
27
|
+
module ReceiverAlias
|
|
28
|
+
# Deep enough for any hand-written selection; a nest beyond it degrades to "names no binding".
|
|
29
|
+
WALK_DEPTH_CAP = 6
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
# @param node [Prism::Node, nil] the receiver expression.
|
|
34
|
+
# @param depth [Integer] recursion depth, internal.
|
|
35
|
+
# @return [Array<Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode>] every variable
|
|
36
|
+
# read the expression can evaluate to; empty when it can evaluate to none.
|
|
37
|
+
def candidates(node, depth = 0)
|
|
38
|
+
return [] if node.nil? || depth > WALK_DEPTH_CAP
|
|
39
|
+
|
|
40
|
+
case node
|
|
41
|
+
when Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode then [node]
|
|
42
|
+
when Prism::ParenthesesNode then candidates(node.body, depth + 1)
|
|
43
|
+
when Prism::StatementsNode then candidates(node.body.last, depth + 1)
|
|
44
|
+
when Prism::ElseNode then candidates(node.statements, depth + 1)
|
|
45
|
+
when Prism::IfNode then branches(node.statements, node.subsequent, depth)
|
|
46
|
+
when Prism::UnlessNode then branches(node.statements, node.else_clause, depth)
|
|
47
|
+
when Prism::OrNode, Prism::AndNode then branches(node.left, node.right, depth)
|
|
48
|
+
else []
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def branches(first, second, depth)
|
|
53
|
+
candidates(first, depth + 1) + candidates(second, depth + 1)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -1275,16 +1275,40 @@ module Rigor
|
|
|
1275
1275
|
end
|
|
1276
1276
|
|
|
1277
1277
|
# Merges two `class_name => { method => kind }` tables, unioning the per-class method maps (so a seeded cross-file
|
|
1278
|
-
# table and the current file's table combine instead of clobbering).
|
|
1278
|
+
# table and the current file's table combine instead of clobbering). A name recorded on both sides — an instance
|
|
1279
|
+
# `def` here and a `class << self` twin there — merges to {Scope::DiscoveryIndex::METHOD_KIND_BOTH} rather than
|
|
1280
|
+
# letting the overlay's kind win (#239).
|
|
1279
1281
|
def deep_merge_class_methods(base, overlay)
|
|
1280
1282
|
return overlay if base.nil? || base.empty?
|
|
1281
1283
|
return base if overlay.empty?
|
|
1282
1284
|
|
|
1283
1285
|
base.merge(overlay) do |_class_name, base_methods, overlay_methods|
|
|
1284
|
-
base_methods
|
|
1286
|
+
merge_method_kinds(base_methods, overlay_methods)
|
|
1285
1287
|
end
|
|
1286
1288
|
end
|
|
1287
1289
|
|
|
1290
|
+
# `{ method => kind }` union that promotes a kind disagreement to `METHOD_KIND_BOTH` instead of clobbering.
|
|
1291
|
+
def merge_method_kinds(base_methods, overlay_methods)
|
|
1292
|
+
base_methods.merge(overlay_methods) do |_method_name, base_kind, overlay_kind|
|
|
1293
|
+
base_kind == overlay_kind ? base_kind : Scope::DiscoveryIndex::METHOD_KIND_BOTH
|
|
1294
|
+
end
|
|
1295
|
+
end
|
|
1296
|
+
|
|
1297
|
+
# The single write path into a `class_name => { method => kind }` existence table. Every recorder goes through
|
|
1298
|
+
# it so a class that defines one name on both sides keeps both facts: the table is keyed by name alone, so a
|
|
1299
|
+
# bare assignment silently replaced the other side's kind and `Scope#discovered_method?` then answered false for
|
|
1300
|
+
# a method the source plainly defines (#239).
|
|
1301
|
+
def record_method_kind(accumulator, class_name, method_name, kind)
|
|
1302
|
+
table = (accumulator[class_name] ||= {})
|
|
1303
|
+
recorded = table[method_name]
|
|
1304
|
+
table[method_name] =
|
|
1305
|
+
if recorded.nil? || recorded == kind
|
|
1306
|
+
kind
|
|
1307
|
+
else
|
|
1308
|
+
Scope::DiscoveryIndex::METHOD_KIND_BOTH
|
|
1309
|
+
end
|
|
1310
|
+
end
|
|
1311
|
+
|
|
1288
1312
|
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
1289
1313
|
# Combined `walk_methods` + `walk_def_nodes` descent. The two walks had identical class / module / singleton-class
|
|
1290
1314
|
# / meta-block traversals and both stopped at `DefNode`; the only divergences are leaf actions (recorded into the
|
|
@@ -1393,8 +1417,7 @@ module Rigor
|
|
|
1393
1417
|
return if members.empty?
|
|
1394
1418
|
|
|
1395
1419
|
class_name = qualified_prefix.join("::")
|
|
1396
|
-
|
|
1397
|
-
members.each { |member| table[member] ||= :instance }
|
|
1420
|
+
members.each { |member| record_method_kind(accumulator, class_name, member, :instance) }
|
|
1398
1421
|
end
|
|
1399
1422
|
|
|
1400
1423
|
# The Symbol member names of a `Data.define(*Symbol)` / `Struct.new(*Symbol [, keyword_init:])` call. For
|
|
@@ -1412,8 +1435,7 @@ module Rigor
|
|
|
1412
1435
|
class_name = qualified_prefix.join("::")
|
|
1413
1436
|
singleton = def_singleton?(def_node, qualified_prefix, in_singleton_class)
|
|
1414
1437
|
kind = singleton ? :singleton : :instance
|
|
1415
|
-
accumulator
|
|
1416
|
-
accumulator[class_name][def_node.name] = kind
|
|
1438
|
+
record_method_kind(accumulator, class_name, def_node.name, kind)
|
|
1417
1439
|
end
|
|
1418
1440
|
|
|
1419
1441
|
# `def Foo.bar` inside `module Foo` (or `def Meta.init` inside `module Meta`) is semantically equivalent to `def
|
|
@@ -1918,7 +1940,7 @@ module Rigor
|
|
|
1918
1940
|
class_name = qualified_prefix.join("::")
|
|
1919
1941
|
new_name = alias_node.new_name.unescaped.to_sym
|
|
1920
1942
|
kind = in_singleton_class ? :singleton : :instance
|
|
1921
|
-
(accumulator
|
|
1943
|
+
record_method_kind(accumulator, class_name, new_name, kind)
|
|
1922
1944
|
end
|
|
1923
1945
|
|
|
1924
1946
|
# Post-pass over the `def_nodes` accumulator: for every `alias` declaration inside a class body, if the original
|
|
@@ -1981,8 +2003,7 @@ module Rigor
|
|
|
1981
2003
|
return if method_name.nil?
|
|
1982
2004
|
|
|
1983
2005
|
class_name = qualified_prefix.join("::")
|
|
1984
|
-
accumulator
|
|
1985
|
-
accumulator[class_name][method_name] = in_singleton_class ? :singleton : :instance
|
|
2006
|
+
record_method_kind(accumulator, class_name, method_name, in_singleton_class ? :singleton : :instance)
|
|
1986
2007
|
end
|
|
1987
2008
|
|
|
1988
2009
|
# The `attr_*` accessor macros that introduce methods Rigor must treat as source-declared. Without this, a class
|
|
@@ -2005,9 +2026,8 @@ module Rigor
|
|
|
2005
2026
|
base = literal_method_name(arg)
|
|
2006
2027
|
next if base.nil?
|
|
2007
2028
|
|
|
2008
|
-
accumulator
|
|
2009
|
-
accumulator
|
|
2010
|
-
accumulator[class_name][:"#{base}="] = kind if writer
|
|
2029
|
+
record_method_kind(accumulator, class_name, base, kind) if reader
|
|
2030
|
+
record_method_kind(accumulator, class_name, :"#{base}=", kind) if writer
|
|
2011
2031
|
end
|
|
2012
2032
|
end
|
|
2013
2033
|
|
|
@@ -2352,7 +2372,7 @@ module Rigor
|
|
|
2352
2372
|
file_index[:def_nodes].each { |cn, methods| (acc[:def_nodes][cn] ||= {}).merge!(methods) }
|
|
2353
2373
|
file_index[:singleton_def_nodes].each { |cn, methods| (acc[:singleton_def_nodes][cn] ||= {}).merge!(methods) }
|
|
2354
2374
|
file_index[:method_visibilities].each { |cn, table| (acc[:method_visibilities][cn] ||= {}).merge!(table) }
|
|
2355
|
-
file_index[:methods].each { |cn, table| (acc[:methods][cn]
|
|
2375
|
+
file_index[:methods].each { |cn, table| acc[:methods][cn] = merge_method_kinds(acc[:methods][cn] || {}, table) }
|
|
2356
2376
|
fold_def_sources(acc, :def_sources, file_index[:def_sources])
|
|
2357
2377
|
fold_def_sources(acc, :singleton_def_sources, file_index[:singleton_def_sources])
|
|
2358
2378
|
end
|
|
@@ -2468,10 +2488,18 @@ module Rigor
|
|
|
2468
2488
|
|
|
2469
2489
|
# Removes, per class, the method names that have a project `def` node, leaving only
|
|
2470
2490
|
# accessor/alias/define_method-introduced methods in the cross-file suppression table.
|
|
2491
|
+
#
|
|
2492
|
+
# `def_nodes` is the INSTANCE-side table, so a name recorded on both sides keeps its singleton half: that half
|
|
2493
|
+
# comes from a `def self.x` / `class << self` definition this rule never covered, and dropping it whole would
|
|
2494
|
+
# reintroduce #239's false `call.undefined-method` across files.
|
|
2471
2495
|
def subtract_def_methods(methods, def_nodes)
|
|
2472
2496
|
methods.each_with_object({}) do |(class_name, table), out|
|
|
2473
2497
|
defs = def_nodes[class_name] || {}
|
|
2474
|
-
kept = table.
|
|
2498
|
+
kept = table.each_with_object({}) do |(method_name, kind), acc|
|
|
2499
|
+
next acc[method_name] = kind unless defs.key?(method_name)
|
|
2500
|
+
|
|
2501
|
+
acc[method_name] = :singleton if kind == Scope::DiscoveryIndex::METHOD_KIND_BOTH
|
|
2502
|
+
end
|
|
2475
2503
|
out[class_name] = kept unless kept.empty?
|
|
2476
2504
|
end
|
|
2477
2505
|
end
|
|
@@ -2517,7 +2545,7 @@ module Rigor
|
|
|
2517
2545
|
(acc[:method_visibilities][class_name] ||= {}).merge!(table)
|
|
2518
2546
|
end
|
|
2519
2547
|
file_methods.each do |class_name, table|
|
|
2520
|
-
(acc[:methods][class_name]
|
|
2548
|
+
acc[:methods][class_name] = merge_method_kinds(acc[:methods][class_name] || {}, table)
|
|
2521
2549
|
end
|
|
2522
2550
|
end
|
|
2523
2551
|
|
|
@@ -2570,24 +2598,47 @@ module Rigor
|
|
|
2570
2598
|
node.rigor_each_child { |child| collect_class_decls(child, qualified_prefix, accumulator) }
|
|
2571
2599
|
end
|
|
2572
2600
|
|
|
2573
|
-
# T1 (template-corpora survey) — record a `Const = Class.new(Super)
|
|
2574
|
-
#
|
|
2575
|
-
#
|
|
2601
|
+
# T1 (template-corpora survey) — record a class-creating constant write (`Const = Class.new(Super)`, the bare
|
|
2602
|
+
# `Class.new` / `Module.new`, and the `Data.define(*sym)` / `Struct.new(*sym)` forms, each with or without a
|
|
2603
|
+
# block) in the cross-file discovery table so a reference to `Const` from ANOTHER file under the same namespace
|
|
2604
|
+
# resolves to the project class instead of falling through to a same-named class elsewhere
|
|
2576
2605
|
# (`Liquid::SyntaxError = Class.new(Error)` referenced in a sibling file's `rescue SyntaxError => e`, which
|
|
2577
2606
|
# otherwise resolved to core `::SyntaxError`). Mirrors the single-file `in_source_constants` answer, which types
|
|
2578
|
-
# `Class.new(Super)` as `Singleton[Super]` (the constructed class answers method lookups through Super's chain)
|
|
2579
|
-
#
|
|
2580
|
-
#
|
|
2581
|
-
#
|
|
2582
|
-
#
|
|
2607
|
+
# `Class.new(Super)` as `Singleton[Super]` (the constructed class answers method lookups through Super's chain)
|
|
2608
|
+
# and every other recognised form as `Singleton[Const]`.
|
|
2609
|
+
#
|
|
2610
|
+
# #271 — the Data/Struct forms are here because their absence was an ACTIVE false positive, not merely a missed
|
|
2611
|
+
# resolution: a nested `Const = Data.define(...)` invisible cross-file lets Ruby's lexical walk continue to the
|
|
2612
|
+
# PARENT namespace's same-named sibling, so `Analysis::PluginFactFingerprint.from_registry(...).opaque?` typed
|
|
2613
|
+
# its receiver as the unrelated `Rigor::Analysis::Result` and reported `call.undefined-method` on correct code.
|
|
2614
|
+
# Nested `Result` / `Entry` / `Config` Data constants shadowing a sibling are ordinary Ruby, and only the
|
|
2615
|
+
# DEFINING file's `in_source_constants` (never part of the project seed) knew about them.
|
|
2583
2616
|
def record_class_new_constant_decl(node, qualified_prefix, accumulator)
|
|
2584
2617
|
rvalue = node.value
|
|
2585
|
-
return unless
|
|
2586
|
-
return if rvalue.block # block form: handled by meta_new_block_body walks
|
|
2618
|
+
return unless meta_new_constant_rvalue?(rvalue)
|
|
2587
2619
|
|
|
2588
2620
|
full = (qualified_prefix + [node.name.to_s]).join("::")
|
|
2589
|
-
|
|
2590
|
-
|
|
2621
|
+
accumulator[full] = Type::Combinator.singleton_of(
|
|
2622
|
+
meta_new_constant_decl_name(rvalue, full, qualified_prefix, accumulator)
|
|
2623
|
+
)
|
|
2624
|
+
end
|
|
2625
|
+
|
|
2626
|
+
# The four recognised class-creating rvalue shapes at constant-write position — the same set
|
|
2627
|
+
# {#meta_new_block_body} recognises, so the cross-file table and the per-file block-as-method walk agree on what
|
|
2628
|
+
# counts as a declaration.
|
|
2629
|
+
def meta_new_constant_rvalue?(rvalue)
|
|
2630
|
+
class_new_call?(rvalue) || module_new_call?(rvalue) ||
|
|
2631
|
+
data_define_call?(rvalue) || struct_new_call?(rvalue)
|
|
2632
|
+
end
|
|
2633
|
+
|
|
2634
|
+
# The name the constant's `Singleton[...]` carries. Only a block-less `Class.new(Super)` borrows its superclass's
|
|
2635
|
+
# name (the constructed class answers lookups through `Super`'s chain and declares nothing of its own); every
|
|
2636
|
+
# other form — Data/Struct members, any block body — owns methods under its OWN qualified name, which is exactly
|
|
2637
|
+
# what the per-file `meta_new_constant_type` answers.
|
|
2638
|
+
def meta_new_constant_decl_name(rvalue, full, qualified_prefix, accumulator)
|
|
2639
|
+
return full if rvalue.block || data_define_call?(rvalue) || struct_new_call?(rvalue)
|
|
2640
|
+
|
|
2641
|
+
class_new_superclass_name(rvalue, qualified_prefix, accumulator) || full
|
|
2591
2642
|
end
|
|
2592
2643
|
|
|
2593
2644
|
# Lexically-qualified name of a `Class.new(Super)` superclass argument, or nil when there is no positional
|