rigortype 0.4.1 → 0.4.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 +1 -1
- data/data/builtins/ruby_core/io.yml +1 -0
- data/data/builtins/ruby_core/proc.yml +4 -0
- data/data/builtins/ruby_core/random.yml +4 -0
- data/data/builtins/ruby_core/struct.yml +3 -0
- data/data/core_overlay/env.rbs +34 -0
- data/data/core_overlay/gc.rbs +50 -0
- data/data/core_overlay/hash.rbs +45 -0
- data/data/core_overlay/integer.rbs +31 -0
- data/data/core_overlay/io_buffer.rbs +27 -0
- data/data/core_overlay/kernel.rbs +40 -0
- data/data/core_overlay/match_data.rbs +28 -0
- data/data/core_overlay/module.rbs +51 -0
- data/data/core_overlay/object_space.rbs +29 -0
- data/data/core_overlay/proc.rbs +45 -0
- data/data/core_overlay/range.rbs +40 -0
- data/data/core_overlay/source_range.rbs +78 -0
- data/data/core_overlay/string.rbs +86 -0
- data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
- data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
- data/docs/handbook/01-getting-started.md +3 -3
- data/docs/handbook/06-classes.md +3 -1
- data/docs/handbook/11-sig-gen.md +6 -0
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/manual/03-configuration.md +1 -1
- data/docs/manual/04-diagnostics.md +2 -1
- data/docs/manual/06-baseline.md +2 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/18-removing-dead-code.md +32 -2
- data/docs/manual/plugins/README.md +6 -1
- data/docs/manual/plugins/rigor-activerecord.md +21 -0
- data/docs/manual/plugins/rigor-alba.md +60 -0
- data/docs/manual/plugins/rigor-minitest.md +22 -4
- data/docs/manual/plugins/rigor-typelizer.md +71 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
- data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
- data/lib/rigor/analysis/check_rules.rb +289 -57
- data/lib/rigor/analysis/dependency_recorder.rb +15 -0
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
- data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
- data/lib/rigor/analysis/erb_template_detector.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +29 -9
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -1
- data/lib/rigor/analysis/incremental_session.rb +14 -1
- data/lib/rigor/analysis/reachability/graph.rb +123 -23
- data/lib/rigor/analysis/reachability/scan.rb +290 -36
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +16 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +87 -5
- data/lib/rigor/analysis/runner/pool_coordinator.rb +50 -11
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +34 -3
- data/lib/rigor/analysis/worker_session.rb +7 -4
- data/lib/rigor/cache/incremental_snapshot.rb +22 -0
- data/lib/rigor/cli/annotate_command.rb +5 -0
- data/lib/rigor/cli/baseline_command.rb +7 -18
- data/lib/rigor/cli/check_command.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +5 -0
- data/lib/rigor/cli/type_of_command.rb +5 -3
- data/lib/rigor/cli/unused_command.rb +13 -7
- data/lib/rigor/cli.rb +10 -2
- data/lib/rigor/configuration/severity_profile.rb +6 -0
- data/lib/rigor/configuration.rb +46 -2
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +283 -14
- data/lib/rigor/environment/required_features.rb +206 -0
- data/lib/rigor/environment.rb +21 -3
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- data/lib/rigor/inference/def_node_resolver.rb +23 -1
- data/lib/rigor/inference/expression_typer.rb +215 -16
- data/lib/rigor/inference/global_write_census.rb +7 -4
- data/lib/rigor/inference/guard_rebinding.rb +1 -1
- data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
- data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
- data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
- data/lib/rigor/inference/in_effect_refinements.rb +256 -0
- data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
- data/lib/rigor/inference/key_presence_guard.rb +303 -0
- data/lib/rigor/inference/macro_block_self_type.rb +59 -12
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
- data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
- data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +259 -28
- data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
- data/lib/rigor/inference/method_dispatcher.rb +44 -11
- data/lib/rigor/inference/module_function_state.rb +2 -1
- data/lib/rigor/inference/narrowing.rb +20 -6
- data/lib/rigor/inference/object_mixins.rb +180 -0
- data/lib/rigor/inference/project_patched_methods.rb +10 -5
- data/lib/rigor/inference/project_patched_scanner.rb +71 -3
- data/lib/rigor/inference/refine_census.rb +239 -0
- data/lib/rigor/inference/refine_self.rb +317 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
- data/lib/rigor/inference/refined_dispatch.rb +272 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
- data/lib/rigor/inference/scope_indexer.rb +687 -60
- data/lib/rigor/inference/statement_evaluator.rb +71 -20
- data/lib/rigor/inference/string_mutation.rb +9 -3
- data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
- data/lib/rigor/inference/version_guard.rb +19 -10
- data/lib/rigor/language_server/project_context.rb +31 -0
- data/lib/rigor/language_server/server.rb +18 -0
- data/lib/rigor/plugin/inflector.rb +1 -1
- data/lib/rigor/plugin/io_boundary.rb +42 -3
- data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
- data/lib/rigor/project_environment.rb +29 -6
- data/lib/rigor/protection/mutation_cache.rb +1 -1
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/reflection/constant_ancestors.rb +127 -11
- data/lib/rigor/reflection/constant_path.rb +13 -2
- data/lib/rigor/reflection.rb +40 -8
- data/lib/rigor/scope/discovery_index.rb +40 -1
- data/lib/rigor/scope/resolution_chain.rb +159 -11
- data/lib/rigor/scope.rb +189 -10
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +55 -5
- data/lib/rigor/sig_gen/method_candidate.rb +2 -2
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
- data/lib/rigor/source/alias_names.rb +52 -0
- data/lib/rigor/source/literals.rb +3 -2
- data/lib/rigor/source/symbol_name.rb +20 -0
- data/lib/rigor/type/combinator.rb +34 -2
- data/lib/rigor/type/float_range.rb +4 -1
- data/lib/rigor/type/nominal.rb +6 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
- data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
- data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
- data/plugins/rigor-alba/sig/alba.rbs +4 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
- data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
- data/sig/rigor/plugin/io_boundary.rbs +3 -0
- data/sig/rigor/scope.rbs +27 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +3 -1
- metadata +66 -1
|
@@ -18,7 +18,7 @@ module Rigor
|
|
|
18
18
|
# assert_kind_of(T, x) → narrow x to T
|
|
19
19
|
# assert_instance_of(T, x) → narrow x to T
|
|
20
20
|
# assert_nil(x) → narrow x to Constant<nil>
|
|
21
|
-
# assert_equal(literal, x) →
|
|
21
|
+
# assert_equal(literal, x) → meet x with Constant<literal> (see "Equality is not identity")
|
|
22
22
|
# assert_match(regex, x) → narrow x to String
|
|
23
23
|
#
|
|
24
24
|
# ### (2) Minitest / Test::Unit `refute_*` / `assert_not_*` (negative) ###
|
|
@@ -26,7 +26,7 @@ module Rigor
|
|
|
26
26
|
# refute_kind_of(T, x) → narrow x AWAY from T
|
|
27
27
|
# refute_instance_of(T, x) → narrow x AWAY from T
|
|
28
28
|
# refute_nil(x) → narrow x AWAY from nil
|
|
29
|
-
# refute_equal(literal, x) →
|
|
29
|
+
# refute_equal(literal, x) → negative Constant<literal> fact (the engine keeps x unchanged)
|
|
30
30
|
#
|
|
31
31
|
# Test::Unit's `assert_not_nil(x)` / `assert_not_equal(...)` / `assert_not_kind_of(...)` /
|
|
32
32
|
# `assert_not_instance_of(...)` share the recognizer with `refute_*` (they're aliases).
|
|
@@ -36,7 +36,7 @@ module Rigor
|
|
|
36
36
|
# _(x).must_be_kind_of(T) → narrow x to T
|
|
37
37
|
# _(x).must_be_instance_of(T) → narrow x to T
|
|
38
38
|
# _(x).must_be_nil → narrow x to Constant<nil>
|
|
39
|
-
# _(x).must_equal(literal) →
|
|
39
|
+
# _(x).must_equal(literal) → meet x with Constant<literal> (see "Equality is not identity")
|
|
40
40
|
# _(x).must_match(regex) → narrow x to String
|
|
41
41
|
#
|
|
42
42
|
# The legacy bare `x.must_be_kind_of(T)` form (Minitest < 6.0 monkey-patched onto Object) is
|
|
@@ -48,7 +48,25 @@ module Rigor
|
|
|
48
48
|
#
|
|
49
49
|
# _(x).wont_be_kind_of(T) → narrow x AWAY from T
|
|
50
50
|
# _(x).wont_be_nil → narrow x AWAY from nil
|
|
51
|
-
# _(x).wont_equal(literal) →
|
|
51
|
+
# _(x).wont_equal(literal) → negative Constant<literal> fact (the engine keeps x unchanged)
|
|
52
|
+
#
|
|
53
|
+
# ## Equality is not identity
|
|
54
|
+
#
|
|
55
|
+
# Both `assert_equal(exp, act)` and `_(act).must_equal(exp)` pass when `exp == act`, with the literal as
|
|
56
|
+
# the receiver. A positive equality fact never replaces the local's type (issue #1678):
|
|
57
|
+
#
|
|
58
|
+
# - `nil` / `true` / `false` / Symbol literals inherit identity `==` (`BasicObject#==`, `Symbol#==`),
|
|
59
|
+
# so `exp == act` holds only for the literal object itself and the local narrows to it.
|
|
60
|
+
# - Integer, Float and String literals compare across classes: `Integer#==` and `Float#==` defer to
|
|
61
|
+
# `act == exp` for a non-numeric argument (a `ModInt` with its own `==` passes `assert_equal 9, m`),
|
|
62
|
+
# numerics compare by value (`1 == 1.0`, `1 == 1r`, `0.0 == -0.0`), and `String#==` defers to
|
|
63
|
+
# `act == exp` for an argument that responds to `to_str`. The local narrows only when every member of
|
|
64
|
+
# its current type is already of the literal's own class, or a `nil` / boolean / Symbol the literal
|
|
65
|
+
# cannot equal; it is then a meet (`Integer` or `0..9` to `Constant<9>`, `1 | 2` to `Constant<2>`).
|
|
66
|
+
# A `Dynamic` local, a member of any other class, and a `0.0` literal keep the local unchanged.
|
|
67
|
+
#
|
|
68
|
+
# The negative forms emit a negative `Constant` fact, which the engine's fact narrowing does not act
|
|
69
|
+
# on, so `refute_equal` / `wont_equal` leave the local unchanged.
|
|
52
70
|
#
|
|
53
71
|
# ## Not yet recognised
|
|
54
72
|
#
|
|
@@ -59,12 +77,14 @@ module Rigor
|
|
|
59
77
|
module AssertionAnalyzer
|
|
60
78
|
module_function
|
|
61
79
|
|
|
62
|
-
|
|
80
|
+
# @param scope — the scope at the call, or nil. The environment resolves class arguments; the local's
|
|
81
|
+
# current type decides whether an equality assertion narrows (nil scope: only identity literals do).
|
|
82
|
+
def contribution_for(call_node, scope:)
|
|
63
83
|
return nil unless call_node.is_a?(Prism::CallNode)
|
|
64
84
|
|
|
65
85
|
fact =
|
|
66
|
-
assert_form_fact(call_node,
|
|
67
|
-
spec_form_fact(call_node,
|
|
86
|
+
assert_form_fact(call_node, scope: scope) ||
|
|
87
|
+
spec_form_fact(call_node, scope: scope)
|
|
68
88
|
return nil if fact.nil?
|
|
69
89
|
|
|
70
90
|
Rigor::FlowContribution.new(post_return_facts: [fact])
|
|
@@ -96,7 +116,7 @@ module Rigor
|
|
|
96
116
|
Ractor.make_shareable(ASSERT_FORM)
|
|
97
117
|
private_constant :ASSERT_FORM
|
|
98
118
|
|
|
99
|
-
def assert_form_fact(call_node,
|
|
119
|
+
def assert_form_fact(call_node, scope:)
|
|
100
120
|
return nil unless call_node.receiver.nil?
|
|
101
121
|
|
|
102
122
|
shape_negative = ASSERT_FORM[call_node.name]
|
|
@@ -104,7 +124,7 @@ module Rigor
|
|
|
104
124
|
|
|
105
125
|
shape, negative = shape_negative
|
|
106
126
|
args = call_node.arguments&.arguments || []
|
|
107
|
-
fact_for_shape(shape, args, negative: negative,
|
|
127
|
+
fact_for_shape(shape, args, negative: negative, scope: scope)
|
|
108
128
|
end
|
|
109
129
|
|
|
110
130
|
# --- _(x).must_* / .wont_* form ---
|
|
@@ -134,7 +154,7 @@ module Rigor
|
|
|
134
154
|
# `narrowing_facts methods:` gate.
|
|
135
155
|
SUPPORTED_METHODS = (ASSERT_FORM.keys + SPEC_MATCHER_FORM.keys).freeze
|
|
136
156
|
|
|
137
|
-
def spec_form_fact(call_node,
|
|
157
|
+
def spec_form_fact(call_node, scope:)
|
|
138
158
|
shape_negative = SPEC_MATCHER_FORM[call_node.name]
|
|
139
159
|
return nil if shape_negative.nil?
|
|
140
160
|
|
|
@@ -143,7 +163,7 @@ module Rigor
|
|
|
143
163
|
|
|
144
164
|
shape, negative = shape_negative
|
|
145
165
|
args = call_node.arguments&.arguments || []
|
|
146
|
-
fact_for_spec_shape(shape, target_local, args, negative: negative,
|
|
166
|
+
fact_for_spec_shape(shape, target_local, args, negative: negative, scope: scope)
|
|
147
167
|
end
|
|
148
168
|
|
|
149
169
|
# `_(x)` returns a `Minitest::Expectation` wrapping x. Some specs use `value(x)` or `expect(x)`
|
|
@@ -166,7 +186,8 @@ module Rigor
|
|
|
166
186
|
|
|
167
187
|
# --- shape resolvers ---
|
|
168
188
|
|
|
169
|
-
def fact_for_shape(shape, args, negative:,
|
|
189
|
+
def fact_for_shape(shape, args, negative:, scope:)
|
|
190
|
+
environment = scope&.environment
|
|
170
191
|
case shape
|
|
171
192
|
when :class_then_local
|
|
172
193
|
return nil unless args.size == 2
|
|
@@ -183,10 +204,7 @@ module Rigor
|
|
|
183
204
|
return nil unless args.size == 2
|
|
184
205
|
return nil unless args[1].is_a?(Prism::LocalVariableReadNode)
|
|
185
206
|
|
|
186
|
-
|
|
187
|
-
return nil if literal.equal?(NO_LITERAL)
|
|
188
|
-
|
|
189
|
-
fact_for(args[1].name, Rigor::Type::Combinator.constant_of(literal), negative: negative)
|
|
207
|
+
equality_fact(args[1].name, args[0], negative: negative, scope: scope)
|
|
190
208
|
when :regex_then_local
|
|
191
209
|
return nil unless args.size == 2
|
|
192
210
|
return nil unless args[1].is_a?(Prism::LocalVariableReadNode)
|
|
@@ -196,7 +214,8 @@ module Rigor
|
|
|
196
214
|
end
|
|
197
215
|
end
|
|
198
216
|
|
|
199
|
-
def fact_for_spec_shape(shape, target_local, args, negative:,
|
|
217
|
+
def fact_for_spec_shape(shape, target_local, args, negative:, scope:)
|
|
218
|
+
environment = scope&.environment
|
|
200
219
|
case shape
|
|
201
220
|
when :class_arg
|
|
202
221
|
return nil unless args.size == 1
|
|
@@ -210,10 +229,7 @@ module Rigor
|
|
|
210
229
|
when :literal_arg
|
|
211
230
|
return nil unless args.size == 1
|
|
212
231
|
|
|
213
|
-
|
|
214
|
-
return nil if literal.equal?(NO_LITERAL)
|
|
215
|
-
|
|
216
|
-
fact_for(target_local, Rigor::Type::Combinator.constant_of(literal), negative: negative)
|
|
232
|
+
equality_fact(target_local, args[0], negative: negative, scope: scope)
|
|
217
233
|
when :regex_arg
|
|
218
234
|
return nil unless args.size == 1
|
|
219
235
|
return nil unless regex_literal?(args[0])
|
|
@@ -222,6 +238,99 @@ module Rigor
|
|
|
222
238
|
end
|
|
223
239
|
end
|
|
224
240
|
|
|
241
|
+
# --- equality (`assert_equal` / `must_equal` and their negations) ---
|
|
242
|
+
|
|
243
|
+
# Classes whose `==` is identity (`BasicObject#==`, or `Symbol#==`), so `literal == act` holds only
|
|
244
|
+
# for the literal object itself, whatever `act` is.
|
|
245
|
+
IDENTITY_EQUALITY_CLASSES = [NilClass, TrueClass, FalseClass, Symbol].freeze
|
|
246
|
+
Ractor.make_shareable(IDENTITY_EQUALITY_CLASSES)
|
|
247
|
+
private_constant :IDENTITY_EQUALITY_CLASSES
|
|
248
|
+
|
|
249
|
+
IDENTITY_EQUALITY_CLASS_NAMES = IDENTITY_EQUALITY_CLASSES.map(&:name).freeze
|
|
250
|
+
Ractor.make_shareable(IDENTITY_EQUALITY_CLASS_NAMES)
|
|
251
|
+
private_constant :IDENTITY_EQUALITY_CLASS_NAMES
|
|
252
|
+
|
|
253
|
+
# Marks a member whose equality with the literal cannot be decided, so the whole meet gives up.
|
|
254
|
+
UNDECIDED = Object.new.freeze
|
|
255
|
+
private_constant :UNDECIDED
|
|
256
|
+
|
|
257
|
+
def equality_fact(local_name, literal_node, negative:, scope:)
|
|
258
|
+
literal = literal_value_for(literal_node)
|
|
259
|
+
return nil if literal.equal?(NO_LITERAL)
|
|
260
|
+
|
|
261
|
+
constant = Rigor::Type::Combinator.constant_of(literal)
|
|
262
|
+
return fact_for(local_name, constant, negative: true) if negative
|
|
263
|
+
return fact_for(local_name, constant, negative: false) if identity_equality?(literal)
|
|
264
|
+
|
|
265
|
+
fact_for(local_name, equality_meet(scope&.local(local_name), literal), negative: false)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def identity_equality?(value)
|
|
269
|
+
IDENTITY_EQUALITY_CLASSES.any? { |klass| value.is_a?(klass) }
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# The members of `current` that `literal == act` admits, or nil when any member's equality with the
|
|
273
|
+
# literal cannot be decided (a Dynamic, a class with its own `==`, a numeric of another class) or no
|
|
274
|
+
# member survives. `0.0 == -0.0`, so a zero Float literal does not pin the sign and never narrows.
|
|
275
|
+
def equality_meet(current, literal)
|
|
276
|
+
return nil if current.nil?
|
|
277
|
+
return nil if literal.is_a?(Float) && literal.zero?
|
|
278
|
+
|
|
279
|
+
kept = meet_members(current, literal)
|
|
280
|
+
return nil if kept.nil? || kept.empty?
|
|
281
|
+
|
|
282
|
+
Rigor::Type::Combinator.union(*kept)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def meet_members(type, literal)
|
|
286
|
+
members = type.is_a?(Rigor::Type::Union) ? type.members : [type]
|
|
287
|
+
kept = []
|
|
288
|
+
members.each do |member|
|
|
289
|
+
met = meet_member(member, literal)
|
|
290
|
+
return nil if met.equal?(UNDECIDED)
|
|
291
|
+
|
|
292
|
+
kept << met unless met.nil?
|
|
293
|
+
end
|
|
294
|
+
kept
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# One member's part of the meet: the narrowed carrier, nil when no value of the member equals the
|
|
298
|
+
# literal, or UNDECIDED.
|
|
299
|
+
def meet_member(member, literal)
|
|
300
|
+
case member
|
|
301
|
+
when Rigor::Type::Constant then meet_constant(member, literal)
|
|
302
|
+
when Rigor::Type::Nominal then meet_nominal(member, literal)
|
|
303
|
+
when Rigor::Type::IntegerRange
|
|
304
|
+
return UNDECIDED unless literal.is_a?(Integer)
|
|
305
|
+
|
|
306
|
+
member.covers?(literal) ? Rigor::Type::Combinator.constant_of(literal) : nil
|
|
307
|
+
else UNDECIDED
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def meet_constant(member, literal)
|
|
312
|
+
value = member.value
|
|
313
|
+
if value.instance_of?(literal.class)
|
|
314
|
+
value == literal ? member : nil
|
|
315
|
+
elsif identity_equality?(value)
|
|
316
|
+
nil
|
|
317
|
+
else
|
|
318
|
+
UNDECIDED
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def meet_nominal(member, literal)
|
|
323
|
+
return UNDECIDED unless member.type_args.empty?
|
|
324
|
+
|
|
325
|
+
if member.class_name == literal.class.name
|
|
326
|
+
Rigor::Type::Combinator.constant_of(literal)
|
|
327
|
+
elsif IDENTITY_EQUALITY_CLASS_NAMES.include?(member.class_name)
|
|
328
|
+
nil
|
|
329
|
+
else
|
|
330
|
+
UNDECIDED
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
225
334
|
def fact_for(local_name, type, negative:)
|
|
226
335
|
return nil if type.nil?
|
|
227
336
|
|
|
@@ -61,12 +61,14 @@ module Rigor
|
|
|
61
61
|
# ADR-37 slice 2 — emits `post_return_facts` for every recognised assertion, method-gated by the
|
|
62
62
|
# engine. The engine routes `:local`-kind facts through `StatementEvaluator#apply_local_post_return_fact`.
|
|
63
63
|
narrowing_facts methods: AssertionAnalyzer::SUPPORTED_METHODS do |call_node, scope|
|
|
64
|
-
AssertionAnalyzer.contribution_for(call_node,
|
|
64
|
+
AssertionAnalyzer.contribution_for(call_node, scope: scope)&.post_return_facts
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
# ADR-88 WD1 — the narrowing facts this plugin contributes are derived
|
|
68
|
-
#
|
|
69
|
-
#
|
|
67
|
+
# ADR-88 WD1 — the narrowing facts this plugin contributes are derived from each assertion call's own AST
|
|
68
|
+
# at its call site (`assert_kind_of(String, x)` ⇒ `x` is `String` on the continuation) and, for
|
|
69
|
+
# `assert_equal` / `must_equal`, from the local's current type in the call's scope, which the engine
|
|
70
|
+
# derives and already tracks as a dependency; there is no cross-file catalog of the plugin's own that a
|
|
71
|
+
# cached diagnostic could depend on. A change to a file's assertions changes that
|
|
70
72
|
# file's own content (re-analysed by the incremental graph already). A stable sentinel declares "no
|
|
71
73
|
# cross-file fact surface", keeping the plugin incremental-capable (a contributing plugin with NO
|
|
72
74
|
# fact / producer / hook makes the incremental snapshot un-reusable every run); `--verify-incremental`
|
|
@@ -126,10 +126,10 @@ module Rigor
|
|
|
126
126
|
end
|
|
127
127
|
|
|
128
128
|
def literal_symbol_or_string(node)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
129
|
+
return nil unless node.is_a?(Prism::SymbolNode) || node.is_a?(Prism::StringNode)
|
|
130
|
+
|
|
131
|
+
# nil for bytes no Symbol holds: the name is later interned (`PolicyIndex#normalize`), which would raise.
|
|
132
|
+
Rigor::Source::SymbolName.of(node.unescaped)&.to_s
|
|
133
133
|
end
|
|
134
134
|
|
|
135
135
|
def constant_receiver_name(node)
|
|
@@ -237,7 +237,7 @@ module Rigor
|
|
|
237
237
|
key_node = element.key
|
|
238
238
|
case key_node
|
|
239
239
|
when Prism::SymbolNode then key_node.unescaped.to_sym
|
|
240
|
-
when Prism::StringNode then key_node
|
|
240
|
+
when Prism::StringNode then Rigor::Source::Literals.symbol_or_string(key_node)
|
|
241
241
|
end
|
|
242
242
|
end
|
|
243
243
|
end
|
|
@@ -1184,7 +1184,7 @@ module Rigor
|
|
|
1184
1184
|
def load_drawn_routes(node, context)
|
|
1185
1185
|
return unless context.file_reader
|
|
1186
1186
|
|
|
1187
|
-
name = symbol_argument(node, 0) || string_argument(node, 0)
|
|
1187
|
+
name = symbol_argument(node, 0) || Rigor::Source::SymbolName.of(string_argument(node, 0))
|
|
1188
1188
|
return unless name
|
|
1189
1189
|
|
|
1190
1190
|
sub_contents = context.file_reader.call("#{name}.rb")
|
|
@@ -123,6 +123,7 @@ module Rigor
|
|
|
123
123
|
return nil if source.empty?
|
|
124
124
|
|
|
125
125
|
result = ::Prism.parse(source)
|
|
126
|
+
source, result = scrub_stray_comment_bytes(source, result)
|
|
126
127
|
_, result = neutralize_rdoc_directives(source, result)
|
|
127
128
|
return nil if !@require_magic_comment && !annotated?(result)
|
|
128
129
|
|
|
@@ -504,6 +505,36 @@ module Rigor
|
|
|
504
505
|
end
|
|
505
506
|
end
|
|
506
507
|
|
|
508
|
+
# A comment is not validated by Ruby, so a byte that is invalid in UTF-8 can sit in an ordinary one
|
|
509
|
+
# (`# caf\xFF`), and upstream's annotation grammar raises on it — which {#call}'s rescue would report as a
|
|
510
|
+
# `source-rbs-synthesis-failed` notice about "the inline-RBS comment grammar" for a file that has none.
|
|
511
|
+
# Such a comment is repaired in place (each bad byte becomes one `?`, so every offset stays identical) and
|
|
512
|
+
# the file re-parsed. A comment upstream would read as an annotation — exactly `#:`, `#[` and `#\s*@rbs`
|
|
513
|
+
# (rbs-inline `annotation_parser.rb`) — is left alone, so a genuinely corrupt annotation still reaches the
|
|
514
|
+
# WD6 fail-soft path. An RDoc directive (`#:nodoc:`) is not an annotation: {#neutralize_rdoc_directives}
|
|
515
|
+
# rewrites it, so it is repaired like any other comment.
|
|
516
|
+
STRAY_COMMENT_ANNOTATION = /\A#(?::|\[|\s*@rbs\b)/
|
|
517
|
+
private_constant :STRAY_COMMENT_ANNOTATION
|
|
518
|
+
|
|
519
|
+
def scrub_stray_comment_bytes(source, prism_result)
|
|
520
|
+
return [source, prism_result] if source.valid_encoding?
|
|
521
|
+
|
|
522
|
+
broken = prism_result.comments.reject { |comment| comment.location.slice.valid_encoding? }
|
|
523
|
+
annotation_like = broken.any? do |comment|
|
|
524
|
+
text = comment.location.slice.scrub
|
|
525
|
+
STRAY_COMMENT_ANNOTATION.match?(text) && !RDOC_DIRECTIVE_COMMENT.match?(text)
|
|
526
|
+
end
|
|
527
|
+
return [source, prism_result] if broken.empty? || annotation_like
|
|
528
|
+
|
|
529
|
+
bytes = source.b
|
|
530
|
+
broken.sort_by { |comment| -comment.location.start_offset }.each do |comment|
|
|
531
|
+
location = comment.location
|
|
532
|
+
bytes[location.start_offset, location.length] = location.slice.scrub { |run| "?" * run.bytesize }.b
|
|
533
|
+
end
|
|
534
|
+
repaired = bytes.force_encoding(source.encoding)
|
|
535
|
+
[repaired, ::Prism.parse(repaired)]
|
|
536
|
+
end
|
|
537
|
+
|
|
507
538
|
# Rewrite every RDoc directive comment to its spaced spelling (`#:nodoc:` -> `# :nodoc:`) so
|
|
508
539
|
# upstream's annotation grammar never sees it, and re-parse. Two reasons this happens here rather
|
|
509
540
|
# than in {#annotated?}: the directive must not gate a file in, AND it must not reach the synthesis,
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require "rigor/inference/declaration_walk"
|
|
6
|
+
|
|
7
|
+
require_relative "serializer_index"
|
|
8
|
+
|
|
9
|
+
module Rigor
|
|
10
|
+
module Plugin
|
|
11
|
+
class Typelizer < Rigor::Plugin::Base
|
|
12
|
+
# A {Rigor::Inference::DeclarationWalk} collector over one file: the classes it declares, and whether each
|
|
13
|
+
# one's own body says `include Typelizer::DSL` / `extend Typelizer::DSL`.
|
|
14
|
+
#
|
|
15
|
+
# A `module` that includes the DSL is deliberately not recorded: typelizer registers the module's own
|
|
16
|
+
# name and `target_serializers` then calls `.descendants` on it, which a plain Module lacks. A class that
|
|
17
|
+
# reaches the DSL through a module's `included do ... end` hook is a known missed root (see the README).
|
|
18
|
+
#
|
|
19
|
+
# Only a statement of the class body itself counts, because only there is `self` the class: the walk is
|
|
20
|
+
# over the body's own statements (through `if` / `unless` / `begin`), never into a `def`, a block, a
|
|
21
|
+
# lambda or a nested class, so `Class.new { include Typelizer::DSL }`, `Other.class_eval { include ... }`,
|
|
22
|
+
# `included do include Typelizer::DSL end` and a `def` that includes it credit nothing to the lexical
|
|
23
|
+
# class. A `class << self` body is a different node and is not entered either.
|
|
24
|
+
class SerializerCollector
|
|
25
|
+
include Rigor::Inference::DeclarationWalk::Collector
|
|
26
|
+
|
|
27
|
+
DSL_MODULE = "Typelizer::DSL"
|
|
28
|
+
DSL_CALLS = %i[include extend].freeze
|
|
29
|
+
|
|
30
|
+
def initialize
|
|
31
|
+
@entries = {}
|
|
32
|
+
@nestings = {}
|
|
33
|
+
@dsl = {}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def on_declaration(node, context, body_context)
|
|
37
|
+
return Rigor::Inference::DeclarationWalk::DESCEND unless node.is_a?(Prism::ClassNode)
|
|
38
|
+
|
|
39
|
+
name = body_context.prefix.join("::")
|
|
40
|
+
unless name.empty?
|
|
41
|
+
superclass = superclass_name(node.superclass)
|
|
42
|
+
@entries[name] = superclass if superclass || !@entries.key?(name)
|
|
43
|
+
@nestings[name] = context.nesting || []
|
|
44
|
+
@dsl[name] = true if dsl_in?(node.body) && !body_context.singleton_cref
|
|
45
|
+
end
|
|
46
|
+
Rigor::Inference::DeclarationWalk::DESCEND
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The classes this file declares, as {SerializerIndex::ClassEntry} values.
|
|
50
|
+
def class_entries
|
|
51
|
+
@entries.map do |name, superclass|
|
|
52
|
+
SerializerIndex::ClassEntry.new(
|
|
53
|
+
name: name, superclass: superclass, nesting: @nestings.fetch(name), dsl: @dsl.fetch(name, false),
|
|
54
|
+
in_dirs: false
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# Whether a statement reachable without entering a def, block, lambda or nested declaration is a
|
|
62
|
+
# receiverless `include` / `extend` of the DSL module.
|
|
63
|
+
def dsl_in?(node)
|
|
64
|
+
case node
|
|
65
|
+
when Prism::StatementsNode then node.body.any? { |child| dsl_in?(child) }
|
|
66
|
+
when Prism::BeginNode then dsl_in?(node.statements) || dsl_in?(node.else_clause)
|
|
67
|
+
when Prism::ElseNode then dsl_in?(node.statements)
|
|
68
|
+
when Prism::IfNode then dsl_in?(node.statements) || dsl_in?(node.subsequent)
|
|
69
|
+
when Prism::UnlessNode then dsl_in?(node.statements) || dsl_in?(node.else_clause)
|
|
70
|
+
when Prism::CallNode then dsl_call?(node)
|
|
71
|
+
else false
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def dsl_call?(node)
|
|
76
|
+
DSL_CALLS.include?(node.name) && node.receiver.nil? && node.block.nil? && dsl_argument?(node)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def dsl_argument?(node)
|
|
80
|
+
arguments = node.arguments&.arguments || []
|
|
81
|
+
arguments.any? { |argument| constant_name(argument)&.delete_prefix("::") == DSL_MODULE }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def superclass_name(node)
|
|
85
|
+
node && constant_name(node)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def constant_name(node)
|
|
89
|
+
case node
|
|
90
|
+
when Prism::ConstantReadNode then node.name.to_s
|
|
91
|
+
when Prism::ConstantPathNode
|
|
92
|
+
parent = node.parent
|
|
93
|
+
return "::#{node.name}" if parent.nil?
|
|
94
|
+
|
|
95
|
+
prefix = constant_name(parent)
|
|
96
|
+
prefix && "#{prefix}::#{node.name}"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require "rigor/inference/declaration_walk"
|
|
6
|
+
|
|
7
|
+
require_relative "serializer_collector"
|
|
8
|
+
require_relative "serializer_index"
|
|
9
|
+
|
|
10
|
+
module Rigor
|
|
11
|
+
module Plugin
|
|
12
|
+
class Typelizer < Rigor::Plugin::Base
|
|
13
|
+
# Reads every `.rb` file under the configured typelizer dirs and the project's `paths:` through the plugin's `IoBoundary`, runs a
|
|
14
|
+
# {SerializerCollector} over each and merges the results into one {SerializerIndex}.
|
|
15
|
+
class SerializerDiscoverer
|
|
16
|
+
def initialize(io_boundary:, dirs:, project_paths: [])
|
|
17
|
+
@io_boundary = io_boundary
|
|
18
|
+
@dirs = dirs
|
|
19
|
+
@project_paths = project_paths
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def discover
|
|
23
|
+
entries = []
|
|
24
|
+
dir_files = ruby_files_under(@dirs)
|
|
25
|
+
(dir_files | project_files).each do |path|
|
|
26
|
+
contents = read_safely(path)
|
|
27
|
+
next if contents.nil?
|
|
28
|
+
|
|
29
|
+
parsed = Prism.parse(contents)
|
|
30
|
+
next unless parsed.success?
|
|
31
|
+
|
|
32
|
+
collector = SerializerCollector.new
|
|
33
|
+
Rigor::Inference::DeclarationWalk.run(
|
|
34
|
+
parsed.value, [collector], Rigor::Inference::DeclarationWalk::Context.root(nesting: [])
|
|
35
|
+
)
|
|
36
|
+
in_dirs = dir_files.include?(path)
|
|
37
|
+
entries.concat(collector.class_entries.map { |entry| entry.with(in_dirs: in_dirs) })
|
|
38
|
+
end
|
|
39
|
+
SerializerIndex.new(classes: merge(entries))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# A class reopened across files: it registers with typelizer if any opening says so.
|
|
45
|
+
def merge(entries)
|
|
46
|
+
entries.group_by(&:name).map do |name, openings|
|
|
47
|
+
headed = openings.find(&:superclass) || openings.first
|
|
48
|
+
SerializerIndex::ClassEntry.new(
|
|
49
|
+
name: name, superclass: headed.superclass, nesting: headed.nesting,
|
|
50
|
+
dsl: openings.any?(&:dsl), in_dirs: openings.any?(&:in_dirs)
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def read_safely(path)
|
|
56
|
+
@io_boundary.read_file(path)
|
|
57
|
+
rescue Plugin::AccessDeniedError, Errno::ENOENT
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The project's `paths:` entries: a directory contributes its `.rb` tree, a `.rb` file itself.
|
|
62
|
+
def project_files
|
|
63
|
+
@project_paths.flat_map do |entry|
|
|
64
|
+
absolute = File.expand_path(entry)
|
|
65
|
+
if @io_boundary.directory?(absolute)
|
|
66
|
+
Dir.glob(File.join(absolute, "**", "*.rb"))
|
|
67
|
+
elsif absolute.end_with?(".rb") && @io_boundary.file?(absolute)
|
|
68
|
+
[absolute]
|
|
69
|
+
else
|
|
70
|
+
[]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def ruby_files_under(roots)
|
|
76
|
+
roots.flat_map do |root|
|
|
77
|
+
absolute = File.expand_path(root)
|
|
78
|
+
# ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
|
|
79
|
+
next [] unless @io_boundary.directory?(absolute)
|
|
80
|
+
|
|
81
|
+
Dir.glob(File.join(absolute, "**", "*.rb"))
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Plugin
|
|
5
|
+
class Typelizer < Rigor::Plugin::Base
|
|
6
|
+
# The project's classes (every file of the project's `paths:` and the typelizer `dirs`), as the {SerializerCollector} found them. A frozen value
|
|
7
|
+
# object the `:serializer_index` producer caches.
|
|
8
|
+
#
|
|
9
|
+
# {#roots} answers the one question, conservatively — a wrong "no" costs a missing root, a wrong "yes"
|
|
10
|
+
# hides a dead class.
|
|
11
|
+
class SerializerIndex
|
|
12
|
+
# A class the project declares. `superclass` is the constant as written (`nil` when absent) and
|
|
13
|
+
# `nesting` the `Module.nesting` chain (innermost first) its header was written under, which is where
|
|
14
|
+
# Ruby resolves that constant. `dsl` is true when its body includes or extends `Typelizer::DSL`; `in_dirs` when a file under the
|
|
15
|
+
# typelizer `dirs` declares it (only such a class is published as a root).
|
|
16
|
+
ClassEntry = Data.define(:name, :superclass, :nesting, :dsl, :in_dirs)
|
|
17
|
+
|
|
18
|
+
def initialize(classes:)
|
|
19
|
+
@classes = classes.to_h { |entry| [entry.name, entry] }.freeze
|
|
20
|
+
freeze
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def empty?
|
|
24
|
+
@classes.empty?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Every class typelizer generates an interface for: a class that registered itself through the DSL, and
|
|
28
|
+
# each subclass of one (`Typelizer.target_serializers` takes `descendants`).
|
|
29
|
+
def roots
|
|
30
|
+
@classes.keys.select { |name| @classes[name].in_dirs && typelized?(name, {}) }.sort
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def typelized?(name, seen)
|
|
36
|
+
entry = @classes[name]
|
|
37
|
+
return false if entry.nil? || seen[name]
|
|
38
|
+
|
|
39
|
+
seen[name] = true
|
|
40
|
+
return true if entry.dsl
|
|
41
|
+
|
|
42
|
+
parent = superclass_of(entry)
|
|
43
|
+
!parent.nil? && typelized?(parent, seen)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Resolves the superclass as written the way Ruby does: against the `Module.nesting` chain of the header,
|
|
47
|
+
# innermost first, then the top level. A compact header (`class Admin::UserSerializer < Base`) does not
|
|
48
|
+
# put `Admin` on the chain, so `Base` is not looked up under it. The first match wins, so a class that
|
|
49
|
+
# shadows the DSL base (`Admin::Base` with no DSL) ends the walk there.
|
|
50
|
+
#
|
|
51
|
+
# Ruby also searches the ancestors of the innermost cref before the top level. That is not modelled, so a
|
|
52
|
+
# name that is not found on the lexical chain is declined (nil: not typelized) when the innermost scope
|
|
53
|
+
# is a class with a superclass, since the constant might live on that superclass.
|
|
54
|
+
def superclass_of(entry)
|
|
55
|
+
written = entry.superclass
|
|
56
|
+
return nil if written.nil?
|
|
57
|
+
|
|
58
|
+
if written.start_with?("::")
|
|
59
|
+
rooted = written.delete_prefix("::")
|
|
60
|
+
return @classes.key?(rooted) ? rooted : nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
lexical = entry.nesting.map { |scope| "#{scope}::#{written}" }.find { |name| @classes.key?(name) }
|
|
64
|
+
return lexical if lexical
|
|
65
|
+
return nil if cref_has_ancestors?(entry.nesting.first)
|
|
66
|
+
|
|
67
|
+
@classes.key?(written) ? written : nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def cref_has_ancestors?(scope)
|
|
71
|
+
!scope.nil? && !@classes[scope]&.superclass.nil?
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|