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/environment.rb
CHANGED
|
@@ -394,32 +394,56 @@ module Rigor
|
|
|
394
394
|
cache_store.fetch_or_compute(
|
|
395
395
|
producer_id: SYNTHESIZER_CACHE_PRODUCER_ID,
|
|
396
396
|
params: {},
|
|
397
|
-
descriptor: descriptor
|
|
397
|
+
descriptor: descriptor,
|
|
398
|
+
generation_cap: synthesizer_generation_cap
|
|
398
399
|
) { invoke_synthesizer_safely(callable, path) || "" }
|
|
399
400
|
end
|
|
400
401
|
|
|
401
|
-
# ADR-32 WD6 — route a synthesizer return value through the per-run
|
|
402
|
+
# ADR-32 WD6 / WD12 — route a synthesizer return value through the per-run reporter. The
|
|
402
403
|
# synthesizer's contract (declared in
|
|
403
|
-
# `plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb`) admits
|
|
404
|
-
# - `String` (non-empty)
|
|
405
|
-
# - `nil` / `""`
|
|
406
|
-
# - `[:error, message]`
|
|
404
|
+
# `plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb`) admits four return shapes:
|
|
405
|
+
# - `String` (non-empty) → successful RBS source
|
|
406
|
+
# - `nil` / `""` → no contribution
|
|
407
|
+
# - `[:error, message]` → parse failed (WD6)
|
|
408
|
+
# - `[:ok, source, [message…]]` → synthesis SUCCEEDED, but an annotation was parsed and not
|
|
409
|
+
# honoured (WD12)
|
|
407
410
|
# The error tuple is converted into a reporter entry + treated as "no contribution" so the analysis
|
|
408
|
-
# pipeline continues.
|
|
411
|
+
# pipeline continues. The WD12 tuple keeps its `source` — the file's other annotations are good, and
|
|
412
|
+
# dropping them to report one omission would be a strictly worse trade. Reporter is `nil` for callers
|
|
413
|
+
# that don't care (legacy Environment.new, tests).
|
|
414
|
+
#
|
|
415
|
+
# A cached entry written before WD12 is a bare `String`, which falls through the `is_a?(Array)` guard
|
|
416
|
+
# unchanged, so no cache-descriptor bump is needed for the new shape.
|
|
409
417
|
def interpret_synthesizer_outcome(outcome, plugin, path, reporter)
|
|
410
|
-
return outcome unless outcome.is_a?(Array)
|
|
418
|
+
return outcome unless outcome.is_a?(Array)
|
|
419
|
+
|
|
420
|
+
case outcome[0]
|
|
421
|
+
when :error
|
|
422
|
+
record_synthesis_entry(reporter, plugin, path, outcome[1], :failed)
|
|
423
|
+
nil
|
|
424
|
+
when :ok
|
|
425
|
+
Array(outcome[2]).each { |m| record_synthesis_entry(reporter, plugin, path, m, :not_honoured) }
|
|
426
|
+
outcome[1]
|
|
427
|
+
else
|
|
428
|
+
outcome
|
|
429
|
+
end
|
|
430
|
+
end
|
|
411
431
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
path: path,
|
|
415
|
-
message: outcome[1].to_s
|
|
416
|
-
)
|
|
417
|
-
nil
|
|
432
|
+
def record_synthesis_entry(reporter, plugin, path, message, kind)
|
|
433
|
+
reporter&.record(plugin_id: plugin.manifest.id, path: path, message: message.to_s, kind: kind)
|
|
418
434
|
end
|
|
419
435
|
|
|
420
436
|
SYNTHESIZER_CACHE_PRODUCER_ID = "plugin.source_rbs_synthesizer"
|
|
421
437
|
private_constant :SYNTHESIZER_CACHE_PRODUCER_ID
|
|
422
438
|
|
|
439
|
+
# One entry per (plugin, source file), all of them live for as long as the file is in the project — a
|
|
440
|
+
# generation count says nothing about staleness here, so this producer declares itself out of
|
|
441
|
+
# `Cache::Store#evict!`'s compaction pass and is bounded only by the size-based LRU pass. A method
|
|
442
|
+
# rather than a constant: `Cache::Store` is not loaded yet when this class body runs.
|
|
443
|
+
def synthesizer_generation_cap
|
|
444
|
+
Cache::Store::UNBOUNDED_GENERATIONS
|
|
445
|
+
end
|
|
446
|
+
|
|
423
447
|
def build_synthesizer_cache_descriptor(plugin, path)
|
|
424
448
|
Cache::Descriptor.new(
|
|
425
449
|
files: [Cache::Descriptor::FileEntry.new(
|
|
@@ -609,6 +609,18 @@ module Rigor
|
|
|
609
609
|
elide_or_union(node.predicate, else_type, then_type)
|
|
610
610
|
end
|
|
611
611
|
|
|
612
|
+
# Issue #286 — the effective optimistic-nil-free cause of an expression, resolving a bare local read (or
|
|
613
|
+
# a local write in value position) through its binding. Mirrors `StatementEvaluator#optimistic_origin_for`.
|
|
614
|
+
def optimistic_origin_for(node)
|
|
615
|
+
recorded = scope.optimistic_origins[node]
|
|
616
|
+
return recorded if recorded
|
|
617
|
+
|
|
618
|
+
case node
|
|
619
|
+
when Prism::LocalVariableReadNode, Prism::LocalVariableWriteNode then scope.optimistic_local(node.name)
|
|
620
|
+
when Prism::InstanceVariableReadNode, Prism::InstanceVariableWriteNode then scope.optimistic_ivar(node.name)
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
|
|
612
624
|
def if_else_type(subsequent)
|
|
613
625
|
return Type::Combinator.constant_of(nil) if subsequent.nil?
|
|
614
626
|
|
|
@@ -632,6 +644,9 @@ module Rigor
|
|
|
632
644
|
# `Constant[false]` fold one branch; `Union[true, false]`, `Dynamic[T]`, and `Top` keep both branches live.
|
|
633
645
|
def constant_predicate_polarity(predicate)
|
|
634
646
|
return nil if predicate.nil?
|
|
647
|
+
# ADR-101 — decline on an optimistically nil-free carrier; see
|
|
648
|
+
# `StatementEvaluator#optimistic_carrier?` for why the gate is here and not in `Narrowing`.
|
|
649
|
+
return nil unless optimistic_origin_for(predicate).nil?
|
|
635
650
|
|
|
636
651
|
Narrowing.predicate_certainty(type_of(predicate))
|
|
637
652
|
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "tmpdir"
|
|
4
4
|
|
|
5
|
+
require_relative "../runtime/jit"
|
|
6
|
+
|
|
5
7
|
module Rigor
|
|
6
8
|
module Inference
|
|
7
9
|
# Generic fork-over-slices map for the embarrassingly-parallel per-file passes of the
|
|
@@ -56,6 +58,13 @@ module Rigor
|
|
|
56
58
|
# (skipping `at_exit` / stdio flush — the payload is durable on disk). Any failure exits non-zero so
|
|
57
59
|
# the parent re-runs the slice in-process.
|
|
58
60
|
def run_worker(slice, block, out_path)
|
|
61
|
+
# Re-arm deferred YJIT in the child, exactly as the check fork pool does
|
|
62
|
+
# ({Analysis::Runner::PoolCoordinator#run_fork_worker}). The parent's deadline thread does not survive
|
|
63
|
+
# `fork`, so without this a worker runs its whole slice un-JITted while the parent JITs the tail it no
|
|
64
|
+
# longer runs — the pool was a *pessimization* on long runs (`coverage --protection --mutation
|
|
65
|
+
# lib/rigor/analysis`: 37s sequential against 67s at eight workers). The child picks up what is left of
|
|
66
|
+
# the parent's window rather than a fresh one; {Runtime::Jit.rearm_after_fork} carries the mechanism.
|
|
67
|
+
Runtime::Jit.rearm_after_fork
|
|
59
68
|
File.binwrite(out_path, Marshal.dump(block.call(slice)))
|
|
60
69
|
exit!(0)
|
|
61
70
|
rescue StandardError
|
|
@@ -117,7 +117,16 @@ module Rigor
|
|
|
117
117
|
:succ, :pred, :next, :abs, :magnitude,
|
|
118
118
|
:bit_length, :to_s, :to_i, :to_int, :to_f,
|
|
119
119
|
:floor, :ceil, :round, :truncate, :chr,
|
|
120
|
-
:inspect, :-@, :+@, :~, :to_r, :to_c
|
|
120
|
+
:inspect, :-@, :+@, :~, :to_r, :to_c,
|
|
121
|
+
# `rationalize` (no-arg form) is the Integer sibling of the already-folded
|
|
122
|
+
# `Float#rationalize` / `Rational#rationalize` — pure, returns `Rational(self, 1)`. The
|
|
123
|
+
# optional-`eps` binary form is NOT mirrored here: unlike `Rational#rationalize` (which reaches
|
|
124
|
+
# `NUMERIC_BINARY`'s catalog fallback via `numeric.yml`'s `nurat_rationalize` leaf entry),
|
|
125
|
+
# `numeric.yml` carries no `rationalize` entry for Integer/Float, so the 1-arg call declines to
|
|
126
|
+
# the RBS tier exactly as it does today — this only closes the 0-arg gap.
|
|
127
|
+
# `abs2` (`self * self`, always a non-negative Integer) is the Integer sibling of the
|
|
128
|
+
# already-folded `Rational#abs2` / `Complex#abs2`.
|
|
129
|
+
:rationalize, :abs2
|
|
121
130
|
].freeze
|
|
122
131
|
FLOAT_UNARY = Set[
|
|
123
132
|
:zero?, :positive?, :negative?, :nonzero?,
|
|
@@ -135,7 +144,12 @@ module Rigor
|
|
|
135
144
|
# `self >= 0`, `Math::PI` for `self < 0`. Pure sign test, deterministic; a NaN receiver yields NaN
|
|
136
145
|
# which `foldable_constant_value?` declines.
|
|
137
146
|
:arg, :angle, :phase,
|
|
138
|
-
:inspect, :-@,
|
|
147
|
+
:inspect, :-@, :+@,
|
|
148
|
+
# `abs2` (`self * self`) is the Float sibling of the already-folded `Rational#abs2` /
|
|
149
|
+
# `Complex#abs2` / (now) `Integer#abs2`. A non-finite receiver (`Infinity`, `NaN`) still folds
|
|
150
|
+
# via `foldable_constant_value?`'s existing NaN guard — `Infinity.abs2` is `Infinity`
|
|
151
|
+
# (foldable), `NaN.abs2` is `NaN` (declined, same as every other NaN-producing Float unary op).
|
|
152
|
+
:abs2
|
|
139
153
|
].freeze
|
|
140
154
|
STRING_UNARY = Set[
|
|
141
155
|
:upcase, :downcase, :capitalize, :swapcase,
|
|
@@ -195,6 +209,11 @@ module Rigor
|
|
|
195
209
|
# `flow.always-truthy-condition`.
|
|
196
210
|
REFLECTIVE_SEND_METHODS = %i[public_send send __send__].to_set.freeze
|
|
197
211
|
|
|
212
|
+
# The self-returners, the `Type::Constant` counterpart of `ShapeDispatch`'s `:shape_self` handlers
|
|
213
|
+
# (ADR-76 WD2 / ADR-78 WD3). Each returns the receiver at run time, so the value-pinned carrier
|
|
214
|
+
# survives the call.
|
|
215
|
+
CONSTANT_SELF_RETURNERS = %i[freeze itself dup clone].to_set.freeze
|
|
216
|
+
|
|
198
217
|
# @return [Rigor::Type::Constant, Rigor::Type::Union, Rigor::Type::IntegerRange, nil]
|
|
199
218
|
def try_dispatch(context)
|
|
200
219
|
receiver = context.receiver
|
|
@@ -205,6 +224,9 @@ module Rigor
|
|
|
205
224
|
first_arg = args.first
|
|
206
225
|
return nil unless first_arg.is_a?(Type::Constant) && first_arg.value.is_a?(Symbol)
|
|
207
226
|
end
|
|
227
|
+
self_return = try_fold_self_return(receiver, method_name, args)
|
|
228
|
+
return self_return if self_return
|
|
229
|
+
|
|
208
230
|
# v0.0.7 — `String#%` against a `Tuple` / `HashShape` argument runs Ruby's format-string engine
|
|
209
231
|
# when both sides are statically constant. The standard `numeric_set_of` path bails on Tuple /
|
|
210
232
|
# HashShape arguments because they are not scalar-Constant carriers, so the special-case sits
|
|
@@ -221,6 +243,32 @@ module Rigor
|
|
|
221
243
|
dispatch_by_arity(receiver_set, method_name, arg_sets)
|
|
222
244
|
end
|
|
223
245
|
|
|
246
|
+
# `freeze` / `itself` / `dup` / `clone` on a value-pinned receiver return the receiver carrier.
|
|
247
|
+
#
|
|
248
|
+
# This sits AHEAD of the catalogue path and deliberately bypasses its purity gate, because the gate
|
|
249
|
+
# asks a question that does not apply. `String#freeze` is classified `purity: mutates_self`
|
|
250
|
+
# (`data/builtins/ruby_core/string.yml`) — correctly, since the C body sets `FL_FREEZE` — and the
|
|
251
|
+
# gate then holds it back from the pure-invoke path. But nothing here is invoked: the receiver type
|
|
252
|
+
# is returned unchanged, and freezing mutates an object's flags, not its value. `Type::Constant`
|
|
253
|
+
# pins a value, not an identity, so the mutation it performs is invisible to the carrier.
|
|
254
|
+
#
|
|
255
|
+
# The gap this closes is `FOO = "bar".freeze`, the standard way to write a String constant in
|
|
256
|
+
# Ruby, which lost its value while `%w[a b].freeze` (a Tuple, via `:shape_self`) and `"bar".-@`
|
|
257
|
+
# (classified `purity: leaf`) both kept theirs. Integer / Float / Symbol had the same hole for a
|
|
258
|
+
# different reason — no `Object`/`Kernel` catalogue owns their self-returners at all.
|
|
259
|
+
#
|
|
260
|
+
# `dup` / `clone` return a NEW object at run time, so the carrier is right about the value and
|
|
261
|
+
# silent about the identity — the same trade `shape_self` already makes, and the same one the
|
|
262
|
+
# existing `"x".dup` fold (which reaches the invoke path, `dup` being `leaf`) already makes.
|
|
263
|
+
# A `clone(freeze: false)` carries an argument and declines here.
|
|
264
|
+
def try_fold_self_return(receiver, method_name, args)
|
|
265
|
+
return nil unless args.empty?
|
|
266
|
+
return nil unless CONSTANT_SELF_RETURNERS.include?(method_name)
|
|
267
|
+
return nil unless receiver.is_a?(Type::Constant)
|
|
268
|
+
|
|
269
|
+
receiver
|
|
270
|
+
end
|
|
271
|
+
|
|
224
272
|
# `Constant<String> % …` — runs the actual `String#%` operation when both sides are statically
|
|
225
273
|
# known. The argument may be:
|
|
226
274
|
# - A `Type::Constant` whose value is a scalar (Integer / Float / String / Symbol). Already handled
|
|
@@ -116,7 +116,7 @@ module Rigor
|
|
|
116
116
|
map = member_map_for_new(members, context)
|
|
117
117
|
return degraded_instance(class_name) if map.nil?
|
|
118
118
|
|
|
119
|
-
Type::Combinator.data_instance_of(members: map, class_name: class_name)
|
|
119
|
+
Type::Combinator.data_instance_of(members: widen_unowned_emptiness(map), class_name: class_name)
|
|
120
120
|
end
|
|
121
121
|
|
|
122
122
|
# Builds the member -> type map from the call's arguments, honouring the keyword vs positional
|
|
@@ -4,6 +4,7 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../../type"
|
|
6
6
|
require_relative "../../source/node_children"
|
|
7
|
+
require_relative "../mutation_widening"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module Inference
|
|
@@ -18,6 +19,39 @@ module Rigor
|
|
|
18
19
|
# Both folders `extend` this module so the projections resolve as their own module functions
|
|
19
20
|
# (matching their `module_function` style).
|
|
20
21
|
module MemberShapeProjection
|
|
22
|
+
# A value object's member holds a **reference** to whatever the constructor was handed, and an
|
|
23
|
+
# EMPTY container literal's emptiness is a fact about that argument at the instant of
|
|
24
|
+
# construction — never a fact about the member. The caller keeps its own alias, and "construct
|
|
25
|
+
# empty, then fill" (`r = R.new([], []); xs.each { |x| r.items << x }`) is the dominant Ruby
|
|
26
|
+
# shape, so of every literal a member map can pin, emptiness is the one falsified most often.
|
|
27
|
+
#
|
|
28
|
+
# It is also the only one whose reads fold to `nil` — `.first` / `.last` / `[]` on an empty
|
|
29
|
+
# container — and `nil` is the receiver type that fires `call.undefined-method` on correct code
|
|
30
|
+
# (issue #293). A non-empty literal's element evidence, by contrast, survives an append and its
|
|
31
|
+
# reads never fold to `nil`, so it is kept intact.
|
|
32
|
+
#
|
|
33
|
+
# So an empty `Tuple` / closed empty `HashShape` is recorded widened to its bare nominal —
|
|
34
|
+
# the same carrier, through the same helpers, that {Inference::MutationWidening} installs when
|
|
35
|
+
# it retracts a mutated local's literal shape.
|
|
36
|
+
def widen_unowned_emptiness(member_map)
|
|
37
|
+
return member_map if member_map.nil?
|
|
38
|
+
|
|
39
|
+
member_map.transform_values { |type| widened_empty_container(type) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def widened_empty_container(type)
|
|
43
|
+
case type
|
|
44
|
+
when Type::Tuple
|
|
45
|
+
type.elements.empty? ? MutationWidening.widen_tuple(type) : type
|
|
46
|
+
when Type::HashShape
|
|
47
|
+
# An OPEN shape claims nothing about the keys it does not list — a read of an unlisted key
|
|
48
|
+
# is already `untyped`, not `nil` — so only the closed literal `{}` carries the emptiness.
|
|
49
|
+
type.closed? && type.pairs.empty? ? MutationWidening.widen_hash_shape(type) : type
|
|
50
|
+
else
|
|
51
|
+
type
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
21
55
|
# A Data/Struct subclass body can redefine a member's synthesised reader (`def x`); when it does,
|
|
22
56
|
# `inst.x` runs that `def`, not the member, so folding the read would be unsound. A real `def` node
|
|
23
57
|
# under the class name is the discriminator (the synthesised reader has none), so an entry in the
|
|
@@ -109,7 +143,7 @@ module Rigor
|
|
|
109
143
|
return nil unless shape.optional_keys.empty?
|
|
110
144
|
return nil unless shape.pairs.keys.all? { |key| instance.members.key?(key) }
|
|
111
145
|
|
|
112
|
-
merged = instance.members.merge(shape.pairs)
|
|
146
|
+
merged = widen_unowned_emptiness(instance.members.merge(shape.pairs))
|
|
113
147
|
yield(merged, instance.class_name)
|
|
114
148
|
end
|
|
115
149
|
end
|
|
@@ -55,7 +55,12 @@ module Rigor
|
|
|
55
55
|
"::io" => ["IO"],
|
|
56
56
|
"::encoding" => %w[Encoding String],
|
|
57
57
|
"::path" => ["String"],
|
|
58
|
-
"::boolean" => %w[TrueClass FalseClass]
|
|
58
|
+
"::boolean" => %w[TrueClass FalseClass],
|
|
59
|
+
# `range[T] = Range[T] | _Range[T]`. Generic, unlike the others, but the strict arm is still a
|
|
60
|
+
# single nominal and the args are irrelevant to this pass. rbs 4.1 rewrote `Array#[]`'s slicing
|
|
61
|
+
# overload from `(::Range[::Integer?])` to `(range[int])`; without the entry both it and the
|
|
62
|
+
# `(int) -> E` overload look alias-typed, so `a[1..2]` resolved to the element type.
|
|
63
|
+
"::range" => ["Range"]
|
|
59
64
|
}.freeze
|
|
60
65
|
private_constant :ALIAS_STRICT_NOMINALS
|
|
61
66
|
|
|
@@ -5,6 +5,7 @@ require_relative "../../type"
|
|
|
5
5
|
require_relative "../../rbs_extended"
|
|
6
6
|
require_relative "../rbs_type_translator"
|
|
7
7
|
require_relative "../void_origin"
|
|
8
|
+
require_relative "../optimistic_origin"
|
|
8
9
|
require_relative "overload_selector"
|
|
9
10
|
|
|
10
11
|
module Rigor
|
|
@@ -362,6 +363,11 @@ module Rigor
|
|
|
362
363
|
# to the *direct*-RBS dispatch (the receiver's own resolvable class): the user-class / Object
|
|
363
364
|
# ancestor fallback nils both out (WD4 defers that, murkier, surface).
|
|
364
365
|
record_void_recovery(method_type, scope, call_node, [class_name, method_name, kind])
|
|
366
|
+
# Issue #286 — the same call site is the one place that still knows the selected overload spelled
|
|
367
|
+
# its miss as `%a{implicitly-returns-nil}` rather than as `?`; the translator below reads the
|
|
368
|
+
# return type only, by the deliberate choice the spec records. Mark the result so a certainty
|
|
369
|
+
# judgment downstream can tell "nil-free because of its class" from "nil-free because we bet".
|
|
370
|
+
record_optimistic_nil_free(method_definition, method_type, scope, call_node)
|
|
365
371
|
|
|
366
372
|
full_type_vars = compose_block_type_vars(method_type, type_vars, block_type)
|
|
367
373
|
|
|
@@ -386,6 +392,16 @@ module Rigor
|
|
|
386
392
|
)
|
|
387
393
|
end
|
|
388
394
|
|
|
395
|
+
# Issue #286 — record that this value's nil-freeness is a bet rather than a class property. Gated on
|
|
396
|
+
# `scope` && `call_node` exactly as {#record_void_recovery} is, which scopes it to the direct-RBS
|
|
397
|
+
# dispatch path.
|
|
398
|
+
def record_optimistic_nil_free(method_definition, method_type, scope, call_node)
|
|
399
|
+
return unless scope && call_node
|
|
400
|
+
return unless OptimisticOrigin.optimistic_overload?(method_definition, method_type)
|
|
401
|
+
|
|
402
|
+
scope.record_optimistic_origin(call_node, OptimisticOrigin::IMPLICITLY_RETURNS_NIL)
|
|
403
|
+
end
|
|
404
|
+
|
|
389
405
|
def void_return?(method_type)
|
|
390
406
|
fun = method_type.type
|
|
391
407
|
fun.respond_to?(:return_type) && fun.return_type.is_a?(RBS::Types::Bases::Void)
|
|
@@ -14,17 +14,38 @@ module Rigor
|
|
|
14
14
|
# * `new(str)` / `new(str, opts)` — constructs a Regexp at fold time when the pattern argument is a
|
|
15
15
|
# `Constant[String]`. The optional second argument may be a `Constant[Integer]` (flag bits), a
|
|
16
16
|
# `Constant[true/false]` (IGNORECASE shorthand), or absent. Returns `Constant[Regexp]`.
|
|
17
|
+
# * `compile(str)` / `compile(str, opts)` — `Regexp.compile` is a documented alias of `Regexp.new`
|
|
18
|
+
# (same C entry point, `rb_reg_s_new`); shares `fold_new` verbatim.
|
|
19
|
+
# * `union(*patterns)` / `union(array)` — builds the combined pattern at inference time when every
|
|
20
|
+
# element is a `Constant[String]` or `Constant[Regexp]`. Delegates to the real `Regexp.union` so the
|
|
21
|
+
# zero-arg (`/(?!)/`), single-array-argument, and pre-built-Regexp-argument forms all match Ruby's
|
|
22
|
+
# own semantics without separate case analysis. Returns `Constant[Regexp]`.
|
|
23
|
+
# * `linear_time?(pattern)` — one `Constant[String]` or `Constant[Regexp]` argument. Returns
|
|
24
|
+
# `Constant[bool]`.
|
|
17
25
|
#
|
|
18
26
|
# === Non-constant / unsupported cases
|
|
19
27
|
#
|
|
20
28
|
# Returns `nil` (deferring to the next dispatcher tier) when:
|
|
21
29
|
# - the receiver is not `Singleton[Regexp]`,
|
|
22
|
-
# - the required pattern argument is not a `Constant[String]
|
|
30
|
+
# - the required pattern argument is not a `Constant[String]` (or, for `union`/`linear_time?`, not a
|
|
31
|
+
# `Constant[String]`/`Constant[Regexp]`),
|
|
32
|
+
# - `union` is given more elements than `UNION_LIMIT` or any element fails the constant check,
|
|
33
|
+
# - `linear_time?` is given anything other than exactly one constant argument (a `timeout:` keyword
|
|
34
|
+
# argument is rejected this way — it would land as a second positional slot),
|
|
23
35
|
# - the method is not in the supported set.
|
|
24
36
|
module RegexpFolding
|
|
25
37
|
REGEXP_ESCAPE_METHODS = Set[:escape, :quote].freeze
|
|
26
38
|
private_constant :REGEXP_ESCAPE_METHODS
|
|
27
39
|
|
|
40
|
+
# `.new` and `.compile` are the same C function under two names — both fold identically.
|
|
41
|
+
REGEXP_NEW_METHODS = Set[:new, :compile].freeze
|
|
42
|
+
private_constant :REGEXP_NEW_METHODS
|
|
43
|
+
|
|
44
|
+
# `Regexp.union` element cap — same rationale as `SHELLWORDS_SPLIT_LIMIT` / `STRING_ARRAY_LIFT_LIMIT`:
|
|
45
|
+
# keep the fold's work bounded even though the source is a static literal.
|
|
46
|
+
UNION_LIMIT = 64
|
|
47
|
+
private_constant :UNION_LIMIT
|
|
48
|
+
|
|
28
49
|
module_function
|
|
29
50
|
|
|
30
51
|
# @return [Rigor::Type, nil] folded result, or nil to defer.
|
|
@@ -34,8 +55,10 @@ module Rigor
|
|
|
34
55
|
args = context.args
|
|
35
56
|
return nil unless SingletonFolding.receiver?(receiver, "Regexp")
|
|
36
57
|
return fold_escape(args) if REGEXP_ESCAPE_METHODS.include?(method_name)
|
|
37
|
-
return fold_new(args) if method_name
|
|
58
|
+
return fold_new(args) if REGEXP_NEW_METHODS.include?(method_name)
|
|
38
59
|
return fold_last_match(context) if method_name == :last_match
|
|
60
|
+
return fold_union(args) if method_name == :union
|
|
61
|
+
return fold_linear_time(args) if method_name == :linear_time?
|
|
39
62
|
|
|
40
63
|
nil
|
|
41
64
|
end
|
|
@@ -111,9 +134,9 @@ module Rigor
|
|
|
111
134
|
end
|
|
112
135
|
|
|
113
136
|
# `Regexp.new(pattern)` / `Regexp.new(pattern, opts)` — constructs the pattern at inference time.
|
|
114
|
-
#
|
|
115
|
-
#
|
|
116
|
-
# the RBS tier.
|
|
137
|
+
# Also serves `Regexp.compile`, its documented alias (`REGEXP_NEW_METHODS`). Delegates to Ruby's
|
|
138
|
+
# real `Regexp.new` so all option forms (Integer flags, `true`/`false`, option strings) are handled
|
|
139
|
+
# without case-analysis; non-constant or invalid arguments decline through to the RBS tier.
|
|
117
140
|
def fold_new(args)
|
|
118
141
|
return nil if args.empty? || args.size > 2
|
|
119
142
|
|
|
@@ -132,6 +155,42 @@ module Rigor
|
|
|
132
155
|
def constant_value_or_nil(type)
|
|
133
156
|
type.is_a?(Type::Constant) ? type.value : nil
|
|
134
157
|
end
|
|
158
|
+
|
|
159
|
+
# `Regexp.union(*patterns)` / `Regexp.union(array)` — folds when every pattern element is a
|
|
160
|
+
# `Constant[String]` or `Constant[Regexp]`. A single `Tuple` argument is unpacked exactly like
|
|
161
|
+
# Ruby's own single-array-argument form (`Regexp.union(["a", "b"])`); anything else is treated as
|
|
162
|
+
# the splatted-arguments form, including zero arguments (`Regexp.union()` → `/(?!)/`). Delegates to
|
|
163
|
+
# the real `Regexp.union` so encoding handling, escaping of String elements, and pass-through of
|
|
164
|
+
# already-`Regexp` elements all match Ruby exactly; declines (rescues to `nil`) on an encoding
|
|
165
|
+
# conflict between elements rather than propagating the exception.
|
|
166
|
+
def fold_union(args)
|
|
167
|
+
pattern_args = args.size == 1 && args.first.is_a?(Type::Tuple) ? args.first.elements : args
|
|
168
|
+
return nil if pattern_args.size > UNION_LIMIT
|
|
169
|
+
return nil unless pattern_args.all? { |arg| union_element?(arg) }
|
|
170
|
+
|
|
171
|
+
Type::Combinator.constant_of(Regexp.union(*pattern_args.map(&:value)))
|
|
172
|
+
rescue StandardError
|
|
173
|
+
nil
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def union_element?(arg)
|
|
177
|
+
arg.is_a?(Type::Constant) && (arg.value.is_a?(String) || arg.value.is_a?(Regexp))
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# `Regexp.linear_time?(pattern)` — one `Constant[String]` or `Constant[Regexp]` argument. A second
|
|
181
|
+
# argument (the `timeout:` keyword, which lands as an extra positional slot) declines rather than
|
|
182
|
+
# being silently ignored. An invalid String pattern raises `RegexpError` when Ruby compiles it
|
|
183
|
+
# internally; rescued to `nil` like every other fold in this file.
|
|
184
|
+
def fold_linear_time(args)
|
|
185
|
+
return nil unless args.size == 1
|
|
186
|
+
|
|
187
|
+
arg = args.first
|
|
188
|
+
return nil unless union_element?(arg)
|
|
189
|
+
|
|
190
|
+
Type::Combinator.constant_of(Regexp.linear_time?(arg.value))
|
|
191
|
+
rescue StandardError
|
|
192
|
+
nil
|
|
193
|
+
end
|
|
135
194
|
end
|
|
136
195
|
end
|
|
137
196
|
end
|