rigortype 0.3.0 → 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/builtins/ruby_core/array.yml +416 -392
- data/data/builtins/ruby_core/file.yml +42 -42
- data/data/builtins/ruby_core/hash.yml +302 -302
- data/data/builtins/ruby_core/io.yml +191 -191
- data/data/builtins/ruby_core/numeric.yml +321 -366
- data/data/builtins/ruby_core/proc.yml +124 -124
- data/data/builtins/ruby_core/range.yml +21 -21
- data/data/builtins/ruby_core/rational.yml +39 -39
- data/data/builtins/ruby_core/re.yml +65 -65
- data/data/builtins/ruby_core/set.yml +106 -106
- data/data/builtins/ruby_core/struct.yml +14 -14
- data/data/core_overlay/pathname.rbs +5 -0
- data/data/core_overlay/resolv.rbs +31 -0
- data/data/core_overlay/string_scanner.rbs +11 -5
- 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/handbook/01-getting-started.md +22 -34
- data/docs/handbook/06-classes.md +1 -1
- data/docs/handbook/07-rbs-and-extended.md +76 -101
- data/docs/handbook/08-understanding-errors.md +114 -247
- data/docs/handbook/09-plugins.md +54 -144
- data/docs/handbook/README.md +5 -3
- data/docs/handbook/appendix-liskov.md +4 -2
- data/docs/handbook/appendix-phpstan.md +14 -7
- data/docs/handbook/appendix-steep.md +4 -2
- data/docs/handbook/appendix-type-theory.md +3 -1
- data/docs/manual/02-cli-reference.md +103 -11
- data/docs/manual/03-configuration.md +16 -1
- data/docs/manual/04-diagnostics.md +36 -4
- data/docs/manual/06-baseline.md +35 -1
- data/docs/manual/08-skills.md +6 -1
- data/docs/manual/09-editor-integration.md +3 -2
- data/docs/manual/12-caching.md +17 -5
- data/docs/manual/15-type-protection-coverage.md +122 -1
- data/docs/manual/plugins/rigor-actioncable.md +32 -0
- data/docs/manual/plugins/rigor-devise.md +4 -2
- 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/void_value_use_collector.rb +21 -2
- data/lib/rigor/analysis/check_rules.rb +79 -19
- 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 +27 -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 +80 -8
- 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/cache/rbs_cache_producer.rb +11 -1
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +38 -0
- data/lib/rigor/cache/store.rb +99 -24
- data/lib/rigor/cli/check_command.rb +61 -34
- data/lib/rigor/cli/check_invocation.rb +84 -0
- data/lib/rigor/cli/coverage_command.rb +15 -2
- data/lib/rigor/cli/coverage_mutation.rb +242 -11
- data/lib/rigor/cli/doctor_command.rb +6 -8
- 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/cli/skill_command.rb +21 -1
- data/lib/rigor/cli/skill_deep_probe.rb +172 -0
- data/lib/rigor/cli/skill_describe.rb +75 -9
- data/lib/rigor/configuration.rb +45 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +2 -1
- data/lib/rigor/environment/default_libraries.rb +5 -4
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +434 -75
- data/lib/rigor/environment.rb +38 -14
- 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/overload_selector.rb +6 -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_resolution.rb +6 -3
- data/lib/rigor/language_server/buffer_table.rb +68 -6
- data/lib/rigor/language_server/diagnostic_publisher.rb +207 -4
- data/lib/rigor/language_server/incremental_sync.rb +159 -0
- 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 +38 -9
- data/lib/rigor/language_server.rb +2 -0
- data/lib/rigor/plugin/base.rb +29 -2
- 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 +195 -47
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +51 -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/cache.rbs +6 -0
- data/sig/rigor/inference/void_origin.rbs +21 -0
- data/sig/rigor/plugin/base.rbs +4 -3
- data/sig/rigor/scope.rbs +8 -0
- metadata +23 -3
data/lib/rigor/scope.rb
CHANGED
|
@@ -24,7 +24,8 @@ module Rigor
|
|
|
24
24
|
:declaration_sourced,
|
|
25
25
|
:source_path, :discovery, :struct_fold_safe_locals,
|
|
26
26
|
:dynamic_origins, :local_origins, :ivar_origins,
|
|
27
|
-
:void_origins
|
|
27
|
+
:void_origins,
|
|
28
|
+
:optimistic_origins, :optimistic_locals, :optimistic_ivars
|
|
28
29
|
|
|
29
30
|
# ADR-53 Track A — the seed-time discovery tables live on the {DiscoveryIndex} the scope carries by a single
|
|
30
31
|
# reference; the per-table readers stay on Scope so engine call sites and plugins are unaffected by the
|
|
@@ -149,7 +150,10 @@ module Rigor
|
|
|
149
150
|
dynamic_origins: {}.compare_by_identity,
|
|
150
151
|
local_origins: EMPTY_ORIGINS,
|
|
151
152
|
ivar_origins: EMPTY_ORIGINS,
|
|
152
|
-
void_origins: {}.compare_by_identity
|
|
153
|
+
void_origins: {}.compare_by_identity,
|
|
154
|
+
optimistic_origins: {}.compare_by_identity,
|
|
155
|
+
optimistic_locals: EMPTY_ORIGINS,
|
|
156
|
+
optimistic_ivars: EMPTY_ORIGINS
|
|
153
157
|
)
|
|
154
158
|
@environment = environment
|
|
155
159
|
@locals = locals
|
|
@@ -168,9 +172,36 @@ module Rigor
|
|
|
168
172
|
@local_origins = local_origins
|
|
169
173
|
@ivar_origins = ivar_origins
|
|
170
174
|
@void_origins = void_origins
|
|
175
|
+
@optimistic_origins = optimistic_origins
|
|
176
|
+
@optimistic_locals = optimistic_locals
|
|
177
|
+
@optimistic_ivars = optimistic_ivars
|
|
171
178
|
freeze
|
|
172
179
|
end
|
|
173
180
|
|
|
181
|
+
# Issue #286 — the {Inference::OptimisticOrigin} cause attached to a call node whose result is nil-free
|
|
182
|
+
# only because `RbsDispatch` reads past `%a{implicitly-returns-nil}`, or `nil` when the value's
|
|
183
|
+
# nil-freeness is a property of its class. Mirrors {#dynamic_origins} / {#void_origins}: advisory
|
|
184
|
+
# metadata, ignored by `==` / `hash`, and never varying a flow decision on its own.
|
|
185
|
+
def record_optimistic_origin(node, cause)
|
|
186
|
+
@optimistic_origins[node] = cause
|
|
187
|
+
self
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def optimistic_local(name) = @optimistic_locals[name.to_sym]
|
|
191
|
+
def optimistic_ivar(name) = @optimistic_ivars[name.to_sym]
|
|
192
|
+
|
|
193
|
+
def with_optimistic_local(name, cause)
|
|
194
|
+
return self if cause.nil?
|
|
195
|
+
|
|
196
|
+
rebuild(optimistic_locals: @optimistic_locals.merge(name.to_sym => cause).freeze)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def with_optimistic_ivar(name, cause)
|
|
200
|
+
return self if cause.nil?
|
|
201
|
+
|
|
202
|
+
rebuild(optimistic_ivars: @optimistic_ivars.merge(name.to_sym => cause).freeze)
|
|
203
|
+
end
|
|
204
|
+
|
|
174
205
|
# ADR-82 WD1 — the propagated origin of the `Dynamic` value currently bound to a local / instance
|
|
175
206
|
# variable, or `nil` when none is tracked. Consulted by `Inference::ProtectionScanner` when a dispatch's
|
|
176
207
|
# receiver is a bare `x` / `@x` read whose own node carries no origin.
|
|
@@ -215,7 +246,8 @@ module Rigor
|
|
|
215
246
|
indexed_narrowings: new_indexed_narrowings,
|
|
216
247
|
method_chain_narrowings: new_chain_narrowings,
|
|
217
248
|
declaration_sourced: drop_declaration_sourced_for(:local, name),
|
|
218
|
-
local_origins: drop_origin(@local_origins, name)
|
|
249
|
+
local_origins: drop_origin(@local_origins, name),
|
|
250
|
+
optimistic_locals: drop_origin(@optimistic_locals, name))
|
|
219
251
|
end
|
|
220
252
|
|
|
221
253
|
def with_fact(fact)
|
|
@@ -281,7 +313,8 @@ module Rigor
|
|
|
281
313
|
indexed_narrowings: new_indexed_narrowings,
|
|
282
314
|
method_chain_narrowings: new_chain_narrowings,
|
|
283
315
|
declaration_sourced: drop_declaration_sourced_for(:ivar, name),
|
|
284
|
-
ivar_origins: drop_origin(@ivar_origins, name)
|
|
316
|
+
ivar_origins: drop_origin(@ivar_origins, name),
|
|
317
|
+
optimistic_ivars: drop_origin(@optimistic_ivars, name))
|
|
285
318
|
end
|
|
286
319
|
|
|
287
320
|
# ADR-58 WD1 — used by the method-entry seed to mark an ivar whose only provenance is the class-ivar index.
|
|
@@ -394,11 +427,13 @@ module Rigor
|
|
|
394
427
|
# recognised `define_method` invocation inside class/module bodies. The `rigor check` undefined-method and
|
|
395
428
|
# wrong-arity rules consult this map to suppress diagnostics for methods the user has defined dynamically,
|
|
396
429
|
# even when no RBS sig describes them.
|
|
430
|
+
# A name defined on both sides of one class records {DiscoveryIndex::METHOD_KIND_BOTH} and matches either kind.
|
|
397
431
|
def discovered_method?(class_name, method_name, kind)
|
|
398
432
|
table = @discovery.discovered_methods[class_name.to_s]
|
|
399
433
|
return false unless table
|
|
400
434
|
|
|
401
|
-
table[method_name.to_sym]
|
|
435
|
+
recorded = table[method_name.to_sym]
|
|
436
|
+
recorded == kind || recorded == DiscoveryIndex::METHOD_KIND_BOTH
|
|
402
437
|
end
|
|
403
438
|
|
|
404
439
|
# ADR-34 § "Decision" — predicate identifying a toplevel-shaped scope (no enclosing `class` / `module` body).
|
|
@@ -724,7 +759,10 @@ module Rigor
|
|
|
724
759
|
dynamic_origins: @dynamic_origins,
|
|
725
760
|
local_origins: @local_origins,
|
|
726
761
|
ivar_origins: @ivar_origins,
|
|
727
|
-
void_origins: @void_origins
|
|
762
|
+
void_origins: @void_origins,
|
|
763
|
+
optimistic_origins: @optimistic_origins,
|
|
764
|
+
optimistic_locals: @optimistic_locals,
|
|
765
|
+
optimistic_ivars: @optimistic_ivars
|
|
728
766
|
)
|
|
729
767
|
self.class.new(
|
|
730
768
|
environment: environment, locals: locals,
|
|
@@ -739,7 +777,10 @@ module Rigor
|
|
|
739
777
|
dynamic_origins: dynamic_origins,
|
|
740
778
|
local_origins: local_origins,
|
|
741
779
|
ivar_origins: ivar_origins,
|
|
742
|
-
void_origins: void_origins
|
|
780
|
+
void_origins: void_origins,
|
|
781
|
+
optimistic_origins: optimistic_origins,
|
|
782
|
+
optimistic_locals: optimistic_locals,
|
|
783
|
+
optimistic_ivars: optimistic_ivars
|
|
743
784
|
)
|
|
744
785
|
end
|
|
745
786
|
|
|
@@ -777,7 +818,10 @@ module Rigor
|
|
|
777
818
|
dynamic_origins: @dynamic_origins,
|
|
778
819
|
local_origins: join_origins(@local_origins, other.local_origins),
|
|
779
820
|
ivar_origins: join_origins(@ivar_origins, other.ivar_origins),
|
|
780
|
-
void_origins: @void_origins
|
|
821
|
+
void_origins: @void_origins,
|
|
822
|
+
optimistic_origins: @optimistic_origins,
|
|
823
|
+
optimistic_locals: join_origins(@optimistic_locals, other.optimistic_locals),
|
|
824
|
+
optimistic_ivars: join_origins(@optimistic_ivars, other.optimistic_ivars)
|
|
781
825
|
)
|
|
782
826
|
end
|
|
783
827
|
|
|
@@ -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
|
|