rigortype 0.3.1 → 0.3.3
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/04-diagnostics.md +8 -0
- 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/always_truthy_condition_collector.rb +8 -0
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +53 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +7 -0
- data/lib/rigor/analysis/check_rules.rb +107 -28
- data/lib/rigor/analysis/incremental_session.rb +156 -11
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +14 -0
- data/lib/rigor/analysis/rule_catalog.rb +5 -1
- 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/analysis/severity_stamp.rb +2 -1
- 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/anonymous_meta_class.rb +81 -0
- data/lib/rigor/inference/expression_typer.rb +62 -4
- data/lib/rigor/inference/fork_map.rb +47 -4
- 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 +158 -12
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +64 -5
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +234 -11
- 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/method_dispatcher.rb +41 -10
- data/lib/rigor/inference/mutation_widening.rb +30 -25
- data/lib/rigor/inference/narrowing.rb +3 -1
- data/lib/rigor/inference/optimistic_origin.rb +123 -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 +197 -55
- data/lib/rigor/inference/singleton_object_constant.rb +63 -0
- data/lib/rigor/inference/statement_evaluator.rb +55 -4
- 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/node_rule_walk.rb +6 -0
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
- data/lib/rigor/protection/closure_kill_oracle.rb +190 -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 +114 -10
- 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/source/node_walker.rb +12 -0
- data/lib/rigor/type/anonymous_class_name.rb +40 -0
- data/lib/rigor/type/nominal.rb +5 -2
- data/lib/rigor/type/singleton.rb +4 -1
- 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 +12 -0
- data/sig/rigor.rbs +4 -0
- metadata +21 -1
data/lib/rigor/scope.rb
CHANGED
|
@@ -23,8 +23,10 @@ module Rigor
|
|
|
23
23
|
:indexed_narrowings, :method_chain_narrowings,
|
|
24
24
|
:declaration_sourced,
|
|
25
25
|
:source_path, :discovery, :struct_fold_safe_locals,
|
|
26
|
+
:opaque_block_self,
|
|
26
27
|
:dynamic_origins, :local_origins, :ivar_origins,
|
|
27
|
-
:void_origins
|
|
28
|
+
:void_origins,
|
|
29
|
+
:optimistic_origins, :optimistic_locals, :optimistic_ivars
|
|
28
30
|
|
|
29
31
|
# ADR-53 Track A — the seed-time discovery tables live on the {DiscoveryIndex} the scope carries by a single
|
|
30
32
|
# reference; the per-table readers stay on Scope so engine call sites and plugins are unaffected by the
|
|
@@ -146,10 +148,14 @@ module Rigor
|
|
|
146
148
|
declaration_sourced: EMPTY_DECLARATION_SOURCED,
|
|
147
149
|
source_path: nil,
|
|
148
150
|
struct_fold_safe_locals: EMPTY_FOLD_SAFE,
|
|
151
|
+
opaque_block_self: false,
|
|
149
152
|
dynamic_origins: {}.compare_by_identity,
|
|
150
153
|
local_origins: EMPTY_ORIGINS,
|
|
151
154
|
ivar_origins: EMPTY_ORIGINS,
|
|
152
|
-
void_origins: {}.compare_by_identity
|
|
155
|
+
void_origins: {}.compare_by_identity,
|
|
156
|
+
optimistic_origins: {}.compare_by_identity,
|
|
157
|
+
optimistic_locals: EMPTY_ORIGINS,
|
|
158
|
+
optimistic_ivars: EMPTY_ORIGINS
|
|
153
159
|
)
|
|
154
160
|
@environment = environment
|
|
155
161
|
@locals = locals
|
|
@@ -164,13 +170,41 @@ module Rigor
|
|
|
164
170
|
@declaration_sourced = declaration_sourced
|
|
165
171
|
@source_path = source_path
|
|
166
172
|
@struct_fold_safe_locals = struct_fold_safe_locals
|
|
173
|
+
@opaque_block_self = opaque_block_self
|
|
167
174
|
@dynamic_origins = dynamic_origins
|
|
168
175
|
@local_origins = local_origins
|
|
169
176
|
@ivar_origins = ivar_origins
|
|
170
177
|
@void_origins = void_origins
|
|
178
|
+
@optimistic_origins = optimistic_origins
|
|
179
|
+
@optimistic_locals = optimistic_locals
|
|
180
|
+
@optimistic_ivars = optimistic_ivars
|
|
171
181
|
freeze
|
|
172
182
|
end
|
|
173
183
|
|
|
184
|
+
# Issue #286 — the {Inference::OptimisticOrigin} cause attached to a call node whose result is nil-free
|
|
185
|
+
# only because `RbsDispatch` reads past `%a{implicitly-returns-nil}`, or `nil` when the value's
|
|
186
|
+
# nil-freeness is a property of its class. Mirrors {#dynamic_origins} / {#void_origins}: advisory
|
|
187
|
+
# metadata, ignored by `==` / `hash`, and never varying a flow decision on its own.
|
|
188
|
+
def record_optimistic_origin(node, cause)
|
|
189
|
+
@optimistic_origins[node] = cause
|
|
190
|
+
self
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def optimistic_local(name) = @optimistic_locals[name.to_sym]
|
|
194
|
+
def optimistic_ivar(name) = @optimistic_ivars[name.to_sym]
|
|
195
|
+
|
|
196
|
+
def with_optimistic_local(name, cause)
|
|
197
|
+
return self if cause.nil?
|
|
198
|
+
|
|
199
|
+
rebuild(optimistic_locals: @optimistic_locals.merge(name.to_sym => cause).freeze)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def with_optimistic_ivar(name, cause)
|
|
203
|
+
return self if cause.nil?
|
|
204
|
+
|
|
205
|
+
rebuild(optimistic_ivars: @optimistic_ivars.merge(name.to_sym => cause).freeze)
|
|
206
|
+
end
|
|
207
|
+
|
|
174
208
|
# ADR-82 WD1 — the propagated origin of the `Dynamic` value currently bound to a local / instance
|
|
175
209
|
# variable, or `nil` when none is tracked. Consulted by `Inference::ProtectionScanner` when a dispatch's
|
|
176
210
|
# receiver is a bare `x` / `@x` read whose own node carries no origin.
|
|
@@ -215,7 +249,8 @@ module Rigor
|
|
|
215
249
|
indexed_narrowings: new_indexed_narrowings,
|
|
216
250
|
method_chain_narrowings: new_chain_narrowings,
|
|
217
251
|
declaration_sourced: drop_declaration_sourced_for(:local, name),
|
|
218
|
-
local_origins: drop_origin(@local_origins, name)
|
|
252
|
+
local_origins: drop_origin(@local_origins, name),
|
|
253
|
+
optimistic_locals: drop_origin(@optimistic_locals, name))
|
|
219
254
|
end
|
|
220
255
|
|
|
221
256
|
def with_fact(fact)
|
|
@@ -242,6 +277,21 @@ module Rigor
|
|
|
242
277
|
rebuild(struct_fold_safe_locals: locals)
|
|
243
278
|
end
|
|
244
279
|
|
|
280
|
+
# Issue #316 — marks a block body whose `self` Rigor does not model. Ruby gives a block no `self` of its
|
|
281
|
+
# own: the yielding method decides, and `instance_eval` / `instance_exec` (the mechanism behind every
|
|
282
|
+
# `self`-rebinding DSL — RSpec example groups, `Class.new { … }`, Rake, Sinatra) is indistinguishable from
|
|
283
|
+
# `Array#each` without knowing the callee. The flag is set at every block entry that leaves `self_type`
|
|
284
|
+
# unnarrowed and is inherited by every scope derived inside the block; it never leaks past the block,
|
|
285
|
+
# because `eval_call` returns the caller's scope unchanged.
|
|
286
|
+
def entering_opaque_block
|
|
287
|
+
return self if @opaque_block_self
|
|
288
|
+
|
|
289
|
+
rebuild(opaque_block_self: true)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# True when this scope sits inside a block whose `self` is unmodelled ({#entering_opaque_block}).
|
|
293
|
+
def opaque_block_self? = @opaque_block_self
|
|
294
|
+
|
|
245
295
|
# True when `name`'s `Struct` member reads are fold-safe in this body (the local is provably never mutated /
|
|
246
296
|
# aliased / escaped).
|
|
247
297
|
def struct_fold_safe?(name)
|
|
@@ -281,7 +331,8 @@ module Rigor
|
|
|
281
331
|
indexed_narrowings: new_indexed_narrowings,
|
|
282
332
|
method_chain_narrowings: new_chain_narrowings,
|
|
283
333
|
declaration_sourced: drop_declaration_sourced_for(:ivar, name),
|
|
284
|
-
ivar_origins: drop_origin(@ivar_origins, name)
|
|
334
|
+
ivar_origins: drop_origin(@ivar_origins, name),
|
|
335
|
+
optimistic_ivars: drop_origin(@optimistic_ivars, name))
|
|
285
336
|
end
|
|
286
337
|
|
|
287
338
|
# ADR-58 WD1 — used by the method-entry seed to mark an ivar whose only provenance is the class-ivar index.
|
|
@@ -319,8 +370,11 @@ module Rigor
|
|
|
319
370
|
# whose type is an open-call-site *lower bound* — firing against a lower bound is a false positive by
|
|
320
371
|
# construction (the ADR-67 WD1 reasoning at the parameter boundary, carried one hop into the body). The
|
|
321
372
|
# distinct kind keeps the inferred-param sites separable from ADR-58's ivar-copy `:local` mark, which a
|
|
322
|
-
# later un-guarding slice (WD6b) needs
|
|
323
|
-
#
|
|
373
|
+
# later un-guarding slice (WD6b) needs — and the two kinds behave OPPOSITELY on both axes: `:local` is
|
|
374
|
+
# dropped by `with_local` and intersected by `join`, while `:inferred_param` is sticky across `with_local`
|
|
375
|
+
# and unioned by `join`. See {#without_inferred_param_mark} below for the clearing contract, and
|
|
376
|
+
# `docs/internal-spec/inference-engine.md` § "Declaration-sourced provenance mark (ADR-58)" for the
|
|
377
|
+
# normative statement of both.
|
|
324
378
|
def with_inferred_param_mark(name)
|
|
325
379
|
rebuild(declaration_sourced: add_declaration_sourced(:inferred_param, name))
|
|
326
380
|
end
|
|
@@ -394,11 +448,13 @@ module Rigor
|
|
|
394
448
|
# recognised `define_method` invocation inside class/module bodies. The `rigor check` undefined-method and
|
|
395
449
|
# wrong-arity rules consult this map to suppress diagnostics for methods the user has defined dynamically,
|
|
396
450
|
# even when no RBS sig describes them.
|
|
451
|
+
# A name defined on both sides of one class records {DiscoveryIndex::METHOD_KIND_BOTH} and matches either kind.
|
|
397
452
|
def discovered_method?(class_name, method_name, kind)
|
|
398
453
|
table = @discovery.discovered_methods[class_name.to_s]
|
|
399
454
|
return false unless table
|
|
400
455
|
|
|
401
|
-
table[method_name.to_sym]
|
|
456
|
+
recorded = table[method_name.to_sym]
|
|
457
|
+
recorded == kind || recorded == DiscoveryIndex::METHOD_KIND_BOTH
|
|
402
458
|
end
|
|
403
459
|
|
|
404
460
|
# ADR-34 § "Decision" — predicate identifying a toplevel-shaped scope (no enclosing `class` / `module` body).
|
|
@@ -470,6 +526,43 @@ module Rigor
|
|
|
470
526
|
Inference::DefNodeResolver.resolve(entry)
|
|
471
527
|
end
|
|
472
528
|
|
|
529
|
+
# Issue #316 — the CONFIDENCE-GATED companion of {#top_level_def_for}, and the only accessor the type
|
|
530
|
+
# inference may bind through. {#top_level_def_for} stays unrestricted because it also serves the
|
|
531
|
+
# *suppression* side (`call.unresolved-toplevel`, `call.undefined-method`): a name the project defines at
|
|
532
|
+
# the top level must never be reported as unresolved, whatever this gate decides.
|
|
533
|
+
#
|
|
534
|
+
# Returns nil — decline to bind, stay silent — when BOTH hold:
|
|
535
|
+
#
|
|
536
|
+
# 1. The call site sits inside a block whose `self` is unmodelled ({#opaque_block_self?}) and no narrowed
|
|
537
|
+
# `self_type` says otherwise. A top-level `def` is a private method on `Object`, so it is *callable*
|
|
538
|
+
# from any `self`; what the analyzer cannot see is whether the block's real `self` gained a PUBLIC
|
|
539
|
+
# same-named method by `include` / `extend`, which wins the MRO over the private `Object` def. RSpec's
|
|
540
|
+
# `output` / `include` / `match` matchers against a project's own `def output` are exactly this.
|
|
541
|
+
# 2. The `def` lives in a DIFFERENT file from the call site. Collocation is the evidence that the two
|
|
542
|
+
# belong to one lexical structure — the `RSpec.describe do; def helper; end; it { helper } end` case
|
|
543
|
+
# v0.0.3 A and #319 deliberately serve. Cross-file, the two share only a name.
|
|
544
|
+
#
|
|
545
|
+
# Both conditions are required, so a top-level helper called from genuine top-level code keeps resolving
|
|
546
|
+
# (cross-file included), and a helper defined beside its DSL-block call site keeps resolving too. When the
|
|
547
|
+
# project pre-pass recorded no source for the name, the file test cannot be answered and the historical
|
|
548
|
+
# bind is kept.
|
|
549
|
+
def bindable_top_level_def_for(method_name)
|
|
550
|
+
node = top_level_def_for(method_name)
|
|
551
|
+
return node if node.nil?
|
|
552
|
+
return node unless @opaque_block_self && @self_type.nil?
|
|
553
|
+
|
|
554
|
+
same_file_top_level_def?(method_name) ? node : nil
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def same_file_top_level_def?(method_name)
|
|
558
|
+
key = Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
|
|
559
|
+
site = discovered_def_sources.dig(key, method_name.to_sym)
|
|
560
|
+
return true if site.nil? || @source_path.nil?
|
|
561
|
+
|
|
562
|
+
File.expand_path(site.sub(/:\d+\z/, "")) == File.expand_path(@source_path)
|
|
563
|
+
end
|
|
564
|
+
private :same_file_top_level_def?
|
|
565
|
+
|
|
473
566
|
# ADR-46 slice 3 — a top-level (`def helper` outside any class) call has NO class ancestry to walk, so unlike
|
|
474
567
|
# {#user_def_for} a miss here records no positive ancestry edge that would re-check the consumer when the
|
|
475
568
|
# method later appears. Record the cross-file edge explicitly: the file defining the top-level method
|
|
@@ -721,10 +814,14 @@ module Rigor
|
|
|
721
814
|
declaration_sourced: @declaration_sourced,
|
|
722
815
|
source_path: @source_path,
|
|
723
816
|
struct_fold_safe_locals: @struct_fold_safe_locals,
|
|
817
|
+
opaque_block_self: @opaque_block_self,
|
|
724
818
|
dynamic_origins: @dynamic_origins,
|
|
725
819
|
local_origins: @local_origins,
|
|
726
820
|
ivar_origins: @ivar_origins,
|
|
727
|
-
void_origins: @void_origins
|
|
821
|
+
void_origins: @void_origins,
|
|
822
|
+
optimistic_origins: @optimistic_origins,
|
|
823
|
+
optimistic_locals: @optimistic_locals,
|
|
824
|
+
optimistic_ivars: @optimistic_ivars
|
|
728
825
|
)
|
|
729
826
|
self.class.new(
|
|
730
827
|
environment: environment, locals: locals,
|
|
@@ -736,10 +833,14 @@ module Rigor
|
|
|
736
833
|
declaration_sourced: declaration_sourced,
|
|
737
834
|
source_path: source_path,
|
|
738
835
|
struct_fold_safe_locals: struct_fold_safe_locals,
|
|
836
|
+
opaque_block_self: opaque_block_self,
|
|
739
837
|
dynamic_origins: dynamic_origins,
|
|
740
838
|
local_origins: local_origins,
|
|
741
839
|
ivar_origins: ivar_origins,
|
|
742
|
-
void_origins: void_origins
|
|
840
|
+
void_origins: void_origins,
|
|
841
|
+
optimistic_origins: optimistic_origins,
|
|
842
|
+
optimistic_locals: optimistic_locals,
|
|
843
|
+
optimistic_ivars: optimistic_ivars
|
|
743
844
|
)
|
|
744
845
|
end
|
|
745
846
|
|
|
@@ -777,7 +878,10 @@ module Rigor
|
|
|
777
878
|
dynamic_origins: @dynamic_origins,
|
|
778
879
|
local_origins: join_origins(@local_origins, other.local_origins),
|
|
779
880
|
ivar_origins: join_origins(@ivar_origins, other.ivar_origins),
|
|
780
|
-
void_origins: @void_origins
|
|
881
|
+
void_origins: @void_origins,
|
|
882
|
+
optimistic_origins: @optimistic_origins,
|
|
883
|
+
optimistic_locals: join_origins(@optimistic_locals, other.optimistic_locals),
|
|
884
|
+
optimistic_ivars: join_origins(@optimistic_ivars, other.optimistic_ivars)
|
|
781
885
|
)
|
|
782
886
|
end
|
|
783
887
|
|
|
@@ -12,6 +12,7 @@ require_relative "../source/node_children"
|
|
|
12
12
|
require_relative "../inference/def_return_typer"
|
|
13
13
|
require_relative "../inference/scope_indexer"
|
|
14
14
|
require_relative "../inference/rbs_type_translator"
|
|
15
|
+
require_relative "meta_class_shape"
|
|
15
16
|
require_relative "rbs_validity"
|
|
16
17
|
|
|
17
18
|
module Rigor
|
|
@@ -61,6 +62,7 @@ module Rigor
|
|
|
61
62
|
@module_function_methods = Set.new
|
|
62
63
|
@class_shells = Set.new
|
|
63
64
|
@class_superclasses = {}
|
|
65
|
+
@meta_layouts = {}
|
|
64
66
|
# Whole-run, NOT per-file: a rendering defect is reported once at the end of the run.
|
|
65
67
|
@unrenderable = []
|
|
66
68
|
end
|
|
@@ -114,7 +116,13 @@ module Rigor
|
|
|
114
116
|
@module_function_methods = Set.new
|
|
115
117
|
@class_shells = Set.new
|
|
116
118
|
@class_superclasses = {}
|
|
119
|
+
@meta_layouts = collect_meta_layouts(scope_index)
|
|
120
|
+
register_meta_classes
|
|
117
121
|
defs = collect_method_definitions(parse_result.value)
|
|
122
|
+
# Candidate construction freezes the per-file maps above (see {#build_candidate}), so every registration
|
|
123
|
+
# pass has to be finished before the first `build_candidate` call. Meta members lead the RETURNED order —
|
|
124
|
+
# a value class's members and constructors read first, ahead of the methods its block body defines.
|
|
125
|
+
meta_candidates = collect_meta_member_candidates(path, scope_index)
|
|
118
126
|
candidates_from_defs = defs.filter_map do |def_node, class_name, kind|
|
|
119
127
|
# An analyzer bug typing one def's body must cost only that def's candidate, never the whole
|
|
120
128
|
# `rigor sig-gen` run. The `check` path recovers each *file* this way (worker_session.rb); sig-gen
|
|
@@ -125,7 +133,8 @@ module Rigor
|
|
|
125
133
|
nil
|
|
126
134
|
end
|
|
127
135
|
obs_ivar_map = build_observed_ivar_map(parse_result.value)
|
|
128
|
-
|
|
136
|
+
meta_candidates + candidates_from_defs +
|
|
137
|
+
collect_attr_candidates(parse_result.value, path, scope_index, obs_ivar_map)
|
|
129
138
|
end
|
|
130
139
|
|
|
131
140
|
# Walks the AST collecting `(def_node, class_name, kind)` tuples for every `def` Rigor can re-type. Slice 1
|
|
@@ -163,8 +172,11 @@ module Rigor
|
|
|
163
172
|
collect_def_node(node, prefix, in_singleton_class, module_function_active, out)
|
|
164
173
|
return
|
|
165
174
|
when Prism::ConstantWriteNode
|
|
166
|
-
|
|
167
|
-
|
|
175
|
+
body = meta_block_body(node, prefix)
|
|
176
|
+
if body
|
|
177
|
+
walk_defs(body, prefix + [node.name.to_s], false, false, out)
|
|
178
|
+
return
|
|
179
|
+
end
|
|
168
180
|
when Prism::StatementsNode
|
|
169
181
|
walk_statements(node, prefix, in_singleton_class, module_function_active, out)
|
|
170
182
|
return
|
|
@@ -192,8 +204,8 @@ module Rigor
|
|
|
192
204
|
# collapse the whole env (the 2026-07-04 redmine `GitAdapter < AbstractAdapter` crash). Only a plain
|
|
193
205
|
# constant superclass is emittable: `class X < Foo` / `class X < Foo::Bar` yields the source token verbatim
|
|
194
206
|
# (RBS resolves it relative to the emitted namespace, matching Ruby's lexical scope). A computed
|
|
195
|
-
# superclass (`
|
|
196
|
-
#
|
|
207
|
+
# superclass (`Class.new`, any other `CallNode`) is left unrecorded — a `Data.define` / `Struct.new` one is
|
|
208
|
+
# already carried by {#register_meta_classes}, and the rest are un-representable, where guessing would
|
|
197
209
|
# misfold.
|
|
198
210
|
def record_superclass(node, full)
|
|
199
211
|
return unless node.is_a?(Prism::ClassNode)
|
|
@@ -202,37 +214,60 @@ module Rigor
|
|
|
202
214
|
@class_superclasses[full] = superclass if superclass
|
|
203
215
|
end
|
|
204
216
|
|
|
205
|
-
# ADR-
|
|
206
|
-
#
|
|
207
|
-
#
|
|
208
|
-
#
|
|
209
|
-
#
|
|
210
|
-
#
|
|
211
|
-
#
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
# keyword wins over the intermediate-segment `module` default.
|
|
215
|
-
def register_data_struct_shell(node, prefix)
|
|
216
|
-
return unless data_or_struct_call?(node.value)
|
|
217
|
+
# The ADR-48 member layouts for this file, in one table keyed by qualified class name. Reading the engine's
|
|
218
|
+
# own tables instead of re-recognising `Data.define` / `Struct.new` here is what keeps sig-gen's view of a
|
|
219
|
+
# value class from drifting from the analyser's: the walker that populates them ({Inference::ScopeIndexer})
|
|
220
|
+
# already covers the constant-assigned form (`Point = Data.define(:x, :y)`), the named-subclass form
|
|
221
|
+
# (`class Point < Data.define(:x, :y)`), a `::Data` receiver, and the `keyword_init:` flag. sig-gen's earlier
|
|
222
|
+
# private recogniser covered none of the last three and was the reason #227's output was wrong rather than
|
|
223
|
+
# merely thin.
|
|
224
|
+
MetaLayout = Data.define(:kind, :member_names, :keyword_init)
|
|
225
|
+
private_constant :MetaLayout
|
|
217
226
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
end
|
|
227
|
+
def collect_meta_layouts(scope_index)
|
|
228
|
+
scope = scope_index.each_value.first
|
|
229
|
+
return {} if scope.nil?
|
|
222
230
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
231
|
+
layouts = {}
|
|
232
|
+
scope.data_member_layouts.each do |name, members|
|
|
233
|
+
layouts[name] = MetaLayout.new(kind: :data, member_names: members, keyword_init: false)
|
|
234
|
+
end
|
|
235
|
+
scope.struct_member_layouts.each do |name, layout|
|
|
236
|
+
layouts[name] = MetaLayout.new(kind: :struct, member_names: layout[:members],
|
|
237
|
+
keyword_init: layout[:keyword_init])
|
|
238
|
+
end
|
|
239
|
+
layouts
|
|
240
|
+
end
|
|
228
241
|
|
|
229
|
-
|
|
230
|
-
|
|
242
|
+
# ADR-14 gap-#3 (e): a `Const = Data.define(...)` / `Const = Struct.new(...)` assignment declares a class the
|
|
243
|
+
# source never spells with a `class` keyword — the runtime stamps an anonymous class at the rvalue and binds
|
|
244
|
+
# it to `Const` — so the generated RBS needs an explicit `class Const` of its own. Without it, references to
|
|
245
|
+
# `Const` in a return type fail to resolve under Steep (the canonical case is
|
|
246
|
+
# `GemResolver::Resolved | GemResolver::Unresolvable`, where `Unresolvable = Data.define(:gem_name, :reason)`).
|
|
247
|
+
#
|
|
248
|
+
# Every layout-carrying class therefore gets the `class` keyword in `@namespace_kinds` (so the leaf wins over
|
|
249
|
+
# the intermediate-segment `module` default), its `::Data` / `::Struct[untyped]` ancestry, and a `@class_shells`
|
|
250
|
+
# entry so the writer declares it even when every member candidate is suppressed as already-declared. The
|
|
251
|
+
# named-subclass form is registered too: its `class` keyword is not in question, but its computed superclass
|
|
252
|
+
# is exactly what {#record_superclass} refuses to guess at.
|
|
253
|
+
def register_meta_classes
|
|
254
|
+
@meta_layouts.each do |class_name, layout|
|
|
255
|
+
@namespace_kinds[class_name] = :class
|
|
256
|
+
@class_shells << class_name
|
|
257
|
+
@class_superclasses[class_name] = MetaClassShape::SUPERCLASSES.fetch(layout.kind)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
231
260
|
|
|
232
|
-
|
|
233
|
-
|
|
261
|
+
# The `do ... end` body of a `Const = Data.define(...) do ... end` assignment, when `Const` carries a layout.
|
|
262
|
+
# Defs inside that block bind on `Const`, NOT on the enclosing namespace — the runtime `class_eval`s the block
|
|
263
|
+
# into the anonymous class it just stamped. Attributing them to the enclosing namespace is what made sig-gen
|
|
264
|
+
# report `VoidOrigin#label` against `Rigor::Inference` and then, because a method-bearing leaf defaults to the
|
|
265
|
+
# `class` keyword, redeclare that module as a class (#227).
|
|
266
|
+
def meta_block_body(node, prefix)
|
|
267
|
+
return nil unless node.value.is_a?(Prism::CallNode)
|
|
268
|
+
return nil unless @meta_layouts.key?((prefix + [node.name.to_s]).join("::"))
|
|
234
269
|
|
|
235
|
-
|
|
270
|
+
node.value.block&.body
|
|
236
271
|
end
|
|
237
272
|
|
|
238
273
|
# Module / class bodies are walked through the `walk_statements` path so `module_function` (no-args)
|
|
@@ -824,6 +859,12 @@ module Rigor
|
|
|
824
859
|
# Skip method bodies — attr_* there would refer to whatever the method is doing dynamically, not a
|
|
825
860
|
# class-level declaration.
|
|
826
861
|
return
|
|
862
|
+
when Prism::ConstantWriteNode
|
|
863
|
+
body = meta_block_body(node, prefix)
|
|
864
|
+
if body
|
|
865
|
+
walk_attr_calls(body, prefix + [node.name.to_s], false, ctx)
|
|
866
|
+
return
|
|
867
|
+
end
|
|
827
868
|
when Prism::CallNode
|
|
828
869
|
collect_attr_call(node, prefix, in_singleton_class, ctx)
|
|
829
870
|
end
|
|
@@ -1065,6 +1106,64 @@ module Rigor
|
|
|
1065
1106
|
when :writer then "def #{method_name}: (#{erased}) -> #{wrapped}"
|
|
1066
1107
|
end
|
|
1067
1108
|
end
|
|
1109
|
+
|
|
1110
|
+
# One candidate per member accessor and constructor {MetaClassShape} renders for each layout-carrying class.
|
|
1111
|
+
# These are the members no `def` or `attr_*` in the source declares, so nothing else in sig-gen can find them —
|
|
1112
|
+
# and once `register_meta_classes` has declared the class, an undeclared member reads as a missing one.
|
|
1113
|
+
def collect_meta_member_candidates(path, scope_index)
|
|
1114
|
+
return [] if @meta_layouts.empty?
|
|
1115
|
+
|
|
1116
|
+
scope = scope_index.each_value.first
|
|
1117
|
+
environment = scope&.environment
|
|
1118
|
+
@meta_layouts.flat_map do |class_name, layout|
|
|
1119
|
+
types = meta_member_types(class_name, layout.member_names)
|
|
1120
|
+
shape = MetaClassShape.of(
|
|
1121
|
+
kind: layout.kind, members: layout.member_names, keyword_init: layout.keyword_init,
|
|
1122
|
+
member_types: types.transform_values { |type| paren_wrap_union(union_erase([type])) }
|
|
1123
|
+
)
|
|
1124
|
+
shape.member_decls.filter_map do |member|
|
|
1125
|
+
meta_member_candidate(path, class_name, member, types, scope, environment)
|
|
1126
|
+
end
|
|
1127
|
+
end
|
|
1128
|
+
end
|
|
1129
|
+
|
|
1130
|
+
def meta_member_candidate(path, class_name, member, types, scope, environment)
|
|
1131
|
+
existing = lookup_existing_method(class_name, member.method_name, member.kind, environment, scope)
|
|
1132
|
+
return nil if declared_on_class_itself?(existing, class_name)
|
|
1133
|
+
|
|
1134
|
+
build_candidate(
|
|
1135
|
+
path: path, class_name: class_name, method_name: member.method_name, kind: member.kind,
|
|
1136
|
+
classification: Classification::NEW_METHOD,
|
|
1137
|
+
inferred_return: types[member.source_member] || Type::Combinator.untyped,
|
|
1138
|
+
rbs: member.rbs
|
|
1139
|
+
)
|
|
1140
|
+
end
|
|
1141
|
+
|
|
1142
|
+
# Whether an RBS declaration found for a member is the user's own, i.e. sits on this very class. Inheritance
|
|
1143
|
+
# is the whole point of the distinction: `::Data.new: () -> bot` and `::Struct.new`'s factory both answer the
|
|
1144
|
+
# `.new` lookup for every value class, and deferring to them is what leaves the arity false positive in place.
|
|
1145
|
+
def declared_on_class_itself?(method_def, class_name)
|
|
1146
|
+
return false if method_def.nil?
|
|
1147
|
+
return false unless method_def.respond_to?(:defined_in)
|
|
1148
|
+
|
|
1149
|
+
method_def.defined_in.to_s.delete_prefix("::") == class_name
|
|
1150
|
+
end
|
|
1151
|
+
|
|
1152
|
+
# `--params=observed` member types. {ObservationCollector} routes `Point.new(...)` call sites to
|
|
1153
|
+
# `[class_name, :initialize]`, so a keyword call site names its member directly, while a positional one is
|
|
1154
|
+
# matched by index — and only from call sites whose arity covers the whole member list, so a one-argument
|
|
1155
|
+
# `Point.new(attrs)` shim cannot type member 0 as `Hash`. Empty without `--params=observed`.
|
|
1156
|
+
def meta_member_types(class_name, members)
|
|
1157
|
+
observations = @observations[[class_name, :initialize]] || []
|
|
1158
|
+
return {} if observations.empty?
|
|
1159
|
+
|
|
1160
|
+
full_arity = observations.select { |obs| obs.positional.size == members.size }
|
|
1161
|
+
members.each_with_index.to_h do |member, index|
|
|
1162
|
+
observed = observations.filter_map { |obs| obs.keyword[member] } +
|
|
1163
|
+
full_arity.filter_map { |obs| obs.positional[index] }
|
|
1164
|
+
[member, observed.empty? ? nil : Type::Combinator.union(*observed)]
|
|
1165
|
+
end.compact
|
|
1166
|
+
end
|
|
1068
1167
|
end
|
|
1069
1168
|
end
|
|
1070
1169
|
end
|
|
@@ -74,6 +74,12 @@ module Rigor
|
|
|
74
74
|
|
|
75
75
|
def index_file(rbs_path, accumulator)
|
|
76
76
|
source = rbs_path.read
|
|
77
|
+
# Pre-parser encoding guard (mirrors `Environment::RbsLoader.invalid_encoding?`): on rbs releases
|
|
78
|
+
# before 4.1 the C lexer could hang on an invalid UTF-8 byte, and a hang is the one failure the
|
|
79
|
+
# rescue below cannot absorb. Skipping is safe here for the same reason the rescue is — placement
|
|
80
|
+
# falls back as for any unparseable file, and the env loader's quarantine warning names the file.
|
|
81
|
+
return unless source.valid_encoding?
|
|
82
|
+
|
|
77
83
|
_, _, decls = RBS::Parser.parse_signature(source)
|
|
78
84
|
record_decls(decls, [], rbs_path, accumulator)
|
|
79
85
|
rescue StandardError
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module SigGen
|
|
5
|
+
# The RBS surface of a class the runtime builds from `Data.define(...)` / `Struct.new(...)` — its ancestry, its
|
|
6
|
+
# synthesised member accessors, and its constructors — rendered from an ADR-48 member layout.
|
|
7
|
+
#
|
|
8
|
+
# Declaring the class without them would be worse than not declaring it at all. `::Data`'s own RBS declares
|
|
9
|
+
# `def self.new: () -> bot` and `::Struct`'s declares only the `Struct.new("Name", :a, :b)` factory, so a
|
|
10
|
+
# subclass that carries no `.new` of its own turns every `Point.new(1, 2)` into a false-positive arity error —
|
|
11
|
+
# where the undeclared class it replaced merely typed as `Dynamic` and reported nothing. The same reasoning
|
|
12
|
+
# covers the member readers and `.[]` (`Point[1, 2]`, which `::Data`'s RBS does not declare): narrowing dispatch
|
|
13
|
+
# from `Dynamic` to a nominal class means everything the runtime synthesises must be declared, or it reads as
|
|
14
|
+
# missing.
|
|
15
|
+
#
|
|
16
|
+
# Member types arrive already erased to RBS and already parenthesised for return position. One pre-wrapped
|
|
17
|
+
# string serves every position this module interpolates into — `(a: (String | Integer))`,
|
|
18
|
+
# `((String | Integer) a)`, `?(String | Integer) a` all parse and validate.
|
|
19
|
+
module MetaClassShape
|
|
20
|
+
# `::Struct` is generic (`class Struct[E]`), and RBS rejects a bare `< ::Struct` with
|
|
21
|
+
# `InvalidTypeApplicationError`. `untyped` is the only element type a layout can justify.
|
|
22
|
+
SUPERCLASSES = { data: "::Data", struct: "::Struct[untyped]" }.freeze
|
|
23
|
+
|
|
24
|
+
# `method_name` / `kind` feed the writer's existing-member matching. `source_member` is the layout member a
|
|
25
|
+
# reader / writer derives from, and nil for a constructor, so the caller can attach the member's carrier to
|
|
26
|
+
# the candidate it builds.
|
|
27
|
+
Member = ::Data.define(:method_name, :kind, :rbs, :source_member)
|
|
28
|
+
Shape = ::Data.define(:superclass, :member_decls)
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# @param kind [:data, :struct]
|
|
33
|
+
# @param members [Array<Symbol>] ordered member names from the ADR-48 layout.
|
|
34
|
+
# @param member_types [Hash{Symbol => String}] erased RBS spellings; a member with no entry renders `untyped`.
|
|
35
|
+
# @param keyword_init [Boolean] the `Struct.new(..., keyword_init: true)` flag; ignored for `:data`.
|
|
36
|
+
# @return [Shape]
|
|
37
|
+
def of(kind:, members:, member_types: {}, keyword_init: false)
|
|
38
|
+
types = members.to_h { |member| [member, member_types[member] || "untyped"] }
|
|
39
|
+
accessors = kind == :struct ? struct_accessors(members, types) : readers(members, types)
|
|
40
|
+
Shape.new(superclass: SUPERCLASSES.fetch(kind),
|
|
41
|
+
member_decls: accessors + constructors(kind, members, types, keyword_init))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def readers(members, types)
|
|
45
|
+
members.map do |member|
|
|
46
|
+
Member.new(method_name: member, kind: :instance, source_member: member,
|
|
47
|
+
rbs: "def #{member}: () -> #{types[member]}")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# A Struct's members are mutable, so each one contributes a writer too. The writer returns the assigned
|
|
52
|
+
# value's type — Ruby's assignment semantics, and the spelling the `attr_writer` path already emits.
|
|
53
|
+
def struct_accessors(members, types)
|
|
54
|
+
writers = members.map do |member|
|
|
55
|
+
Member.new(method_name: :"#{member}=", kind: :instance, source_member: member,
|
|
56
|
+
rbs: "def #{member}=: (#{types[member]}) -> #{types[member]}")
|
|
57
|
+
end
|
|
58
|
+
readers(members, types) + writers
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# `.new` and its `.[]` alias share one overload list.
|
|
62
|
+
def constructors(kind, members, types, keyword_init)
|
|
63
|
+
overloads = constructor_overloads(kind, members, types, keyword_init).join(" | ")
|
|
64
|
+
%i[new []].map do |name|
|
|
65
|
+
Member.new(method_name: name, kind: :singleton, source_member: nil, rbs: "def self.#{name}: #{overloads}")
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# `Data` requires every member; a Struct fills a missing one with `nil`, so its positions are all optional.
|
|
70
|
+
#
|
|
71
|
+
# `keyword_init: true` accepts keyword arguments only. Every other layout gets BOTH forms, because the flag
|
|
72
|
+
# reads `false` for an absent `keyword_init:` as well as for a literal `keyword_init: false` — and since Ruby
|
|
73
|
+
# 3.2 the absent case, by far the dominant one, accepts both. Emitting both is the false-positive-free reading
|
|
74
|
+
# of an ambiguity the layout cannot resolve.
|
|
75
|
+
def constructor_overloads(kind, members, types, keyword_init)
|
|
76
|
+
optional = kind == :struct ? "?" : ""
|
|
77
|
+
keyword = "(#{members.map { |m| "#{optional}#{m}: #{types[m]}" }.join(', ')}) -> instance"
|
|
78
|
+
return [keyword] if kind == :struct && keyword_init
|
|
79
|
+
|
|
80
|
+
[keyword, "(#{members.map { |m| "#{optional}#{types[m]} #{m}" }.join(', ')}) -> instance"]
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -66,7 +66,7 @@ module Rigor
|
|
|
66
66
|
|
|
67
67
|
def render_classes(items)
|
|
68
68
|
items.group_by(&:class_name).each do |class_name, methods|
|
|
69
|
-
@out.puts(
|
|
69
|
+
@out.puts(declaration_header(methods.first, class_name))
|
|
70
70
|
methods.each do |candidate|
|
|
71
71
|
tag = case candidate.classification
|
|
72
72
|
when Classification::NEW_METHOD then "[new]"
|
|
@@ -81,6 +81,17 @@ module Rigor
|
|
|
81
81
|
end
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
+
# The keyword and ancestry for a printed group, from the same per-file maps the `--write` path reads. Print
|
|
85
|
+
# mode used to hard-code `class`, which turned a module into a class the moment it held an emittable method —
|
|
86
|
+
# output that raises `RBS::DuplicatedDeclarationError` on load if the real `module` is declared anywhere else
|
|
87
|
+
# (#227). Defaulting to `class` when the map has no entry keeps the pre-existing spelling for a leaf class.
|
|
88
|
+
def declaration_header(candidate, class_name)
|
|
89
|
+
return "module #{class_name}" if candidate.namespace_kinds[class_name] == :module
|
|
90
|
+
|
|
91
|
+
superclass = candidate.class_superclasses[class_name]
|
|
92
|
+
superclass ? "class #{class_name} < #{superclass}" : "class #{class_name}"
|
|
93
|
+
end
|
|
94
|
+
|
|
84
95
|
def render_diff(candidates)
|
|
85
96
|
if candidates.empty?
|
|
86
97
|
@out.puts("No candidates")
|
|
@@ -127,6 +138,7 @@ module Rigor
|
|
|
127
138
|
when :updated then render_write_updated(result)
|
|
128
139
|
when :skipped_outside_sig_root then render_write_skipped(result)
|
|
129
140
|
when :skipped_invalid_rbs then render_write_invalid(result)
|
|
141
|
+
when :skipped_invalid_encoding then render_write_invalid_encoding(result)
|
|
130
142
|
end
|
|
131
143
|
end
|
|
132
144
|
end
|
|
@@ -153,6 +165,13 @@ module Rigor
|
|
|
153
165
|
@out.puts(" This is a bug in Rigor's RBS rendering, not in your code — please report it.")
|
|
154
166
|
end
|
|
155
167
|
|
|
168
|
+
# The EXISTING target file is not valid UTF-8, so the writer will not merge into it — the opposite
|
|
169
|
+
# attribution from {#render_write_invalid}: this one is the file's problem, and the fix is the user's.
|
|
170
|
+
def render_write_invalid_encoding(result)
|
|
171
|
+
@out.puts("REFUSED #{result.target_path} — the existing file is not valid UTF-8, so it was not updated")
|
|
172
|
+
@out.puts(" Re-save the file as UTF-8 and re-run; sig-gen never modifies a file it cannot read faithfully.")
|
|
173
|
+
end
|
|
174
|
+
|
|
156
175
|
def render_write_json(results)
|
|
157
176
|
@out.puts(JSON.pretty_generate({ results: results.map(&:to_h) }))
|
|
158
177
|
end
|
|
@@ -11,13 +11,15 @@ module Rigor
|
|
|
11
11
|
# - `target_path` — `.rbs` file the writer was responsible for (`nil` when the source path falls outside
|
|
12
12
|
# the project signature tree, in which case `action` is `:skipped_outside_sig_root`).
|
|
13
13
|
# - `action` — one of `:created` / `:updated` / `:noop` / `:skipped_outside_sig_root` /
|
|
14
|
-
# `:skipped_invalid_rbs`.
|
|
14
|
+
# `:skipped_invalid_rbs` / `:skipped_invalid_encoding`.
|
|
15
15
|
# - `applied` — the {MethodCandidate}s that actually landed on disk.
|
|
16
16
|
# - `skipped` — the {MethodCandidate}s the writer declined (e.g. tighter-return without `--overwrite`). Each
|
|
17
17
|
# entry pairs the candidate with a skip reason keyword (`:user_authored`).
|
|
18
|
-
# - `error` — the
|
|
19
|
-
# not parse, so it was NOT written (writing it would poison the project's sig tree — the
|
|
20
|
-
# quarantines an unparseable `.rbs`, taking every other type in that file down with it)
|
|
18
|
+
# - `error` — the refusal cause, when `action` is a refusal: for `:skipped_invalid_rbs` the file the writer
|
|
19
|
+
# assembled does not parse, so it was NOT written (writing it would poison the project's sig tree — the
|
|
20
|
+
# consumer quarantines an unparseable `.rbs`, taking every other type in that file down with it); for
|
|
21
|
+
# `:skipped_invalid_encoding` the EXISTING target file is not valid UTF-8, so the writer refuses to merge
|
|
22
|
+
# into content it cannot read faithfully.
|
|
21
23
|
class WriteResult
|
|
22
24
|
attr_reader :source_path, :target_path, :action, :applied, :skipped, :error
|
|
23
25
|
|
data/lib/rigor/sig_gen/writer.rb
CHANGED
|
@@ -275,6 +275,18 @@ module Rigor
|
|
|
275
275
|
|
|
276
276
|
def update_existing(source_path, target, candidates)
|
|
277
277
|
source = target.read
|
|
278
|
+
# Pre-parser encoding guard, mirroring `Environment::RbsLoader.invalid_encoding?`: handed invalid
|
|
279
|
+
# UTF-8, `RBS::Parser` raises a bare `ArgumentError` on rbs 4.1 (not the `ParsingError` that
|
|
280
|
+
# `parse_signature` below rescues) and could hang outright on the older releases the gemspec
|
|
281
|
+
# supports. The refusal is loud — unlike an unparseable file (`:noop`, surfaced by `rigor check`'s
|
|
282
|
+
# own quarantine warning), a mojibake file would otherwise fail with a stack trace naming neither
|
|
283
|
+
# the file nor the fix.
|
|
284
|
+
unless source.valid_encoding?
|
|
285
|
+
return WriteResult.new(source_path: source_path, target_path: target,
|
|
286
|
+
action: :skipped_invalid_encoding,
|
|
287
|
+
error: "#{target} is not valid UTF-8")
|
|
288
|
+
end
|
|
289
|
+
|
|
278
290
|
decls = parse_signature(source)
|
|
279
291
|
return WriteResult.new(source_path: source_path, target_path: target, action: :noop) if decls.nil?
|
|
280
292
|
|
data/lib/rigor/sig_gen.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "sig_gen/classification"
|
|
4
4
|
require_relative "sig_gen/method_candidate"
|
|
5
5
|
require_relative "sig_gen/observed_call"
|
|
6
|
+
require_relative "sig_gen/meta_class_shape"
|
|
6
7
|
require_relative "sig_gen/type_elaborator"
|
|
7
8
|
require_relative "sig_gen/observation_collector"
|
|
8
9
|
require_relative "sig_gen/generator"
|