rigortype 0.4.2 → 0.4.4
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/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +28 -5
- data/docs/manual/02-cli-reference.md +23 -4
- data/docs/manual/03-configuration.md +21 -8
- data/docs/manual/04-diagnostics.md +44 -2
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/10-mcp-server.md +32 -3
- 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 +26 -9
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- data/lib/rigor/analysis/check_rules/rule_ids.rb +64 -0
- data/lib/rigor/analysis/check_rules.rb +68 -57
- data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
- data/lib/rigor/analysis/incremental.rb +12 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -3
- data/lib/rigor/analysis/incremental_session.rb +207 -40
- 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 +139 -15
- data/lib/rigor/analysis/reachability/scan.rb +374 -173
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- data/lib/rigor/analysis/rule_catalog.rb +6 -1
- data/lib/rigor/analysis/run_cache_key.rb +31 -7
- data/lib/rigor/analysis/run_cache_probe.rb +6 -4
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +74 -13
- data/lib/rigor/analysis/runner/pool_coordinator.rb +133 -19
- data/lib/rigor/analysis/runner/project_pre_passes.rb +46 -1
- data/lib/rigor/analysis/runner.rb +62 -18
- data/lib/rigor/analysis/severity_stamp.rb +34 -1
- data/lib/rigor/analysis/worker_session.rb +58 -2
- data/lib/rigor/cache/descriptor.rb +40 -6
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +56 -12
- 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/mcp_command.rb +1 -1
- 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/config_audit.rb +111 -15
- data/lib/rigor/configuration/dependencies.rb +19 -11
- 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 +79 -10
- data/lib/rigor/environment/required_features.rb +4 -2
- data/lib/rigor/environment.rb +12 -5
- data/lib/rigor/inference/block_call_timing.rb +5 -0
- data/lib/rigor/inference/builtins/cruby_definers.rb +22 -5
- data/lib/rigor/inference/captured_locals.rb +69 -6
- data/lib/rigor/inference/definer_resolution.rb +16 -7
- data/lib/rigor/inference/expression_typer.rb +117 -29
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +21 -17
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- 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/module_function_state.rb +80 -13
- data/lib/rigor/inference/narrowing.rb +184 -68
- 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/reflective_ivar_write.rb +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +416 -1
- data/lib/rigor/inference/scope_indexer.rb +565 -61
- data/lib/rigor/inference/self_call_ivar_writes.rb +341 -0
- data/lib/rigor/inference/statement_evaluator.rb +307 -60
- 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 +133 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +802 -0
- data/lib/rigor/lens/members.rb +203 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +322 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +41 -0
- data/lib/rigor/mcp/server.rb +61 -20
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/project_environment.rb +23 -1
- data/lib/rigor/protection/mutation_cache.rb +2 -1
- data/lib/rigor/scope.rb +33 -17
- data/lib/rigor/sig_gen/generator.rb +45 -5
- data/lib/rigor/sig_gen/layout_index.rb +36 -0
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- data/lib/rigor/sig_gen/writer.rb +47 -34
- 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/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +28 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
- 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 +21 -1
|
@@ -1019,7 +1019,7 @@ module Rigor
|
|
|
1019
1019
|
self_type = self_type_override ||
|
|
1020
1020
|
(kind == :singleton ? Type::Combinator.singleton_of(class_name) : instance_type)
|
|
1021
1021
|
|
|
1022
|
-
keywords_last =
|
|
1022
|
+
keywords_last = ArgumentNodes.keyword_hash_last?(call_node, args)
|
|
1023
1023
|
call_site = [class_name, method_name, kind]
|
|
1024
1024
|
if keywords_last && args.last.is_a?(Type::HashShape)
|
|
1025
1025
|
distributed = distributed_keyword_return(method_definition, self_type, instance_type, type_vars, args,
|
|
@@ -1129,21 +1129,9 @@ module Rigor
|
|
|
1129
1129
|
dynamic_value && !joined.is_a?(Type::Dynamic) ? Type::Combinator.dynamic(joined) : joined
|
|
1130
1130
|
end
|
|
1131
1131
|
|
|
1132
|
-
# Issue #1727 — whether `args`' last entry is the call's keyword hash (`f(a, k: 1)`), which the overload
|
|
1133
|
-
# selector then matches against an overload's keywords. Only the AST tells a keyword hash from a
|
|
1134
|
-
# braced positional one (`f(a, { k: 1 })`), so a dispatch without the call node, or one whose
|
|
1135
|
-
# arguments are not the node's own (a forwarded `...`, a `send` re-dispatch), answers false and keeps
|
|
1136
|
-
# reading every argument positionally.
|
|
1137
|
-
def keyword_arguments_last?(call_node, args)
|
|
1138
|
-
return false if args.empty? || !call_node.respond_to?(:arguments)
|
|
1139
|
-
|
|
1140
|
-
arguments = call_node.arguments&.arguments
|
|
1141
|
-
!arguments.nil? && arguments.size == args.size && arguments.last.is_a?(Prism::KeywordHashNode)
|
|
1142
|
-
end
|
|
1143
|
-
|
|
1144
1132
|
# Issue #1801 — the indices of `args` the call passes as splat arguments (`f(*xs, k: 1)`), whose positional
|
|
1145
|
-
# count is not known statically, or nil without one. As for {
|
|
1146
|
-
# node's arguments are exactly `args` answers.
|
|
1133
|
+
# count is not known statically, or nil without one. As for {ArgumentNodes.keyword_hash_last?}, only a call
|
|
1134
|
+
# whose node's arguments are exactly `args` answers.
|
|
1147
1135
|
def splat_indices(call_node, args)
|
|
1148
1136
|
return nil if args.empty? || !call_node.respond_to?(:arguments)
|
|
1149
1137
|
|
|
@@ -1729,7 +1717,7 @@ module Rigor
|
|
|
1729
1717
|
|
|
1730
1718
|
def probe_block_param_types(receiver:, method_name:, args:, environment:, scope: nil, call_node: nil)
|
|
1731
1719
|
args ||= []
|
|
1732
|
-
keywords_last =
|
|
1720
|
+
keywords_last = ArgumentNodes.keyword_hash_last?(call_node, args)
|
|
1733
1721
|
splat = splat_indices(call_node, args)
|
|
1734
1722
|
case receiver
|
|
1735
1723
|
when Type::Union
|
|
@@ -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
|
|
@@ -11,8 +11,8 @@ module Rigor
|
|
|
11
11
|
# [#1197](https://github.com/rigortype/rigor/issues/1197)). Four readers used to compute it in four
|
|
12
12
|
# places. They disagree with each other and, in places, with Ruby. Each entry point below reproduces
|
|
13
13
|
# one reader's answer exactly: this module gives the readings one place to live, and does not yet make
|
|
14
|
-
# them agree. A change to one reading lands here, beside the other three.
|
|
15
|
-
#
|
|
14
|
+
# them agree. A change to one reading lands here, beside the other three. An example is the reset a bare
|
|
15
|
+
# `public`, `private` or `protected` makes.
|
|
16
16
|
#
|
|
17
17
|
# - The singleton def-node table (`ScopeIndexer#walk_singleton_body`) reads
|
|
18
18
|
# {.each_singleton_sibling} and {.each_singleton_copy}. A bare call covers the later direct
|
|
@@ -78,23 +78,49 @@ module Rigor
|
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
-
# The defs a named call copies onto the module's singleton, for the singleton def-node table.
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
81
|
+
# The defs a named call copies onto the module's singleton, for the singleton def-node table. Ruby
|
|
82
|
+
# copies the method in effect at the call (#1550), so a later def of the name redefines only the
|
|
83
|
+
# instance method. This reading approximates that from the body's direct `statements`: it yields
|
|
84
|
+
# `name, def_node` for each Symbol or String argument, where `def_node` is the last receiverless def
|
|
85
|
+
# of that name written as a direct statement before the call.
|
|
86
|
+
#
|
|
87
|
+
# A name declines (nothing is yielded) when no def precedes the call, where Ruby raises `NameError` or
|
|
88
|
+
# copies a def another file may load first, and when a statement after that def and before the call
|
|
89
|
+
# may rebind it in a way this reading cannot name: a def nested in control flow or a block, an `alias`,
|
|
90
|
+
# an `undef`, or a receiverless or `self.` `alias_method`, `define_method`, `attr_*`, `remove_method`
|
|
91
|
+
# or `undef_method` call. Such a call or `alias` with a non-literal name anywhere before the call, even
|
|
92
|
+
# before the def, declines every name. A `module_function def x` argument names nothing here.
|
|
93
|
+
#
|
|
94
|
+
# It does not see `send(:define_method, ...)`, a `define_method` on an explicit module receiver, a
|
|
95
|
+
# `prepend`, another file's singleton defs, or that a `rescue` clause's statements may run after an
|
|
96
|
+
# `ensure` call's (#1969).
|
|
87
97
|
def each_singleton_copy(call, statements)
|
|
88
|
-
|
|
89
|
-
|
|
98
|
+
call_start = call.location.start_offset
|
|
99
|
+
in_effect = {}
|
|
100
|
+
statements.each do |stmt|
|
|
101
|
+
next unless stmt.location.end_offset <= call_start
|
|
102
|
+
|
|
103
|
+
if stmt.is_a?(Prism::DefNode)
|
|
104
|
+
in_effect[stmt.name] = stmt if stmt.receiver.nil?
|
|
105
|
+
else
|
|
106
|
+
each_rebound_name(stmt) { |name| in_effect[name] = :unknown }
|
|
107
|
+
end
|
|
90
108
|
end
|
|
91
109
|
call.arguments&.arguments&.each do |arg|
|
|
92
110
|
name = symbol_argument_name(arg)
|
|
93
|
-
def_node = name &&
|
|
94
|
-
yield name, def_node if def_node
|
|
111
|
+
def_node = name && !in_effect.key?(ANY_NAME) && in_effect[name]
|
|
112
|
+
yield name, def_node if def_node.is_a?(Prism::DefNode)
|
|
95
113
|
end
|
|
96
114
|
end
|
|
97
115
|
|
|
116
|
+
# The key {.each_singleton_copy} records when a statement may rebind a name it cannot read.
|
|
117
|
+
ANY_NAME = Object.new.freeze
|
|
118
|
+
|
|
119
|
+
# Calls that may define, alias or remove an instance method of the receiverless module.
|
|
120
|
+
REBINDING_CALLS = %i[
|
|
121
|
+
alias_method define_method attr attr_reader attr_writer attr_accessor remove_method undef_method
|
|
122
|
+
].freeze
|
|
123
|
+
|
|
98
124
|
# The deferred-ranges reading, over one class-ish body's subtree. Appends to `offsets` the start of
|
|
99
125
|
# every bare call; each def that starts after one is a module function
|
|
100
126
|
# ({.deferred_module_function?}). Appends to `ranges` one row per named call, in the table's
|
|
@@ -173,6 +199,46 @@ module Rigor
|
|
|
173
199
|
active && kind == :instance
|
|
174
200
|
end
|
|
175
201
|
|
|
202
|
+
# Yields each instance-method name `node` may rebind on the module whose body holds it, or
|
|
203
|
+
# {ANY_NAME} when one is not a literal. It does not enter a nested class, module or `class <<` body,
|
|
204
|
+
# whose methods belong to another module.
|
|
205
|
+
def each_rebound_name(node, &)
|
|
206
|
+
case node
|
|
207
|
+
when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode then nil
|
|
208
|
+
when Prism::DefNode then yield node.name if node.receiver.nil?
|
|
209
|
+
else
|
|
210
|
+
each_own_rebound_name(node, &)
|
|
211
|
+
node.compact_child_nodes.each { |child| each_rebound_name(child, &) }
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# The names `node` itself may rebind, apart from its children: an `alias`, an `undef` or a
|
|
216
|
+
# {REBINDING_CALLS} call on the module.
|
|
217
|
+
def each_own_rebound_name(node, &)
|
|
218
|
+
case node
|
|
219
|
+
when Prism::AliasMethodNode then yield symbol_argument_name(node.new_name) || ANY_NAME
|
|
220
|
+
when Prism::UndefNode then node.names.each { |name| yield symbol_argument_name(name) || ANY_NAME }
|
|
221
|
+
when Prism::CallNode
|
|
222
|
+
each_rebinding_call_name(node, &) if REBINDING_CALLS.include?(node.name) &&
|
|
223
|
+
(node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# The names a {REBINDING_CALLS} call may rebind: the first argument of `alias_method` and
|
|
228
|
+
# `define_method`, every argument of the others (and its writer, for the `attr_*` family), or
|
|
229
|
+
# {ANY_NAME} for a non-literal one.
|
|
230
|
+
def each_rebinding_call_name(call)
|
|
231
|
+
arguments = call.arguments&.arguments || []
|
|
232
|
+
arguments = arguments.first(1) if %i[alias_method define_method].include?(call.name)
|
|
233
|
+
arguments.each do |arg|
|
|
234
|
+
name = symbol_argument_name(arg)
|
|
235
|
+
next yield(ANY_NAME) unless name
|
|
236
|
+
|
|
237
|
+
yield name
|
|
238
|
+
yield :"#{name}=" if call.name.start_with?("attr")
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
176
242
|
# The prescan's call arm.
|
|
177
243
|
def prescan_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
|
|
178
244
|
if bare?(node)
|
|
@@ -208,7 +274,8 @@ module Rigor
|
|
|
208
274
|
Source::Literals.symbol_or_string(arg)
|
|
209
275
|
end
|
|
210
276
|
|
|
211
|
-
private_class_method :prescan_call, :prescan_eval_call, :symbol_argument_name
|
|
277
|
+
private_class_method :prescan_call, :prescan_eval_call, :symbol_argument_name, :each_rebound_name,
|
|
278
|
+
:each_own_rebound_name, :each_rebinding_call_name
|
|
212
279
|
end
|
|
213
280
|
end
|
|
214
281
|
end
|