rigortype 0.0.1
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 +7 -0
- data/LICENSE +373 -0
- data/README.md +152 -0
- data/exe/rigor +9 -0
- data/lib/rigor/analysis/check_rules.rb +503 -0
- data/lib/rigor/analysis/diagnostic.rb +35 -0
- data/lib/rigor/analysis/fact_store.rb +133 -0
- data/lib/rigor/analysis/result.rb +29 -0
- data/lib/rigor/analysis/runner.rb +119 -0
- data/lib/rigor/ast/type_node.rb +41 -0
- data/lib/rigor/ast.rb +22 -0
- data/lib/rigor/cli/type_of_command.rb +160 -0
- data/lib/rigor/cli/type_of_renderer.rb +88 -0
- data/lib/rigor/cli/type_scan_command.rb +160 -0
- data/lib/rigor/cli/type_scan_renderer.rb +165 -0
- data/lib/rigor/cli/type_scan_report.rb +32 -0
- data/lib/rigor/cli.rb +195 -0
- data/lib/rigor/configuration.rb +49 -0
- data/lib/rigor/environment/class_registry.rb +141 -0
- data/lib/rigor/environment/rbs_hierarchy.rb +64 -0
- data/lib/rigor/environment/rbs_loader.rb +244 -0
- data/lib/rigor/environment.rb +177 -0
- data/lib/rigor/inference/acceptance.rb +444 -0
- data/lib/rigor/inference/block_parameter_binder.rb +198 -0
- data/lib/rigor/inference/closure_escape_analyzer.rb +191 -0
- data/lib/rigor/inference/coverage_scanner.rb +85 -0
- data/lib/rigor/inference/expression_typer.rb +831 -0
- data/lib/rigor/inference/fallback.rb +35 -0
- data/lib/rigor/inference/fallback_tracer.rb +64 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +102 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +169 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +421 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +336 -0
- data/lib/rigor/inference/method_dispatcher.rb +213 -0
- data/lib/rigor/inference/method_parameter_binder.rb +257 -0
- data/lib/rigor/inference/multi_target_binder.rb +143 -0
- data/lib/rigor/inference/narrowing.rb +1008 -0
- data/lib/rigor/inference/rbs_type_translator.rb +219 -0
- data/lib/rigor/inference/scope_indexer.rb +468 -0
- data/lib/rigor/inference/statement_evaluator.rb +1017 -0
- data/lib/rigor/rbs_extended.rb +98 -0
- data/lib/rigor/scope.rb +340 -0
- data/lib/rigor/source/node_locator.rb +104 -0
- data/lib/rigor/source/node_walker.rb +37 -0
- data/lib/rigor/source.rb +15 -0
- data/lib/rigor/testing.rb +65 -0
- data/lib/rigor/trinary.rb +108 -0
- data/lib/rigor/type/accepts_result.rb +109 -0
- data/lib/rigor/type/bot.rb +57 -0
- data/lib/rigor/type/combinator.rb +148 -0
- data/lib/rigor/type/constant.rb +90 -0
- data/lib/rigor/type/dynamic.rb +60 -0
- data/lib/rigor/type/hash_shape.rb +246 -0
- data/lib/rigor/type/nominal.rb +83 -0
- data/lib/rigor/type/singleton.rb +65 -0
- data/lib/rigor/type/top.rb +56 -0
- data/lib/rigor/type/tuple.rb +84 -0
- data/lib/rigor/type/union.rb +65 -0
- data/lib/rigor/type.rb +23 -0
- data/lib/rigor/version.rb +5 -0
- data/lib/rigor.rb +29 -0
- data/sig/rigor/analysis/fact_store.rbs +51 -0
- data/sig/rigor/ast.rbs +11 -0
- data/sig/rigor/environment.rbs +59 -0
- data/sig/rigor/inference.rbs +151 -0
- data/sig/rigor/rbs_extended.rbs +22 -0
- data/sig/rigor/scope.rbs +49 -0
- data/sig/rigor/source.rbs +20 -0
- data/sig/rigor/testing.rbs +9 -0
- data/sig/rigor/trinary.rbs +29 -0
- data/sig/rigor/type.rbs +171 -0
- data/sig/rigor.rbs +70 -0
- metadata +260 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbs"
|
|
4
|
+
|
|
5
|
+
require_relative "../type"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Inference
|
|
9
|
+
# Translates `RBS::Types::*` instances into `Rigor::Type` values.
|
|
10
|
+
#
|
|
11
|
+
# Slice 4 phase 2d adds two pieces of generic plumbing:
|
|
12
|
+
# - `RBS::Types::ClassInstance` arguments are translated recursively
|
|
13
|
+
# so `Array[Integer]` becomes `Nominal["Array", [Nominal["Integer"]]]`
|
|
14
|
+
# (and `Hash[Symbol, Integer]` becomes `Nominal["Hash", [...]]`).
|
|
15
|
+
# - `RBS::Types::Variable` consults a caller-supplied substitution
|
|
16
|
+
# map (`type_vars:`) keyed by the variable's RBS name. When the
|
|
17
|
+
# variable is bound, the bound `Rigor::Type` is returned unchanged;
|
|
18
|
+
# when it is not bound, the variable degrades to `Dynamic[Top]` so
|
|
19
|
+
# uninstantiated generics keep their fail-soft behavior.
|
|
20
|
+
#
|
|
21
|
+
# Slice 5 phase 1 maps tuples and records to their dedicated shape
|
|
22
|
+
# carriers:
|
|
23
|
+
# - `RBS::Types::Tuple` becomes `Rigor::Type::Tuple[...]` so the
|
|
24
|
+
# arity and per-position element types survive the boundary.
|
|
25
|
+
# - `RBS::Types::Record` becomes an exact closed
|
|
26
|
+
# `Rigor::Type::HashShape{...}`, carrying required and optional
|
|
27
|
+
# fields intact.
|
|
28
|
+
# Element and value types are translated recursively under the
|
|
29
|
+
# caller's `self_type` / `instance_type` / `type_vars` context.
|
|
30
|
+
#
|
|
31
|
+
# Interface and intersection types still degrade to `Dynamic[Top]`;
|
|
32
|
+
# they are bound to acceptance and dispatch rules that Slice 5+
|
|
33
|
+
# will replace.
|
|
34
|
+
#
|
|
35
|
+
# The optional `self_type:` and `instance_type:` arguments are the
|
|
36
|
+
# Rigor counterparts of RBS's `self` and `instance` tokens:
|
|
37
|
+
# - `self_type` substitutes for `Bases::Self`. Inside an instance
|
|
38
|
+
# method body it is `Nominal[C]`; inside a singleton method body
|
|
39
|
+
# it is `Singleton[C]`.
|
|
40
|
+
# - `instance_type` substitutes for `Bases::Instance` and is always
|
|
41
|
+
# `Nominal[C]` regardless of which method body we are in.
|
|
42
|
+
# When either argument is omitted, the corresponding token degrades
|
|
43
|
+
# to Dynamic[Top].
|
|
44
|
+
# rubocop:disable Metrics/ModuleLength
|
|
45
|
+
module RbsTypeTranslator
|
|
46
|
+
# Hash-based dispatch keeps `translate` linear and dodges the
|
|
47
|
+
# bookkeeping costs of a 20-arm `case` (RuboCop AbcSize/CCN/Length
|
|
48
|
+
# all spike on that shape). Anonymous RBS-type subclasses are not
|
|
49
|
+
# expected; the table only maps the concrete leaf classes shipped
|
|
50
|
+
# by the `rbs` gem.
|
|
51
|
+
TRANSLATORS = {
|
|
52
|
+
RBS::Types::Bases::Top => :translate_top,
|
|
53
|
+
RBS::Types::Bases::Bottom => :translate_bot,
|
|
54
|
+
RBS::Types::Bases::Any => :translate_untyped,
|
|
55
|
+
RBS::Types::Bases::Nil => :translate_nil,
|
|
56
|
+
RBS::Types::Bases::Bool => :translate_bool,
|
|
57
|
+
RBS::Types::Bases::Self => :translate_self,
|
|
58
|
+
RBS::Types::Bases::Instance => :translate_instance,
|
|
59
|
+
RBS::Types::Bases::Class => :translate_untyped,
|
|
60
|
+
RBS::Types::Bases::Void => :translate_untyped,
|
|
61
|
+
RBS::Types::Optional => :translate_optional,
|
|
62
|
+
RBS::Types::Union => :translate_union,
|
|
63
|
+
RBS::Types::Literal => :translate_literal,
|
|
64
|
+
RBS::Types::ClassInstance => :translate_class_instance,
|
|
65
|
+
RBS::Types::Tuple => :translate_tuple,
|
|
66
|
+
RBS::Types::Record => :translate_record,
|
|
67
|
+
RBS::Types::Proc => :translate_proc_nominal,
|
|
68
|
+
RBS::Types::ClassSingleton => :translate_class_singleton,
|
|
69
|
+
RBS::Types::Alias => :translate_untyped,
|
|
70
|
+
RBS::Types::Intersection => :translate_untyped,
|
|
71
|
+
RBS::Types::Variable => :translate_variable,
|
|
72
|
+
RBS::Types::Interface => :translate_untyped
|
|
73
|
+
}.freeze
|
|
74
|
+
private_constant :TRANSLATORS
|
|
75
|
+
|
|
76
|
+
EMPTY_TYPE_VARS = {}.freeze
|
|
77
|
+
private_constant :EMPTY_TYPE_VARS
|
|
78
|
+
|
|
79
|
+
class << self
|
|
80
|
+
# @param rbs_type [RBS::Types::Bases::Base, RBS::Types::ClassInstance, ...]
|
|
81
|
+
# @param self_type [Rigor::Type, nil] substitute for `Bases::Self`.
|
|
82
|
+
# @param instance_type [Rigor::Type, nil] substitute for
|
|
83
|
+
# `Bases::Instance`. Defaults to `nil`, which degrades to
|
|
84
|
+
# Dynamic[Top].
|
|
85
|
+
# @param type_vars [Hash{Symbol => Rigor::Type}] substitution map
|
|
86
|
+
# for `Bases::Variable`. Keys are the RBS variable names (e.g.,
|
|
87
|
+
# `:Elem`); values are Rigor types that replace the variable.
|
|
88
|
+
# Variables that are not bound in the map degrade to Dynamic[Top].
|
|
89
|
+
# @return [Rigor::Type]
|
|
90
|
+
def translate(rbs_type, self_type: nil, instance_type: nil, type_vars: EMPTY_TYPE_VARS)
|
|
91
|
+
handler = TRANSLATORS[rbs_type.class]
|
|
92
|
+
return send(handler, rbs_type, self_type, instance_type, type_vars) if handler
|
|
93
|
+
|
|
94
|
+
Type::Combinator.untyped
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def translate_top(_rbs_type, _self_type, _instance_type, _type_vars)
|
|
100
|
+
Type::Combinator.top
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def translate_bot(_rbs_type, _self_type, _instance_type, _type_vars)
|
|
104
|
+
Type::Combinator.bot
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def translate_untyped(_rbs_type, _self_type, _instance_type, _type_vars)
|
|
108
|
+
Type::Combinator.untyped
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def translate_nil(_rbs_type, _self_type, _instance_type, _type_vars)
|
|
112
|
+
Type::Combinator.constant_of(nil)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# `bool` in RBS denotes `true | false`. We fold it to that union
|
|
116
|
+
# eagerly so downstream comparisons (e.g., `result == Constant[true]`)
|
|
117
|
+
# remain structural. Memoized at the module level because the
|
|
118
|
+
# union is value-equal across calls.
|
|
119
|
+
def translate_bool(_rbs_type, _self_type, _instance_type, _type_vars)
|
|
120
|
+
BOOL_UNION
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
BOOL_UNION = Type::Combinator.union(
|
|
124
|
+
Type::Combinator.constant_of(true),
|
|
125
|
+
Type::Combinator.constant_of(false)
|
|
126
|
+
).freeze
|
|
127
|
+
private_constant :BOOL_UNION
|
|
128
|
+
|
|
129
|
+
def translate_self(_rbs_type, self_type, _instance_type, _type_vars)
|
|
130
|
+
self_type || Type::Combinator.untyped
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def translate_instance(_rbs_type, _self_type, instance_type, _type_vars)
|
|
134
|
+
instance_type || Type::Combinator.untyped
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def translate_optional(rbs_type, self_type, instance_type, type_vars)
|
|
138
|
+
inner = translate(rbs_type.type, self_type: self_type, instance_type: instance_type, type_vars: type_vars)
|
|
139
|
+
Type::Combinator.union(inner, Type::Combinator.constant_of(nil))
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def translate_union(rbs_type, self_type, instance_type, type_vars)
|
|
143
|
+
members = rbs_type.types.map do |t|
|
|
144
|
+
translate(t, self_type: self_type, instance_type: instance_type, type_vars: type_vars)
|
|
145
|
+
end
|
|
146
|
+
Type::Combinator.union(*members)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def translate_literal(rbs_type, _self_type, _instance_type, _type_vars)
|
|
150
|
+
Type::Combinator.constant_of(rbs_type.literal)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Slice 4 phase 2d translates the type arguments recursively so
|
|
154
|
+
# `Array[Integer]` round-trips into `Nominal["Array", [Nominal["Integer"]]]`.
|
|
155
|
+
# Variables inside the args participate in substitution through
|
|
156
|
+
# the same `type_vars:` map.
|
|
157
|
+
def translate_class_instance(rbs_type, self_type, instance_type, type_vars)
|
|
158
|
+
name = rbs_type.name.relative!.to_s
|
|
159
|
+
translated_args = rbs_type.args.map do |arg|
|
|
160
|
+
translate(arg, self_type: self_type, instance_type: instance_type, type_vars: type_vars)
|
|
161
|
+
end
|
|
162
|
+
Type::Combinator.nominal_of(name, type_args: translated_args)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Slice 5 phase 1: preserve tuple precision through the
|
|
166
|
+
# boundary. Each positional element type is translated
|
|
167
|
+
# recursively under the caller's substitution context, and the
|
|
168
|
+
# resulting list is wrapped in a `Rigor::Type::Tuple`.
|
|
169
|
+
def translate_tuple(rbs_type, self_type, instance_type, type_vars)
|
|
170
|
+
elements = rbs_type.types.map do |t|
|
|
171
|
+
translate(t, self_type: self_type, instance_type: instance_type, type_vars: type_vars)
|
|
172
|
+
end
|
|
173
|
+
Type::Combinator.tuple_of(*elements)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Slice 5: preserve hash-record precision through the boundary.
|
|
177
|
+
# RBS records use Symbol keys; the translator keeps them as
|
|
178
|
+
# Symbol keys on the resulting exact closed HashShape so
|
|
179
|
+
# erasure can round-trip back to `{ a: T, ?b: U }` syntax.
|
|
180
|
+
def translate_record(rbs_type, self_type, instance_type, type_vars)
|
|
181
|
+
pairs = rbs_type.fields.each_with_object({}) do |(key, value), acc|
|
|
182
|
+
acc[key] = translate(value, self_type: self_type, instance_type: instance_type, type_vars: type_vars)
|
|
183
|
+
end
|
|
184
|
+
optional_pairs = rbs_type.optional_fields.each_with_object({}) do |(key, value), acc|
|
|
185
|
+
acc[key] = translate(value, self_type: self_type, instance_type: instance_type, type_vars: type_vars)
|
|
186
|
+
end
|
|
187
|
+
Type::Combinator.hash_shape_of(
|
|
188
|
+
pairs.merge(optional_pairs),
|
|
189
|
+
required_keys: pairs.keys,
|
|
190
|
+
optional_keys: optional_pairs.keys,
|
|
191
|
+
extra_keys: :closed
|
|
192
|
+
)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def translate_proc_nominal(_rbs_type, _self_type, _instance_type, _type_vars)
|
|
196
|
+
Type::Combinator.nominal_of(Proc)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# `singleton(Foo)` is the type of the constant `Foo` itself
|
|
200
|
+
# (the class object). With the dedicated Singleton type added in
|
|
201
|
+
# Slice 4 phase 2b, we map directly to `Singleton[Foo]`.
|
|
202
|
+
def translate_class_singleton(rbs_type, _self_type, _instance_type, _type_vars)
|
|
203
|
+
name = rbs_type.name.relative!.to_s
|
|
204
|
+
Type::Combinator.singleton_of(name)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Slice 4 phase 2d. Looks up the variable's RBS name in the
|
|
208
|
+
# substitution map; bound variables are replaced inline, free
|
|
209
|
+
# variables degrade to Dynamic[Top]. We use `fetch` with a
|
|
210
|
+
# default rather than `[]` so a deliberate `nil` binding (a
|
|
211
|
+
# caller mistake) is never silently consumed.
|
|
212
|
+
def translate_variable(rbs_type, _self_type, _instance_type, type_vars)
|
|
213
|
+
type_vars.fetch(rbs_type.name) { Type::Combinator.untyped }
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
# rubocop:enable Metrics/ModuleLength
|
|
218
|
+
end
|
|
219
|
+
end
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../scope"
|
|
6
|
+
require_relative "../type"
|
|
7
|
+
require_relative "statement_evaluator"
|
|
8
|
+
|
|
9
|
+
module Rigor
|
|
10
|
+
module Inference
|
|
11
|
+
# Builds a per-node scope index for a Prism program by running
|
|
12
|
+
# `Rigor::Inference::StatementEvaluator` over the root and recording
|
|
13
|
+
# the entry scope visible at every node. Expression-interior nodes
|
|
14
|
+
# the evaluator does not specialise (call receivers, arguments,
|
|
15
|
+
# array/hash elements, ...) inherit their nearest statement-y
|
|
16
|
+
# ancestor's recorded scope, so a downstream caller that looks up
|
|
17
|
+
# the scope for any Prism node in the tree always gets the scope
|
|
18
|
+
# that was effectively visible at that point.
|
|
19
|
+
#
|
|
20
|
+
# The CLI commands `rigor type-of` and `rigor type-scan` consume
|
|
21
|
+
# the index so that local-variable bindings established earlier in
|
|
22
|
+
# the program are visible to the typer when probing later nodes.
|
|
23
|
+
# Without the index, both commands would type every node under an
|
|
24
|
+
# empty scope and miss the constant-folding / dispatch precision
|
|
25
|
+
# that Slice 3 phase 2's StatementEvaluator unlocks.
|
|
26
|
+
#
|
|
27
|
+
# The returned object is an identity-comparing Hash:
|
|
28
|
+
#
|
|
29
|
+
# ```ruby
|
|
30
|
+
# index = Rigor::Inference::ScopeIndexer.index(program, default_scope: Scope.empty)
|
|
31
|
+
# index[some_prism_node] #=> the Rigor::Scope visible at that node
|
|
32
|
+
# ```
|
|
33
|
+
#
|
|
34
|
+
# Nodes that are not part of the program subtree (e.g. synthesised
|
|
35
|
+
# virtual nodes that the caller looks up after the fact) yield the
|
|
36
|
+
# `default_scope`. The returned Hash is mutable in principle but
|
|
37
|
+
# callers MUST treat it as read-only; the indexer itself never
|
|
38
|
+
# exposes a way to update it past construction.
|
|
39
|
+
# rubocop:disable Metrics/ModuleLength
|
|
40
|
+
module ScopeIndexer
|
|
41
|
+
module_function
|
|
42
|
+
|
|
43
|
+
# Build the scope index for a Prism program subtree.
|
|
44
|
+
#
|
|
45
|
+
# @param root [Prism::Node] usually a `Prism::ProgramNode`, but any
|
|
46
|
+
# subtree the caller wants the indexer to walk works.
|
|
47
|
+
# @param default_scope [Rigor::Scope] the scope used for the root,
|
|
48
|
+
# and the fallback returned for any Prism node not contained in
|
|
49
|
+
# `root`'s subtree.
|
|
50
|
+
# @return [Hash{Prism::Node => Rigor::Scope}] identity-comparing
|
|
51
|
+
# table whose default value is `default_scope`.
|
|
52
|
+
def index(root, default_scope:) # rubocop:disable Metrics/AbcSize
|
|
53
|
+
# Slice A-declarations. Build the declaration overrides
|
|
54
|
+
# first so every scope handed to the StatementEvaluator
|
|
55
|
+
# already carries the table; structural sharing through
|
|
56
|
+
# `Scope#with_local` / `#with_fact` / `#with_self_type`
|
|
57
|
+
# propagates it across every derived scope.
|
|
58
|
+
declared_types, discovered_classes = build_declaration_artifacts(root)
|
|
59
|
+
seeded_scope = default_scope
|
|
60
|
+
.with_declared_types(declared_types)
|
|
61
|
+
.with_discovered_classes(discovered_classes)
|
|
62
|
+
|
|
63
|
+
# Slice 7 phase 2. Pre-pass over every class/module body
|
|
64
|
+
# to collect the per-class ivar accumulator. Seeded after
|
|
65
|
+
# declared_types so the rvalue typer in the pre-pass can
|
|
66
|
+
# see declaration overrides.
|
|
67
|
+
class_ivars = build_class_ivar_index(root, seeded_scope)
|
|
68
|
+
seeded_scope = seeded_scope.with_class_ivars(class_ivars)
|
|
69
|
+
|
|
70
|
+
# Slice 7 phase 6. Same pre-pass shape for cvars (per
|
|
71
|
+
# class) and globals (program-wide). Globals are also
|
|
72
|
+
# materialised into the top-level scope's `globals` map
|
|
73
|
+
# so reads at the top level (and in CLI probes that do
|
|
74
|
+
# not enter a method body) observe the precise type
|
|
75
|
+
# without consulting the accumulator on every lookup.
|
|
76
|
+
class_cvars = build_class_cvar_index(root, seeded_scope)
|
|
77
|
+
seeded_scope = seeded_scope.with_class_cvars(class_cvars)
|
|
78
|
+
program_globals = build_program_global_index(root, seeded_scope)
|
|
79
|
+
seeded_scope = seeded_scope.with_program_globals(program_globals)
|
|
80
|
+
program_globals.each { |name, type| seeded_scope = seeded_scope.with_global(name, type) }
|
|
81
|
+
|
|
82
|
+
# Slice 7 phase 9. In-source constant value tracking.
|
|
83
|
+
# Walks every ConstantWriteNode/ConstantPathWriteNode in
|
|
84
|
+
# the program and types its rvalue under a scope that
|
|
85
|
+
# carries the surrounding qualified prefix as
|
|
86
|
+
# `self_type`, so the rvalue typer sees in-class
|
|
87
|
+
# references resolve correctly. Multiple writes to the
|
|
88
|
+
# same qualified name union via `Type::Combinator.union`.
|
|
89
|
+
in_source_constants = build_in_source_constants(root, seeded_scope)
|
|
90
|
+
seeded_scope = seeded_scope.with_in_source_constants(in_source_constants)
|
|
91
|
+
|
|
92
|
+
# Slice 7 phase 12. In-source method discovery. Walks
|
|
93
|
+
# every class/module body for `Prism::DefNode` and
|
|
94
|
+
# recognised `define_method` calls and records the
|
|
95
|
+
# introduced method names. `rigor check` consults the
|
|
96
|
+
# table to suppress false positives for methods the
|
|
97
|
+
# user has defined but no RBS sig describes.
|
|
98
|
+
discovered_methods = build_discovered_methods(root)
|
|
99
|
+
seeded_scope = seeded_scope.with_discovered_methods(discovered_methods)
|
|
100
|
+
|
|
101
|
+
table = {}.compare_by_identity
|
|
102
|
+
table.default = seeded_scope
|
|
103
|
+
|
|
104
|
+
on_enter = ->(node, scope) { table[node] = scope unless table.key?(node) }
|
|
105
|
+
StatementEvaluator.new(scope: seeded_scope, on_enter: on_enter).evaluate(root)
|
|
106
|
+
|
|
107
|
+
propagate(root, table, seeded_scope)
|
|
108
|
+
table
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Slice 7 phase 2. Builds the class-level ivar accumulator
|
|
112
|
+
# by walking every `Prism::ClassNode` / `Prism::ModuleNode`
|
|
113
|
+
# body, descending into each nested `Prism::DefNode`, and
|
|
114
|
+
# typing every `Prism::InstanceVariableWriteNode` rvalue
|
|
115
|
+
# under a scope that carries the appropriate `self_type`
|
|
116
|
+
# for that def (singleton vs instance). The rvalue is
|
|
117
|
+
# typed with NO local bindings — the pre-pass lacks
|
|
118
|
+
# statement-level threading — so `@x = 1` records
|
|
119
|
+
# `Constant[1]` but `@x = some_local + 1` records
|
|
120
|
+
# `Dynamic[Top]` (since `some_local` is unbound at
|
|
121
|
+
# pre-pass time). Multiple writes to the same ivar union
|
|
122
|
+
# via `Type::Combinator.union`.
|
|
123
|
+
def build_class_ivar_index(root, default_scope)
|
|
124
|
+
accumulator = {}
|
|
125
|
+
walk_class_ivars(root, [], default_scope, accumulator)
|
|
126
|
+
accumulator.transform_values(&:freeze).freeze
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def walk_class_ivars(node, qualified_prefix, default_scope, accumulator)
|
|
130
|
+
return unless node.is_a?(Prism::Node)
|
|
131
|
+
|
|
132
|
+
case node
|
|
133
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
134
|
+
name = qualified_name_for(node.constant_path)
|
|
135
|
+
if name
|
|
136
|
+
child_prefix = qualified_prefix + [name]
|
|
137
|
+
walk_class_ivars(node.body, child_prefix, default_scope, accumulator) if node.body
|
|
138
|
+
return
|
|
139
|
+
end
|
|
140
|
+
when Prism::DefNode
|
|
141
|
+
collect_def_ivar_writes(node, qualified_prefix, default_scope, accumulator)
|
|
142
|
+
return
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
node.compact_child_nodes.each do |child|
|
|
146
|
+
walk_class_ivars(child, qualified_prefix, default_scope, accumulator)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def collect_def_ivar_writes(def_node, qualified_prefix, default_scope, accumulator)
|
|
151
|
+
return if def_node.body.nil? || qualified_prefix.empty?
|
|
152
|
+
|
|
153
|
+
class_name = qualified_prefix.join("::")
|
|
154
|
+
self_type =
|
|
155
|
+
if def_node.receiver.is_a?(Prism::SelfNode)
|
|
156
|
+
Type::Combinator.singleton_of(class_name)
|
|
157
|
+
else
|
|
158
|
+
Type::Combinator.nominal_of(class_name)
|
|
159
|
+
end
|
|
160
|
+
body_scope = default_scope.with_self_type(self_type)
|
|
161
|
+
|
|
162
|
+
gather_ivar_writes(def_node.body, body_scope, class_name, accumulator)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
IVAR_BARRIER_NODES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode].freeze
|
|
166
|
+
private_constant :IVAR_BARRIER_NODES
|
|
167
|
+
|
|
168
|
+
def gather_ivar_writes(node, scope, class_name, accumulator)
|
|
169
|
+
return unless node.is_a?(Prism::Node)
|
|
170
|
+
|
|
171
|
+
record_ivar_write(node, scope, class_name, accumulator) if node.is_a?(Prism::InstanceVariableWriteNode)
|
|
172
|
+
|
|
173
|
+
# Don't recurse into nested defs, classes, or modules; their
|
|
174
|
+
# ivars belong to their own enclosing class.
|
|
175
|
+
return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
176
|
+
|
|
177
|
+
node.compact_child_nodes.each { |c| gather_ivar_writes(c, scope, class_name, accumulator) }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def record_ivar_write(node, scope, class_name, accumulator)
|
|
181
|
+
rvalue_type = scope.type_of(node.value)
|
|
182
|
+
accumulator[class_name] ||= {}
|
|
183
|
+
existing = accumulator[class_name][node.name]
|
|
184
|
+
accumulator[class_name][node.name] =
|
|
185
|
+
existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Slice 7 phase 6 — class-cvar pre-pass. Same shape as the
|
|
189
|
+
# ivar pre-pass but collects `Prism::ClassVariableWriteNode`
|
|
190
|
+
# writes inside ANY def body (instance or singleton) of the
|
|
191
|
+
# enclosing class, because Ruby cvars are shared across both
|
|
192
|
+
# facets. The resulting table is seeded into both instance
|
|
193
|
+
# and singleton method bodies through
|
|
194
|
+
# `Scope#class_cvars_for`.
|
|
195
|
+
def build_class_cvar_index(root, default_scope)
|
|
196
|
+
accumulator = {}
|
|
197
|
+
walk_class_cvars(root, [], default_scope, accumulator)
|
|
198
|
+
accumulator.transform_values(&:freeze).freeze
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def walk_class_cvars(node, qualified_prefix, default_scope, accumulator)
|
|
202
|
+
return unless node.is_a?(Prism::Node)
|
|
203
|
+
|
|
204
|
+
case node
|
|
205
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
206
|
+
name = qualified_name_for(node.constant_path)
|
|
207
|
+
if name
|
|
208
|
+
child_prefix = qualified_prefix + [name]
|
|
209
|
+
walk_class_cvars(node.body, child_prefix, default_scope, accumulator) if node.body
|
|
210
|
+
return
|
|
211
|
+
end
|
|
212
|
+
when Prism::DefNode
|
|
213
|
+
collect_def_cvar_writes(node, qualified_prefix, default_scope, accumulator)
|
|
214
|
+
return
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
node.compact_child_nodes.each do |child|
|
|
218
|
+
walk_class_cvars(child, qualified_prefix, default_scope, accumulator)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def collect_def_cvar_writes(def_node, qualified_prefix, default_scope, accumulator)
|
|
223
|
+
return if def_node.body.nil? || qualified_prefix.empty?
|
|
224
|
+
|
|
225
|
+
class_name = qualified_prefix.join("::")
|
|
226
|
+
body_scope = default_scope.with_self_type(Type::Combinator.nominal_of(class_name))
|
|
227
|
+
gather_cvar_writes(def_node.body, body_scope, class_name, accumulator)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def gather_cvar_writes(node, scope, class_name, accumulator)
|
|
231
|
+
return unless node.is_a?(Prism::Node)
|
|
232
|
+
|
|
233
|
+
record_cvar_write(node, scope, class_name, accumulator) if node.is_a?(Prism::ClassVariableWriteNode)
|
|
234
|
+
return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
235
|
+
|
|
236
|
+
node.compact_child_nodes.each { |c| gather_cvar_writes(c, scope, class_name, accumulator) }
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def record_cvar_write(node, scope, class_name, accumulator)
|
|
240
|
+
rvalue_type = scope.type_of(node.value)
|
|
241
|
+
accumulator[class_name] ||= {}
|
|
242
|
+
existing = accumulator[class_name][node.name]
|
|
243
|
+
accumulator[class_name][node.name] =
|
|
244
|
+
existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Slice 7 phase 6 — program-global pre-pass. Globals are
|
|
248
|
+
# process-wide so the accumulator is a flat
|
|
249
|
+
# `Hash[Symbol, Type::t]` populated from every
|
|
250
|
+
# `Prism::GlobalVariableWriteNode` in the program (top-level
|
|
251
|
+
# AND inside method bodies). The same accumulator is
|
|
252
|
+
# seeded into every method body and the top-level scope.
|
|
253
|
+
def build_program_global_index(root, default_scope)
|
|
254
|
+
accumulator = {}
|
|
255
|
+
gather_global_writes(root, default_scope, accumulator)
|
|
256
|
+
accumulator.freeze
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def gather_global_writes(node, scope, accumulator)
|
|
260
|
+
return unless node.is_a?(Prism::Node)
|
|
261
|
+
|
|
262
|
+
record_global_write(node, scope, accumulator) if node.is_a?(Prism::GlobalVariableWriteNode)
|
|
263
|
+
node.compact_child_nodes.each { |c| gather_global_writes(c, scope, accumulator) }
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def record_global_write(node, scope, accumulator)
|
|
267
|
+
rvalue_type = scope.type_of(node.value)
|
|
268
|
+
existing = accumulator[node.name]
|
|
269
|
+
accumulator[node.name] =
|
|
270
|
+
existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Slice 7 phase 9 — in-source constant value pre-pass.
|
|
274
|
+
# Walks the entire program (top-level AND inside class /
|
|
275
|
+
# module / def bodies) for `Prism::ConstantWriteNode` and
|
|
276
|
+
# `Prism::ConstantPathWriteNode`, types each rvalue, and
|
|
277
|
+
# accumulates by qualified name. Constants defined inside
|
|
278
|
+
# a class body are qualified with the surrounding class
|
|
279
|
+
# path; constants written via a path (`Foo::BAR = ...`)
|
|
280
|
+
# use the rendered path as-is.
|
|
281
|
+
def build_in_source_constants(root, default_scope)
|
|
282
|
+
accumulator = {}
|
|
283
|
+
walk_constant_writes(root, [], default_scope, accumulator)
|
|
284
|
+
accumulator.freeze
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def walk_constant_writes(node, qualified_prefix, default_scope, accumulator) # rubocop:disable Metrics/CyclomaticComplexity
|
|
288
|
+
return unless node.is_a?(Prism::Node)
|
|
289
|
+
|
|
290
|
+
case node
|
|
291
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
292
|
+
name = qualified_name_for(node.constant_path)
|
|
293
|
+
if name
|
|
294
|
+
child_prefix = qualified_prefix + [name]
|
|
295
|
+
walk_constant_writes(node.body, child_prefix, default_scope, accumulator) if node.body
|
|
296
|
+
return
|
|
297
|
+
end
|
|
298
|
+
when Prism::ConstantWriteNode
|
|
299
|
+
record_constant_write(node, qualified_prefix, default_scope, accumulator, node.name.to_s)
|
|
300
|
+
return
|
|
301
|
+
when Prism::ConstantPathWriteNode
|
|
302
|
+
full = qualified_name_for(node.target)
|
|
303
|
+
record_constant_write(node, [], default_scope, accumulator, full) if full
|
|
304
|
+
return
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
node.compact_child_nodes.each do |child|
|
|
308
|
+
walk_constant_writes(child, qualified_prefix, default_scope, accumulator)
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def record_constant_write(node, qualified_prefix, default_scope, accumulator, base_name)
|
|
313
|
+
full = qualified_prefix.empty? ? base_name : "#{qualified_prefix.join('::')}::#{base_name}"
|
|
314
|
+
body_scope = default_scope
|
|
315
|
+
unless qualified_prefix.empty?
|
|
316
|
+
body_scope = body_scope.with_self_type(Type::Combinator.singleton_of(qualified_prefix.join("::")))
|
|
317
|
+
end
|
|
318
|
+
rvalue_type = body_scope.type_of(node.value)
|
|
319
|
+
existing = accumulator[full]
|
|
320
|
+
accumulator[full] = existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Slice 7 phase 12 — in-source method discovery pre-pass.
|
|
324
|
+
# Walks every class/module body and records the methods
|
|
325
|
+
# introduced via `Prism::DefNode` (instance + singleton)
|
|
326
|
+
# and via recognised `define_method(:name) { ... }` calls.
|
|
327
|
+
# The returned table maps qualified class name to a
|
|
328
|
+
# `Hash[Symbol, :instance | :singleton]`.
|
|
329
|
+
def build_discovered_methods(root)
|
|
330
|
+
accumulator = {}
|
|
331
|
+
walk_methods(root, [], false, accumulator)
|
|
332
|
+
accumulator.transform_values(&:freeze).freeze
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def walk_methods(node, qualified_prefix, in_singleton_class, accumulator) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
336
|
+
return unless node.is_a?(Prism::Node)
|
|
337
|
+
|
|
338
|
+
case node
|
|
339
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
340
|
+
name = qualified_name_for(node.constant_path)
|
|
341
|
+
if name
|
|
342
|
+
child_prefix = qualified_prefix + [name]
|
|
343
|
+
walk_methods(node.body, child_prefix, false, accumulator) if node.body
|
|
344
|
+
return
|
|
345
|
+
end
|
|
346
|
+
when Prism::SingletonClassNode
|
|
347
|
+
if node.expression.is_a?(Prism::SelfNode) && node.body
|
|
348
|
+
walk_methods(node.body, qualified_prefix, true, accumulator)
|
|
349
|
+
return
|
|
350
|
+
end
|
|
351
|
+
when Prism::DefNode
|
|
352
|
+
record_def_method(node, qualified_prefix, in_singleton_class, accumulator)
|
|
353
|
+
return
|
|
354
|
+
when Prism::CallNode
|
|
355
|
+
record_define_method(node, qualified_prefix, in_singleton_class, accumulator) if node.name == :define_method
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
node.compact_child_nodes.each do |child|
|
|
359
|
+
walk_methods(child, qualified_prefix, in_singleton_class, accumulator)
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def record_def_method(def_node, qualified_prefix, in_singleton_class, accumulator)
|
|
364
|
+
return if qualified_prefix.empty?
|
|
365
|
+
|
|
366
|
+
class_name = qualified_prefix.join("::")
|
|
367
|
+
kind = def_node.receiver.is_a?(Prism::SelfNode) || in_singleton_class ? :singleton : :instance
|
|
368
|
+
accumulator[class_name] ||= {}
|
|
369
|
+
accumulator[class_name][def_node.name] = kind
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def record_define_method(call_node, qualified_prefix, in_singleton_class, accumulator)
|
|
373
|
+
return if qualified_prefix.empty?
|
|
374
|
+
return if call_node.arguments.nil? || call_node.arguments.arguments.empty?
|
|
375
|
+
|
|
376
|
+
first_arg = call_node.arguments.arguments.first
|
|
377
|
+
method_name = literal_method_name(first_arg)
|
|
378
|
+
return if method_name.nil?
|
|
379
|
+
|
|
380
|
+
class_name = qualified_prefix.join("::")
|
|
381
|
+
accumulator[class_name] ||= {}
|
|
382
|
+
accumulator[class_name][method_name] = in_singleton_class ? :singleton : :instance
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def literal_method_name(node)
|
|
386
|
+
return nil unless node.is_a?(Prism::SymbolNode) || node.is_a?(Prism::StringNode)
|
|
387
|
+
|
|
388
|
+
node.unescaped&.to_sym
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Walks the program once for `Prism::ModuleNode` and
|
|
392
|
+
# `Prism::ClassNode`, recording the `Singleton[<qualified>]`
|
|
393
|
+
# type for the outermost `constant_path` node of each
|
|
394
|
+
# declaration. Inner segments of a `class Foo::Bar::Baz`
|
|
395
|
+
# path remain real references (resolved through the
|
|
396
|
+
# ordinary lexical walk), so we annotate ONLY the topmost
|
|
397
|
+
# path node. Nested declarations contribute their fully
|
|
398
|
+
# qualified path: `class A::B; class C; ...` produces
|
|
399
|
+
# `A::B` for the outer and `A::B::C` for the inner.
|
|
400
|
+
def build_declaration_artifacts(root)
|
|
401
|
+
identity_table = {}.compare_by_identity
|
|
402
|
+
discovered = {}
|
|
403
|
+
record_declarations(root, [], identity_table, discovered)
|
|
404
|
+
[identity_table.freeze, discovered.freeze]
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def record_declarations(node, qualified_prefix, identity_table, discovered)
|
|
408
|
+
return unless node.is_a?(Prism::Node)
|
|
409
|
+
|
|
410
|
+
case node
|
|
411
|
+
when Prism::ModuleNode, Prism::ClassNode
|
|
412
|
+
name = qualified_name_for(node.constant_path)
|
|
413
|
+
if name
|
|
414
|
+
full = (qualified_prefix + [name]).join("::")
|
|
415
|
+
singleton = Type::Combinator.singleton_of(full)
|
|
416
|
+
identity_table[node.constant_path] = singleton
|
|
417
|
+
discovered[full] = singleton
|
|
418
|
+
child_prefix = qualified_prefix + [name]
|
|
419
|
+
record_declarations(node.body, child_prefix, identity_table, discovered) if node.body
|
|
420
|
+
return
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
node.compact_child_nodes.each do |child|
|
|
425
|
+
record_declarations(child, qualified_prefix, identity_table, discovered)
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def qualified_name_for(constant_path_node)
|
|
430
|
+
case constant_path_node
|
|
431
|
+
when Prism::ConstantReadNode
|
|
432
|
+
constant_path_node.name.to_s
|
|
433
|
+
when Prism::ConstantPathNode
|
|
434
|
+
render_constant_path(constant_path_node)
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def render_constant_path(node)
|
|
439
|
+
prefix =
|
|
440
|
+
case node.parent
|
|
441
|
+
when Prism::ConstantReadNode then "#{node.parent.name}::"
|
|
442
|
+
when Prism::ConstantPathNode then "#{render_constant_path(node.parent)}::"
|
|
443
|
+
else ""
|
|
444
|
+
end
|
|
445
|
+
"#{prefix}#{node.name}"
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Walks `node`'s subtree DFS and fills in scope entries for every
|
|
449
|
+
# Prism node the StatementEvaluator did not visit (i.e. expression-
|
|
450
|
+
# interior nodes like the receiver/args of a CallNode). Those
|
|
451
|
+
# nodes inherit their nearest recorded ancestor's scope.
|
|
452
|
+
def propagate(node, table, parent_scope)
|
|
453
|
+
return unless node.is_a?(Prism::Node)
|
|
454
|
+
|
|
455
|
+
current_scope =
|
|
456
|
+
if table.key?(node)
|
|
457
|
+
table[node]
|
|
458
|
+
else
|
|
459
|
+
table[node] = parent_scope
|
|
460
|
+
parent_scope
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
node.compact_child_nodes.each { |child| propagate(child, table, current_scope) }
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
# rubocop:enable Metrics/ModuleLength
|
|
467
|
+
end
|
|
468
|
+
end
|