rigortype 0.3.7 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/docs/handbook/11-sig-gen.md +24 -14
- data/docs/manual/02-cli-reference.md +7 -0
- data/docs/manual/04-diagnostics.md +1 -1
- data/lib/rigor/analysis/crash_signature.rb +78 -6
- data/lib/rigor/analysis/incremental_session.rb +19 -7
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +142 -17
- data/lib/rigor/analysis/runner/pool_coordinator.rb +232 -47
- data/lib/rigor/analysis/runner/run_snapshots.rb +7 -2
- data/lib/rigor/analysis/runner.rb +66 -7
- data/lib/rigor/analysis/worker_session.rb +28 -3
- data/lib/rigor/builtins/imported_refinements.rb +11 -9
- data/lib/rigor/cache/annotation_location.rb +72 -0
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +39 -0
- data/lib/rigor/cache/store.rb +8 -1
- data/lib/rigor/cli/sig_gen_command.rb +29 -0
- data/lib/rigor/environment/failure_slot.rb +28 -0
- data/lib/rigor/environment/rbs_loader.rb +228 -57
- data/lib/rigor/environment.rb +87 -10
- data/lib/rigor/inference/hkt_registry.rb +16 -4
- data/lib/rigor/inference/hkt_sugar_translator.rb +15 -21
- data/lib/rigor/plugin/registry.rb +25 -2
- data/lib/rigor/rbs_extended/envelope_scanner.rb +8 -7
- data/lib/rigor/rbs_extended/hkt_directives.rb +16 -1
- data/lib/rigor/rbs_extended/reporter.rb +93 -13
- data/lib/rigor/rbs_extended.rb +6 -1
- data/lib/rigor/sig_gen/generator.rb +66 -80
- data/lib/rigor/sig_gen/renderer.rb +13 -6
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +6 -0
- metadata +3 -1
|
@@ -6,39 +6,28 @@ require_relative "hkt_body"
|
|
|
6
6
|
module Rigor
|
|
7
7
|
module Inference
|
|
8
8
|
class HktSugarTranslator
|
|
9
|
-
class << self
|
|
10
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
11
|
-
def translate(rbs_type, uri:, params_set:, name_scope:)
|
|
12
|
-
new(uri: uri, params_set: params_set, name_scope: name_scope).translate(rbs_type)
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
|
|
16
9
|
attr_reader :recursive
|
|
17
10
|
|
|
18
|
-
def initialize(uri:, params_set
|
|
11
|
+
def initialize(uri:, params_set:)
|
|
19
12
|
@uri = uri
|
|
20
13
|
@params_set = params_set
|
|
21
|
-
@name_scope = name_scope
|
|
22
14
|
@recursive = false
|
|
23
15
|
end
|
|
24
16
|
|
|
17
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
25
18
|
def translate(type)
|
|
26
19
|
case type
|
|
27
20
|
when RBS::Types::Alias
|
|
28
|
-
if type.name.to_s.sub(/\A::/, "") == @uri.to_s
|
|
21
|
+
if type.name.to_s.sub(/\A::/, "") == @uri.to_s && !type.args.empty?
|
|
29
22
|
@recursive = true
|
|
30
23
|
args = type.args.map { |a| translate(a) }
|
|
31
24
|
HktBody::AppRef.new(uri: @uri, args: args)
|
|
32
25
|
else
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
# or TypeLeaf for concrete Rigor::Type.
|
|
39
|
-
|
|
40
|
-
# Since Rigor doesn't support generic aliases outside of HKT AppRef,
|
|
41
|
-
# we can just translate to TypeLeaf.
|
|
26
|
+
# Everything else degrades to a concrete leaf: HktBody has no AliasRef node and Rigor
|
|
27
|
+
# resolves non-HKT aliases through RbsLoader. A bare self-reference with no type
|
|
28
|
+
# arguments (`type Foo::x[T] = ... | Foo::x`) lands here too — it is malformed for a
|
|
29
|
+
# parameterized alias (`rbs validate` rejects it), so it must not build an empty-args
|
|
30
|
+
# `AppRef`, which `HktBody` rejects.
|
|
42
31
|
fallback_to_type_leaf(type)
|
|
43
32
|
end
|
|
44
33
|
when RBS::Types::ClassInstance
|
|
@@ -72,17 +61,22 @@ module Rigor
|
|
|
72
61
|
fallback_to_type_leaf(type)
|
|
73
62
|
end
|
|
74
63
|
end
|
|
75
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
76
64
|
|
|
77
65
|
private
|
|
78
66
|
|
|
67
|
+
# The `else` arm and the unbound-variable / non-recursive-alias arms all degrade a subterm the
|
|
68
|
+
# HKT body grammar has no node for to a concrete `Rigor::Type` leaf. `RbsTypeTranslator` has no
|
|
69
|
+
# name-scope parameter — it never resolves relative names — and the alias decls this walk reads
|
|
70
|
+
# come off a `resolve_type_names`-d environment, so names arrive already absolute and there is
|
|
71
|
+
# nothing a scope would do here. `alias_expander: nil` is deliberate too: a nested alias reached
|
|
72
|
+
# in this fallback degrades to `Dynamic[Top]` rather than pulling a second alias body into an
|
|
73
|
+
# HKT definition.
|
|
79
74
|
def fallback_to_type_leaf(type)
|
|
80
75
|
require_relative "rbs_type_translator" unless defined?(RbsTypeTranslator)
|
|
81
76
|
rigor_type = RbsTypeTranslator.translate(
|
|
82
77
|
type,
|
|
83
78
|
alias_expander: nil,
|
|
84
79
|
type_vars: {},
|
|
85
|
-
name_scope: @name_scope,
|
|
86
80
|
self_type: Rigor::Type::Combinator.untyped,
|
|
87
81
|
instance_type: Rigor::Type::Combinator.untyped
|
|
88
82
|
)
|
|
@@ -316,9 +316,25 @@ module Rigor
|
|
|
316
316
|
# determined by the user's `plugins:` list); user `.rbs` overlays merge on top of this overlay last.
|
|
317
317
|
# Returns `Inference::HktRegistry::EMPTY` when no plugin contributes HKT entries so callers can skip the
|
|
318
318
|
# merge.
|
|
319
|
+
#
|
|
320
|
+
# Issue #791 — a plugin's contribution is read inside a per-plugin rescue that re-raises the SAME
|
|
321
|
+
# exception class with the plugin named, and with the original backtrace, so the
|
|
322
|
+
# `rbs.coverage.hkt-scan-failed` row this ends up on says WHICH plugin to remove instead of only that
|
|
323
|
+
# "the plugin overlay" failed. `Exception#exception(message)` clones rather than re-constructing, so
|
|
324
|
+
# an exception class with a non-standard `initialize` survives the re-raise; `cause` is set to the
|
|
325
|
+
# original automatically. The aggregation itself (a duplicate URI across two plugins, say) is not
|
|
326
|
+
# attributable to one plugin and raises unnamed — the stage still is, which is what the row needs.
|
|
319
327
|
def hkt_overlay_registry
|
|
320
|
-
registrations =
|
|
321
|
-
definitions =
|
|
328
|
+
registrations = []
|
|
329
|
+
definitions = []
|
|
330
|
+
plugins.each do |plugin|
|
|
331
|
+
manifest = plugin.manifest
|
|
332
|
+
registrations.concat(manifest.hkt_registrations)
|
|
333
|
+
definitions.concat(manifest.hkt_definitions)
|
|
334
|
+
rescue StandardError => e
|
|
335
|
+
raise e, "plugin #{safe_plugin_id(plugin).inspect} raised while contributing HKT " \
|
|
336
|
+
"registrations: #{e.message}", e.backtrace
|
|
337
|
+
end
|
|
322
338
|
return Inference::HktRegistry::EMPTY if registrations.empty? && definitions.empty?
|
|
323
339
|
|
|
324
340
|
Inference::HktRegistry.new(registrations: registrations, definitions: definitions)
|
|
@@ -455,6 +471,13 @@ module Rigor
|
|
|
455
471
|
rescue StandardError
|
|
456
472
|
[]
|
|
457
473
|
end
|
|
474
|
+
|
|
475
|
+
# Issue #791 — the id to name in the HKT-overlay failure message, derived through {#safe_manifest}
|
|
476
|
+
# because a raising manifest read is one of the shapes that gets here. Mirrors `Runner#safe_plugin_id`
|
|
477
|
+
# (the `:plugin_loader` envelope's own naming) so both failure surfaces name a plugin the same way.
|
|
478
|
+
def safe_plugin_id(plugin)
|
|
479
|
+
safe_manifest(plugin)&.id || plugin.class.to_s
|
|
480
|
+
end
|
|
458
481
|
end
|
|
459
482
|
|
|
460
483
|
# Assigned after the class body completes — `Registry.new` runs at assignment time and `#initialize` calls
|
|
@@ -22,14 +22,15 @@ module Rigor
|
|
|
22
22
|
#
|
|
23
23
|
# ## Why it parses rather than reading the built environment
|
|
24
24
|
#
|
|
25
|
-
# {ConformanceChecker}, the obvious model, walks `RbsLoader`'s built env. An envelope
|
|
25
|
+
# {ConformanceChecker}, the obvious model, walks `RbsLoader`'s built env. An envelope did not: the
|
|
26
26
|
# diagnostic has to name **where the bound was written** (`sig/foo.rbs:12`), and the ADR-54 env
|
|
27
|
-
# cache
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
27
|
+
# cache reduced every `RBS::Location` to a zero-range `<cached>` sentinel, so a warm run lost both
|
|
28
|
+
# the position and the only evidence of which file a declaration came from. That particular loss is
|
|
29
|
+
# closed now — #696 restored the buffer name, #799 an annotation's position — so what still decides
|
|
30
|
+
# it is the rest: parsing the project's own signature sources is a few milliseconds over a tree
|
|
31
|
+
# Rigor already globs, it is identical warm and cold, and it enforces ADR-103 WD6's trust rule
|
|
32
|
+
# structurally: a `%a{pure}` in rbs core or in a gem's shipped RBS is never read, so it cannot bound
|
|
33
|
+
# a project method that happens to share its key.
|
|
33
34
|
#
|
|
34
35
|
# Malformed payloads and `pure`-versus-`effect` contradictions are recorded on a {Reporter} the
|
|
35
36
|
# scanner owns and ride out on {Result#unresolved}. They surface no diagnostic in this slice:
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../inference/hkt_registry"
|
|
4
4
|
require_relative "../inference/hkt_body_parser"
|
|
5
|
+
require_relative "reporter"
|
|
5
6
|
|
|
6
7
|
module Rigor
|
|
7
8
|
module RbsExtended
|
|
@@ -286,10 +287,24 @@ module Rigor
|
|
|
286
287
|
source_location.respond_to?(:start_line) ? source_location.start_line : nil
|
|
287
288
|
end
|
|
288
289
|
|
|
290
|
+
# Issue #785 — routes a declined directive to the per-run reporter. The FIRST arm is the one that
|
|
291
|
+
# matters: the production reporter is {RbsExtended::Reporter}, whose surface is `record_unresolved` /
|
|
292
|
+
# `record_lossy_projection` / `record_hkt_error` — it has neither `#record` nor `#<<`, so before this
|
|
293
|
+
# arm existed every call here fell off the end of the `if` and every malformed HKT directive was
|
|
294
|
+
# dropped in every real run, while the doc comments claimed an `:info` entry was recorded. The
|
|
295
|
+
# position is flattened at record time through {RbsExtended::Reporter.position_of}, the one place
|
|
296
|
+
# every stream flattens through since #805, because the entry has to reach the pool drain channel as
|
|
297
|
+
# primitives (see {RbsExtended::Reporter}).
|
|
298
|
+
#
|
|
299
|
+
# The `#record` / `#<<` arms stay for the collecting doubles the directive specs and plugin authors
|
|
300
|
+
# use, which is the contract `scan_rbs_loader`'s `@param reporter [#record, nil]` documents.
|
|
289
301
|
def record_hkt_error(reporter, message, source_location)
|
|
290
302
|
return if reporter.nil?
|
|
291
303
|
|
|
292
|
-
if reporter.respond_to?(:
|
|
304
|
+
if reporter.respond_to?(:record_hkt_error)
|
|
305
|
+
path, line, column = Reporter.position_of(source_location)
|
|
306
|
+
reporter.record_hkt_error(message: message, path: path, line: line, column: column)
|
|
307
|
+
elsif reporter.respond_to?(:record)
|
|
293
308
|
reporter.record(directive: "hkt", message: message, source_location: source_location)
|
|
294
309
|
elsif reporter.respond_to?(:<<)
|
|
295
310
|
reporter << { directive: "hkt", message: message, source_location: source_location }
|
|
@@ -6,7 +6,7 @@ module Rigor
|
|
|
6
6
|
# cannot surface at the point of failure (the parsers are fail-soft, returning `nil` so call sites fall back
|
|
7
7
|
# to the RBS-declared type).
|
|
8
8
|
#
|
|
9
|
-
# Owns
|
|
9
|
+
# Owns three event streams:
|
|
10
10
|
#
|
|
11
11
|
# - `#unresolved_payloads` — `rigor:v1:*` directive payloads the resolver could not turn into a
|
|
12
12
|
# {Rigor::Type}. Surface as `dynamic.rbs-extended.unresolved` `:info` diagnostics.
|
|
@@ -14,21 +14,60 @@ module Rigor
|
|
|
14
14
|
# `required_of` / `readonly_of`) applied to a carrier that does not preserve shape information (anything
|
|
15
15
|
# other than `Type::HashShape` / `Type::Tuple`). Surface as `dynamic.shape.lossy-projection` `:info`
|
|
16
16
|
# diagnostics.
|
|
17
|
+
# - `#hkt_directive_errors` — malformed `rigor:v1:hkt_register` / `rigor:v1:hkt_define` directives the
|
|
18
|
+
# ADR-20 parser declined. Surface as `dynamic.rbs-extended.hkt-directive-invalid` `:info` diagnostics.
|
|
17
19
|
#
|
|
18
20
|
# Mutable through the run; consumed once by {Rigor::Analysis::Runner} at end-of-run. Each event is
|
|
19
|
-
# deduplicated by `(payload,
|
|
20
|
-
# lossy-projection
|
|
21
|
+
# deduplicated by its whole entry — `(payload, path, line, column)` for unresolved, `(head, path, line,
|
|
22
|
+
# column)` for lossy-projection, `(message, path, line, column)` for an hkt directive — so a single
|
|
23
|
+
# annotation read from many call sites yields one diagnostic.
|
|
21
24
|
#
|
|
22
25
|
# The reporter is intentionally thread-safe via a coarse `Mutex` because the inference engine may read the
|
|
23
26
|
# same method definition from multiple files in parallel; the critical sections are short (Array#include? +
|
|
24
27
|
# Array#<<) so the lock contention is negligible.
|
|
25
28
|
class Reporter
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
# Every entry carries its position as `(path, line, column)` primitives, flattened from the parser's
|
|
30
|
+
# `RBS::Location` by {.position_of} at record time. Issues #785 (hkt) and #805 (the two older streams).
|
|
31
|
+
# Two reasons, and either alone decides it. All three streams are drained out of every fork-pool worker
|
|
32
|
+
# ({Rigor::Analysis::WorkerSession#drain_reporters}), and an `RBS::Location` is a C-extension object with
|
|
33
|
+
# no `_dump`, so a `Marshal.dump` of the drain payload raises `TypeError` — which kills the worker at
|
|
34
|
+
# drain time and degrades the run to in-process re-analysis. And an `RBS::Location` compares equal only
|
|
35
|
+
# against a location over the SAME `RBS::Buffer` object, so two workers that each read the same `.rbs`
|
|
36
|
+
# would hand the coordinator two entries the dedup cannot collapse — `--workers=N` would then print N
|
|
37
|
+
# copies of a row `--workers=0` prints once.
|
|
38
|
+
#
|
|
39
|
+
# The record methods take the triple rather than the location for the same reason: a door that accepts
|
|
40
|
+
# an `RBS::Location` is a door a future caller reintroduces the bug through.
|
|
41
|
+
UnresolvedEntry = Data.define(:payload, :path, :line, :column)
|
|
42
|
+
LossyProjectionEntry = Data.define(:head, :path, :line, :column)
|
|
43
|
+
HktDirectiveEntry = Data.define(:message, :path, :line, :column)
|
|
44
|
+
|
|
45
|
+
# Flattens an `RBS::Location` (or anything answering the same readers) to the `(path, line, column)`
|
|
46
|
+
# triple every entry carries. `column` is 1-based, since `RBS::Location#start_column` is 0-based and
|
|
47
|
+
# diagnostics are not. Each component is `nil` when the location cannot supply it, and the diagnostic
|
|
48
|
+
# then falls back to `.rigor.yml:1:1`.
|
|
49
|
+
#
|
|
50
|
+
# Fail-soft by construction, like every parser that calls it: a location that raises while being read
|
|
51
|
+
# costs the entry its position, never the run.
|
|
52
|
+
#
|
|
53
|
+
# @param source_location [RBS::Location, nil]
|
|
54
|
+
# @return [Array(String, nil, Integer, nil, Integer, nil)]
|
|
55
|
+
def self.position_of(source_location)
|
|
56
|
+
return [nil, nil, nil] if source_location.nil?
|
|
57
|
+
|
|
58
|
+
buffer = source_location.respond_to?(:buffer) ? source_location.buffer : nil
|
|
59
|
+
name = buffer.respond_to?(:name) ? buffer.name.to_s : ""
|
|
60
|
+
line = source_location.respond_to?(:start_line) ? source_location.start_line : nil
|
|
61
|
+
column = source_location.respond_to?(:start_column) ? source_location.start_column + 1 : nil
|
|
62
|
+
[name.empty? ? nil : name, line, column]
|
|
63
|
+
rescue StandardError
|
|
64
|
+
[nil, nil, nil]
|
|
65
|
+
end
|
|
28
66
|
|
|
29
67
|
def initialize
|
|
30
68
|
@unresolved_payloads = []
|
|
31
69
|
@lossy_projections = []
|
|
70
|
+
@hkt_directive_errors = []
|
|
32
71
|
@mutex = Mutex.new
|
|
33
72
|
end
|
|
34
73
|
|
|
@@ -42,11 +81,13 @@ module Rigor
|
|
|
42
81
|
@mutex.synchronize { @lossy_projections.dup.freeze }
|
|
43
82
|
end
|
|
44
83
|
|
|
45
|
-
# Records a `dynamic.rbs-extended.unresolved` event. The
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
def record_unresolved(payload:,
|
|
49
|
-
entry = UnresolvedEntry.new(
|
|
84
|
+
# Records a `dynamic.rbs-extended.unresolved` event. The position triple is the source annotation's
|
|
85
|
+
# `.rbs` file / line / 1-based column — {.position_of} flattens the caller's `RBS::Location` into it —
|
|
86
|
+
# each `nil` when the caller had no location (the diagnostic then falls back to `.rigor.yml:1:1`).
|
|
87
|
+
def record_unresolved(payload:, path: nil, line: nil, column: nil)
|
|
88
|
+
entry = UnresolvedEntry.new(
|
|
89
|
+
payload: frozen_text(payload), path: frozen_text(path), line: line, column: column
|
|
90
|
+
)
|
|
50
91
|
@mutex.synchronize do
|
|
51
92
|
return if @unresolved_payloads.include?(entry)
|
|
52
93
|
|
|
@@ -56,8 +97,11 @@ module Rigor
|
|
|
56
97
|
|
|
57
98
|
# Records a `dynamic.shape.lossy-projection` event for one of the five shape-projection heads. `head` MUST
|
|
58
99
|
# be a String (`"pick_of"`, `"omit_of"`, …); the diagnostic message identifies which projection degraded.
|
|
59
|
-
|
|
60
|
-
|
|
100
|
+
# The position triple is read exactly as {#record_unresolved}'s is.
|
|
101
|
+
def record_lossy_projection(head:, path: nil, line: nil, column: nil)
|
|
102
|
+
entry = LossyProjectionEntry.new(
|
|
103
|
+
head: frozen_text(head), path: frozen_text(path), line: line, column: column
|
|
104
|
+
)
|
|
61
105
|
@mutex.synchronize do
|
|
62
106
|
return if @lossy_projections.include?(entry)
|
|
63
107
|
|
|
@@ -65,10 +109,46 @@ module Rigor
|
|
|
65
109
|
end
|
|
66
110
|
end
|
|
67
111
|
|
|
112
|
+
# @return [Array<HktDirectiveEntry>] frozen snapshot of the accumulated hkt-directive failures.
|
|
113
|
+
def hkt_directive_errors
|
|
114
|
+
@mutex.synchronize { @hkt_directive_errors.dup.freeze }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Records a `dynamic.rbs-extended.hkt-directive-invalid` event: one malformed ADR-20 HKT directive the
|
|
118
|
+
# {Rigor::RbsExtended::HktDirectives} parser declined. `message` names what the parser objected to; the
|
|
119
|
+
# position triple is the annotation's `.rbs` file / line / 1-based column, each `nil` when the caller had
|
|
120
|
+
# no location (the diagnostic then falls back to `.rigor.yml:1:1`).
|
|
121
|
+
#
|
|
122
|
+
# Every String is frozen individually, not just the enclosing `Data`: the entry crosses the pool drain
|
|
123
|
+
# channel, whose stated invariant is that its payload is `Ractor.shareable?` as well as Marshal-clean.
|
|
124
|
+
def record_hkt_error(message:, path: nil, line: nil, column: nil)
|
|
125
|
+
entry = HktDirectiveEntry.new(
|
|
126
|
+
message: frozen_text(message), path: frozen_text(path), line: line, column: column
|
|
127
|
+
)
|
|
128
|
+
@mutex.synchronize do
|
|
129
|
+
return if @hkt_directive_errors.include?(entry)
|
|
130
|
+
|
|
131
|
+
@hkt_directive_errors << entry
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
68
135
|
# True when no events have accumulated. Used by callers that want to skip the diagnostic-emission pass
|
|
69
136
|
# entirely on the common no-event path.
|
|
70
137
|
def empty?
|
|
71
|
-
@mutex.synchronize
|
|
138
|
+
@mutex.synchronize do
|
|
139
|
+
@unresolved_payloads.empty? && @lossy_projections.empty? && @hkt_directive_errors.empty?
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
# `nil` stays `nil` — the diagnostic falls back to `.rigor.yml:1:1` on a missing path. Anything else
|
|
146
|
+
# becomes an individually frozen String: the `Data` wrapper is frozen on its own, but the pool drain's
|
|
147
|
+
# stated invariant is that its payload is `Ractor.shareable?`, which is a DEEP freeze. Coercing here also
|
|
148
|
+
# means a caller that hands the door an `RBS::Location` by mistake stores its `#to_s`, not the object —
|
|
149
|
+
# the entry cannot become un-Marshalable from the outside.
|
|
150
|
+
def frozen_text(value)
|
|
151
|
+
value.nil? ? nil : value.to_s.dup.freeze
|
|
72
152
|
end
|
|
73
153
|
end
|
|
74
154
|
end
|
data/lib/rigor/rbs_extended.rb
CHANGED
|
@@ -812,10 +812,15 @@ module Rigor
|
|
|
812
812
|
# ADR-13 slice 3b — guards every reporter call so the in-RbsExtended-module call sites can record events
|
|
813
813
|
# uniformly without nil-checking each time. When the reporter is nil (the v0.1.0 → v0.1.3 default for call
|
|
814
814
|
# sites that do not yet thread `environment:`), the call is a no-op and the parser stays fail-soft.
|
|
815
|
+
#
|
|
816
|
+
# Issue #805 — the location is flattened to `(path, line, column)` here, at record time, because the
|
|
817
|
+
# entry is drained out of every fork-pool worker and an `RBS::Location` neither survives `Marshal.dump`
|
|
818
|
+
# nor compares equal across two workers' buffers (see {Rigor::RbsExtended::Reporter}).
|
|
815
819
|
def record_unresolved(reporter, payload, source_location)
|
|
816
820
|
return if reporter.nil?
|
|
817
821
|
|
|
818
|
-
|
|
822
|
+
path, line, column = Reporter.position_of(source_location)
|
|
823
|
+
reporter.record_unresolved(payload: payload, path: path, line: line, column: column)
|
|
819
824
|
end
|
|
820
825
|
end
|
|
821
826
|
end
|
|
@@ -24,12 +24,16 @@ module Rigor
|
|
|
24
24
|
# body's last expression to derive an inferred return, looks up the project's existing RBS declaration (if
|
|
25
25
|
# any), and emits one {MethodCandidate} per def.
|
|
26
26
|
#
|
|
27
|
-
# The
|
|
28
|
-
# -
|
|
29
|
-
# defs are
|
|
30
|
-
# - Parameter
|
|
31
|
-
# `--params=observed`
|
|
32
|
-
#
|
|
27
|
+
# The scope, as it stands after the slices that widened the MVP:
|
|
28
|
+
# - Instance and singleton methods inside a nameable `class` / `module` body are considered. Top-level /
|
|
29
|
+
# DSL-block defs have no nameable receiver and are not candidates.
|
|
30
|
+
# - Parameter types are `untyped` per ADR-14 § "Robustness principle compliance" clause 2 unless
|
|
31
|
+
# `--params=observed` supplies call-site observations; the parameter LIST always mirrors the def's runtime
|
|
32
|
+
# shape (required / optional / rest / trailing / keyword / keyword-rest / forwarding / block). Issue #778
|
|
33
|
+
# retired the slice-1 gate that skipped every shape beyond required positionals as
|
|
34
|
+
# `sig.skipped.complex-shape`: the body typer binds every parameter to `untyped` when no RBS declares the
|
|
35
|
+
# method, so the inferred return is the same clause-1 answer for every shape, and the renderer the
|
|
36
|
+
# `initialize` stub already used spells them all.
|
|
33
37
|
# - A `Dynamic[top]` inferred return becomes `sig.skipped.untyped-return` — emitting `untyped` would obscure
|
|
34
38
|
# rather than help.
|
|
35
39
|
# - Tighter-return detection compares the RBS-erased spellings only when the existing declared return
|
|
@@ -650,8 +654,8 @@ module Rigor
|
|
|
650
654
|
end
|
|
651
655
|
|
|
652
656
|
# Emits `def initialize: (<shape>) -> void`. The return is always `void` because Ruby's `initialize`
|
|
653
|
-
# return value is never meaningful. The parameter list mirrors the runtime shape
|
|
654
|
-
#
|
|
657
|
+
# return value is never meaningful. The parameter list mirrors the runtime shape through the same
|
|
658
|
+
# {#render_param_list} every other def renders with.
|
|
655
659
|
#
|
|
656
660
|
# When `--params=observed` populates `@observations` for `[class_name, :initialize]` (via the
|
|
657
661
|
# `ObservationCollector`'s `.new` → `:initialize` routing), positional and keyword arg types come from the
|
|
@@ -659,7 +663,7 @@ module Rigor
|
|
|
659
663
|
# clause 2.
|
|
660
664
|
def initialize_stub_candidate(path, def_node, class_name)
|
|
661
665
|
params = def_node.parameters
|
|
662
|
-
rbs = "def initialize: (#{
|
|
666
|
+
rbs = "def initialize: (#{render_param_list(params, class_name, :initialize)})" \
|
|
663
667
|
"#{block_signature_suffix(params)} -> void"
|
|
664
668
|
build_candidate(
|
|
665
669
|
path: path, class_name: class_name, method_name: :initialize,
|
|
@@ -668,57 +672,81 @@ module Rigor
|
|
|
668
672
|
)
|
|
669
673
|
end
|
|
670
674
|
|
|
671
|
-
def
|
|
675
|
+
# The RBS parameter list of a def, mirroring its runtime shape position by position: `untyped` per
|
|
676
|
+
# required positional, `?untyped` per optional, `*untyped` for a rest, one `untyped` per trailing
|
|
677
|
+
# positional after the rest, `name: untyped` / `?name: untyped` per keyword, `**untyped` for a keyword
|
|
678
|
+
# rest, and `*untyped, **untyped` for `...` forwarding (its block half is {#block_signature_suffix}'s).
|
|
679
|
+
# `**nil` declares that no keywords are accepted, which RBS cannot spell, so it renders nothing.
|
|
680
|
+
#
|
|
681
|
+
# Per ADR-5 clause 2 every position is `untyped` unless `--params=observed` supplied call-site
|
|
682
|
+
# observations, in which case each leading positional and each keyword carries the union of the types the
|
|
683
|
+
# arity-compatible observations passed there ({#arity_matched_observations}). The rest, the trailing
|
|
684
|
+
# positionals and the keyword rest stay `untyped`: an observation does not say which of its arguments the
|
|
685
|
+
# splat absorbed. Before #778 only `initialize` rendered through this path and every other def with a
|
|
686
|
+
# shape beyond required positionals was skipped as `sig.skipped.complex-shape`.
|
|
687
|
+
def render_param_list(params, class_name, method_name)
|
|
672
688
|
return "" unless params.is_a?(Prism::ParametersNode)
|
|
673
689
|
|
|
674
|
-
observations =
|
|
675
|
-
offset = 0
|
|
690
|
+
observations = arity_matched_observations(class_name, method_name, params)
|
|
676
691
|
parts = []
|
|
677
|
-
|
|
678
|
-
params.requireds.
|
|
679
|
-
|
|
680
|
-
end
|
|
681
|
-
offset += params.requireds.size
|
|
682
|
-
|
|
683
|
-
params.optionals.each_with_index do |_, i|
|
|
684
|
-
parts << initialize_positional_type(observations, offset + i, "?")
|
|
685
|
-
end
|
|
686
|
-
|
|
692
|
+
params.requireds.each_index { |i| parts << positional_type(observations, i, "") }
|
|
693
|
+
offset = params.requireds.size
|
|
694
|
+
params.optionals.each_index { |i| parts << positional_type(observations, offset + i, "?") }
|
|
687
695
|
parts << "*untyped" if params.rest
|
|
696
|
+
parts.concat(Array.new(params.posts.size, "untyped"))
|
|
688
697
|
params.keywords.each { |kw| parts << render_keyword_param(kw, observations) }
|
|
689
|
-
parts
|
|
698
|
+
parts.concat(keyword_rest_parts(params))
|
|
690
699
|
parts.join(", ")
|
|
691
700
|
end
|
|
692
701
|
|
|
702
|
+
def keyword_rest_parts(params)
|
|
703
|
+
case params.keyword_rest
|
|
704
|
+
when Prism::ForwardingParameterNode
|
|
705
|
+
# `...` forwards positionals as well as keywords; a rest already rendered covers the former.
|
|
706
|
+
params.rest ? ["**untyped"] : ["*untyped", "**untyped"]
|
|
707
|
+
when Prism::KeywordRestParameterNode then ["**untyped"]
|
|
708
|
+
else []
|
|
709
|
+
end
|
|
710
|
+
end
|
|
711
|
+
|
|
693
712
|
# The RBS block suffix for a `def` that takes a `&block` parameter, e.g. ` ?{ (*untyped) -> untyped }`.
|
|
694
713
|
# A block belongs AFTER the parameter parens in RBS (`(params) ?{ block } -> ret`), not inside them —
|
|
695
714
|
# emitting it as a comma-joined member produced `(**untyped, ?{ (?) -> void })`, which RBS rejects
|
|
696
715
|
# (`optional keyword argument type is expected`) and which then collapsed the whole env build. sig-gen
|
|
697
716
|
# never observes the block's own signature, so it is rendered maximally lenient (ADR-5): an optional
|
|
698
|
-
# block (`?{`, since a `&block` need not be passed) taking `*untyped` and returning `untyped`.
|
|
699
|
-
# "" when the method takes no block.
|
|
717
|
+
# block (`?{`, since a `&block` need not be passed) taking `*untyped` and returning `untyped`. `...`
|
|
718
|
+
# forwards a block too and gets the same suffix. Returns "" when the method takes no block.
|
|
700
719
|
def block_signature_suffix(params)
|
|
701
720
|
return "" unless params.is_a?(Prism::ParametersNode)
|
|
702
|
-
return ""
|
|
721
|
+
return "" unless params.block || params.keyword_rest.is_a?(Prism::ForwardingParameterNode)
|
|
703
722
|
|
|
704
723
|
" ?{ (*untyped) -> untyped }"
|
|
705
724
|
end
|
|
706
725
|
|
|
707
|
-
#
|
|
708
|
-
#
|
|
709
|
-
# overload the
|
|
710
|
-
|
|
726
|
+
# Observations under `[class_name, method_name]` whose positional arity the def accepts: at least its
|
|
727
|
+
# required count (leading plus trailing), and, unless a rest or `...` absorbs the surplus, at most
|
|
728
|
+
# required plus optional. An arity outside that window describes a different overload the signature
|
|
729
|
+
# cannot express, so it says nothing about these positions. #778 widened the window for ordinary defs
|
|
730
|
+
# from the exact required count — `def optional(text = "x")` called as `optional` AND as `optional("y")`
|
|
731
|
+
# now credits both call sites.
|
|
732
|
+
def arity_matched_observations(class_name, method_name, params)
|
|
711
733
|
return [] if @observations.empty?
|
|
712
734
|
|
|
713
|
-
list = @observations[[class_name,
|
|
714
|
-
min = params.requireds.size
|
|
715
|
-
max = min + params.optionals.size
|
|
716
|
-
list.select { |obs|
|
|
735
|
+
list = @observations[[class_name, method_name]] || []
|
|
736
|
+
min = params.requireds.size + params.posts.size
|
|
737
|
+
max = unbounded_positionals?(params) ? Float::INFINITY : min + params.optionals.size
|
|
738
|
+
list.select { |obs| obs.positional.size.between?(min, max) }
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
def unbounded_positionals?(params)
|
|
742
|
+
!params.rest.nil? || params.keyword_rest.is_a?(Prism::ForwardingParameterNode)
|
|
717
743
|
end
|
|
718
744
|
|
|
719
|
-
|
|
745
|
+
# A bare union is a valid positional type in RBS (`(String | Integer, ?String | Integer)`), so unlike the
|
|
746
|
+
# return position it is not parenthesised.
|
|
747
|
+
def positional_type(observations, index, prefix)
|
|
720
748
|
types = observations.filter_map { |obs| obs.positional[index] }
|
|
721
|
-
"#{prefix}#{
|
|
749
|
+
"#{prefix}#{union_erase(types)}"
|
|
722
750
|
end
|
|
723
751
|
|
|
724
752
|
def render_keyword_param(keyword, observations)
|
|
@@ -746,10 +774,6 @@ module Rigor
|
|
|
746
774
|
return nil if initialize_excludes?(def_node, kind)
|
|
747
775
|
return initialize_stub_candidate(path, def_node, class_name) if non_trivial_initialize?(def_node, kind)
|
|
748
776
|
|
|
749
|
-
unless simple_parameter_shape?(def_node.parameters)
|
|
750
|
-
return skipped(path, def_node, class_name, kind, :complex_shape)
|
|
751
|
-
end
|
|
752
|
-
|
|
753
777
|
inferred = infer_return_type(def_node, scope_index)
|
|
754
778
|
return skipped(path, def_node, class_name, kind, :untyped_return) if inferred.nil? || dynamic_top?(inferred)
|
|
755
779
|
|
|
@@ -763,20 +787,6 @@ module Rigor
|
|
|
763
787
|
end
|
|
764
788
|
end
|
|
765
789
|
|
|
766
|
-
# Required positionals only; the MVP's body-typing path gives well-defined returns for that shape.
|
|
767
|
-
# Optional / rest / keyword / block parameters route through the `sig.skipped.complex-shape` reason until
|
|
768
|
-
# slices 3+ widen the param policy.
|
|
769
|
-
def simple_parameter_shape?(params)
|
|
770
|
-
return true if params.nil?
|
|
771
|
-
return false unless params.is_a?(Prism::ParametersNode)
|
|
772
|
-
|
|
773
|
-
params.optionals.empty? &&
|
|
774
|
-
params.rest.nil? &&
|
|
775
|
-
params.keywords.empty? &&
|
|
776
|
-
params.keyword_rest.nil? &&
|
|
777
|
-
params.block.nil?
|
|
778
|
-
end
|
|
779
|
-
|
|
780
790
|
# Mirrors the `def.return-type-mismatch` rule's body-type extraction: type the implicit-return expression
|
|
781
791
|
# under the scope the indexer associated with the body. The parameter bindings (typed `untyped` per the
|
|
782
792
|
# indexer's default) come from `with_local` inside `StatementEvaluator`; the result is the carrier the
|
|
@@ -991,8 +1001,8 @@ module Rigor
|
|
|
991
1001
|
end
|
|
992
1002
|
|
|
993
1003
|
def render_rbs_line(def_node, inferred, class_name, kind)
|
|
994
|
-
|
|
995
|
-
head =
|
|
1004
|
+
params = def_node.parameters
|
|
1005
|
+
head = "(#{render_param_list(params, class_name, def_node.name)})#{block_signature_suffix(params)}"
|
|
996
1006
|
prefix = method_def_prefix(class_name, def_node.name, kind)
|
|
997
1007
|
"#{prefix}#{def_node.name}: #{head} -> #{paren_wrap_union(elaborated_rbs(inferred))}"
|
|
998
1008
|
end
|
|
@@ -1028,30 +1038,6 @@ module Rigor
|
|
|
1028
1038
|
false
|
|
1029
1039
|
end
|
|
1030
1040
|
|
|
1031
|
-
def required_arity(def_node)
|
|
1032
|
-
params = def_node.parameters
|
|
1033
|
-
params.is_a?(Prism::ParametersNode) ? params.requireds.size : 0
|
|
1034
|
-
end
|
|
1035
|
-
|
|
1036
|
-
# Per ADR-5 clause 2 the default is `untyped` for every position. Observed-policy callers
|
|
1037
|
-
# (`--params=observed`) pass an `observations:` map at construction time; the generator unions
|
|
1038
|
-
# per-position arg types whose tuple arity matches the def's required-positional count. Observations from
|
|
1039
|
-
# arities other than the def's count are discarded — they describe a different overload the MVP does not
|
|
1040
|
-
# emit.
|
|
1041
|
-
def render_param_list(class_name, method_name, arity)
|
|
1042
|
-
tuples = matching_observations(class_name, method_name, arity)
|
|
1043
|
-
return Array.new(arity, "untyped").join(", ") if tuples.empty?
|
|
1044
|
-
|
|
1045
|
-
Array.new(arity) { |i| union_erase(tuples.map { |obs| obs.positional[i] }) }.join(", ")
|
|
1046
|
-
end
|
|
1047
|
-
|
|
1048
|
-
def matching_observations(class_name, method_name, arity)
|
|
1049
|
-
return [] if @observations.empty?
|
|
1050
|
-
|
|
1051
|
-
list = @observations[[class_name, method_name]] || []
|
|
1052
|
-
list.select { |obs| obs.positional.size == arity }
|
|
1053
|
-
end
|
|
1054
|
-
|
|
1055
1041
|
def union_erase(types)
|
|
1056
1042
|
return "untyped" if types.empty?
|
|
1057
1043
|
return elaborated_rbs(types.first) if types.size == 1
|
|
@@ -14,7 +14,8 @@ module Rigor
|
|
|
14
14
|
# - `:diff` — a unified-style diff comparing the existing RBS spelling (if any) against the inferred
|
|
15
15
|
# spelling. The MVP renders a minimal "- declared / + inferred" block; full per-file diffing arrives with
|
|
16
16
|
# slice 2's `--write` merge.
|
|
17
|
-
# - `:json` — machine-readable payload with the same classification table as `:print
|
|
17
|
+
# - `:json` — machine-readable payload with the same classification table as `:print`, plus every `skipped`
|
|
18
|
+
# row with its `skip_reason` (#778).
|
|
18
19
|
class Renderer
|
|
19
20
|
def initialize(out:)
|
|
20
21
|
@out = out
|
|
@@ -27,11 +28,10 @@ module Rigor
|
|
|
27
28
|
# {Classification} constants to include; an empty
|
|
28
29
|
# array means "all emittable classifications".
|
|
29
30
|
def render(candidates:, mode:, format:, selection:)
|
|
30
|
-
filtered = filter(candidates, selection)
|
|
31
|
-
|
|
32
31
|
case format
|
|
33
|
-
when "json" then render_json(
|
|
32
|
+
when "json" then render_json(filter(candidates, selection, with_skipped: true))
|
|
34
33
|
when "text"
|
|
34
|
+
filtered = filter(candidates, selection)
|
|
35
35
|
mode == :diff ? render_diff(filtered) : render_print(filtered)
|
|
36
36
|
else
|
|
37
37
|
raise ArgumentError, "unsupported format: #{format}"
|
|
@@ -40,9 +40,16 @@ module Rigor
|
|
|
40
40
|
|
|
41
41
|
private
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
# The emittable rows the selection asks for. JSON also carries every `skipped` row whatever the selection:
|
|
44
|
+
# ADR-14 makes the JSON payload the surface where `sig.skipped.*` is reported, and a consumer asking why a
|
|
45
|
+
# method is missing from its `sig/` needs the reason next to the rows that did emit (#778 — the rows were
|
|
46
|
+
# built with their `skip_reason` and then dropped here). `equivalent` rows stay out: nothing to do,
|
|
47
|
+
# nothing to explain.
|
|
48
|
+
def filter(candidates, selection, with_skipped: false)
|
|
44
49
|
active = selection.empty? ? Classification::EMITTABLE : selection
|
|
45
|
-
candidates.select
|
|
50
|
+
candidates.select do |c|
|
|
51
|
+
active.include?(c.classification) || (with_skipped && c.classification == Classification::SKIPPED)
|
|
52
|
+
end
|
|
46
53
|
end
|
|
47
54
|
|
|
48
55
|
def render_print(candidates)
|
data/lib/rigor/version.rb
CHANGED
data/sig/rigor/environment.rbs
CHANGED
|
@@ -13,6 +13,12 @@ module Rigor
|
|
|
13
13
|
attr_reader synthetic_method_index: untyped?
|
|
14
14
|
attr_reader project_patched_methods: untyped?
|
|
15
15
|
attr_reader hkt_registry: untyped?
|
|
16
|
+
# Issue #784 — `[error_class_name, first_message_line, raw_frame_or_nil, stage]` when either stage of
|
|
17
|
+
# the HKT-registry build raised at its seam (`:scan`, the RBS `type`-alias scan; `:overlay`, the
|
|
18
|
+
# plugin-manifest aggregation — #791), nil when both built or were never demanded. Declared `untyped?`
|
|
19
|
+
# like its neighbours:
|
|
20
|
+
# `rigor sig-gen` emits nothing for this delegating reader, and the tuple shape is documented here.
|
|
21
|
+
def hkt_scan_failure: () -> untyped?
|
|
16
22
|
|
|
17
23
|
def self.default: () -> Environment
|
|
18
24
|
def self.for_project: (?root: String, ?libraries: Array[String], ?signature_paths: Array[String | _ToPath]?, ?cache_store: untyped?, ?plugin_registry: untyped?, ?dependency_source_index: untyped?, ?rbs_extended_reporter: untyped?, ?boundary_cross_reporter: untyped?, ?source_rbs_synthesis_reporter: untyped?, ?bundler_bundle_path: String?, ?bundler_auto_detect: bool, ?synthetic_method_index: untyped?, ?project_patched_methods: untyped?) -> Environment
|