rigortype 0.1.5 → 0.1.6
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 +36 -50
- data/lib/rigor/analysis/buffer_binding.rb +36 -0
- data/lib/rigor/analysis/check_rules.rb +11 -1
- data/lib/rigor/analysis/dependency_source_inference/index.rb +14 -1
- data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +105 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +32 -12
- data/lib/rigor/analysis/project_scan.rb +39 -0
- data/lib/rigor/analysis/runner.rb +309 -22
- data/lib/rigor/analysis/worker_session.rb +14 -2
- data/lib/rigor/builtins/hkt_builtins.rb +342 -0
- data/lib/rigor/builtins/static_return_refinements.rb +120 -0
- data/lib/rigor/cache/store.rb +33 -3
- data/lib/rigor/cli/lsp_command.rb +129 -0
- data/lib/rigor/cli/type_of_command.rb +44 -5
- data/lib/rigor/cli.rb +74 -12
- data/lib/rigor/configuration.rb +38 -2
- data/lib/rigor/environment/hkt_registry_holder.rb +33 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +45 -2
- data/lib/rigor/environment/reporters.rb +40 -0
- data/lib/rigor/environment.rb +106 -9
- data/lib/rigor/inference/acceptance.rb +48 -3
- data/lib/rigor/inference/expression_typer.rb +47 -0
- data/lib/rigor/inference/hkt_body.rb +171 -0
- data/lib/rigor/inference/hkt_body_parser.rb +363 -0
- data/lib/rigor/inference/hkt_reducer.rb +256 -0
- data/lib/rigor/inference/hkt_registry.rb +223 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +125 -30
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +87 -0
- data/lib/rigor/inference/method_dispatcher.rb +154 -3
- data/lib/rigor/inference/project_patched_methods.rb +70 -0
- data/lib/rigor/inference/project_patched_scanner.rb +210 -0
- data/lib/rigor/inference/scope_indexer.rb +156 -12
- data/lib/rigor/inference/statement_evaluator.rb +106 -6
- data/lib/rigor/inference/synthetic_method_scanner.rb +94 -16
- data/lib/rigor/language_server/buffer_table.rb +63 -0
- data/lib/rigor/language_server/completion_provider.rb +438 -0
- data/lib/rigor/language_server/debouncer.rb +86 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +167 -0
- data/lib/rigor/language_server/document_symbol_provider.rb +142 -0
- data/lib/rigor/language_server/folding_range_provider.rb +75 -0
- data/lib/rigor/language_server/hover_provider.rb +74 -0
- data/lib/rigor/language_server/hover_renderer.rb +312 -0
- data/lib/rigor/language_server/loop.rb +71 -0
- data/lib/rigor/language_server/project_context.rb +145 -0
- data/lib/rigor/language_server/selection_range_provider.rb +93 -0
- data/lib/rigor/language_server/server.rb +384 -0
- data/lib/rigor/language_server/signature_help_provider.rb +249 -0
- data/lib/rigor/language_server/synchronized_writer.rb +28 -0
- data/lib/rigor/language_server/uri.rb +40 -0
- data/lib/rigor/language_server.rb +29 -0
- data/lib/rigor/plugin/base.rb +63 -0
- data/lib/rigor/plugin/macro/heredoc_template.rb +125 -11
- data/lib/rigor/plugin/manifest.rb +54 -7
- data/lib/rigor/plugin/registry.rb +19 -0
- data/lib/rigor/rbs_extended/hkt_directives.rb +326 -0
- data/lib/rigor/rbs_extended.rb +82 -2
- data/lib/rigor/sig_gen/generator.rb +12 -3
- data/lib/rigor/type/app.rb +107 -0
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +8 -4
- data/sig/rigor/inference.rbs +2 -0
- data/sig/rigor.rbs +3 -1
- metadata +54 -1
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../inference/hkt_registry"
|
|
4
|
+
require_relative "../inference/hkt_body"
|
|
5
|
+
require_relative "../inference/hkt_body_parser"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Builtins
|
|
9
|
+
# ADR-20 slices 2c + 3 — Rigor-bundled Lightweight HKT
|
|
10
|
+
# registrations that ship with every analyzer instance.
|
|
11
|
+
# The set is intentionally small at v0.1.x: only the URIs
|
|
12
|
+
# whose payoff justifies hardcoded definitions. Plugin
|
|
13
|
+
# authors register more URIs through their manifests; user
|
|
14
|
+
# `.rbs` overlays register through the
|
|
15
|
+
# `%a{rigor:v1:hkt_register}` /
|
|
16
|
+
# `%a{rigor:v1:hkt_define}` annotations Slice 1 ships.
|
|
17
|
+
#
|
|
18
|
+
# Today's contents:
|
|
19
|
+
#
|
|
20
|
+
# - `json::value[K]` — the recursive sum stdlib's
|
|
21
|
+
# `JSON.parse` returns. Body:
|
|
22
|
+
#
|
|
23
|
+
# nil | true | false | Integer | Float | String
|
|
24
|
+
# | Array[App[json::value, K]]
|
|
25
|
+
# | Hash[K, App[json::value, K]]
|
|
26
|
+
#
|
|
27
|
+
# The reducer handles the self-recursive `App` nodes via
|
|
28
|
+
# lazy "tying-the-knot" (see {HktReducer}). `K = String`
|
|
29
|
+
# matches stdlib's default key handling; `K = Symbol`
|
|
30
|
+
# matches `symbolize_names: true`.
|
|
31
|
+
module HktBuiltins
|
|
32
|
+
module_function
|
|
33
|
+
|
|
34
|
+
# Built via the body-string parser (slice 2b/2c) so the
|
|
35
|
+
# bundled overlay exercises the same authoring surface
|
|
36
|
+
# third-party plugins use. The body matches what user
|
|
37
|
+
# `.rbs` overlays would write through a
|
|
38
|
+
# `%a{rigor:v1:hkt_define: ...body=...}` annotation.
|
|
39
|
+
JSON_VALUE_BODY = "nil | true | false | Integer | Float | String | " \
|
|
40
|
+
"Array[App[json::value, K]] | Hash[K, App[json::value, K]]"
|
|
41
|
+
private_constant :JSON_VALUE_BODY
|
|
42
|
+
|
|
43
|
+
def json_value_body_tree
|
|
44
|
+
Rigor::Inference::HktBodyParser.parse(JSON_VALUE_BODY, params: [:K])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# `csv::parsed[K]` — `Array[Array[K | nil]]` (CSV.parse's
|
|
48
|
+
# no-headers shape: an Array of rows; each row is an
|
|
49
|
+
# Array of optionally-nil cell values). When
|
|
50
|
+
# `headers: true` the runtime returns a `CSV::Table` /
|
|
51
|
+
# `CSV::Row` shape instead — that case is NOT covered
|
|
52
|
+
# by the bundled override (CSV::Row is its own class
|
|
53
|
+
# with Hash + Array access; a future slice may add a
|
|
54
|
+
# separate URI or a discriminator hook for it).
|
|
55
|
+
CSV_PARSED_BODY = "Array[Array[K | nil]]"
|
|
56
|
+
private_constant :CSV_PARSED_BODY
|
|
57
|
+
|
|
58
|
+
def csv_parsed_body_tree
|
|
59
|
+
Rigor::Inference::HktBodyParser.parse(CSV_PARSED_BODY, params: [:K])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def json_value_registration
|
|
63
|
+
Rigor::Inference::HktRegistry::Registration.new(
|
|
64
|
+
uri: :"json::value",
|
|
65
|
+
arity: 1,
|
|
66
|
+
variance: [:out],
|
|
67
|
+
bound: Rigor::Type::Combinator.untyped
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def json_value_definition
|
|
72
|
+
Rigor::Inference::HktRegistry.definition_with_body_tree(
|
|
73
|
+
uri: :"json::value",
|
|
74
|
+
params: [:K],
|
|
75
|
+
body_tree: json_value_body_tree,
|
|
76
|
+
source_path: __FILE__,
|
|
77
|
+
source_line: __LINE__ - 5
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def csv_parsed_registration
|
|
82
|
+
Rigor::Inference::HktRegistry::Registration.new(
|
|
83
|
+
uri: :"csv::parsed",
|
|
84
|
+
arity: 1,
|
|
85
|
+
variance: [:out],
|
|
86
|
+
bound: Rigor::Type::Combinator.untyped
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def csv_parsed_definition
|
|
91
|
+
Rigor::Inference::HktRegistry.definition_with_body_tree(
|
|
92
|
+
uri: :"csv::parsed",
|
|
93
|
+
params: [:K],
|
|
94
|
+
body_tree: csv_parsed_body_tree,
|
|
95
|
+
source_path: __FILE__,
|
|
96
|
+
source_line: __LINE__ - 5
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @return [Rigor::Inference::HktRegistry] frozen registry
|
|
101
|
+
# pre-seeded with all bundled HKT registrations +
|
|
102
|
+
# bodies. Allocated fresh each call rather than
|
|
103
|
+
# memoised — memoisation through a module-level
|
|
104
|
+
# `@registry` ivar surfaces a `Ractor::IsolationError`
|
|
105
|
+
# in pool workers (the ivar's contents include
|
|
106
|
+
# `HktBody::AppRef` Symbol-keyed structures that the
|
|
107
|
+
# current Ractor shareability audit hasn't yet been
|
|
108
|
+
# walked through). The registry is small enough that
|
|
109
|
+
# per-Environment construction is acceptable; an
|
|
110
|
+
# eager-frozen constant is a future optimisation
|
|
111
|
+
# once ADR-15 phase 4b.x covers the dependency graph.
|
|
112
|
+
def registry
|
|
113
|
+
Rigor::Inference::HktRegistry.new(
|
|
114
|
+
registrations: [json_value_registration, csv_parsed_registration],
|
|
115
|
+
definitions: [json_value_definition, csv_parsed_definition]
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# ADR-20 slice 3 — hardcoded `(class_name, method_name,
|
|
120
|
+
# kind) => HKT application` table consulted by the
|
|
121
|
+
# dispatcher's new HKT-builtin tier. Sits ABOVE
|
|
122
|
+
# `RbsDispatch.try_dispatch` so a known stdlib method
|
|
123
|
+
# (`JSON.parse`, `JSON.parse!`) gets the reduced HKT
|
|
124
|
+
# type instead of the upstream rbs gem's `untyped`
|
|
125
|
+
# return. The annotation-based `%a{rigor:v1:return:
|
|
126
|
+
# App[...]}` path (parsed by
|
|
127
|
+
# `RbsExtended.parse_return_type_override`) is the
|
|
128
|
+
# general extension surface for user-authored sigs;
|
|
129
|
+
# this table is the Rigor-bundled shortcut for the
|
|
130
|
+
# handful of stdlib methods whose RBS declarations
|
|
131
|
+
# cannot be cleanly overridden via RBS overlay merging.
|
|
132
|
+
#
|
|
133
|
+
# Each entry maps to a hash with `:uri` and `:args`
|
|
134
|
+
# (an array of Ruby class names). The dispatcher
|
|
135
|
+
# builds `Type::App.new(uri, args.map { Nominal })`,
|
|
136
|
+
# then reduces via the env's `hkt_registry` so the
|
|
137
|
+
# caller observes the unfolded form
|
|
138
|
+
# (`Union[nil, true, false, ..., Array[App[json::value,
|
|
139
|
+
# String]], Hash[String, App[json::value, String]]]`)
|
|
140
|
+
# rather than the opaque carrier.
|
|
141
|
+
JSON_VALUE_SPEC = {
|
|
142
|
+
uri: :"json::value",
|
|
143
|
+
args: ["String"],
|
|
144
|
+
discriminator: :json_symbolize_names,
|
|
145
|
+
post_reduce: nil
|
|
146
|
+
}.freeze
|
|
147
|
+
private_constant :JSON_VALUE_SPEC
|
|
148
|
+
|
|
149
|
+
# YAML / Psych.safe_load reuse the json::value reducer
|
|
150
|
+
# for the JSON-equivalent leaf set BUT additionally
|
|
151
|
+
# honour `permitted_classes: [<Class>, ...]` literal
|
|
152
|
+
# Array arguments, unioning each permitted class as an
|
|
153
|
+
# extra arm of the result. Slice 2c-bis behaviour.
|
|
154
|
+
YAML_SAFE_VALUE_SPEC = {
|
|
155
|
+
uri: :"json::value",
|
|
156
|
+
args: ["String"],
|
|
157
|
+
discriminator: :json_symbolize_names,
|
|
158
|
+
post_reduce: :yaml_permitted_classes
|
|
159
|
+
}.freeze
|
|
160
|
+
private_constant :YAML_SAFE_VALUE_SPEC
|
|
161
|
+
|
|
162
|
+
CSV_PARSED_SPEC = {
|
|
163
|
+
uri: :"csv::parsed",
|
|
164
|
+
args: ["String"],
|
|
165
|
+
discriminator: nil,
|
|
166
|
+
post_reduce: nil
|
|
167
|
+
}.freeze
|
|
168
|
+
private_constant :CSV_PARSED_SPEC
|
|
169
|
+
|
|
170
|
+
METHOD_RETURN_OVERRIDES = {
|
|
171
|
+
# JSON — stdlib's `json` library. Upstream rbs declares
|
|
172
|
+
# `(string, ?options) -> untyped`; the HKT-builtin tier
|
|
173
|
+
# tightens to the recursive `json::value[K]` union.
|
|
174
|
+
# `load_file` / `load_file!` share the `?options` slot
|
|
175
|
+
# so the `symbolize_names: true` discriminator applies
|
|
176
|
+
# to them too (just like `parse` / `load`).
|
|
177
|
+
["JSON", :parse, :singleton] => JSON_VALUE_SPEC,
|
|
178
|
+
["JSON", :parse!, :singleton] => JSON_VALUE_SPEC,
|
|
179
|
+
["JSON", :load, :singleton] => JSON_VALUE_SPEC,
|
|
180
|
+
["JSON", :load_file, :singleton] => JSON_VALUE_SPEC,
|
|
181
|
+
["JSON", :load_file!, :singleton] => JSON_VALUE_SPEC,
|
|
182
|
+
# YAML.safe_load / Psych.safe_load — default
|
|
183
|
+
# `permitted_classes: []` admits exactly the JSON
|
|
184
|
+
# vocabulary (nil / true / false / Integer / Float /
|
|
185
|
+
# String / Array / Hash), so the json::value tree
|
|
186
|
+
# also describes them. When the call passes a literal
|
|
187
|
+
# `permitted_classes: [Date, Symbol, ...]` Array, the
|
|
188
|
+
# `:yaml_permitted_classes` post_reduce unions each
|
|
189
|
+
# named class into the result. Non-literal options
|
|
190
|
+
# (a variable, a constant reference, a `+ classes`
|
|
191
|
+
# concat) silently no-op and the caller observes the
|
|
192
|
+
# base json::value envelope only. YAML.load /
|
|
193
|
+
# YAML.unsafe_load deliberately stay out of the
|
|
194
|
+
# override table — they can return ANY Ruby object
|
|
195
|
+
# and have no useful HKT envelope.
|
|
196
|
+
["YAML", :safe_load, :singleton] => YAML_SAFE_VALUE_SPEC,
|
|
197
|
+
["YAML", :safe_load_file, :singleton] => YAML_SAFE_VALUE_SPEC,
|
|
198
|
+
["Psych", :safe_load, :singleton] => YAML_SAFE_VALUE_SPEC,
|
|
199
|
+
["Psych", :safe_load_file, :singleton] => YAML_SAFE_VALUE_SPEC,
|
|
200
|
+
# CSV.parse / CSV.read — no-headers shape only.
|
|
201
|
+
# Upstream rbs declares broader return shapes but
|
|
202
|
+
# the common case is `Array[Array[String?]]` which
|
|
203
|
+
# the `csv::parsed[String]` URI matches. The
|
|
204
|
+
# `headers: true` shape (`CSV::Table` of `CSV::Row`)
|
|
205
|
+
# is NOT covered — calls passing the option fall
|
|
206
|
+
# through to the upstream RBS type. CSV.foreach also
|
|
207
|
+
# falls through (it yields rows rather than
|
|
208
|
+
# returning a typed structure).
|
|
209
|
+
["CSV", :parse, :singleton] => CSV_PARSED_SPEC,
|
|
210
|
+
["CSV", :read, :singleton] => CSV_PARSED_SPEC
|
|
211
|
+
}.freeze
|
|
212
|
+
|
|
213
|
+
# @return [Rigor::Type, nil] the reduced HKT type for
|
|
214
|
+
# the given (class_name, method_name, kind) triple,
|
|
215
|
+
# or `nil` when no built-in override is registered.
|
|
216
|
+
# When `arg_types` is supplied AND the entry carries a
|
|
217
|
+
# `:discriminator` symbol, the discriminator may swap
|
|
218
|
+
# the spec's default args for an alternate (e.g.
|
|
219
|
+
# `JSON.parse(str, symbolize_names: true)` discriminates
|
|
220
|
+
# `K = Symbol` instead of the default `K = String`).
|
|
221
|
+
def method_return_override(class_name:, method_name:, kind:, arg_types: nil, hkt_registry: nil)
|
|
222
|
+
spec = METHOD_RETURN_OVERRIDES[[class_name, method_name.to_sym, kind]]
|
|
223
|
+
return nil unless spec
|
|
224
|
+
|
|
225
|
+
args = discriminated_args(spec, arg_types)
|
|
226
|
+
registration = hkt_registry&.registration(spec[:uri])
|
|
227
|
+
bound = registration&.bound || Rigor::Type::Combinator.untyped
|
|
228
|
+
app = Rigor::Type::App.new(spec[:uri], args, bound: bound)
|
|
229
|
+
|
|
230
|
+
reduced =
|
|
231
|
+
if hkt_registry.nil? || !hkt_registry.defined?(spec[:uri])
|
|
232
|
+
app
|
|
233
|
+
else
|
|
234
|
+
hkt_registry.reduce(app) || app
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
apply_post_reduce(spec[:post_reduce], reduced, arg_types)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Per-spec discriminator dispatch. Slice 3 ships one
|
|
241
|
+
# built-in discriminator (`json_symbolize_names`) that
|
|
242
|
+
# observes the optional 2nd argument's `HashShape` for a
|
|
243
|
+
# literal `symbolize_names: true` entry. Plugin / Rigor-
|
|
244
|
+
# bundled callers wanting their own discriminators add a
|
|
245
|
+
# branch here.
|
|
246
|
+
def discriminated_args(spec, arg_types)
|
|
247
|
+
default_args = spec[:args].map { |n| Rigor::Type::Nominal.new(n) }
|
|
248
|
+
return default_args if arg_types.nil?
|
|
249
|
+
return default_args unless spec[:discriminator] == :json_symbolize_names
|
|
250
|
+
return default_args unless json_symbolize_names?(arg_types)
|
|
251
|
+
|
|
252
|
+
[Rigor::Type::Nominal.new("Symbol")]
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Returns true iff the call-site's 2nd argument is a
|
|
256
|
+
# `Type::HashShape` carrying a literal
|
|
257
|
+
# `symbolize_names: true` entry. Anything else
|
|
258
|
+
# (no second arg, non-HashShape, missing key, non-literal
|
|
259
|
+
# `true`) returns false so the default `K = String`
|
|
260
|
+
# branch wins.
|
|
261
|
+
def json_symbolize_names?(arg_types)
|
|
262
|
+
return false unless arg_types.is_a?(Array) && arg_types.size >= 2
|
|
263
|
+
|
|
264
|
+
opts = arg_types[1]
|
|
265
|
+
return false unless opts.is_a?(Rigor::Type::HashShape)
|
|
266
|
+
|
|
267
|
+
value = opts.pairs[:symbolize_names] || opts.pairs["symbolize_names"]
|
|
268
|
+
value.is_a?(Rigor::Type::Constant) && value.value == true
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Slice 2c-bis — post-reduce hook. Receives the already-
|
|
272
|
+
# reduced `Type` and the call-site's `arg_types`; returns
|
|
273
|
+
# a (possibly augmented) `Type`. `kind = nil` is the
|
|
274
|
+
# identity (passes the reduced type through unchanged).
|
|
275
|
+
# Only `:yaml_permitted_classes` is implemented today;
|
|
276
|
+
# plugin / Rigor-bundled callers wanting their own
|
|
277
|
+
# post-reduce hooks add a branch here.
|
|
278
|
+
def apply_post_reduce(kind, reduced, arg_types)
|
|
279
|
+
case kind
|
|
280
|
+
when :yaml_permitted_classes
|
|
281
|
+
augment_with_yaml_permitted_classes(reduced, arg_types)
|
|
282
|
+
else
|
|
283
|
+
# `nil` (no post-reduce declared) and any future
|
|
284
|
+
# unrecognised kind both pass the reduced type
|
|
285
|
+
# through unchanged. Unknown kinds are silently
|
|
286
|
+
# tolerated rather than raised because adding a
|
|
287
|
+
# new kind on a Rigor upgrade should not crash a
|
|
288
|
+
# stale METHOD_RETURN_OVERRIDES entry on the
|
|
289
|
+
# caller side.
|
|
290
|
+
reduced
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Inspects arg_types for a `permitted_classes: [<Class>,
|
|
295
|
+
# ...]` literal Array in the options Hash and unions
|
|
296
|
+
# each named class into the reduced result. Non-literal
|
|
297
|
+
# `permitted_classes:` values (a variable, a constant
|
|
298
|
+
# reference, a concat) silently no-op and the caller
|
|
299
|
+
# observes the base json::value envelope only. Defensive
|
|
300
|
+
# against the various ways Ruby literal arrays surface
|
|
301
|
+
# as Rigor types: `Tuple[Singleton<Date>]` for a single
|
|
302
|
+
# element, `Tuple[Singleton<Date>, Singleton<Symbol>]`
|
|
303
|
+
# for multiple, `Nominal[Array, [Singleton<...>]]` if
|
|
304
|
+
# the analyzer widened (rare for literal arrays).
|
|
305
|
+
def augment_with_yaml_permitted_classes(reduced, arg_types)
|
|
306
|
+
return reduced unless arg_types.is_a?(Array) && arg_types.size >= 2
|
|
307
|
+
|
|
308
|
+
opts = arg_types[1]
|
|
309
|
+
return reduced unless opts.is_a?(Rigor::Type::HashShape)
|
|
310
|
+
|
|
311
|
+
value = opts.pairs[:permitted_classes] || opts.pairs["permitted_classes"]
|
|
312
|
+
return reduced if value.nil?
|
|
313
|
+
|
|
314
|
+
extras = permitted_class_nominals(value)
|
|
315
|
+
return reduced if extras.empty?
|
|
316
|
+
|
|
317
|
+
Rigor::Type::Combinator.union(reduced, *extras)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Extract Singleton-class elements from a Tuple or
|
|
321
|
+
# Array-shape carrier, mapping each to its Nominal
|
|
322
|
+
# counterpart. Returns an empty array when no static
|
|
323
|
+
# Singletons are reachable (e.g. value is `Dynamic[T]`,
|
|
324
|
+
# element types are non-Singleton, etc.).
|
|
325
|
+
def permitted_class_nominals(value)
|
|
326
|
+
candidates =
|
|
327
|
+
if value.is_a?(Rigor::Type::Tuple)
|
|
328
|
+
value.elements
|
|
329
|
+
elsif value.is_a?(Rigor::Type::Nominal) && value.class_name == "Array" && value.type_args.size == 1
|
|
330
|
+
element = value.type_args.first
|
|
331
|
+
element.is_a?(Rigor::Type::Union) ? element.members : [element]
|
|
332
|
+
else
|
|
333
|
+
[]
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
candidates.filter_map do |c|
|
|
337
|
+
c.is_a?(Rigor::Type::Singleton) ? Rigor::Type::Nominal.new(c.class_name) : nil
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Builtins
|
|
7
|
+
# Static return-type refinements for stdlib (or other built-in)
|
|
8
|
+
# methods whose upstream RBS signature is broader than the
|
|
9
|
+
# method's documented behaviour and where adding a
|
|
10
|
+
# `%a{rigor:v1:return: ...}` annotation upstream is impractical
|
|
11
|
+
# (the RBS lives in the vendored `ruby/rbs` submodule).
|
|
12
|
+
#
|
|
13
|
+
# This tier sits in `MethodDispatcher.dispatch` between the
|
|
14
|
+
# HKT-builtin tier (which handles parametric / shape-bearing
|
|
15
|
+
# returns like `JSON.parse`'s `json::value`) and the RBS
|
|
16
|
+
# dispatch tier (the canonical lookup). It is consulted only
|
|
17
|
+
# when the method name and arg shape match an entry in the
|
|
18
|
+
# table below, so the standard RBS path stays in charge of
|
|
19
|
+
# every other call.
|
|
20
|
+
#
|
|
21
|
+
# Match policy:
|
|
22
|
+
#
|
|
23
|
+
# - Entries are keyed by `(owner_class_name, method_name,
|
|
24
|
+
# kind)`. `owner_class_name` is the class that *defines*
|
|
25
|
+
# the method (e.g., `"Kernel"`), not necessarily the
|
|
26
|
+
# receiver's static class — Kernel methods are mixed into
|
|
27
|
+
# every non-BasicObject class, so a `__dir__` call on any
|
|
28
|
+
# receiver routes here.
|
|
29
|
+
# - `kind: :both` matches both the singleton-receiver
|
|
30
|
+
# shape (`Kernel.__dir__`, `Singleton[Kernel]` receiver)
|
|
31
|
+
# AND the instance-receiver shape (an implicit-self call
|
|
32
|
+
# like `__dir__` inside any class body, or `obj.__dir__`
|
|
33
|
+
# on an instance).
|
|
34
|
+
# - `kind: :singleton` / `kind: :instance` restrict the
|
|
35
|
+
# match to one of the two shapes.
|
|
36
|
+
# - The handler is called with `(arg_types)` so future
|
|
37
|
+
# entries can refine based on argument types (e.g. a
|
|
38
|
+
# `File.expand_path(string)` entry that returns
|
|
39
|
+
# `non-empty-string` regardless of the upstream return).
|
|
40
|
+
#
|
|
41
|
+
# The override fires ABOVE RBS dispatch — if RBS would have
|
|
42
|
+
# returned a wider type (`String?` for `Kernel#__dir__`), the
|
|
43
|
+
# override returns the refined union (`non-empty-string | nil`)
|
|
44
|
+
# instead. RBS erasure of the refined return goes back to the
|
|
45
|
+
# original upstream shape, so downstream RBS-shaped observers
|
|
46
|
+
# see no difference.
|
|
47
|
+
module StaticReturnRefinements
|
|
48
|
+
# Pre-built carrier reused across calls so structural
|
|
49
|
+
# equality matches across analyzer invocations.
|
|
50
|
+
NON_EMPTY_STRING_OR_NIL = Type::Combinator.union(
|
|
51
|
+
Type::Combinator.non_empty_string,
|
|
52
|
+
Type::Combinator.constant_of(nil)
|
|
53
|
+
).freeze
|
|
54
|
+
private_constant :NON_EMPTY_STRING_OR_NIL
|
|
55
|
+
|
|
56
|
+
# `Kernel#__dir__` returns the canonical directory of the
|
|
57
|
+
# source file the call appears in, or `nil` when the file
|
|
58
|
+
# is invalid / not available (typically `-e` and similar
|
|
59
|
+
# one-liner contexts). When non-nil the value is always a
|
|
60
|
+
# filesystem-canonical path — never the empty string — so
|
|
61
|
+
# `non-empty-string` is exact.
|
|
62
|
+
KERNEL_DIR = ->(_arg_types) { NON_EMPTY_STRING_OR_NIL }
|
|
63
|
+
private_constant :KERNEL_DIR
|
|
64
|
+
|
|
65
|
+
# Frozen ((owner_class_name, method_name, kind) => handler)
|
|
66
|
+
# table. The kind tag is `:both`, `:singleton`, or
|
|
67
|
+
# `:instance`. New entries SHOULD prefer `:both` unless the
|
|
68
|
+
# singleton- and instance-side shapes genuinely differ.
|
|
69
|
+
OVERRIDES = {
|
|
70
|
+
["Kernel", :__dir__, :both] => KERNEL_DIR
|
|
71
|
+
}.freeze
|
|
72
|
+
private_constant :OVERRIDES
|
|
73
|
+
|
|
74
|
+
# Looks up a refined return type for the given call.
|
|
75
|
+
#
|
|
76
|
+
# @param owner_class_name [String, nil] the class on which
|
|
77
|
+
# the method is defined (e.g., `"Kernel"`). Pass `nil`
|
|
78
|
+
# when the caller hasn't resolved a defining owner yet —
|
|
79
|
+
# the lookup will then fall back to matching by
|
|
80
|
+
# `(method_name, kind)` against entries whose owner is
|
|
81
|
+
# currently in the table.
|
|
82
|
+
# @param method_name [Symbol]
|
|
83
|
+
# @param kind [Symbol] one of `:singleton`, `:instance`. The
|
|
84
|
+
# caller passes the shape of the actual call site; the
|
|
85
|
+
# table stores `:both` for entries that match either.
|
|
86
|
+
# @param arg_types [Array<Rigor::Type>] positional argument
|
|
87
|
+
# types. Forwarded to the handler so future entries can
|
|
88
|
+
# discriminate on argument shape.
|
|
89
|
+
# @return [Rigor::Type, nil] the refined return type, or
|
|
90
|
+
# `nil` when no override matches.
|
|
91
|
+
def self.lookup(owner_class_name:, method_name:, kind:, arg_types: [])
|
|
92
|
+
return nil if owner_class_name.nil?
|
|
93
|
+
|
|
94
|
+
method_sym = method_name.to_sym
|
|
95
|
+
handler = OVERRIDES[[owner_class_name, method_sym, :both]] ||
|
|
96
|
+
OVERRIDES[[owner_class_name, method_sym, kind]]
|
|
97
|
+
handler&.call(arg_types)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Indexed view by `(method_name, kind)` — used by the
|
|
101
|
+
# dispatcher when the receiver's owner is not yet resolved
|
|
102
|
+
# but the method name alone uniquely identifies a stdlib
|
|
103
|
+
# override (today: `__dir__` → Kernel). The table is small
|
|
104
|
+
# and the index rebuild cost trivial, but precomputing keeps
|
|
105
|
+
# `dispatch`'s hot path free of an O(n) scan.
|
|
106
|
+
OWNERS_BY_METHOD = OVERRIDES.each_with_object({}) do |((owner, mname, _kind), _h), acc|
|
|
107
|
+
acc[mname] ||= []
|
|
108
|
+
acc[mname] << owner unless acc[mname].include?(owner)
|
|
109
|
+
end.freeze
|
|
110
|
+
private_constant :OWNERS_BY_METHOD
|
|
111
|
+
|
|
112
|
+
# @return [Array<String>] the candidate owner class names
|
|
113
|
+
# for a bare method-name lookup. Empty when no override
|
|
114
|
+
# names this method.
|
|
115
|
+
def self.owners_for(method_name)
|
|
116
|
+
OWNERS_BY_METHOD[method_name.to_sym] || []
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
data/lib/rigor/cache/store.rb
CHANGED
|
@@ -31,8 +31,22 @@ module Rigor
|
|
|
31
31
|
|
|
32
32
|
VALID_PRODUCER_ID = /\A[a-z][a-z0-9._-]*\z/
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
# @param root [String] cache root directory.
|
|
35
|
+
# @param read_only [Boolean] when true, every disk-side
|
|
36
|
+
# side-effect is suppressed: `fetch_or_compute` still
|
|
37
|
+
# reads existing entries (hits) and still runs the
|
|
38
|
+
# producer block on miss, but it does NOT write the
|
|
39
|
+
# produced value to disk, does NOT update the
|
|
40
|
+
# `schema_version.txt` marker, and does NOT touch the
|
|
41
|
+
# on-disk root directory. The in-process memo is still
|
|
42
|
+
# populated so repeated lookups within the same run stay
|
|
43
|
+
# cheap. Used by editor mode so multiple buffer-mode
|
|
44
|
+
# invocations can read from the same cache concurrently
|
|
45
|
+
# without churning it. See
|
|
46
|
+
# `docs/design/20260516-editor-mode.md` § "Cache behaviour".
|
|
47
|
+
def initialize(root:, read_only: false)
|
|
35
48
|
@root = root.to_s.dup.freeze
|
|
49
|
+
@read_only = read_only
|
|
36
50
|
@hits = 0
|
|
37
51
|
@misses = 0
|
|
38
52
|
@writes = 0
|
|
@@ -59,6 +73,13 @@ module Rigor
|
|
|
59
73
|
|
|
60
74
|
attr_reader :root
|
|
61
75
|
|
|
76
|
+
# @return [Boolean] whether this Store suppresses disk writes
|
|
77
|
+
# (`schema_version.txt`, entry creation). Reads are
|
|
78
|
+
# unaffected.
|
|
79
|
+
def read_only?
|
|
80
|
+
@read_only
|
|
81
|
+
end
|
|
82
|
+
|
|
62
83
|
# Returns a frozen snapshot of this Store's per-run hit / miss /
|
|
63
84
|
# write counters. The bookkeeping is in-memory only — every new
|
|
64
85
|
# `Store.new` starts at zero — so the counters reflect activity
|
|
@@ -167,10 +188,10 @@ module Rigor
|
|
|
167
188
|
end
|
|
168
189
|
|
|
169
190
|
value = block.call
|
|
170
|
-
write_entry(path, descriptor, value, serialize: serialize)
|
|
191
|
+
write_entry(path, descriptor, value, serialize: serialize) unless @read_only
|
|
171
192
|
@monitor.synchronize do
|
|
172
193
|
record(:misses, producer_id)
|
|
173
|
-
record(:writes, producer_id)
|
|
194
|
+
record(:writes, producer_id) unless @read_only
|
|
174
195
|
@memo[memo_key] = value
|
|
175
196
|
end
|
|
176
197
|
value
|
|
@@ -302,6 +323,15 @@ module Rigor
|
|
|
302
323
|
end
|
|
303
324
|
|
|
304
325
|
def ensure_schema_version!
|
|
326
|
+
# Read-only stores never touch the cache root — no mkdir,
|
|
327
|
+
# no marker write, no destructive clear on schema
|
|
328
|
+
# mismatch. A stale or wrong-schema marker simply yields
|
|
329
|
+
# nothing back (entries read through the version check
|
|
330
|
+
# are content-keyed, so a write under the new schema
|
|
331
|
+
# never collides with a read under the old). The next
|
|
332
|
+
# writable run will repair the cache.
|
|
333
|
+
return if @read_only
|
|
334
|
+
|
|
305
335
|
FileUtils.mkdir_p(@root)
|
|
306
336
|
marker = File.join(@root, "schema_version.txt")
|
|
307
337
|
current = Descriptor::SCHEMA_VERSION.to_s
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optionparser"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
class CLI
|
|
7
|
+
# Executes the `rigor lsp` command.
|
|
8
|
+
#
|
|
9
|
+
# See `docs/design/20260517-language-server.md` for the design.
|
|
10
|
+
# Slice 1 (this commit) ships the CLI subcommand entry point.
|
|
11
|
+
# The actual stdio JSON-RPC reader / writer is queued for slice 2;
|
|
12
|
+
# invoking `rigor lsp` at slice 1 returns immediately after
|
|
13
|
+
# validating the transport flag.
|
|
14
|
+
class LspCommand
|
|
15
|
+
USAGE = "Usage: rigor lsp [options]"
|
|
16
|
+
|
|
17
|
+
def initialize(argv:, out:, err:)
|
|
18
|
+
@argv = argv
|
|
19
|
+
@out = out
|
|
20
|
+
@err = err
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [Integer] CLI exit status.
|
|
24
|
+
def run
|
|
25
|
+
options = parse_options
|
|
26
|
+
return CLI::EXIT_USAGE if options == :usage_error
|
|
27
|
+
|
|
28
|
+
transport = options.fetch(:transport)
|
|
29
|
+
unless transport == "stdio"
|
|
30
|
+
@err.puts("rigor lsp: unsupported transport: #{transport.inspect} (only `stdio` is supported in v1)")
|
|
31
|
+
return CLI::EXIT_USAGE
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
require_relative "../language_server"
|
|
35
|
+
require_relative "../configuration"
|
|
36
|
+
require "language_server-protocol"
|
|
37
|
+
|
|
38
|
+
# STDIN is read frame-by-frame via the gem's `Io::Reader`;
|
|
39
|
+
# STDOUT is wrapped in `SynchronizedWriter` so concurrent
|
|
40
|
+
# writes from the main dispatch thread + the Debouncer's
|
|
41
|
+
# async threads don't interleave frames. The Loop runs
|
|
42
|
+
# until either STDIN hits EOF or `server.exited?`; the
|
|
43
|
+
# process then exits with the server's recorded code
|
|
44
|
+
# (0 after a clean shutdown+exit, 1 otherwise).
|
|
45
|
+
writer = LanguageServer::SynchronizedWriter.new(
|
|
46
|
+
::LanguageServer::Protocol::Transport::Io::Writer.new($stdout)
|
|
47
|
+
)
|
|
48
|
+
server, loop_runner = build_server(writer: writer, config_path: options.fetch(:config))
|
|
49
|
+
loop_runner.run
|
|
50
|
+
server.exit_code || 0
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Builds the full collaborator graph from a fresh
|
|
56
|
+
# `Configuration` + `ProjectContext`. Returns `[server,
|
|
57
|
+
# loop]` so the caller drives the loop and reads
|
|
58
|
+
# `server.exit_code` for the process exit status.
|
|
59
|
+
def build_server(writer:, config_path:) # rubocop:disable Metrics/MethodLength
|
|
60
|
+
configuration = Configuration.load(config_path)
|
|
61
|
+
# ProjectContext caches Environment + Cache::Store across
|
|
62
|
+
# requests so hover / publish hit the warm path. Invalidated
|
|
63
|
+
# by `workspace/didChangeWatchedFiles` and
|
|
64
|
+
# `workspace/didChangeConfiguration`.
|
|
65
|
+
project_context = LanguageServer::ProjectContext.new(configuration: configuration)
|
|
66
|
+
# Single source of truth for buffer state — threaded to
|
|
67
|
+
# Server + all three providers.
|
|
68
|
+
buffer_table = LanguageServer::BufferTable.new
|
|
69
|
+
debouncer = LanguageServer::Debouncer.new
|
|
70
|
+
publisher = LanguageServer::DiagnosticPublisher.new(
|
|
71
|
+
writer: writer, buffer_table: buffer_table, project_context: project_context,
|
|
72
|
+
debouncer: debouncer, debounce_seconds: 0.2
|
|
73
|
+
)
|
|
74
|
+
server = LanguageServer::Server.new(
|
|
75
|
+
buffer_table: buffer_table,
|
|
76
|
+
publisher: publisher,
|
|
77
|
+
hover_provider: LanguageServer::HoverProvider.new(
|
|
78
|
+
buffer_table: buffer_table, project_context: project_context
|
|
79
|
+
),
|
|
80
|
+
document_symbol_provider: LanguageServer::DocumentSymbolProvider.new(
|
|
81
|
+
buffer_table: buffer_table, project_context: project_context
|
|
82
|
+
),
|
|
83
|
+
completion_provider: LanguageServer::CompletionProvider.new(
|
|
84
|
+
buffer_table: buffer_table, project_context: project_context
|
|
85
|
+
),
|
|
86
|
+
signature_help_provider: LanguageServer::SignatureHelpProvider.new(
|
|
87
|
+
buffer_table: buffer_table, project_context: project_context
|
|
88
|
+
),
|
|
89
|
+
folding_range_provider: LanguageServer::FoldingRangeProvider.new(
|
|
90
|
+
buffer_table: buffer_table, project_context: project_context
|
|
91
|
+
),
|
|
92
|
+
selection_range_provider: LanguageServer::SelectionRangeProvider.new(
|
|
93
|
+
buffer_table: buffer_table, project_context: project_context
|
|
94
|
+
),
|
|
95
|
+
project_context: project_context
|
|
96
|
+
)
|
|
97
|
+
loop_runner = LanguageServer::Loop.new(
|
|
98
|
+
reader: ::LanguageServer::Protocol::Transport::Io::Reader.new($stdin),
|
|
99
|
+
writer: writer,
|
|
100
|
+
server: server
|
|
101
|
+
)
|
|
102
|
+
[server, loop_runner]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def parse_options
|
|
106
|
+
options = { transport: "stdio", log: nil, config: nil }
|
|
107
|
+
|
|
108
|
+
parser = OptionParser.new do |opts|
|
|
109
|
+
opts.banner = USAGE
|
|
110
|
+
opts.on("--transport=NAME", "Transport (default: stdio; only stdio supported in v1)") do |value|
|
|
111
|
+
options[:transport] = value
|
|
112
|
+
end
|
|
113
|
+
opts.on("--log=PATH", "Write LSP wire log + server debug to PATH (default: stderr)") do |value|
|
|
114
|
+
options[:log] = value
|
|
115
|
+
end
|
|
116
|
+
opts.on("--config=PATH", "Path to the Rigor configuration file") do |value|
|
|
117
|
+
options[:config] = value
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
parser.parse!(@argv)
|
|
121
|
+
options
|
|
122
|
+
rescue OptionParser::ParseError => e
|
|
123
|
+
@err.puts(e.message)
|
|
124
|
+
@err.puts(USAGE)
|
|
125
|
+
:usage_error
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|