rigortype 0.4.2 → 0.4.3
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/docs/handbook/11-sig-gen.md +12 -5
- data/docs/manual/02-cli-reference.md +19 -1
- data/docs/manual/03-configuration.md +6 -4
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/12-caching.md +18 -1
- data/docs/manual/14-rails-quickstart.md +2 -2
- data/docs/manual/18-removing-dead-code.md +16 -9
- data/lib/rigor/analysis/check_rules/rule_ids.rb +7 -0
- data/lib/rigor/analysis/check_rules.rb +23 -22
- data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -3
- data/lib/rigor/analysis/incremental_session.rb +89 -22
- data/lib/rigor/analysis/path_expansion.rb +62 -6
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
- data/lib/rigor/analysis/reachability/graph.rb +119 -15
- data/lib/rigor/analysis/reachability/scan.rb +332 -173
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- data/lib/rigor/analysis/run_cache_probe.rb +1 -1
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +26 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +28 -9
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
- data/lib/rigor/analysis/runner.rb +29 -11
- data/lib/rigor/analysis/worker_session.rb +1 -1
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +36 -8
- data/lib/rigor/cli/check_command.rb +138 -39
- data/lib/rigor/cli/docs_command.rb +5 -4
- data/lib/rigor/cli/lens_command.rb +144 -0
- data/lib/rigor/cli/lsp_command.rb +5 -2
- data/lib/rigor/cli/skill_command.rb +3 -3
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/unused_command.rb +157 -24
- data/lib/rigor/cli.rb +30 -4
- data/lib/rigor/configuration.rb +15 -0
- data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
- data/lib/rigor/environment/installed_gem_set.rb +38 -11
- data/lib/rigor/environment/lockfile_resolver.rb +37 -6
- data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
- data/lib/rigor/environment/rbs_loader.rb +33 -0
- data/lib/rigor/environment/required_features.rb +4 -2
- data/lib/rigor/environment.rb +12 -5
- data/lib/rigor/inference/builtins/cruby_definers.rb +22 -5
- data/lib/rigor/inference/captured_locals.rb +43 -3
- data/lib/rigor/inference/expression_typer.rb +58 -25
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +21 -17
- data/lib/rigor/inference/match_rebinding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -16
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
- data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +52 -3
- data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
- data/lib/rigor/inference/project_patched_methods.rb +2 -2
- data/lib/rigor/inference/project_patched_scanner.rb +38 -24
- data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +112 -40
- data/lib/rigor/inference/refined_dispatch.rb +62 -14
- data/lib/rigor/inference/repeated_or_writes.rb +2 -0
- data/lib/rigor/inference/scope_indexer.rb +140 -5
- data/lib/rigor/inference/statement_evaluator.rb +223 -31
- data/lib/rigor/inference/untyped_rejoin.rb +75 -0
- data/lib/rigor/language_server/project_context.rb +56 -5
- data/lib/rigor/lens/anchor.rb +115 -0
- data/lib/rigor/lens/contract_reader.rb +118 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +433 -0
- data/lib/rigor/lens/members.rb +153 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +280 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +40 -0
- data/lib/rigor/project_environment.rb +20 -1
- data/lib/rigor/protection/mutation_cache.rb +2 -1
- data/lib/rigor/scope.rb +22 -11
- data/lib/rigor/sig_gen/generator.rb +45 -5
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
- data/sig/rigor/scope.rbs +3 -0
- data/sig/rigor.rbs +6 -3
- data/skills/rigor-project-init/references/02-configure.md +2 -2
- metadata +18 -1
|
@@ -29,7 +29,7 @@ module Rigor
|
|
|
29
29
|
# `StructInstance`, the member map built from the call's arguments (positional or keyword per the
|
|
30
30
|
# class's `keyword_init`). A form / arity mismatch degrades to `Dynamic[top]` rather than a wrong
|
|
31
31
|
# member map. `.members` on the class folds.
|
|
32
|
-
# 3. member reads + `[]` / `to_h` / `deconstruct` / `deconstruct_keys` / `members`
|
|
32
|
+
# 3. member reads + `[]` / `to_h` / `deconstruct` / `deconstruct_keys` / `members` on a
|
|
33
33
|
# *fresh* `StructInstance` -> the precise projected type; member *setters* (`s.x = v`) return the
|
|
34
34
|
# assigned value type. Unhandled / stored-receiver calls return nil so the pipeline projects the
|
|
35
35
|
# instance to its nominal through RbsDispatch.
|
|
@@ -38,8 +38,9 @@ module Rigor
|
|
|
38
38
|
module StructFolding
|
|
39
39
|
module_function
|
|
40
40
|
|
|
41
|
-
# The `[]` / `to_h` / `deconstruct` / `members`
|
|
41
|
+
# The `[]` / `to_h` / `deconstruct` / `members` projections
|
|
42
42
|
# and the reader-redefinition guard are shared with {DataFolding}.
|
|
43
|
+
# `Struct` has no `#with` (issue #1335): that projection stays on {DataFolding} only.
|
|
43
44
|
extend MemberShapeProjection
|
|
44
45
|
|
|
45
46
|
# Issue #595 / #525 — the shared materialisation test; see {StructMaterialization}.
|
|
@@ -239,10 +240,6 @@ module Rigor
|
|
|
239
240
|
when :deconstruct then instance_deconstruct(instance)
|
|
240
241
|
when :deconstruct_keys then instance_deconstruct_keys(instance, args)
|
|
241
242
|
when :members then instance_members(instance)
|
|
242
|
-
when :with
|
|
243
|
-
instance_with(instance, args) do |members, class_name|
|
|
244
|
-
Type::Combinator.struct_instance_of(members: members, class_name: class_name)
|
|
245
|
-
end
|
|
246
243
|
end
|
|
247
244
|
end
|
|
248
245
|
|
|
@@ -332,7 +329,7 @@ module Rigor
|
|
|
332
329
|
node = context.call_node
|
|
333
330
|
return false if node.nil?
|
|
334
331
|
|
|
335
|
-
materialization_call?(node.receiver, context.
|
|
332
|
+
materialization_call?(node.receiver, context.scope)
|
|
336
333
|
end
|
|
337
334
|
|
|
338
335
|
# ADR-48 slice 4 — precise mutated-member re-typing. After a `local.member = v` setter on a
|
|
@@ -34,18 +34,20 @@ module Rigor
|
|
|
34
34
|
# Issue #595 / #525 — the ONE materialisation test. Both places that must answer "was this receiver
|
|
35
35
|
# expression's struct newly built?" go through it: this module's direct member-read gate above, and
|
|
36
36
|
# `ExpressionTyper`'s caller-side `:self`-grant arm. They had forked answers once (the grant arm was
|
|
37
|
-
# tightened for #591 while this one still trusted any chained call),
|
|
38
|
-
# composed `x.dup_self.with(indent: 9)
|
|
39
|
-
#
|
|
37
|
+
# tightened for #591 while this one still trusted any chained call), so both consumers must share this
|
|
38
|
+
# one answer. A `.with` call is never a materialisation: the composed `x.dup_self.with(indent: 9)`
|
|
39
|
+
# shape once slipped through as one, and `Struct#with` no longer exists to fold (#1335).
|
|
40
40
|
#
|
|
41
41
|
# @param node — the receiver EXPRESSION.
|
|
42
|
-
# @param
|
|
43
|
-
def materialization_call?(node,
|
|
42
|
+
# @param scope — the scope the factory and ancestor lookups resolve against.
|
|
43
|
+
def materialization_call?(node, scope)
|
|
44
44
|
return false unless node.is_a?(Prism::CallNode)
|
|
45
45
|
|
|
46
46
|
case node.name
|
|
47
47
|
when :new, :[] then struct_class_expression?(node.receiver, scope)
|
|
48
|
-
|
|
48
|
+
# `Struct` has no `#with` (#1335), so a `.with` call is never a materialisation here; `Data#with` is
|
|
49
|
+
# a frozen copy and needs no freshness gate.
|
|
50
|
+
when :with then false
|
|
49
51
|
else fresh_factory_call?(node, scope)
|
|
50
52
|
end
|
|
51
53
|
end
|
|
@@ -127,26 +129,6 @@ module Rigor
|
|
|
127
129
|
found
|
|
128
130
|
end
|
|
129
131
|
|
|
130
|
-
# `.with` copies the receiver into a new instance — unless the struct wrote its own, which is free to
|
|
131
|
-
# return `self` and would reopen the very hole above.
|
|
132
|
-
#
|
|
133
|
-
# An ANONYMOUS carrier cannot have one: a `StructInstance` with no class name came from a blockless
|
|
134
|
-
# `Struct.new(…)` (the block form defers in {fold_struct_new} before any instance exists), and a
|
|
135
|
-
# blockless factory defines nothing but the generated accessors. So it is the NAMED carriers that
|
|
136
|
-
# need the lookup — and a named one with no scope to ask refuses, since unproven is not fresh.
|
|
137
|
-
def hand_written_with?(receiver, scope)
|
|
138
|
-
return true unless receiver.is_a?(Type::StructInstance)
|
|
139
|
-
|
|
140
|
-
class_name = receiver.class_name
|
|
141
|
-
return false if class_name.nil?
|
|
142
|
-
return true if scope.nil?
|
|
143
|
-
|
|
144
|
-
# The ANCESTOR walk, not the own-class table: a `with` contributed by an included module is just as
|
|
145
|
-
# free to return `self`. Both consumers reach the identical answer because there is one lookup here
|
|
146
|
-
# rather than a parameter each of them supplies (#598 review).
|
|
147
|
-
!scope.user_def_through_ancestors(class_name, :with).first.nil?
|
|
148
|
-
end
|
|
149
|
-
|
|
150
132
|
# An expression naming the struct class itself. All three definition forms ADR-48 supports qualify,
|
|
151
133
|
# because each of them really does construct: a constant whose member layout the project side-table
|
|
152
134
|
# recorded (`Point.new(…)`), a local holding a `StructClass` carrier (`c = Struct.new(:x, :y)` then
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module MethodDispatcher
|
|
8
|
+
# Issue #1357 — `Kernel#Rational(x, 1)` converts its argument. CRuby's `nurat_convert` returns a
|
|
9
|
+
# `Rational` for a `Float` / `Integer` / `Rational` / `String`-free numerator and for a `BigDecimal` (which
|
|
10
|
+
# has `to_r`; NaN and Infinity raise). A `Complex` is unwrapped only when its imaginary part is an exact
|
|
11
|
+
# zero, otherwise `a / 1` comes back, a `Complex`, so a `Complex` numerator answers `Rational | Complex`.
|
|
12
|
+
# The core RBS spells the `1`-denominator form `[T < Numeric] (T value, 1) -> T`, which binds `T` to the
|
|
13
|
+
# argument's own class and read `Rational(f, 1)` as a `Float`. Any other numerator (a user `Numeric`
|
|
14
|
+
# without `to_r`) keeps the RBS answer.
|
|
15
|
+
module UnitDenominatorRational
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
CONVERTING = Set["Float", "Integer", "Rational", "BigDecimal"].freeze
|
|
19
|
+
private_constant :CONVERTING
|
|
20
|
+
|
|
21
|
+
# Answers `Rational` (or `Rational | Complex`), or nil when the call is not the unit-denominator form.
|
|
22
|
+
def answer(method_name, args)
|
|
23
|
+
return nil unless method_name == :Rational && args.size.between?(2, 3)
|
|
24
|
+
return nil if args.size == 3 && !exception_keyword?(args[2])
|
|
25
|
+
|
|
26
|
+
denominator = args[1]
|
|
27
|
+
return nil unless denominator.is_a?(Type::Constant) && denominator.value.eql?(1)
|
|
28
|
+
|
|
29
|
+
classes = numerator_classes(args.first)
|
|
30
|
+
return nil if classes.nil?
|
|
31
|
+
|
|
32
|
+
members = [Type::Combinator.nominal_of("Rational")]
|
|
33
|
+
members << Type::Combinator.nominal_of("Complex") if classes.include?(:complex)
|
|
34
|
+
Type::Combinator.union(*members)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The trailing `exception:` keyword hash: a `HashShape` whose only key is `:exception`. Its value
|
|
38
|
+
# does not change the answer (a Float that cannot convert raises either way).
|
|
39
|
+
def exception_keyword?(type)
|
|
40
|
+
type.is_a?(Type::HashShape) && type.pairs.keys == [:exception]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# `[:converting]`, `[:complex]` or both for a recognised numerator; nil when any member is unknown.
|
|
44
|
+
def numerator_classes(type)
|
|
45
|
+
case type
|
|
46
|
+
when Type::Union then union_classes(type)
|
|
47
|
+
when Type::IntegerRange then [:converting]
|
|
48
|
+
when Type::Refined then type.base.is_a?(Type::Nominal) ? numerator_classes(type.base) : nil
|
|
49
|
+
when Type::Constant then class_kind(type.value.class.name)
|
|
50
|
+
when Type::Nominal then type.type_args.empty? ? class_kind(type.class_name) : nil
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def union_classes(union)
|
|
55
|
+
kinds = union.members.map { |member| numerator_classes(member) }
|
|
56
|
+
kinds.any?(&:nil?) ? nil : kinds.flatten.uniq
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def class_kind(name)
|
|
60
|
+
return [:complex] if name == "Complex"
|
|
61
|
+
|
|
62
|
+
CONVERTING.include?(name) ? [:converting] : nil
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -11,6 +11,7 @@ require_relative "dynamic_origin"
|
|
|
11
11
|
require_relative "flow_tracer"
|
|
12
12
|
require_relative "void_tail_summary"
|
|
13
13
|
require_relative "method_dispatcher/call_context"
|
|
14
|
+
require_relative "method_dispatcher/argument_nodes"
|
|
14
15
|
require_relative "method_dispatcher/constant_folding"
|
|
15
16
|
require_relative "method_dispatcher/literal_string_folding"
|
|
16
17
|
require_relative "method_dispatcher/shape_dispatch"
|
|
@@ -39,6 +40,7 @@ require_relative "method_dispatcher/kernel_dispatch"
|
|
|
39
40
|
require_relative "method_dispatcher/universal_object_dispatch"
|
|
40
41
|
require_relative "method_dispatcher/singleton_mixin_dispatch"
|
|
41
42
|
require_relative "method_dispatcher/method_folding"
|
|
43
|
+
require_relative "method_dispatcher/optional_count"
|
|
42
44
|
|
|
43
45
|
module Rigor
|
|
44
46
|
module Inference
|
|
@@ -105,6 +107,16 @@ module Rigor
|
|
|
105
107
|
call_node: nil, scope: nil, plugin_typed_sink: nil, skip_project_patched: false)
|
|
106
108
|
return nil if receiver_type.nil?
|
|
107
109
|
|
|
110
|
+
# Issue #1393 — a `nil` count on `min` / `max` / `min_by` / `max_by` is the no-count call.
|
|
111
|
+
nil_count = OptionalCount.split(receiver_type, method_name, arg_types, block: !block_type.nil?)
|
|
112
|
+
if nil_count
|
|
113
|
+
return resolve_nil_count(nil_count.first, receiver_type: receiver_type, method_name: method_name,
|
|
114
|
+
block_type: block_type, environment: environment,
|
|
115
|
+
call_node: call_node, scope: scope,
|
|
116
|
+
plugin_typed_sink: plugin_typed_sink,
|
|
117
|
+
skip_project_patched: skip_project_patched)
|
|
118
|
+
end
|
|
119
|
+
|
|
108
120
|
# Build the call context once and thread it — unchanged — through every tier
|
|
109
121
|
# (`_DispatchTier#try_dispatch`). The dispatcher's own private fallback tiers still read
|
|
110
122
|
# the positional locals below; only the tier modules consume the context object.
|
|
@@ -288,6 +300,20 @@ module Rigor
|
|
|
288
300
|
try_composite_receiver(context)
|
|
289
301
|
end
|
|
290
302
|
|
|
303
|
+
# The `nil` part of an optional count dispatches as the no-argument call, the rest as written; a count
|
|
304
|
+
# that is both answers the union of the two, or declines when either declines. See {OptionalCount}.
|
|
305
|
+
def resolve_nil_count(rest, **common)
|
|
306
|
+
return resolve(arg_types: [], **common) if rest.nil?
|
|
307
|
+
|
|
308
|
+
# The counted leg runs first: a leg's plugin answer records on the scope (or buffers in the sink, #1101),
|
|
309
|
+
# and a declining counted leg must not leave the no-argument leg's record behind for a call that answers nil.
|
|
310
|
+
counted = resolve(arg_types: [rest], **common)
|
|
311
|
+
return nil if counted.nil?
|
|
312
|
+
|
|
313
|
+
without = resolve(arg_types: [], **common)
|
|
314
|
+
without && Type::Combinator.union(without, counted)
|
|
315
|
+
end
|
|
316
|
+
|
|
291
317
|
# ADR-100 WD4 — records the transitive `-> void` provenance when the called method is a
|
|
292
318
|
# discovered project `def` whose {VoidTailSummary} chain ends in an author-declared `-> void`
|
|
293
319
|
# leaf on the same owner. Guards cheapest-first: the internal dispatcher callers nil `scope` /
|
|
@@ -1170,7 +1196,7 @@ module Rigor
|
|
|
1170
1196
|
set_lift = set_new_lift(receiver_type.class_name, arg_types)
|
|
1171
1197
|
return set_lift if set_lift
|
|
1172
1198
|
|
|
1173
|
-
hash_lift = hash_new_lift(receiver_type.class_name, arg_types)
|
|
1199
|
+
hash_lift = hash_new_lift(receiver_type.class_name, arg_types, context&.call_node)
|
|
1174
1200
|
return hash_lift if hash_lift
|
|
1175
1201
|
|
|
1176
1202
|
regexp_lift = regexp_new_lift(receiver_type.class_name, arg_types)
|
|
@@ -1445,9 +1471,15 @@ module Rigor
|
|
|
1445
1471
|
# form (`Hash.new { |h, k| … }`) keep the bare `Nominal[Hash]` answer — the block's value
|
|
1446
1472
|
# type is not available at this `:new` dispatch site, and conservatively leaving the read
|
|
1447
1473
|
# as today's behaviour is precision-additive.
|
|
1448
|
-
|
|
1474
|
+
#
|
|
1475
|
+
# Issue #1812 — a brace-less keyword hash is `Hash#initialize`'s `capacity:` keyword, not the
|
|
1476
|
+
# default (`Hash.new(capacity: 10).default` is `nil`), so it is dropped before counting the
|
|
1477
|
+
# positionals. A braced hash (`Hash.new({ capacity: 10 })`) is a positional and stays the default.
|
|
1478
|
+
def hash_new_lift(class_name, arg_types, call_node = nil)
|
|
1449
1479
|
return nil unless class_name == "Hash"
|
|
1450
|
-
|
|
1480
|
+
|
|
1481
|
+
arg_types = hash_new_positionals(arg_types, call_node)
|
|
1482
|
+
return nil unless arg_types&.size == 1
|
|
1451
1483
|
|
|
1452
1484
|
default = arg_types.first
|
|
1453
1485
|
return nil if default.nil?
|
|
@@ -1456,6 +1488,23 @@ module Rigor
|
|
|
1456
1488
|
Type::Combinator.nominal_of("Hash", type_args: [Type::Combinator.untyped, value])
|
|
1457
1489
|
end
|
|
1458
1490
|
|
|
1491
|
+
# `Hash.new`'s positional argument types: `arg_types` without a trailing brace-less keyword hash. A literal
|
|
1492
|
+
# send (`Hash.send(:new, capacity: 1)`) dispatches with the outer call node, whose arguments are the
|
|
1493
|
+
# selector symbol followed by `arg_types`' own, and is read through that offset. When the node's arguments
|
|
1494
|
+
# correspond otherwise, a sole hash argument may be the keyword hash, so the answer is nil (decline) and the
|
|
1495
|
+
# bare `Hash` stands; any other argument cannot be a keyword hash and is kept.
|
|
1496
|
+
def hash_new_positionals(arg_types, call_node)
|
|
1497
|
+
return arg_types[0...-1] if ArgumentNodes.keyword_hash_last?(call_node, arg_types)
|
|
1498
|
+
|
|
1499
|
+
arguments = call_node.respond_to?(:arguments) ? call_node.arguments&.arguments : nil
|
|
1500
|
+
return arg_types if arguments.nil? || arguments.size == arg_types.size
|
|
1501
|
+
if arguments.size == arg_types.size + 1 && arguments.first.is_a?(Prism::SymbolNode)
|
|
1502
|
+
return arguments.last.is_a?(Prism::KeywordHashNode) ? arg_types[0...-1] : arg_types
|
|
1503
|
+
end
|
|
1504
|
+
|
|
1505
|
+
arg_types.last.is_a?(Type::HashShape) ? nil : arg_types
|
|
1506
|
+
end
|
|
1507
|
+
|
|
1459
1508
|
# `Range.new(b, e)` / `Range.new(b, e, excl)` — folds to `Constant[Range]` when both
|
|
1460
1509
|
# endpoints are `Constant[Integer]` or both are `Constant[String]`, and the optional third
|
|
1461
1510
|
# argument is a `Constant[true/false]`. Nil endpoints (beginless / endless ranges) are not
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../source/node_children"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Inference
|
|
9
|
+
module ProjectPatchedScanner
|
|
10
|
+
# Finds the `def`s of a `pre_eval:` file written under a definedness guard naming the same method
|
|
11
|
+
# (`def foo = 1 unless method_defined?(:foo)`): polyfills that run only when nothing bound the name before.
|
|
12
|
+
module DefinednessGuard
|
|
13
|
+
DEFINEDNESS_PREDICATES = %i[
|
|
14
|
+
method_defined? private_method_defined? public_method_defined? protected_method_defined? respond_to?
|
|
15
|
+
].freeze
|
|
16
|
+
INSTANCE_METHOD_LISTS = %i[
|
|
17
|
+
instance_methods public_instance_methods private_instance_methods protected_instance_methods
|
|
18
|
+
].freeze
|
|
19
|
+
private_constant :DEFINEDNESS_PREDICATES, :INSTANCE_METHOD_LISTS
|
|
20
|
+
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
# The `def` nodes (by identity) that sit on the "name not yet defined" side of a definedness guard naming the
|
|
24
|
+
# same method: `def foo = 1 unless method_defined?(:foo)`, or the same inside an `unless` / `if !` block.
|
|
25
|
+
def defs(root)
|
|
26
|
+
found = {}.compare_by_identity
|
|
27
|
+
collect_guarded(root, found)
|
|
28
|
+
found
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def collect_guarded(node, found)
|
|
32
|
+
return unless node.is_a?(Prism::Node)
|
|
33
|
+
|
|
34
|
+
if node.is_a?(Prism::IfNode) || node.is_a?(Prism::UnlessNode)
|
|
35
|
+
name, negated = definedness_guard(node.predicate)
|
|
36
|
+
mark_guarded_defs(undefined_branch(node, negated), name, found) if name
|
|
37
|
+
end
|
|
38
|
+
node.rigor_each_child { |child| collect_guarded(child, found) }
|
|
39
|
+
end
|
|
40
|
+
private_class_method :collect_guarded
|
|
41
|
+
|
|
42
|
+
# `[method_name, negated]` for a predicate like `method_defined?(:m)` / `!respond_to?(:m)`, else nil.
|
|
43
|
+
def definedness_guard(predicate)
|
|
44
|
+
predicate = predicate.body if predicate.is_a?(Prism::ParenthesesNode) && predicate.body
|
|
45
|
+
predicate = predicate.body.first if predicate.is_a?(Prism::StatementsNode) && predicate.body.size == 1
|
|
46
|
+
if predicate.is_a?(Prism::CallNode) && predicate.name == :! && predicate.receiver
|
|
47
|
+
name, negated = definedness_guard(predicate.receiver)
|
|
48
|
+
return name && [name, !negated]
|
|
49
|
+
end
|
|
50
|
+
name = guard_method_name(predicate)
|
|
51
|
+
name && [name, false]
|
|
52
|
+
end
|
|
53
|
+
private_class_method :definedness_guard
|
|
54
|
+
|
|
55
|
+
def guard_method_name(call)
|
|
56
|
+
return unless call.is_a?(Prism::CallNode)
|
|
57
|
+
|
|
58
|
+
listed = %i[include? member?].include?(call.name)
|
|
59
|
+
listed &&= call.receiver.is_a?(Prism::CallNode) && INSTANCE_METHOD_LISTS.include?(call.receiver.name)
|
|
60
|
+
return unless listed || DEFINEDNESS_PREDICATES.include?(call.name)
|
|
61
|
+
|
|
62
|
+
arg = call.arguments&.arguments&.first
|
|
63
|
+
arg.unescaped.to_sym if arg.is_a?(Prism::SymbolNode) || arg.is_a?(Prism::StringNode)
|
|
64
|
+
end
|
|
65
|
+
private_class_method :guard_method_name
|
|
66
|
+
|
|
67
|
+
# The branch that runs when the guarded name is NOT defined yet.
|
|
68
|
+
def undefined_branch(node, negated)
|
|
69
|
+
branch_when_true = node.is_a?(Prism::IfNode) ? node.statements : node.else_clause
|
|
70
|
+
branch_when_false = node.is_a?(Prism::IfNode) ? node.subsequent : node.statements
|
|
71
|
+
# The predicate is true when the name is defined (unless negated).
|
|
72
|
+
negated ? branch_when_true : branch_when_false
|
|
73
|
+
end
|
|
74
|
+
private_class_method :undefined_branch
|
|
75
|
+
|
|
76
|
+
def mark_guarded_defs(node, name, found)
|
|
77
|
+
return unless node.is_a?(Prism::Node)
|
|
78
|
+
|
|
79
|
+
found[node] = true if node.is_a?(Prism::DefNode) && node.name == name
|
|
80
|
+
node.rigor_each_child { |child| mark_guarded_defs(child, name, found) } unless node.is_a?(Prism::DefNode)
|
|
81
|
+
end
|
|
82
|
+
private_class_method :mark_guarded_defs
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -36,14 +36,14 @@ module Rigor
|
|
|
36
36
|
attr_reader :by_key, :write_census
|
|
37
37
|
|
|
38
38
|
# @param entries — flat list of declarations observed during the pre-pass.
|
|
39
|
-
#
|
|
39
|
+
# Last-write-wins on `(class_name, method_name, kind)` duplicates, as at runtime (ADR-17); the
|
|
40
40
|
# `pre-eval.duplicate-declaration` diagnostic emission stays decoupled from registry behaviour.
|
|
41
41
|
# @param write_census — issue #1367: the `pre_eval:` files' {GlobalWriteCensus}, which the `global.*` write
|
|
42
42
|
# rules join with the project's, since a patch file is loaded ahead of the code it patches.
|
|
43
43
|
def initialize(entries: [], write_census: GlobalWriteCensus::EMPTY)
|
|
44
44
|
@by_key = entries.each_with_object({}) do |entry, acc|
|
|
45
45
|
key = [entry.class_name, entry.method_name, entry.kind]
|
|
46
|
-
acc[key]
|
|
46
|
+
acc[key] = entry
|
|
47
47
|
end.freeze
|
|
48
48
|
@write_census = write_census.frozen? ? write_census : write_census.dup.freeze
|
|
49
49
|
freeze
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "project_patched_methods"
|
|
6
|
+
require_relative "project_patched_definedness_guard"
|
|
6
7
|
require_relative "../analysis/dependency_source_inference/return_type_heuristic"
|
|
7
8
|
require_relative "../source/alias_names"
|
|
8
9
|
require_relative "../source/constant_path"
|
|
@@ -35,6 +36,13 @@ module Rigor
|
|
|
35
36
|
PendingAlias = Data.define(:class_name, :new_name, :old_name, :kind, :source_path, :source_line)
|
|
36
37
|
private_constant :PendingAlias
|
|
37
38
|
|
|
39
|
+
# A `def` written under a definedness guard (`def foo ... unless method_defined?(:foo)`): a polyfill that runs
|
|
40
|
+
# only when nothing bound the name before it, so it never replaces an earlier binding.
|
|
41
|
+
GuardedDef = Data.define(:entry) do
|
|
42
|
+
def source_path = entry.source_path
|
|
43
|
+
end
|
|
44
|
+
private_constant :GuardedDef
|
|
45
|
+
|
|
38
46
|
module_function
|
|
39
47
|
|
|
40
48
|
# @param paths — absolute paths to the pre-eval files. The runner has already
|
|
@@ -58,29 +66,28 @@ module Rigor
|
|
|
58
66
|
end
|
|
59
67
|
|
|
60
68
|
# ADR-17 § "Failure modes" — when two pre-eval entries declare the same `(class_name, method_name,
|
|
61
|
-
# kind)` triple,
|
|
62
|
-
#
|
|
63
|
-
#
|
|
69
|
+
# kind)` triple, the last-listed one wins, as at runtime where the later `def` replaces the earlier. Emit
|
|
70
|
+
# one `:info` `pre-eval.duplicate-declaration` diagnostic per SHADOWED entry, reported on that earlier
|
|
71
|
+
# declaration (the declaration that never takes effect) and naming the
|
|
72
|
+
# winning declaration it yields to.
|
|
64
73
|
def duplicate_declaration_diagnostics(entries)
|
|
65
|
-
|
|
66
|
-
entries.
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
seen[key] = entry
|
|
83
|
-
end
|
|
74
|
+
winners = entries.to_h { |entry| [[entry.class_name, entry.method_name, entry.kind], entry] }
|
|
75
|
+
entries.filter_map do |entry|
|
|
76
|
+
winner = winners[[entry.class_name, entry.method_name, entry.kind]]
|
|
77
|
+
next if winner.equal?(entry)
|
|
78
|
+
|
|
79
|
+
build_diagnostic(
|
|
80
|
+
path: entry.source_path,
|
|
81
|
+
line: entry.source_line,
|
|
82
|
+
column: 1,
|
|
83
|
+
severity: :info,
|
|
84
|
+
rule: "pre-eval.duplicate-declaration",
|
|
85
|
+
message: "pre-eval duplicate declaration: " \
|
|
86
|
+
"#{entry.class_name}##{entry.method_name} " \
|
|
87
|
+
"(#{entry.kind}) is declared again at " \
|
|
88
|
+
"#{winner.source_path}:#{winner.source_line}. " \
|
|
89
|
+
"The last-listed declaration wins; this entry is shadowed."
|
|
90
|
+
)
|
|
84
91
|
end
|
|
85
92
|
end
|
|
86
93
|
private_class_method :duplicate_declaration_diagnostics
|
|
@@ -97,14 +104,17 @@ module Rigor
|
|
|
97
104
|
# Within one file the later binding of a name replaces the earlier, as at runtime: the alias-method-chain
|
|
98
105
|
# idiom (`def foo`, `alias_method :foo_without_x, :foo`, `alias_method :foo, :foo_with_x`) leaves `foo` the
|
|
99
106
|
# alias. Bindings of one name in DIFFERENT files are what `pre-eval.duplicate-declaration` reports, and the
|
|
100
|
-
# registry keeps the
|
|
107
|
+
# registry keeps the last of those, as Ruby does.
|
|
101
108
|
def resolve_aliases(collected)
|
|
102
109
|
bound = {}
|
|
103
110
|
per_file = collected.group_by(&:source_path).values.map do |items|
|
|
104
111
|
final = {}
|
|
105
112
|
items.each do |item|
|
|
106
113
|
entry = item.is_a?(PendingAlias) ? alias_entry(item, bound) : item
|
|
114
|
+
entry = entry.entry if entry.is_a?(GuardedDef)
|
|
107
115
|
key = [entry.class_name, entry.method_name, entry.kind]
|
|
116
|
+
next if item.is_a?(GuardedDef) && bound.key?(key)
|
|
117
|
+
|
|
108
118
|
bound[key] = entry
|
|
109
119
|
final.delete(key)
|
|
110
120
|
final[key] = entry
|
|
@@ -138,6 +148,7 @@ module Rigor
|
|
|
138
148
|
return
|
|
139
149
|
end
|
|
140
150
|
|
|
151
|
+
Thread.current[:rigor_guarded_defs] = DefinednessGuard.defs(parse_result.value)
|
|
141
152
|
walk_node(parse_result.value, [], false, path, entries)
|
|
142
153
|
# Issue #1367 — the file's `global.*` write facts: a patch file may alias a special or give a class the
|
|
143
154
|
# method a setter asks for, as any project file may.
|
|
@@ -151,6 +162,8 @@ module Rigor
|
|
|
151
162
|
"#{e.class}: #{e.message}. Pre-evaluation skipped for this file; " \
|
|
152
163
|
"the rest of the run proceeds."
|
|
153
164
|
)
|
|
165
|
+
ensure
|
|
166
|
+
Thread.current[:rigor_guarded_defs] = nil
|
|
154
167
|
end
|
|
155
168
|
private_class_method :scan_file
|
|
156
169
|
|
|
@@ -233,11 +246,12 @@ module Rigor
|
|
|
233
246
|
kind = node.receiver.is_a?(Prism::SelfNode) || in_singleton_class ? :singleton : :instance
|
|
234
247
|
line = node.location&.start_line || 1
|
|
235
248
|
return_type = Analysis::DependencySourceInference::ReturnTypeHeuristic.extract(node)
|
|
236
|
-
|
|
249
|
+
entry = ProjectPatchedMethods::Entry.new(
|
|
237
250
|
class_name: class_name, method_name: node.name, kind: kind,
|
|
238
251
|
source_path: source_path, source_line: line,
|
|
239
252
|
return_type: return_type
|
|
240
253
|
)
|
|
254
|
+
entries << (Thread.current[:rigor_guarded_defs]&.key?(node) ? GuardedDef.new(entry: entry) : entry)
|
|
241
255
|
end
|
|
242
256
|
private_class_method :record_def_node
|
|
243
257
|
|