rigortype 0.3.1 → 0.3.2
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 +10 -10
- data/data/core_overlay/pathname.rbs +5 -0
- data/data/core_overlay/resolv.rbs +31 -0
- data/data/core_overlay/string_scanner.rbs +5 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +10 -6
- data/data/vendored_gem_sigs/bundler/bundler.rbs +39 -27
- data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +6 -0
- data/data/vendored_gem_sigs/prism/prism_supplement.rbs +7 -0
- data/data/vendored_gem_sigs/racc/racc.rbs +67 -0
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +20 -9
- data/docs/manual/02-cli-reference.md +71 -11
- data/docs/manual/03-configuration.md +16 -1
- data/docs/manual/12-caching.md +17 -5
- data/docs/manual/15-type-protection-coverage.md +122 -1
- data/docs/manual/plugins/rigor-rbs-inline.md +22 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -0
- data/lib/rigor/analysis/check_rules.rb +45 -6
- data/lib/rigor/analysis/incremental_session.rb +156 -11
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +14 -0
- data/lib/rigor/analysis/run_cache_key.rb +17 -1
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +201 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +28 -10
- data/lib/rigor/analysis/runner/pool_coordinator.rb +7 -10
- data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -1
- data/lib/rigor/analysis/runner.rb +78 -7
- data/lib/rigor/bleeding_edge.rb +132 -7
- data/lib/rigor/cache/descriptor.rb +6 -1
- data/lib/rigor/cache/engine_source.rb +162 -0
- data/lib/rigor/cache/incremental_snapshot.rb +87 -18
- data/lib/rigor/cli/check_command.rb +49 -28
- data/lib/rigor/cli/coverage_command.rb +15 -2
- data/lib/rigor/cli/coverage_mutation.rb +242 -11
- data/lib/rigor/cli/fused_protection_renderer.rb +10 -0
- data/lib/rigor/cli/fused_protection_report.rb +12 -3
- data/lib/rigor/cli/mutation_fork_scan.rb +64 -0
- data/lib/rigor/cli/mutation_protection_renderer.rb +12 -0
- data/lib/rigor/cli/mutation_protection_report.rb +26 -5
- data/lib/rigor/cli/protection_renderer.rb +13 -0
- data/lib/rigor/cli/protection_report.rb +11 -3
- data/lib/rigor/cli/show_bleedingedge_command.rb +17 -4
- data/lib/rigor/cli/sig_gen_command.rb +5 -4
- data/lib/rigor/configuration.rb +45 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +2 -1
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +434 -75
- data/lib/rigor/environment.rb +28 -13
- data/lib/rigor/inference/expression_typer.rb +15 -0
- data/lib/rigor/inference/fork_map.rb +9 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +50 -2
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +35 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +16 -0
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +64 -5
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +137 -9
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +124 -1
- data/lib/rigor/inference/mutation_widening.rb +30 -25
- data/lib/rigor/inference/narrowing.rb +3 -1
- data/lib/rigor/inference/optimistic_origin.rb +58 -0
- data/lib/rigor/inference/protection_scanner.rb +18 -2
- data/lib/rigor/inference/receiver_alias.rb +57 -0
- data/lib/rigor/inference/scope_indexer.rb +78 -27
- data/lib/rigor/inference/statement_evaluator.rb +40 -2
- data/lib/rigor/language_server/buffer_table.rb +22 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +203 -4
- data/lib/rigor/language_server/project_context.rb +45 -0
- data/lib/rigor/language_server/publish_batcher.rb +82 -0
- data/lib/rigor/language_server/server.rb +20 -1
- data/lib/rigor/language_server.rb +1 -0
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
- data/lib/rigor/protection/closure_kill_oracle.rb +171 -0
- data/lib/rigor/protection/dependency_closure.rb +59 -0
- data/lib/rigor/protection/diagnostic_oracle.rb +12 -8
- data/lib/rigor/protection/discovery_seed.rb +116 -0
- data/lib/rigor/protection/kill_signature.rb +31 -0
- data/lib/rigor/protection/mutation_cache.rb +355 -0
- data/lib/rigor/protection/mutation_scanner.rb +55 -11
- data/lib/rigor/protection/mutator.rb +26 -4
- data/lib/rigor/runtime/jit.rb +63 -1
- data/lib/rigor/scope/discovery_index.rb +9 -0
- data/lib/rigor/scope.rb +52 -8
- data/lib/rigor/sig_gen/generator.rb +130 -31
- data/lib/rigor/sig_gen/layout_index.rb +6 -0
- data/lib/rigor/sig_gen/meta_class_shape.rb +84 -0
- data/lib/rigor/sig_gen/renderer.rb +20 -1
- data/lib/rigor/sig_gen/write_result.rb +6 -4
- data/lib/rigor/sig_gen/writer.rb +12 -0
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +6 -6
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/result_shape.rb +146 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +177 -12
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +74 -7
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +257 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +69 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +87 -11
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +34 -4
- data/sig/rigor/inference/void_origin.rbs +6 -3
- data/sig/rigor/scope.rbs +8 -0
- metadata +17 -1
|
@@ -65,9 +65,11 @@ module Rigor
|
|
|
65
65
|
# intentionally read-only and appended LAST so user-supplied `signature_paths` win on name conflicts.
|
|
66
66
|
def build_env_for(libraries:, signature_paths:, virtual_rbs: [])
|
|
67
67
|
rbs_loader = RBS::EnvironmentLoader.new
|
|
68
|
-
libraries
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
libraries = libraries_without_shadowed_bigdecimal_math(libraries)
|
|
69
|
+
loaded_libraries = libraries.select do |library|
|
|
70
|
+
rbs_loader.has_library?(library: library, version: nil)
|
|
71
|
+
end
|
|
72
|
+
loaded_libraries.each do |library|
|
|
71
73
|
rbs_loader.add(library: library, version: nil)
|
|
72
74
|
end
|
|
73
75
|
# Project `signature_paths:` are loaded per-file by {.add_project_signatures} AFTER `from_loader`,
|
|
@@ -76,16 +78,7 @@ module Rigor
|
|
|
76
78
|
# type-of query then degrades to `Dynamic[top]` — the "sig looks harmful" failure of the 2026-07-06
|
|
77
79
|
# mastodon coverage note). Per-file loading quarantines the broken file instead. Vendored / core-overlay
|
|
78
80
|
# sigs are Rigor-shipped and trusted, so they stay on the loader's fast batch path.
|
|
79
|
-
|
|
80
|
-
rbs_loader.add(path: path) if path.directory?
|
|
81
|
-
end
|
|
82
|
-
# Rigor-owned core overlay — loaded LAST so an upstream declaration always wins on conflict; these
|
|
83
|
-
# reopenings only fill genuine holes (e.g. `Numeric#to_f`/`to_i`/`to_r`, which upstream RBS
|
|
84
|
-
# declares on the concrete subclasses but not on the abstract `Numeric` that Rigor's
|
|
85
|
-
# arithmetic-chain widening produces).
|
|
86
|
-
core_overlay_sig_paths.each do |path|
|
|
87
|
-
rbs_loader.add(path: path) if path.directory?
|
|
88
|
-
end
|
|
81
|
+
add_bundled_signatures(rbs_loader, loaded_libraries.to_set(&:to_s))
|
|
89
82
|
env = RBS::Environment.from_loader(rbs_loader)
|
|
90
83
|
add_project_signatures(env, signature_paths)
|
|
91
84
|
add_virtual_rbs(env, virtual_rbs)
|
|
@@ -94,10 +87,66 @@ module Rigor
|
|
|
94
87
|
stub_missing_referenced_types(env, resolved, project_sig_files(signature_paths))
|
|
95
88
|
end
|
|
96
89
|
|
|
90
|
+
# rbs ships `stdlib/bigdecimal/` and `stdlib/bigdecimal-math/` as two libraries, so
|
|
91
|
+
# `Environment::DEFAULT_LIBRARIES` names both. But `RBS::EnvironmentLoader` resolves a library to an
|
|
92
|
+
# INSTALLED GEM's `sig/` in preference to rbs's own `stdlib/` copy, and the `bigdecimal` gem has
|
|
93
|
+
# shipped `sig/big_math.rbs` — declaring the very same `BigMath` module — since 4.0. On such a host
|
|
94
|
+
# both declarations land in one environment, `RBS::DefinitionBuilder` raises
|
|
95
|
+
# `DuplicatedMethodDefinitionError` on `BigMath.E`, and the WHOLE module silently degrades to
|
|
96
|
+
# `Dynamic[top]` (issue #299) — every `BigMath` call, real method and typo alike, stops being
|
|
97
|
+
# witnessed.
|
|
98
|
+
#
|
|
99
|
+
# `bigdecimal-math` is the entry to drop, not `bigdecimal`: the gem's copy is maintained alongside
|
|
100
|
+
# the implementation, types `log10` / `log1p` / `expm1` / `tan` / `tanh` (which rbs's copy omits
|
|
101
|
+
# outright), and accepts `real | BigDecimal` where rbs's accepts only `BigDecimal`. Dropping it is
|
|
102
|
+
# therefore a strict typing gain, not a trade.
|
|
103
|
+
#
|
|
104
|
+
# Host-dependent by construction, and deliberately so: where `bigdecimal` resolves to rbs's own
|
|
105
|
+
# `stdlib/bigdecimal/` (an older gem with no `sig/`, or no gem at all), nothing collides, nothing is
|
|
106
|
+
# dropped, and `bigdecimal-math` still supplies `BigMath`.
|
|
107
|
+
BIGDECIMAL_LIBRARY = "bigdecimal"
|
|
108
|
+
private_constant :BIGDECIMAL_LIBRARY
|
|
109
|
+
BIGDECIMAL_MATH_LIBRARY = "bigdecimal-math"
|
|
110
|
+
private_constant :BIGDECIMAL_MATH_LIBRARY
|
|
111
|
+
# The one basename that decides it: `BigMath` lives in `big_math.rbs` on both sides.
|
|
112
|
+
BIG_MATH_SIG_BASENAME = "big_math.rbs"
|
|
113
|
+
private_constant :BIG_MATH_SIG_BASENAME
|
|
114
|
+
|
|
115
|
+
# @param libraries [Array<String>] the resolved library list, `DEFAULT_LIBRARIES` included.
|
|
116
|
+
# @return [Array<String>] the same list, minus `bigdecimal-math` when `bigdecimal` already brings
|
|
117
|
+
# `BigMath` in.
|
|
118
|
+
def libraries_without_shadowed_bigdecimal_math(libraries)
|
|
119
|
+
return libraries unless libraries.include?(BIGDECIMAL_LIBRARY) && libraries.include?(BIGDECIMAL_MATH_LIBRARY)
|
|
120
|
+
return libraries unless bigdecimal_library_declares_big_math?
|
|
121
|
+
|
|
122
|
+
libraries - [BIGDECIMAL_MATH_LIBRARY]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Resolves `library: "bigdecimal"` on a THROWAWAY loader (never the one building the env) and reports
|
|
126
|
+
# whether any directory it resolves to ships `big_math.rbs`. Fails soft to `false`: an unresolvable
|
|
127
|
+
# library must not take the whole environment build down, and keeping both entries is the
|
|
128
|
+
# pre-existing behaviour.
|
|
129
|
+
def bigdecimal_library_declares_big_math?
|
|
130
|
+
probe = ::RBS::EnvironmentLoader.new(core_root: nil)
|
|
131
|
+
return false unless probe.has_library?(library: BIGDECIMAL_LIBRARY, version: nil)
|
|
132
|
+
|
|
133
|
+
probe.add(library: BIGDECIMAL_LIBRARY, version: nil)
|
|
134
|
+
shadowed = false
|
|
135
|
+
probe.each_dir { |_source, dir| shadowed ||= dir.join(BIG_MATH_SIG_BASENAME).file? }
|
|
136
|
+
shadowed
|
|
137
|
+
rescue StandardError
|
|
138
|
+
false
|
|
139
|
+
end
|
|
140
|
+
|
|
97
141
|
# True when `content` parses as an RBS signature. {#virtual_rbs_collision_quarantined} uses this to
|
|
98
142
|
# tell a collision-dropped virtual entry (parses, but absent from the env) from a parse-failed one
|
|
99
143
|
# (the synthesizer's own WD6 skip, reported separately).
|
|
100
144
|
def parseable_rbs?(content)
|
|
145
|
+
# Pre-parser encoding guard ({.invalid_encoding?}): invalid UTF-8 raises `ArgumentError` (not
|
|
146
|
+
# `ParsingError`) out of `RBS::Parser.magic_comment`'s regex on rbs 4.1, escaping the rescue below,
|
|
147
|
+
# and could hang the C lexer outright on the older releases the gemspec supports.
|
|
148
|
+
return false if invalid_encoding?(content)
|
|
149
|
+
|
|
101
150
|
::RBS::Parser.parse_signature(::RBS::Buffer.new(name: "(rigor: virtual parse check)", content: content))
|
|
102
151
|
true
|
|
103
152
|
rescue ::RBS::BaseError
|
|
@@ -148,21 +197,29 @@ module Rigor
|
|
|
148
197
|
# calls to `Dynamic[Top]` instead (the same no-false-positive contract as the dependency-source
|
|
149
198
|
# tier).
|
|
150
199
|
#
|
|
151
|
-
# Detection
|
|
152
|
-
#
|
|
153
|
-
#
|
|
154
|
-
#
|
|
155
|
-
# quickly.
|
|
200
|
+
# Detection reads the PROJECT declarations and mirrors rbs's own membership test
|
|
201
|
+
# ({.unresolved_referenced_types}); it is bounded to `signature_paths` classes (stdlib / vendored RBS
|
|
202
|
+
# is well-formed) and to {MAX_STUB_PASSES} iterations — a fresh stub can expose a deeper reference the
|
|
203
|
+
# first pass could not see past, but empty stubs reference nothing, so the fixpoint converges quickly.
|
|
156
204
|
MAX_STUB_PASSES = 5
|
|
157
205
|
|
|
158
206
|
def stub_missing_referenced_types(base_env, resolved, project_files)
|
|
159
207
|
return resolved if project_files.empty?
|
|
160
208
|
|
|
209
|
+
previous = nil
|
|
161
210
|
MAX_STUB_PASSES.times do
|
|
162
211
|
missing = unresolved_referenced_types(resolved, project_files)
|
|
163
212
|
break if missing.empty?
|
|
164
213
|
|
|
165
|
-
|
|
214
|
+
# Bound the fixpoint by PROGRESS, not by the cap alone. A pass that appends no declaration, or
|
|
215
|
+
# that re-detects the set it already saw, cannot converge — and before this guard the cap was the
|
|
216
|
+
# only stop, so the pathological input paid the full detection sweep five times over (measured on
|
|
217
|
+
# herb: five passes, nothing synthesized). The cap stays as the backstop for a genuinely deepening
|
|
218
|
+
# chain of references.
|
|
219
|
+
current = missing.to_set
|
|
220
|
+
break if current == previous || !append_stub_declarations(base_env, missing)
|
|
221
|
+
|
|
222
|
+
previous = current
|
|
166
223
|
resolved = base_env.resolve_type_names
|
|
167
224
|
end
|
|
168
225
|
resolved
|
|
@@ -240,25 +297,48 @@ module Rigor
|
|
|
240
297
|
end
|
|
241
298
|
end
|
|
242
299
|
|
|
300
|
+
# The quarantine note for a file rejected by {.invalid_encoding?} — worded to be distinct from any
|
|
301
|
+
# rbs-emitted parse error so specs (and users) can tell Rigor's pre-parser skip from the parser's own
|
|
302
|
+
# UTF-8 diagnostics.
|
|
303
|
+
INVALID_ENCODING_NOTE = "not valid UTF-8 — skipped before reaching the RBS parser"
|
|
304
|
+
|
|
305
|
+
# Pre-parser guard for content Rigor hands to `RBS::Parser`. On rbs 4.1+ an invalid UTF-8 byte is a
|
|
306
|
+
# clean `ParsingError` (ruby/rbs#2983), but on the older releases the gemspec supports (`>= 3.0,
|
|
307
|
+
# < 5.0`) the C lexer could infinite-loop or abort on it (fixed upstream in ruby/rbs#2973) — a hang no
|
|
308
|
+
# `rescue` can catch, and the one failure mode the quarantine's fail-soft rescues cannot absorb. So
|
|
309
|
+
# the check runs before the parser on every rbs version: uniform behaviour, and the quarantine note
|
|
310
|
+
# stays actionable ("fix the file's encoding") rather than version-dependent.
|
|
311
|
+
def invalid_encoding?(content)
|
|
312
|
+
!content.valid_encoding?
|
|
313
|
+
end
|
|
314
|
+
|
|
243
315
|
# Parse one project `.rbs` into `[buffer, directives, decls]`, or nil when it is unparseable /
|
|
244
|
-
# unreadable. Mirrors `RBS::EnvironmentLoader#each_signature`'s per-file parse so
|
|
245
|
-
# identically to the loader's batch path.
|
|
316
|
+
# unreadable / not valid UTF-8. Mirrors `RBS::EnvironmentLoader#each_signature`'s per-file parse so
|
|
317
|
+
# the decls register identically to the loader's batch path.
|
|
246
318
|
def parse_signature_file(file)
|
|
247
|
-
|
|
319
|
+
content = File.read(file, encoding: "UTF-8")
|
|
320
|
+
return nil if invalid_encoding?(content)
|
|
321
|
+
|
|
322
|
+
buffer = ::RBS::Buffer.new(name: file, content: content)
|
|
248
323
|
_buffer, directives, decls = ::RBS::Parser.parse_signature(buffer)
|
|
249
324
|
[buffer, directives, decls]
|
|
250
325
|
rescue ::RBS::ParsingError, Errno::ENOENT, Errno::EISDIR, Errno::EACCES
|
|
251
326
|
nil
|
|
252
327
|
end
|
|
253
328
|
|
|
254
|
-
# The project `signature_paths:` files that FAIL to parse
|
|
255
|
-
# (sorted, deterministic). Detection is independent of
|
|
256
|
-
# even on a cache hit (where the env was already built
|
|
257
|
-
# re-parses the user's own (usually small) `sig/` set, and
|
|
258
|
-
# `signature_paths:`.
|
|
329
|
+
# The project `signature_paths:` files that FAIL to parse (or are not valid UTF-8), as
|
|
330
|
+
# `[absolute_path, first_error_line]` pairs (sorted, deterministic). Detection is independent of
|
|
331
|
+
# {.add_project_signatures} so the warning fires even on a cache hit (where the env was already built
|
|
332
|
+
# with the file quarantined). Cheap: it only re-parses the user's own (usually small) `sig/` set, and
|
|
333
|
+
# returns empty immediately when there is no `signature_paths:`.
|
|
259
334
|
def quarantined_project_signatures(signature_paths)
|
|
260
335
|
project_sig_files(signature_paths).sort.filter_map do |file|
|
|
261
|
-
|
|
336
|
+
# The note carries the path itself because the warn composer prints only this element — a
|
|
337
|
+
# `ParsingError` message embeds its `path:line:` prefix, so the composer never adds one.
|
|
338
|
+
content = File.read(file, encoding: "UTF-8")
|
|
339
|
+
next [file, "#{file}: #{INVALID_ENCODING_NOTE}"] if invalid_encoding?(content)
|
|
340
|
+
|
|
341
|
+
buffer = ::RBS::Buffer.new(name: file, content: content)
|
|
262
342
|
::RBS::Parser.parse_signature(buffer)
|
|
263
343
|
nil
|
|
264
344
|
rescue ::RBS::ParsingError => e
|
|
@@ -268,26 +348,122 @@ module Rigor
|
|
|
268
348
|
end
|
|
269
349
|
end
|
|
270
350
|
|
|
271
|
-
#
|
|
272
|
-
#
|
|
273
|
-
#
|
|
351
|
+
# The `::`-stripped names of every type a PROJECT signature references that no loaded declaration
|
|
352
|
+
# provides — the input to {.append_stub_declarations}.
|
|
353
|
+
#
|
|
354
|
+
# Detection READS the declarations; it does not build them. Until #207 it built every project class
|
|
355
|
+
# instance- and singleton-side with a throwaway `RBS::DefinitionBuilder` and recovered the name from
|
|
356
|
+
# the raised `NoTypeFoundError` — correct by construction, and **7.84M allocations, a third of a cold
|
|
357
|
+
# `check lib`** on Rigor's own tree, to find nothing once `sig/` is self-consistent. The walk below
|
|
358
|
+
# costs ~21k.
|
|
359
|
+
#
|
|
360
|
+
# It mirrors rbs's raise sites, so the answer is the builder's:
|
|
361
|
+
#
|
|
362
|
+
# * `DefinitionBuilder#validate_type_presence` over the ARGS of a super class, a module `self` type,
|
|
363
|
+
# and a mixin. The name itself is deliberately NOT checked: a missing super class or mixin raises
|
|
364
|
+
# `NoSuperclassFoundError` / `NoMixinFoundError`, which this pass has never stubbed.
|
|
365
|
+
# * `VarianceCalculator#type` over every method type `validate_type_params` reaches, which raises for
|
|
366
|
+
# `ClassInstance` / `Interface` / `Alias` only. It skips `initialize`, and it is not called for the
|
|
367
|
+
# singleton side — so a name reachable only through `def initialize:` or `def self.x:` is not
|
|
368
|
+
# reported here, exactly as the builder did not report it. Stubbing those names anyway cost
|
|
369
|
+
# allocations on the corpus and changed no diagnostic.
|
|
370
|
+
#
|
|
371
|
+
# Membership is decided with the builder's own predicate ({.declared_reference?}), so a name reported
|
|
372
|
+
# here is one no declaration in the env provides. A resolvable name can never be reported, which is
|
|
373
|
+
# what keeps the stub safe: ADR-5 tier 2 trades precision for a fail-soft, and a stub for a name that
|
|
374
|
+
# WOULD have resolved would shadow a real type instead.
|
|
375
|
+
#
|
|
376
|
+
# One reduction in scope. The builder also walked each project class's ANCESTORS, so a dangling
|
|
377
|
+
# reference inside a *gem's* signature reachable from a project class was reported too; this walk
|
|
378
|
+
# reads project declarations only. No such name occurs across the eight RBS-shipping projects the
|
|
379
|
+
# change was measured on, and the cost of missing one is the fail-soft `Dynamic` ADR-5 tier 2 already
|
|
380
|
+
# accepts — not a false diagnostic. A project INTERFACE's own dangling reference is likewise not
|
|
381
|
+
# reported, and likewise was not by the builder (`validate_type_params` does not variance-walk the
|
|
382
|
+
# methods a class imports from an interface) — pinned by spec so the two stay together.
|
|
383
|
+
#
|
|
384
|
+
# Equivalence with the builder sweep is pinned by spec, which keeps the sweep as its oracle. Full
|
|
385
|
+
# evaluation: `docs/notes/20260730-stub-pass1-static-detection-evaluation.md`.
|
|
274
386
|
def unresolved_referenced_types(env, project_files)
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
env.class_decls.
|
|
387
|
+
missing = {}
|
|
388
|
+
checked = {}
|
|
389
|
+
env.class_decls.each_value do |entry|
|
|
278
390
|
next unless project_entry?(entry, project_files)
|
|
279
391
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
392
|
+
entry_declarations(entry).each { |decl| collect_declaration_references(env, decl, missing, checked) }
|
|
393
|
+
end
|
|
394
|
+
missing.keys
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# Decl-level references the builder validates: a super class's / module self type's / mixin's type
|
|
398
|
+
# ARGUMENTS, plus every member. `super_class` is a `Declarations::Class` accessor and `self_types` a
|
|
399
|
+
# `Declarations::Module` one, so both are reached behind a shape check.
|
|
400
|
+
def collect_declaration_references(env, decl, missing, checked)
|
|
401
|
+
if decl.respond_to?(:super_class)
|
|
402
|
+
decl.super_class&.args&.each { |arg| collect_type_references(env, arg, missing, checked) }
|
|
403
|
+
end
|
|
404
|
+
if decl.respond_to?(:self_types)
|
|
405
|
+
decl.self_types&.each do |self_type|
|
|
406
|
+
self_type.args.each { |arg| collect_type_references(env, arg, missing, checked) }
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
collect_member_references(env, decl.members, missing, checked)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# Member-level references. `initialize` and the singleton side are skipped because
|
|
413
|
+
# `validate_type_params` never reaches them (see {.unresolved_referenced_types}); `:singleton_instance`
|
|
414
|
+
# (`def self?.x`) defines the instance side too, so it is NOT skipped.
|
|
415
|
+
def collect_member_references(env, members, missing, checked)
|
|
416
|
+
members.each do |member|
|
|
417
|
+
next if member.respond_to?(:kind) && member.kind == :singleton
|
|
418
|
+
|
|
419
|
+
case member
|
|
420
|
+
when ::RBS::AST::Members::MethodDefinition
|
|
421
|
+
next if member.name == :initialize
|
|
422
|
+
|
|
423
|
+
member.overloads.each do |overload|
|
|
424
|
+
collect_type_references(env, overload.method_type, missing, checked)
|
|
425
|
+
end
|
|
426
|
+
when ::RBS::AST::Members::AttrReader, ::RBS::AST::Members::AttrWriter,
|
|
427
|
+
::RBS::AST::Members::AttrAccessor
|
|
428
|
+
collect_type_references(env, member.type, missing, checked)
|
|
429
|
+
when ::RBS::AST::Members::Include, ::RBS::AST::Members::Extend,
|
|
430
|
+
::RBS::AST::Members::Prepend
|
|
431
|
+
member.args.each { |arg| collect_type_references(env, arg, missing, checked) }
|
|
288
432
|
end
|
|
289
433
|
end
|
|
290
|
-
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# Walks one type (or method type) and appends the names no declaration provides. Only the three node
|
|
437
|
+
# classes `VarianceCalculator#type` raises for carry a checkable name; everything else is traversed for
|
|
438
|
+
# the types nested inside it.
|
|
439
|
+
def collect_type_references(env, type, missing, checked)
|
|
440
|
+
return if type.nil?
|
|
441
|
+
|
|
442
|
+
case type
|
|
443
|
+
when ::RBS::Types::ClassInstance, ::RBS::Types::Interface, ::RBS::Types::Alias
|
|
444
|
+
name = type.name
|
|
445
|
+
name = name.absolute! unless name.absolute?
|
|
446
|
+
missing[name.to_s.sub(/\A::/, "")] = true unless declared_reference?(env, name, checked)
|
|
447
|
+
end
|
|
448
|
+
return unless type.respond_to?(:each_type)
|
|
449
|
+
|
|
450
|
+
type.each_type do |nested|
|
|
451
|
+
collect_type_references(env, nested, missing, checked)
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# `DefinitionBuilder#validate_type_name`'s membership test, memoised per env walk (a project's method
|
|
456
|
+
# signatures name the same handful of types over and over). A name whose normalization raises is
|
|
457
|
+
# treated as declared: repairing it is not this pass's job, and a wrong stub is the worse failure.
|
|
458
|
+
def declared_reference?(env, name, checked)
|
|
459
|
+
key = name.to_s
|
|
460
|
+
return checked[key] if checked.key?(key)
|
|
461
|
+
|
|
462
|
+
checked[key] = begin
|
|
463
|
+
env.type_name?(env.normalize_type_name(name))
|
|
464
|
+
rescue StandardError
|
|
465
|
+
true
|
|
466
|
+
end
|
|
291
467
|
end
|
|
292
468
|
|
|
293
469
|
# Normalises a `class_decls` entry's representative declaration across the gemspec's supported RBS
|
|
@@ -304,6 +480,24 @@ module Rigor
|
|
|
304
480
|
end
|
|
305
481
|
end
|
|
306
482
|
|
|
483
|
+
# Collects the AST declaration nodes behind a `class_decls` entry across the supported RBS range (`rbs
|
|
484
|
+
# >= 3.0, < 5.0`). RBS 4's `ModuleEntry` / `ClassEntry` expose `each_decl` yielding bare AST
|
|
485
|
+
# declarations; RBS 3.x exposes `decls`, an array of `MultiEntry::D` wrappers whose `#decl` is the AST
|
|
486
|
+
# declaration. The single-`decl` shape is handled defensively so the loader survives an rbs-gem minor
|
|
487
|
+
# bump. Class-side because both the env-build detection walk and the instance-side
|
|
488
|
+
# `#names_synthesized_in` need it, and the guard must stay single-rooted.
|
|
489
|
+
def entry_declarations(entry)
|
|
490
|
+
if entry.respond_to?(:each_decl)
|
|
491
|
+
[].tap { |acc| entry.each_decl { |decl| acc << decl } }
|
|
492
|
+
elsif entry.respond_to?(:decls)
|
|
493
|
+
entry.decls.map { |d| d.respond_to?(:decl) ? d.decl : d }
|
|
494
|
+
elsif entry.respond_to?(:decl)
|
|
495
|
+
[entry.decl]
|
|
496
|
+
else
|
|
497
|
+
[]
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
|
|
307
501
|
# Appends freshly-parsed declarations to an `RBS::Environment` across the gemspec's supported RBS
|
|
308
502
|
# range (`rbs >= 3.0, < 5.0`). RBS 4.x wraps the declarations in an `RBS::Source::RBS` and takes them
|
|
309
503
|
# through `env.add_source`; RBS 3.x has neither `RBS::Source` nor `add_source` and instead registers
|
|
@@ -336,9 +530,17 @@ module Rigor
|
|
|
336
530
|
end
|
|
337
531
|
|
|
338
532
|
# Adds empty stub declarations for the missing referenced types (and any enclosing namespace they
|
|
339
|
-
# need) to the pre-resolve env, tagged with {SYNTHETIC_STUB_BUFFER}.
|
|
340
|
-
#
|
|
341
|
-
#
|
|
533
|
+
# need) to the pre-resolve env, tagged with {SYNTHETIC_STUB_BUFFER}. Returns true when at least one
|
|
534
|
+
# declaration landed, so {.stub_missing_referenced_types} can stop on a pass that made no progress.
|
|
535
|
+
#
|
|
536
|
+
# Each name gets the declaration kind its own syntax requires ({.stub_declaration_for}), and each
|
|
537
|
+
# declaration is validated ALONE before it joins the buffer. Declaring every name `class` — the shape
|
|
538
|
+
# before #237 — made a dangling interface (`_Foo`) or type-alias (`foo`) reference unparseable, and
|
|
539
|
+
# since the batch shares one buffer the `RBS::BaseError` rescue below then discarded every stub in it,
|
|
540
|
+
# well-formed ones included. Measured on herb, whose 74 missing names are all dangling type aliases:
|
|
541
|
+
# the pass synthesized nothing at all, leaving the project's 48 signature files inert, while still
|
|
542
|
+
# paying for the detection sweep. A per-declaration check costs one small parse per name and bounds
|
|
543
|
+
# the damage of one bad name to itself.
|
|
342
544
|
#
|
|
343
545
|
# Names already declared in `base_env` are skipped — exactly the `declared.include?` guard
|
|
344
546
|
# {.collect_missing_namespaces} applies. Without it, stubbing a nested reference (`Foo::Bar::Baz`)
|
|
@@ -356,17 +558,45 @@ module Rigor
|
|
|
356
558
|
(1...parts.length).each { |i| names << parts[0, i].join("::") }
|
|
357
559
|
end
|
|
358
560
|
names = names.reject { |name| declared.include?(name) }.to_set
|
|
359
|
-
return if names.empty?
|
|
561
|
+
return false if names.empty?
|
|
360
562
|
|
|
361
|
-
source = names.sort_by { |n| n.count(":") }.
|
|
362
|
-
|
|
363
|
-
|
|
563
|
+
source = names.sort_by { |n| n.count(":") }.filter_map do |name|
|
|
564
|
+
declaration = stub_declaration_for(name, names)
|
|
565
|
+
declaration if parseable_rbs?(declaration)
|
|
364
566
|
end.join
|
|
567
|
+
return false if source.empty?
|
|
568
|
+
|
|
365
569
|
buffer = ::RBS::Buffer.new(name: SYNTHETIC_STUB_BUFFER, content: source)
|
|
366
570
|
_, directives, decls = ::RBS::Parser.parse_signature(buffer)
|
|
367
571
|
add_parsed_decls(base_env, buffer, directives, decls)
|
|
572
|
+
true
|
|
368
573
|
rescue ::RBS::BaseError
|
|
369
|
-
|
|
574
|
+
false
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
# The declaration one stubbed name needs, keyed on the syntax of its leaf:
|
|
578
|
+
#
|
|
579
|
+
# * a name other stubbed names nest inside is a namespace, so `module`;
|
|
580
|
+
# * an RBS interface name (`_Foo`) may only be declared `interface`;
|
|
581
|
+
# * a type-alias name (`foo`) may only be declared `type`, and aliases `untyped` so a value of that
|
|
582
|
+
# type reads as `Dynamic[Top]` — the honest answer for a type Rigor invented;
|
|
583
|
+
# * anything else is `class` (referenced types appear in instance position far more often than as
|
|
584
|
+
# mixins).
|
|
585
|
+
#
|
|
586
|
+
# The interface stub is FP-safe without joining {#synthesized_type_names}: {RbsTypeTranslator} maps
|
|
587
|
+
# every interface type to untyped, and the nil / non-nil acceptance guards in {Analysis::CheckRules}
|
|
588
|
+
# treat a method-less interface as accepting everything.
|
|
589
|
+
def stub_declaration_for(name, names)
|
|
590
|
+
leaf = name.split("::").last.to_s
|
|
591
|
+
if names.any? { |other| other != name && other.start_with?("#{name}::") }
|
|
592
|
+
"module #{name}\nend\n"
|
|
593
|
+
elsif leaf.start_with?("_")
|
|
594
|
+
"interface #{name}\nend\n"
|
|
595
|
+
elsif leaf.match?(/\A[a-z]/)
|
|
596
|
+
"type #{name} = untyped\n"
|
|
597
|
+
else
|
|
598
|
+
"class #{name}\nend\n"
|
|
599
|
+
end
|
|
370
600
|
end
|
|
371
601
|
|
|
372
602
|
# The `::`-stripped names of every class / module / class-alias declaration already present in
|
|
@@ -389,6 +619,9 @@ module Rigor
|
|
|
389
619
|
|
|
390
620
|
virtual_rbs.each do |filename, content|
|
|
391
621
|
next if content.nil? || content.empty?
|
|
622
|
+
# Same pre-parser guard as {.parse_signature_file}: a synthesizer echoing project bytes can carry
|
|
623
|
+
# invalid UTF-8, which pre-4.1 rbs lexers could hang on — and a hang escapes the rescue below.
|
|
624
|
+
next if invalid_encoding?(content.to_s)
|
|
392
625
|
|
|
393
626
|
buffer = ::RBS::Buffer.new(name: filename.to_s, content: content.to_s)
|
|
394
627
|
_, directives, decls = ::RBS::Parser.parse_signature(buffer)
|
|
@@ -437,6 +670,71 @@ module Rigor
|
|
|
437
670
|
[Pathname(CORE_OVERLAY_SIGS_ROOT)]
|
|
438
671
|
end
|
|
439
672
|
|
|
673
|
+
# Bundled signature sources that SUPPLEMENT a stdlib library's own declarations rather than stand
|
|
674
|
+
# alone: each re-opens a class the named `RBS::EnvironmentLoader` library declares with a mixin
|
|
675
|
+
# (`CGI::QueryExtension`), a superclass (`Prism::Result`), or an `| ...` overload continuation
|
|
676
|
+
# (`StringScanner#[]`, `Resolv#initialize`). Loading one into an environment built WITHOUT its
|
|
677
|
+
# library does not fail the env build — `RBS::DefinitionBuilder` raises later
|
|
678
|
+
# (`NoMixinFoundError` / `NoSuperclassFoundError` / `InvalidOverloadMethodError`), Rigor fails soft,
|
|
679
|
+
# and the WHOLE re-opened class silently degrades to `Dynamic[top]` (issue #299's narrow-environment
|
|
680
|
+
# tail). So each supplement is loaded ONLY when its library actually resolved; in production every
|
|
681
|
+
# gating library is in `Environment::DEFAULT_LIBRARIES`, so the full environment is unchanged.
|
|
682
|
+
#
|
|
683
|
+
# `core_overlay/pathname.rbs` also carries an `| ...` continuation but is deliberately NOT listed:
|
|
684
|
+
# its base (`Pathname#expand_path`) lives in rbs's `core/pathname.rbs` on every supported rbs
|
|
685
|
+
# release (3.10 / 4.0 / 4.1), so the continuation is safe — and applicable — with no library loaded.
|
|
686
|
+
#
|
|
687
|
+
# Keyed by `data/vendored_gem_sigs/` directory basename.
|
|
688
|
+
LIBRARY_SUPPLEMENT_VENDORED_DIRS = {
|
|
689
|
+
"cgi" => "cgi",
|
|
690
|
+
"prism" => "prism"
|
|
691
|
+
}.freeze
|
|
692
|
+
private_constant :LIBRARY_SUPPLEMENT_VENDORED_DIRS
|
|
693
|
+
|
|
694
|
+
# Keyed by `data/core_overlay/` file basename. Files not listed here re-open Ruby-core classes (or
|
|
695
|
+
# add self-contained declarations) and stay unconditional.
|
|
696
|
+
LIBRARY_SUPPLEMENT_CORE_OVERLAYS = {
|
|
697
|
+
"resolv.rbs" => "resolv",
|
|
698
|
+
"string_scanner.rbs" => "strscan"
|
|
699
|
+
}.freeze
|
|
700
|
+
private_constant :LIBRARY_SUPPLEMENT_CORE_OVERLAYS
|
|
701
|
+
|
|
702
|
+
# Adds the Rigor-shipped signature sources to `rbs_loader`: every `data/vendored_gem_sigs/<gem>/`
|
|
703
|
+
# directory, then the `data/core_overlay/` files — the overlay LAST so an upstream declaration
|
|
704
|
+
# always wins on conflict (these reopenings only fill genuine holes, e.g. `Numeric#to_f`/`to_i`/
|
|
705
|
+
# `to_r`, which upstream RBS declares on the concrete subclasses but not on the abstract `Numeric`
|
|
706
|
+
# that Rigor's arithmetic-chain widening produces). The overlay is added per-file, not
|
|
707
|
+
# per-directory, because the `LIBRARY_SUPPLEMENT_CORE_OVERLAYS` files must be gated individually.
|
|
708
|
+
#
|
|
709
|
+
# @param loaded_library_names [Set<String>] libraries that actually resolved on this loader.
|
|
710
|
+
def add_bundled_signatures(rbs_loader, loaded_library_names)
|
|
711
|
+
vendored_gem_sig_paths.each do |path|
|
|
712
|
+
next unless path.directory?
|
|
713
|
+
next unless supplement_dependency_loaded?(LIBRARY_SUPPLEMENT_VENDORED_DIRS, path, loaded_library_names)
|
|
714
|
+
|
|
715
|
+
rbs_loader.add(path: path)
|
|
716
|
+
end
|
|
717
|
+
core_overlay_sig_paths.each do |dir|
|
|
718
|
+
next unless dir.directory?
|
|
719
|
+
|
|
720
|
+
dir.children.sort.each do |file|
|
|
721
|
+
next unless file.file? && file.extname == ".rbs"
|
|
722
|
+
next unless supplement_dependency_loaded?(LIBRARY_SUPPLEMENT_CORE_OVERLAYS, file, loaded_library_names)
|
|
723
|
+
|
|
724
|
+
rbs_loader.add(path: file)
|
|
725
|
+
end
|
|
726
|
+
end
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
# @param supplements [Hash{String => String}] basename → gating library map.
|
|
730
|
+
# @param path [Pathname] the vendored directory or overlay file to test.
|
|
731
|
+
# @param loaded_library_names [Set<String>] libraries that actually resolved on this loader.
|
|
732
|
+
# @return [Boolean] true when `path` carries no library dependency, or its library loaded.
|
|
733
|
+
def supplement_dependency_loaded?(supplements, path, loaded_library_names)
|
|
734
|
+
library = supplements[path.basename.to_s]
|
|
735
|
+
library.nil? || loaded_library_names.include?(library)
|
|
736
|
+
end
|
|
737
|
+
|
|
440
738
|
# Rigor-owned per-gem RBS overlays (`data/gem_overlay/<gem>/`), ADR-72. Unlike the unconditional
|
|
441
739
|
# `core_overlay`, each gem's overlay is loaded ONLY when that gem is locked in the project's
|
|
442
740
|
# Gemfile.lock but ships no RBS of its own — {Environment.for_project} decides eligibility and passes
|
|
@@ -506,10 +804,11 @@ module Rigor
|
|
|
506
804
|
@virtual_rbs = virtual_rbs.map { |name, content| [name.to_s.dup.freeze, content.to_s.dup.freeze].freeze }.freeze
|
|
507
805
|
# Per-loader memoization bucket. Held as a single mutable Hash so the loader instance itself can be
|
|
508
806
|
# `.freeze`d (per ADR-15 reflection-facade contract) without losing the lazy-memo behaviour. Slot
|
|
509
|
-
# names currently consulted: `:env`, `:env_loaded`, `:env_build_warned`, `:
|
|
510
|
-
# `:instance_definitions_table`, `:singleton_definitions_table`.
|
|
511
|
-
# `{ ... }` literal) so Rigor's `HashShape` narrowing doesn't
|
|
512
|
-
# state and fold post-initial slot reads (e.g.
|
|
807
|
+
# names currently consulted: `:env`, `:env_loaded`, `:env_build_warned`, `:definition_build_warned`,
|
|
808
|
+
# `:builder`, `:reflection`, `:instance_definitions_table`, `:singleton_definitions_table`.
|
|
809
|
+
# Constructed via `Hash.new` (NOT a `{ ... }` literal) so Rigor's `HashShape` narrowing doesn't
|
|
810
|
+
# infer a fixed key set from the initial state and fold post-initial slot reads (e.g.
|
|
811
|
+
# `@state[:env_loaded]`) to a constant `nil`.
|
|
513
812
|
@state = Hash.new # rubocop:disable Style/EmptyLiteral
|
|
514
813
|
@instance_definition_cache = {}
|
|
515
814
|
@singleton_definition_cache = {}
|
|
@@ -580,6 +879,11 @@ module Rigor
|
|
|
580
879
|
# that mention them could build (e.g. an unavailable `DRb::DRbServer`, or a stale
|
|
581
880
|
# `Textbringer::EditorError`). Recovered off the built env like {#synthesized_namespaces}, so it
|
|
582
881
|
# survives the marshalled-env cache.
|
|
882
|
+
#
|
|
883
|
+
# Class / module stubs only — the `interface` and `type` stubs {.stub_declaration_for} also emits live in
|
|
884
|
+
# `interface_decls` / `type_alias_decls`, not `class_decls`. That is deliberate: this list exists to feed
|
|
885
|
+
# {#synthesized_type_names}, whose consumers key on a nominal receiver's class name, and both of those
|
|
886
|
+
# kinds already read as untyped through {Inference::RbsTypeTranslator}.
|
|
583
887
|
def synthesized_stub_types
|
|
584
888
|
names_synthesized_in(SYNTHETIC_STUB_BUFFER)
|
|
585
889
|
end
|
|
@@ -963,7 +1267,7 @@ module Rigor
|
|
|
963
1267
|
return [] if e.nil?
|
|
964
1268
|
|
|
965
1269
|
names = e.class_decls.filter_map do |type_name, entry|
|
|
966
|
-
decls = entry_declarations(entry)
|
|
1270
|
+
decls = self.class.entry_declarations(entry)
|
|
967
1271
|
next if decls.empty?
|
|
968
1272
|
next unless decls.all? { |decl| synthetic_decl?(decl, buffer_name) }
|
|
969
1273
|
|
|
@@ -972,23 +1276,6 @@ module Rigor
|
|
|
972
1276
|
names.sort_by { |name| name.count("::") }
|
|
973
1277
|
end
|
|
974
1278
|
|
|
975
|
-
# Collects the AST declaration nodes behind a `class_decls` entry across the supported RBS range (`rbs
|
|
976
|
-
# >= 3.0, < 5.0`). RBS 4's `ModuleEntry` / `ClassEntry` expose `each_decl` yielding bare AST
|
|
977
|
-
# declarations; RBS 3.x exposes `decls`, an array of `MultiEntry::D` wrappers whose `#decl` is the AST
|
|
978
|
-
# declaration. The single-`decl` shape is handled defensively so the loader survives an rbs-gem minor
|
|
979
|
-
# bump.
|
|
980
|
-
def entry_declarations(entry)
|
|
981
|
-
if entry.respond_to?(:each_decl)
|
|
982
|
-
[].tap { |acc| entry.each_decl { |decl| acc << decl } }
|
|
983
|
-
elsif entry.respond_to?(:decls)
|
|
984
|
-
entry.decls.map { |d| d.respond_to?(:decl) ? d.decl : d }
|
|
985
|
-
elsif entry.respond_to?(:decl)
|
|
986
|
-
[entry.decl]
|
|
987
|
-
else
|
|
988
|
-
[]
|
|
989
|
-
end
|
|
990
|
-
end
|
|
991
|
-
|
|
992
1279
|
# True when an AST declaration was emitted into `buffer_name` (one of the synthetic-source sentinels)
|
|
993
1280
|
# — identified by the buffer name on its location.
|
|
994
1281
|
def synthetic_decl?(decl, buffer_name)
|
|
@@ -1230,7 +1517,8 @@ module Rigor
|
|
|
1230
1517
|
return nil unless env.class_decls.key?(rbs_name)
|
|
1231
1518
|
|
|
1232
1519
|
builder.build_instance(rbs_name)
|
|
1233
|
-
rescue ::RBS::BaseError
|
|
1520
|
+
rescue ::RBS::BaseError => e
|
|
1521
|
+
warn_about_definition_build_failure(class_name, e)
|
|
1234
1522
|
nil
|
|
1235
1523
|
end
|
|
1236
1524
|
|
|
@@ -1243,10 +1531,81 @@ module Rigor
|
|
|
1243
1531
|
return nil unless env.class_decls.key?(rbs_name)
|
|
1244
1532
|
|
|
1245
1533
|
builder.build_singleton(rbs_name)
|
|
1246
|
-
rescue ::RBS::BaseError
|
|
1534
|
+
rescue ::RBS::BaseError => e
|
|
1535
|
+
warn_about_definition_build_failure(class_name, e)
|
|
1247
1536
|
nil
|
|
1248
1537
|
end
|
|
1249
1538
|
|
|
1539
|
+
# The third twin of {#warn_about_quarantined_signatures} / {#warn_about_virtual_rbs_collisions}: name,
|
|
1540
|
+
# once per class per PROCESS, a `RBS::DefinitionBuilder` failure caught in
|
|
1541
|
+
# {#build_instance_definition} / {#build_singleton_definition}'s rescue. Without it, `class_known?`
|
|
1542
|
+
# stays true (it only consults {#known_class_names_set}, never a definition build), so every call on
|
|
1543
|
+
# the class — real methods and typos alike — silently degrades to `Dynamic[top]`.
|
|
1544
|
+
#
|
|
1545
|
+
# STRUCTURAL DIVERGENCE from its two siblings: they fire from the single central site at the end of
|
|
1546
|
+
# {#env}, because the whole env is built eagerly and every quarantine/collision is already known by
|
|
1547
|
+
# then. Definition builds are LAZY (ADR-54 WD1 — built on demand per class the FIRST time a caller asks,
|
|
1548
|
+
# long after {#env} has already run), so there is no later central checkpoint to fire from before the
|
|
1549
|
+
# affected classes even exist. This warns inline at the rescue site instead, gated on
|
|
1550
|
+
# `@state[:definition_build_warned]` (keyed by class name) so the instance and singleton sides — and
|
|
1551
|
+
# any re-entry once the per-process `@instance_definition_cache` / `@singleton_definition_cache`
|
|
1552
|
+
# memoize the failure — warn at most once per class name, cache-hit runs included (a definition build
|
|
1553
|
+
# is per-process regardless of the RBS-env cache tier). `@state` is per-LOADER-INSTANCE, not
|
|
1554
|
+
# process-global, so under the fork-based analysis pool each worker holds its own loader and its own
|
|
1555
|
+
# `@state`: a class whose definition fails can print its warning once per worker that happens to touch
|
|
1556
|
+
# it, i.e. more than once in a single `rigor check` run. Deduplicating that across processes is out of
|
|
1557
|
+
# scope here — see [#295](https://github.com/rigortype/rigor/issues/295).
|
|
1558
|
+
def warn_about_definition_build_failure(class_name, error)
|
|
1559
|
+
warned = (@state[:definition_build_warned] ||= {})
|
|
1560
|
+
key = class_name.to_s
|
|
1561
|
+
return if warned[key]
|
|
1562
|
+
|
|
1563
|
+
warned[key] = true
|
|
1564
|
+
first_line = error.message.to_s.lines.first.to_s.strip
|
|
1565
|
+
buffers = definition_build_conflict_buffers(error)
|
|
1566
|
+
collisions =
|
|
1567
|
+
if buffers.empty?
|
|
1568
|
+
""
|
|
1569
|
+
else
|
|
1570
|
+
listed = buffers.first(QUARANTINE_WARN_LIMIT)
|
|
1571
|
+
more = buffers.size - listed.size
|
|
1572
|
+
lines = listed.map { |name| " - #{name}" }
|
|
1573
|
+
lines << " … and #{more} more" if more.positive?
|
|
1574
|
+
"\n Colliding declaration(s):\n#{lines.join("\n")}"
|
|
1575
|
+
end
|
|
1576
|
+
warn(
|
|
1577
|
+
"rigor: RBS definition build failed for `#{class_name}`: #{error.class}: #{first_line}\n " \
|
|
1578
|
+
"Rigor still treats the class as known, so calls into it now silently degrade to\n " \
|
|
1579
|
+
"`Dynamic[top]` — real methods and typos alike — instead of resolving normally.#{collisions}"
|
|
1580
|
+
)
|
|
1581
|
+
end
|
|
1582
|
+
|
|
1583
|
+
# The definition-build twin of {#env_build_conflict_buffers}: the declaration source file(s) named by a
|
|
1584
|
+
# `RBS::DefinitionBuilder` failure, so {#warn_about_definition_build_failure} can name the colliding
|
|
1585
|
+
# declarations rather than only the exception's class and message. The payload shape varies by error
|
|
1586
|
+
# class (`references/rbs`'s `lib/rbs/errors.rb`): `DuplicatedMethodDefinitionError` and
|
|
1587
|
+
# `InvalidOverloadMethodError` carry `#members` (each with a `#location`);
|
|
1588
|
+
# `DuplicatedInterfaceMethodDefinitionError` carries a single `#member`; most others
|
|
1589
|
+
# (`NoSuperclassFoundError`, `UnknownMethodAliasError`, `RecursiveAncestorError`, …) carry only a bare
|
|
1590
|
+
# `#location`. Falls back to an empty list — the warning still names the class and the exception's own
|
|
1591
|
+
# message — for the remainder (e.g. `SuperclassMismatchError`, which carries neither).
|
|
1592
|
+
def definition_build_conflict_buffers(error)
|
|
1593
|
+
locations =
|
|
1594
|
+
if error.respond_to?(:members) && error.members
|
|
1595
|
+
Array(error.members).map(&:location)
|
|
1596
|
+
elsif error.respond_to?(:member) && error.member
|
|
1597
|
+
[error.member.location]
|
|
1598
|
+
elsif error.respond_to?(:location)
|
|
1599
|
+
[error.location]
|
|
1600
|
+
else
|
|
1601
|
+
[]
|
|
1602
|
+
end
|
|
1603
|
+
|
|
1604
|
+
locations.compact.filter_map { |loc| loc.buffer&.name }.map(&:to_s).uniq.freeze
|
|
1605
|
+
rescue ::RBS::BaseError, StandardError
|
|
1606
|
+
[].freeze
|
|
1607
|
+
end
|
|
1608
|
+
|
|
1250
1609
|
# Resolve an RBS class/module ALIAS to its canonical declared name. `class Mutex = Thread::Mutex`
|
|
1251
1610
|
# lives only in `class_alias_decls`, so `class_known?` reports it (it checks that table) but the
|
|
1252
1611
|
# definition builder — which only knows `class_decls` — could not enumerate its methods, leaving alias
|