rigortype 0.1.3 → 0.1.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 +125 -31
- data/lib/rigor/analysis/check_rules.rb +10 -18
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +75 -0
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +47 -21
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference/index.rb +32 -3
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference.rb +1 -0
- data/lib/rigor/analysis/diagnostic.rb +0 -2
- data/lib/rigor/analysis/fact_store.rb +11 -3
- data/lib/rigor/analysis/rule_catalog.rb +2 -2
- data/lib/rigor/analysis/runner.rb +114 -3
- data/lib/rigor/builtins/imported_refinements.rb +360 -55
- data/lib/rigor/cache/descriptor.rb +1 -1
- data/lib/rigor/cache/store.rb +1 -1
- data/lib/rigor/cli/diff_command.rb +1 -1
- data/lib/rigor/cli/sig_gen_command.rb +173 -0
- data/lib/rigor/cli/type_of_command.rb +1 -1
- data/lib/rigor/cli/type_scan_renderer.rb +1 -1
- data/lib/rigor/cli/type_scan_report.rb +2 -2
- data/lib/rigor/cli.rb +9 -1
- data/lib/rigor/configuration/dependencies.rb +2 -2
- data/lib/rigor/configuration.rb +2 -2
- data/lib/rigor/environment.rb +35 -4
- data/lib/rigor/flow_contribution/conflict.rb +2 -2
- data/lib/rigor/flow_contribution/element.rb +1 -1
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/flow_contribution/merge_result.rb +1 -1
- data/lib/rigor/flow_contribution/merger.rb +3 -3
- data/lib/rigor/flow_contribution.rb +2 -2
- data/lib/rigor/inference/block_parameter_binder.rb +0 -2
- data/lib/rigor/inference/coverage_scanner.rb +1 -1
- data/lib/rigor/inference/expression_typer.rb +67 -11
- data/lib/rigor/inference/fallback.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +3 -5
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +0 -12
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +118 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +6 -11
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +27 -11
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +0 -4
- data/lib/rigor/inference/method_dispatcher.rb +146 -2
- data/lib/rigor/inference/method_parameter_binder.rb +1 -3
- data/lib/rigor/inference/narrowing.rb +2 -4
- data/lib/rigor/inference/rbs_type_translator.rb +0 -2
- data/lib/rigor/inference/scope_indexer.rb +14 -9
- data/lib/rigor/inference/statement_evaluator.rb +7 -7
- data/lib/rigor/plugin/io_boundary.rb +0 -2
- data/lib/rigor/plugin/loader.rb +2 -2
- data/lib/rigor/plugin/manifest.rb +30 -9
- data/lib/rigor/plugin/registry.rb +11 -0
- data/lib/rigor/plugin/services.rb +1 -1
- data/lib/rigor/plugin/type_node_resolver.rb +52 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/reporter.rb +91 -0
- data/lib/rigor/rbs_extended.rb +131 -32
- data/lib/rigor/scope.rb +25 -8
- data/lib/rigor/sig_gen/classification.rb +36 -0
- data/lib/rigor/sig_gen/generator.rb +1048 -0
- data/lib/rigor/sig_gen/layout_index.rb +108 -0
- data/lib/rigor/sig_gen/method_candidate.rb +62 -0
- data/lib/rigor/sig_gen/observation_collector.rb +391 -0
- data/lib/rigor/sig_gen/observed_call.rb +62 -0
- data/lib/rigor/sig_gen/path_mapper.rb +116 -0
- data/lib/rigor/sig_gen/renderer.rb +157 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +92 -0
- data/lib/rigor/sig_gen/write_result.rb +48 -0
- data/lib/rigor/sig_gen/writer.rb +530 -0
- data/lib/rigor/sig_gen.rb +25 -0
- data/lib/rigor/type/bound_method.rb +79 -0
- data/lib/rigor/type/combinator.rb +195 -2
- data/lib/rigor/type/constant.rb +13 -0
- data/lib/rigor/type/hash_shape.rb +0 -2
- data/lib/rigor/type/union.rb +20 -1
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/generic.rb +62 -0
- data/lib/rigor/type_node/identifier.rb +30 -0
- data/lib/rigor/type_node/indexed_access.rb +41 -0
- data/lib/rigor/type_node/integer_literal.rb +29 -0
- data/lib/rigor/type_node/name_scope.rb +52 -0
- data/lib/rigor/type_node/resolver_chain.rb +56 -0
- data/lib/rigor/type_node/string_literal.rb +29 -0
- data/lib/rigor/type_node/symbol_literal.rb +28 -0
- data/lib/rigor/type_node/union.rb +42 -0
- data/lib/rigor/type_node.rb +29 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +2 -0
- data/sig/rigor/analysis/check_rules/always_truthy_condition_collector.rbs +10 -0
- data/sig/rigor/analysis/check_rules/dead_assignment_collector.rbs +10 -0
- data/sig/rigor/analysis/dependency_source_inference/gem_resolver.rbs +25 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +9 -0
- data/sig/rigor/cli/diff_command.rbs +4 -0
- data/sig/rigor/cli/explain_command.rbs +4 -0
- data/sig/rigor/cli/sig_gen_command.rbs +4 -0
- data/sig/rigor/cli/type_scan_command.rbs +3 -0
- data/sig/rigor/environment.rbs +5 -2
- data/sig/rigor/inference/builtins/method_catalog.rbs +4 -0
- data/sig/rigor/inference/builtins/numeric_catalog.rbs +3 -0
- data/sig/rigor/inference/builtins.rbs +2 -0
- data/sig/rigor/plugin/access_denied_error.rbs +3 -0
- data/sig/rigor/plugin/base.rbs +6 -0
- data/sig/rigor/plugin/fact_store.rbs +11 -0
- data/sig/rigor/plugin/io_boundary.rbs +4 -0
- data/sig/rigor/plugin/load_error.rbs +6 -0
- data/sig/rigor/plugin/loader.rbs +20 -0
- data/sig/rigor/plugin/manifest.rbs +9 -0
- data/sig/rigor/plugin/registry.rbs +3 -0
- data/sig/rigor/plugin/services.rbs +3 -0
- data/sig/rigor/plugin/trust_policy.rbs +4 -0
- data/sig/rigor/plugin/type_node_resolver.rbs +3 -0
- data/sig/rigor/plugin.rbs +8 -0
- data/sig/rigor/scope.rbs +4 -2
- data/sig/rigor/type.rbs +28 -6
- metadata +52 -1
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "strscan"
|
|
4
4
|
|
|
5
5
|
require_relative "../type"
|
|
6
|
+
require_relative "../type_node"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
module Builtins
|
|
@@ -118,6 +119,38 @@ module Rigor
|
|
|
118
119
|
return nil unless args.size == 1
|
|
119
120
|
|
|
120
121
|
Type::Combinator.int_mask_of(args.first)
|
|
122
|
+
},
|
|
123
|
+
# ADR-13 § "Canonical type-function additions" — five
|
|
124
|
+
# shape-projection type functions that the
|
|
125
|
+
# `rigor-typescript-utility-types` plugin (and any other
|
|
126
|
+
# plugin that ships a shape-projection vocabulary) maps
|
|
127
|
+
# onto. Phase A handles `HashShape` carriers; non-shape
|
|
128
|
+
# inputs return the input unchanged (the lossy-projection
|
|
129
|
+
# diagnostic lands in slice 5).
|
|
130
|
+
"pick_of" => lambda { |args|
|
|
131
|
+
return nil unless args.size == 2
|
|
132
|
+
|
|
133
|
+
Type::Combinator.pick_of(args[0], args[1])
|
|
134
|
+
},
|
|
135
|
+
"omit_of" => lambda { |args|
|
|
136
|
+
return nil unless args.size == 2
|
|
137
|
+
|
|
138
|
+
Type::Combinator.omit_of(args[0], args[1])
|
|
139
|
+
},
|
|
140
|
+
"partial_of" => lambda { |args|
|
|
141
|
+
return nil unless args.size == 1
|
|
142
|
+
|
|
143
|
+
Type::Combinator.partial_of(args.first)
|
|
144
|
+
},
|
|
145
|
+
"required_of" => lambda { |args|
|
|
146
|
+
return nil unless args.size == 1
|
|
147
|
+
|
|
148
|
+
Type::Combinator.required_of(args.first)
|
|
149
|
+
},
|
|
150
|
+
"readonly_of" => lambda { |args|
|
|
151
|
+
return nil unless args.size == 1
|
|
152
|
+
|
|
153
|
+
Type::Combinator.readonly_of(args.first)
|
|
121
154
|
}
|
|
122
155
|
}.freeze
|
|
123
156
|
private_constant :PARAMETERISED_TYPE_BUILDERS
|
|
@@ -151,11 +184,46 @@ module Rigor
|
|
|
151
184
|
# `rigor:v1:return:` (or sibling) directive. Accepts
|
|
152
185
|
# the bare-name forms `lookup` already handles plus the
|
|
153
186
|
# parameterised forms documented on {Parser}.
|
|
187
|
+
# @param name_scope [Rigor::TypeNode::NameScope, nil]
|
|
188
|
+
# ADR-13 slice 3 — when provided, the parser consults the
|
|
189
|
+
# scope's `#resolver` chain after the built-in registry
|
|
190
|
+
# and built-in parametric forms but before the RBS Nominal
|
|
191
|
+
# fallback. `nil` (default) preserves the slice-1 / slice-2
|
|
192
|
+
# behaviour of consulting only built-ins + RBS.
|
|
193
|
+
# @param reporter [Rigor::RbsExtended::Reporter, nil]
|
|
194
|
+
# ADR-13 slice 3b — collector that the Resolver feeds
|
|
195
|
+
# `dynamic.shape.lossy-projection` events into when a
|
|
196
|
+
# shape-projection head (`pick_of`, `omit_of`,
|
|
197
|
+
# `partial_of`, `required_of`, `readonly_of`) is applied
|
|
198
|
+
# to a carrier that does not preserve shape information.
|
|
199
|
+
# `nil` (default) suppresses event accumulation; legacy
|
|
200
|
+
# call sites that have no reporter to thread keep the
|
|
201
|
+
# pre-slice-3b silent fall-through.
|
|
202
|
+
# @param source_location [RBS::Location, nil] location
|
|
203
|
+
# attribution for the events the Resolver records. Carries
|
|
204
|
+
# the annotation's filename / line / column so the runner
|
|
205
|
+
# can stamp diagnostics with the user-visible source site.
|
|
154
206
|
# @return [Rigor::Type, nil] the resolved refinement
|
|
155
207
|
# carrier, or `nil` when the payload is unparseable or
|
|
156
|
-
# names a refinement / class
|
|
157
|
-
def parse(payload)
|
|
158
|
-
Parser.new(
|
|
208
|
+
# names a refinement / class no registered source resolved.
|
|
209
|
+
def parse(payload, name_scope: nil, reporter: nil, source_location: nil)
|
|
210
|
+
Parser.new(
|
|
211
|
+
payload.to_s,
|
|
212
|
+
name_scope: name_scope,
|
|
213
|
+
reporter: reporter,
|
|
214
|
+
source_location: source_location
|
|
215
|
+
).parse
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Builder helpers reachable from the Resolver. They live on
|
|
219
|
+
# the module so the Resolver does not have to import the
|
|
220
|
+
# `private_constant` builder hashes.
|
|
221
|
+
def parametric_type_builder(name)
|
|
222
|
+
PARAMETERISED_TYPE_BUILDERS[name]
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def parametric_int_builder(name)
|
|
226
|
+
PARAMETERISED_INT_BUILDERS[name]
|
|
159
227
|
end
|
|
160
228
|
|
|
161
229
|
def known?(name)
|
|
@@ -185,23 +253,38 @@ module Rigor
|
|
|
185
253
|
# soft (returns `nil` from `parse`) on any deviation so the
|
|
186
254
|
# `RBS::Extended` directive site can fall back to the
|
|
187
255
|
# RBS-declared type rather than crash on a typo.
|
|
188
|
-
|
|
189
|
-
|
|
256
|
+
#
|
|
257
|
+
# ADR-13 slice 3 split the original "scan + resolve" loop
|
|
258
|
+
# into two passes: the parser emits a {Rigor::TypeNode} AST,
|
|
259
|
+
# and a sibling {Resolver} walks the AST to produce a
|
|
260
|
+
# {Rigor::Type} carrier — consulting the built-in registry,
|
|
261
|
+
# the plugin {Rigor::TypeNode::ResolverChain}, and finally
|
|
262
|
+
# the RBS Nominal fallback in that order. Plugin resolvers
|
|
263
|
+
# never see partial parses.
|
|
264
|
+
class Parser
|
|
265
|
+
def initialize(input, name_scope: nil, reporter: nil, source_location: nil)
|
|
190
266
|
@scanner = StringScanner.new(input.strip)
|
|
267
|
+
@resolver = Resolver.new(
|
|
268
|
+
name_scope: name_scope,
|
|
269
|
+
reporter: reporter,
|
|
270
|
+
source_location: source_location
|
|
271
|
+
)
|
|
191
272
|
end
|
|
192
273
|
|
|
193
274
|
def parse
|
|
194
|
-
|
|
195
|
-
return nil if
|
|
196
|
-
|
|
197
|
-
# v0.0.7 — trailing `[K]` indexed-access projects
|
|
198
|
-
#
|
|
199
|
-
#
|
|
200
|
-
|
|
201
|
-
|
|
275
|
+
ast = parse_type_ast
|
|
276
|
+
return nil if ast.nil?
|
|
277
|
+
|
|
278
|
+
# v0.0.7 — trailing `[K]` indexed-access projects into
|
|
279
|
+
# the parsed type. Multiple `[K]` segments chain
|
|
280
|
+
# (`Tuple[A, B, C][1][0]`). Each segment wraps the
|
|
281
|
+
# previous AST in an {IndexedAccess} node so the chain
|
|
282
|
+
# composes cleanly through the resolver pass.
|
|
283
|
+
ast = parse_indexed_access_chain_ast(ast)
|
|
284
|
+
return nil if ast.nil?
|
|
202
285
|
return nil unless @scanner.eos?
|
|
203
286
|
|
|
204
|
-
|
|
287
|
+
@resolver.resolve_ast(ast)
|
|
205
288
|
end
|
|
206
289
|
|
|
207
290
|
private
|
|
@@ -214,68 +297,70 @@ module Rigor
|
|
|
214
297
|
SIMPLE_NAME = /[a-z][a-z0-9_-]*/
|
|
215
298
|
CLASS_NAME = /[A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*/
|
|
216
299
|
SIGNED_INT = /-?\d+/
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
300
|
+
# ADR-13 follow-up — literal-key tokens at type-arg
|
|
301
|
+
# position. Symbol literals match Ruby's bare-symbol
|
|
302
|
+
# identifier shape (`:name`); string literals are
|
|
303
|
+
# double-quoted without escape sequences (the most
|
|
304
|
+
# common TS-style key-union shape). The `?<value>`
|
|
305
|
+
# capture lets the parser pull the inner text without
|
|
306
|
+
# post-stripping the delimiter.
|
|
307
|
+
SYMBOL_LITERAL = /:(?<value>[a-zA-Z_][a-zA-Z0-9_]*[?!=]?)/
|
|
308
|
+
STRING_LITERAL = /"(?<value>[^"\\]*)"/
|
|
309
|
+
private_constant :SIMPLE_NAME, :CLASS_NAME, :SIGNED_INT, :SYMBOL_LITERAL, :STRING_LITERAL
|
|
310
|
+
|
|
311
|
+
def parse_type_ast
|
|
220
312
|
if (class_name = @scanner.scan(CLASS_NAME))
|
|
221
|
-
return
|
|
313
|
+
return parse_class_arg_tail_ast(class_name)
|
|
222
314
|
end
|
|
223
315
|
|
|
224
316
|
name = @scanner.scan(SIMPLE_NAME)
|
|
225
317
|
return nil if name.nil?
|
|
226
318
|
|
|
227
319
|
case @scanner.peek(1)
|
|
228
|
-
when "[" then
|
|
229
|
-
when "<" then
|
|
230
|
-
else
|
|
320
|
+
when "[" then parse_bracket_args_ast(name)
|
|
321
|
+
when "<" then parse_angle_bounds_ast(name)
|
|
322
|
+
else TypeNode::Identifier.new(name: name)
|
|
231
323
|
end
|
|
232
324
|
end
|
|
233
325
|
|
|
234
|
-
|
|
235
|
-
# opening brackets are present. Each index consumes one
|
|
236
|
-
# type argument; multi-arg `[K1, K2]` fails (the spec
|
|
237
|
-
# specifies a single key).
|
|
238
|
-
def parse_indexed_access_chain(type)
|
|
326
|
+
def parse_indexed_access_chain_ast(ast)
|
|
239
327
|
loop do
|
|
240
328
|
skip_ws
|
|
241
329
|
break unless @scanner.peek(1) == "["
|
|
242
330
|
|
|
243
331
|
@scanner.getch
|
|
244
|
-
args =
|
|
332
|
+
args = parse_type_arg_list_ast
|
|
245
333
|
return nil if args.nil? || args.size != 1
|
|
246
334
|
return nil unless @scanner.getch == "]"
|
|
247
335
|
|
|
248
|
-
|
|
336
|
+
ast = TypeNode::IndexedAccess.new(receiver: ast, key: args.first)
|
|
249
337
|
end
|
|
250
|
-
|
|
338
|
+
ast
|
|
251
339
|
end
|
|
252
340
|
|
|
253
|
-
def
|
|
254
|
-
builder = PARAMETERISED_TYPE_BUILDERS[name]
|
|
255
|
-
return nil if builder.nil?
|
|
256
|
-
|
|
341
|
+
def parse_bracket_args_ast(name)
|
|
257
342
|
@scanner.getch # consume '['
|
|
258
|
-
args =
|
|
343
|
+
args = parse_type_arg_list_ast
|
|
259
344
|
return nil if args.nil?
|
|
260
345
|
return nil unless @scanner.getch == "]"
|
|
261
346
|
|
|
262
|
-
|
|
347
|
+
TypeNode::Generic.new(head: name, args: args)
|
|
263
348
|
end
|
|
264
349
|
|
|
265
|
-
def
|
|
266
|
-
builder = PARAMETERISED_INT_BUILDERS[name]
|
|
267
|
-
return nil if builder.nil?
|
|
268
|
-
|
|
350
|
+
def parse_angle_bounds_ast(name)
|
|
269
351
|
@scanner.getch # consume '<'
|
|
270
352
|
bounds = parse_int_bound_list
|
|
271
353
|
return nil if bounds.nil?
|
|
272
354
|
return nil unless @scanner.getch == ">"
|
|
273
355
|
|
|
274
|
-
|
|
356
|
+
TypeNode::Generic.new(
|
|
357
|
+
head: name,
|
|
358
|
+
args: bounds.map { |b| TypeNode::IntegerLiteral.new(value: b) }
|
|
359
|
+
)
|
|
275
360
|
end
|
|
276
361
|
|
|
277
|
-
def
|
|
278
|
-
collect_separated_list {
|
|
362
|
+
def parse_type_arg_list_ast
|
|
363
|
+
collect_separated_list { parse_type_arg_ast }
|
|
279
364
|
end
|
|
280
365
|
|
|
281
366
|
def parse_int_bound_list
|
|
@@ -298,34 +383,63 @@ module Rigor
|
|
|
298
383
|
items
|
|
299
384
|
end
|
|
300
385
|
|
|
301
|
-
|
|
386
|
+
# ADR-13 follow-up — admits `:a | "b"` literal-union
|
|
387
|
+
# forms by parsing a single arg first, then optionally
|
|
388
|
+
# folding a chain of `| <single_arg>` into a
|
|
389
|
+
# {TypeNode::Union}. Leaves the existing single-arg
|
|
390
|
+
# path bit-for-bit untouched when no `|` follows.
|
|
391
|
+
def parse_type_arg_ast
|
|
302
392
|
skip_ws
|
|
393
|
+
first = parse_single_type_arg_ast
|
|
394
|
+
return nil if first.nil?
|
|
395
|
+
|
|
396
|
+
members = [first]
|
|
397
|
+
loop do
|
|
398
|
+
skip_ws
|
|
399
|
+
break unless @scanner.peek(1) == "|"
|
|
400
|
+
|
|
401
|
+
@scanner.getch
|
|
402
|
+
skip_ws
|
|
403
|
+
following = parse_single_type_arg_ast
|
|
404
|
+
return nil if following.nil?
|
|
405
|
+
|
|
406
|
+
members << following
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Engine cannot see the `members << following`
|
|
410
|
+
# mutation inside the loop, so it folds the
|
|
411
|
+
# `size == 1` guard to a constant; the loop body
|
|
412
|
+
# actually grows the tuple, so the guard is real.
|
|
413
|
+
members.size == 1 ? first : TypeNode::Union.new(nodes: members) # rigor:disable flow.always-truthy-condition
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def parse_single_type_arg_ast
|
|
303
417
|
if (class_name = @scanner.scan(CLASS_NAME))
|
|
304
|
-
|
|
418
|
+
parse_class_arg_tail_ast(class_name)
|
|
305
419
|
elsif (literal = @scanner.scan(SIGNED_INT))
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
420
|
+
TypeNode::IntegerLiteral.new(value: Integer(literal))
|
|
421
|
+
elsif @scanner.scan(SYMBOL_LITERAL)
|
|
422
|
+
TypeNode::SymbolLiteral.new(value: @scanner[:value].to_sym)
|
|
423
|
+
elsif @scanner.scan(STRING_LITERAL)
|
|
424
|
+
TypeNode::StringLiteral.new(value: @scanner[:value])
|
|
310
425
|
else
|
|
311
|
-
|
|
426
|
+
parse_type_ast
|
|
312
427
|
end
|
|
313
428
|
end
|
|
314
429
|
|
|
315
430
|
# Class-name-headed type argument with optional `[T_1,
|
|
316
431
|
# …]` type-args tail. Used so `key_of[Hash[Symbol,
|
|
317
432
|
# Integer]]` parses as the projection of a parameterised
|
|
318
|
-
# nominal carrier rather than rejecting the inner
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
return Type::Combinator.nominal_of(class_name) unless @scanner.peek(1) == "["
|
|
433
|
+
# nominal carrier rather than rejecting the inner brackets.
|
|
434
|
+
def parse_class_arg_tail_ast(class_name)
|
|
435
|
+
return TypeNode::Identifier.new(name: class_name) unless @scanner.peek(1) == "["
|
|
322
436
|
|
|
323
437
|
@scanner.getch # consume '['
|
|
324
|
-
args =
|
|
438
|
+
args = parse_type_arg_list_ast
|
|
325
439
|
return nil if args.nil?
|
|
326
440
|
return nil unless @scanner.getch == "]"
|
|
327
441
|
|
|
328
|
-
|
|
442
|
+
TypeNode::Generic.new(head: class_name, args: args)
|
|
329
443
|
end
|
|
330
444
|
|
|
331
445
|
def parse_int_bound
|
|
@@ -341,6 +455,197 @@ module Rigor
|
|
|
341
455
|
end
|
|
342
456
|
end
|
|
343
457
|
private_constant :Parser
|
|
458
|
+
|
|
459
|
+
# AST → {Rigor::Type} resolver. ADR-13's resolution order
|
|
460
|
+
# for every named-type production:
|
|
461
|
+
#
|
|
462
|
+
# 1. Built-in `ImportedRefinements.lookup` (no-arg
|
|
463
|
+
# refinements like `non-empty-string`).
|
|
464
|
+
# 2. Built-in parametric builders
|
|
465
|
+
# (`PARAMETERISED_TYPE_BUILDERS` for `[...]` forms,
|
|
466
|
+
# `PARAMETERISED_INT_BUILDERS` for `<...>` forms).
|
|
467
|
+
# 3. Plugin resolver chain from the supplied
|
|
468
|
+
# {Rigor::TypeNode::NameScope}, if any.
|
|
469
|
+
# 4. RBS Nominal fallback for class-shaped names
|
|
470
|
+
# (PascalCase head, with or without type args).
|
|
471
|
+
#
|
|
472
|
+
# Returns `nil` when every step declined — preserves the
|
|
473
|
+
# parser's fail-soft contract so callers fall back to the
|
|
474
|
+
# RBS-declared type instead of raising.
|
|
475
|
+
class Resolver
|
|
476
|
+
# ADR-13 slice 3b — heads that consume a shape-bearing
|
|
477
|
+
# first argument. When the first arg is not a `HashShape`
|
|
478
|
+
# / `Tuple` (per {Rigor::Type::Combinator.shape_projection_lossy?}),
|
|
479
|
+
# the projection degrades to "input unchanged" and the
|
|
480
|
+
# Resolver records a `dynamic.shape.lossy-projection`
|
|
481
|
+
# event on the reporter (if any).
|
|
482
|
+
SHAPE_PROJECTION_HEADS = %w[pick_of omit_of partial_of required_of readonly_of].freeze
|
|
483
|
+
private_constant :SHAPE_PROJECTION_HEADS
|
|
484
|
+
|
|
485
|
+
def initialize(name_scope: nil, reporter: nil, source_location: nil)
|
|
486
|
+
@chain = name_scope&.resolver
|
|
487
|
+
@class_context = name_scope&.class_context
|
|
488
|
+
@type_alias_table = name_scope&.type_alias_table || {}
|
|
489
|
+
@reporter = reporter
|
|
490
|
+
@source_location = source_location
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# ADR-13 follow-up — every leaf-literal AST node
|
|
494
|
+
# (`IntegerLiteral` / `SymbolLiteral` / `StringLiteral`)
|
|
495
|
+
# carries a Ruby value that lifts directly to a
|
|
496
|
+
# `Constant<value>` carrier through the same helper.
|
|
497
|
+
LITERAL_AST_NODES = [
|
|
498
|
+
TypeNode::IntegerLiteral, TypeNode::SymbolLiteral, TypeNode::StringLiteral
|
|
499
|
+
].freeze
|
|
500
|
+
private_constant :LITERAL_AST_NODES
|
|
501
|
+
|
|
502
|
+
def resolve_ast(node)
|
|
503
|
+
case node
|
|
504
|
+
when TypeNode::Identifier then resolve_identifier(node)
|
|
505
|
+
when TypeNode::Generic then resolve_generic(node)
|
|
506
|
+
when TypeNode::IndexedAccess then resolve_indexed_access(node)
|
|
507
|
+
when TypeNode::Union then resolve_union(node)
|
|
508
|
+
when *LITERAL_AST_NODES then Type::Combinator.constant_of(node.value)
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
# ADR-13 follow-up — resolves each node recursively and
|
|
513
|
+
# folds into a `Type::Combinator.union(...)`. When any
|
|
514
|
+
# node resolves to `nil` (unknown name, plugin decline,
|
|
515
|
+
# RBS Nominal fallback miss), the whole union collapses
|
|
516
|
+
# to `nil` so the caller falls back to the underlying
|
|
517
|
+
# RBS-declared type rather than a half-resolved Union
|
|
518
|
+
# carrier.
|
|
519
|
+
def resolve_union(node)
|
|
520
|
+
resolved = node.nodes.map { |child| resolve_ast(child) }
|
|
521
|
+
return nil if resolved.any?(&:nil?)
|
|
522
|
+
|
|
523
|
+
Type::Combinator.union(*resolved)
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
# Public {Rigor::Plugin::TypeNodeResolver}-shaped interface
|
|
527
|
+
# so a {Rigor::TypeNode::NameScope} can point its
|
|
528
|
+
# `#resolver` at the Resolver itself. Plugin resolvers
|
|
529
|
+
# call `scope.resolver.resolve(arg, scope)` to recursively
|
|
530
|
+
# resolve a nested argument through the FULL pass
|
|
531
|
+
# (built-in registry → plugin chain → RBS fallback), not
|
|
532
|
+
# just back through the chain. The `_scope` argument is
|
|
533
|
+
# ignored — the Resolver owns the scope state internally.
|
|
534
|
+
def resolve(node, _scope)
|
|
535
|
+
resolve_ast(node)
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
private
|
|
539
|
+
|
|
540
|
+
CLASS_SHAPED_HEAD = /\A[A-Z]/
|
|
541
|
+
private_constant :CLASS_SHAPED_HEAD
|
|
542
|
+
|
|
543
|
+
def resolve_identifier(node)
|
|
544
|
+
if class_shaped?(node.name)
|
|
545
|
+
chain_type = consult_chain(node)
|
|
546
|
+
return chain_type unless chain_type.nil?
|
|
547
|
+
|
|
548
|
+
return Type::Combinator.nominal_of(node.name)
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
builtin = ImportedRefinements.lookup(node.name)
|
|
552
|
+
return builtin unless builtin.nil?
|
|
553
|
+
|
|
554
|
+
consult_chain(node)
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def resolve_generic(node)
|
|
558
|
+
builtin = try_builtin_parametric(node)
|
|
559
|
+
return builtin unless builtin.nil?
|
|
560
|
+
|
|
561
|
+
chain_type = consult_chain(node)
|
|
562
|
+
return chain_type unless chain_type.nil?
|
|
563
|
+
|
|
564
|
+
return nil unless class_shaped?(node.head)
|
|
565
|
+
|
|
566
|
+
args = resolve_args(node.args)
|
|
567
|
+
return nil if args.nil?
|
|
568
|
+
|
|
569
|
+
Type::Combinator.nominal_of(node.head, type_args: args)
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def resolve_indexed_access(node)
|
|
573
|
+
receiver = resolve_ast(node.receiver)
|
|
574
|
+
return nil if receiver.nil?
|
|
575
|
+
|
|
576
|
+
key = resolve_ast(node.key)
|
|
577
|
+
return nil if key.nil?
|
|
578
|
+
|
|
579
|
+
Type::Combinator.indexed_access(receiver, key)
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
def try_builtin_parametric(node)
|
|
583
|
+
try_parametric_type_builder(node) || try_parametric_int_builder(node)
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
def try_parametric_type_builder(node)
|
|
587
|
+
builder = ImportedRefinements.parametric_type_builder(node.head)
|
|
588
|
+
return nil if builder.nil?
|
|
589
|
+
|
|
590
|
+
args = resolve_args(node.args)
|
|
591
|
+
return nil if args.nil?
|
|
592
|
+
|
|
593
|
+
result = builder.call(args)
|
|
594
|
+
record_lossy_projection_if_applicable(node, args, result)
|
|
595
|
+
result
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
# ADR-13 slice 3b — record one `dynamic.shape.lossy-projection`
|
|
599
|
+
# event per (head, source_location) pair when the projection
|
|
600
|
+
# actually degraded. The builders return the source carrier
|
|
601
|
+
# unchanged on non-HashShape / non-Tuple receivers (see
|
|
602
|
+
# `Type::Combinator.pick_of` / `omit_of` and the
|
|
603
|
+
# HashShape-only `partial_of` / `required_of` /
|
|
604
|
+
# `readonly_of`), so detection is "first arg was lossy".
|
|
605
|
+
def record_lossy_projection_if_applicable(node, args, result)
|
|
606
|
+
return if @reporter.nil?
|
|
607
|
+
return if result.nil?
|
|
608
|
+
return unless SHAPE_PROJECTION_HEADS.include?(node.head)
|
|
609
|
+
return if args.empty?
|
|
610
|
+
return unless Type::Combinator.shape_projection_lossy?(args.first)
|
|
611
|
+
|
|
612
|
+
@reporter.record_lossy_projection(
|
|
613
|
+
head: node.head,
|
|
614
|
+
source_location: @source_location
|
|
615
|
+
)
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def try_parametric_int_builder(node)
|
|
619
|
+
builder = ImportedRefinements.parametric_int_builder(node.head)
|
|
620
|
+
return nil if builder.nil?
|
|
621
|
+
|
|
622
|
+
bounds = node.args.map { |a| a.is_a?(TypeNode::IntegerLiteral) ? a.value : nil }
|
|
623
|
+
return nil if bounds.any?(&:nil?)
|
|
624
|
+
|
|
625
|
+
builder.call(bounds)
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
def resolve_args(args)
|
|
629
|
+
resolved = args.map { |a| resolve_ast(a) }
|
|
630
|
+
resolved.any?(&:nil?) ? nil : resolved
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
def consult_chain(node)
|
|
634
|
+
return nil if @chain.nil?
|
|
635
|
+
|
|
636
|
+
scope = TypeNode::NameScope.new(
|
|
637
|
+
resolver: self,
|
|
638
|
+
class_context: @class_context,
|
|
639
|
+
type_alias_table: @type_alias_table
|
|
640
|
+
)
|
|
641
|
+
@chain.resolve(node, scope)
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
def class_shaped?(name)
|
|
645
|
+
name.match?(CLASS_SHAPED_HEAD)
|
|
646
|
+
end
|
|
647
|
+
end
|
|
648
|
+
private_constant :Resolver
|
|
344
649
|
end
|
|
345
650
|
end
|
|
346
651
|
end
|
|
@@ -21,7 +21,7 @@ module Rigor
|
|
|
21
21
|
# See ADR-2 § "Registration, Configuration, and Caching" for
|
|
22
22
|
# the design rationale and ADR-6 for the storage backend
|
|
23
23
|
# decisions that consume this schema.
|
|
24
|
-
class Descriptor
|
|
24
|
+
class Descriptor
|
|
25
25
|
# Bumped on incompatible schema changes. The storage layer
|
|
26
26
|
# mixes this into the cache key, so a bump implicitly
|
|
27
27
|
# invalidates every cached value. v2 added the
|
data/lib/rigor/cache/store.rb
CHANGED
|
@@ -21,7 +21,7 @@ module Rigor
|
|
|
21
21
|
# next write replaces the bad entry. The trailing SHA-256 catches
|
|
22
22
|
# accidental corruption (partial writes, FS errors); it is **not**
|
|
23
23
|
# a security boundary, per ADR-2's trusted-gem trust model.
|
|
24
|
-
class Store
|
|
24
|
+
class Store
|
|
25
25
|
# Header literal: 5-byte ASCII magic, 1-byte separator, 1-byte
|
|
26
26
|
# format version. Bumped on incompatible on-disk format changes
|
|
27
27
|
# (independent of {Descriptor::SCHEMA_VERSION}, which covers
|
|
@@ -29,7 +29,7 @@ module Rigor
|
|
|
29
29
|
# is `1` when any new diagnostic appears, `0` otherwise —
|
|
30
30
|
# so adding new errors fails CI but legacy errors recorded
|
|
31
31
|
# in the baseline don't.
|
|
32
|
-
class DiffCommand
|
|
32
|
+
class DiffCommand
|
|
33
33
|
USAGE = "Usage: rigor diff [options] <baseline.json> [paths...]"
|
|
34
34
|
|
|
35
35
|
def initialize(argv:, out:, err:)
|