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
|
@@ -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
|
|
@@ -43,8 +44,11 @@ module Rigor
|
|
|
43
44
|
# * `block_type:` is ignored; method types that constrain the block return type are not yet honored.
|
|
44
45
|
# * Keyword arguments are not threaded through call_arg_types, so overloads with required keywords
|
|
45
46
|
# are skipped (they cannot match the empty kwargs we send).
|
|
46
|
-
# * Method-level type parameters
|
|
47
|
-
#
|
|
47
|
+
# * Method-level type parameters bind only from two positions: the block return type (Slice 6 phase C)
|
|
48
|
+
# and a positional parameter whose declared type is EXACTLY a type variable (issue #303 —
|
|
49
|
+
# `def foo[T]: (T) -> T` binds `T` from the first argument, and carries it into a generic return
|
|
50
|
+
# such as `-> Array[T]`). A variable reachable only through a container position (`Array[T] arg`),
|
|
51
|
+
# a rest positional (`*T`), or a keyword parameter is still unbound and degrades to `Dynamic[Top]`.
|
|
48
52
|
#
|
|
49
53
|
# See docs/adr/4-type-inference-engine.md for the broader plan.
|
|
50
54
|
# rubocop:disable Metrics/ModuleLength
|
|
@@ -60,6 +64,14 @@ module Rigor
|
|
|
60
64
|
# objects answer to methods their RBS omits (`ActionController::Base`, `Hash`, …).
|
|
61
65
|
ALLOWED_RBS_COMPLETE_ANCESTORS = ["Rigor::Plugin::Base"].freeze
|
|
62
66
|
|
|
67
|
+
# Shared empty returns for the argument-position type-variable binding (issue #303). The
|
|
68
|
+
# no-candidate answer is by far the common case — every non-generic overload takes it — so it must
|
|
69
|
+
# not allocate.
|
|
70
|
+
EMPTY_TYPE_VARS = {}.freeze
|
|
71
|
+
private_constant :EMPTY_TYPE_VARS
|
|
72
|
+
EMPTY_TYPE_PARAM_NAMES = [].freeze
|
|
73
|
+
private_constant :EMPTY_TYPE_PARAM_NAMES
|
|
74
|
+
|
|
63
75
|
# @param receiver [Rigor::Type]
|
|
64
76
|
# @param method_name [Symbol]
|
|
65
77
|
# @param args [Array<Rigor::Type>]
|
|
@@ -354,16 +366,9 @@ module Rigor
|
|
|
354
366
|
)
|
|
355
367
|
return nil unless method_type
|
|
356
368
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
# call node, so the `static.value-use.void` check rule can fire when this `top` is used in value
|
|
361
|
-
# context. Recording is gated on `scope` && `call_node` being present, which naturally scopes it
|
|
362
|
-
# to the *direct*-RBS dispatch (the receiver's own resolvable class): the user-class / Object
|
|
363
|
-
# ancestor fallback nils both out (WD4 defers that, murkier, surface).
|
|
364
|
-
record_void_recovery(method_type, scope, call_node, [class_name, method_name, kind])
|
|
365
|
-
|
|
366
|
-
full_type_vars = compose_block_type_vars(method_type, type_vars, block_type)
|
|
369
|
+
call_site = [class_name, method_name, kind]
|
|
370
|
+
record_dispatch_provenance(method_definition, method_type, scope, call_node, call_site)
|
|
371
|
+
full_type_vars = compose_type_vars(method_type, type_vars, args, block_type, scope, call_node, call_site)
|
|
367
372
|
|
|
368
373
|
RbsTypeTranslator.translate(
|
|
369
374
|
method_type.type.return_type,
|
|
@@ -373,6 +378,34 @@ module Rigor
|
|
|
373
378
|
)
|
|
374
379
|
end
|
|
375
380
|
|
|
381
|
+
# The two provenance side-tables the return-typing tier is the last place able to populate, recorded
|
|
382
|
+
# together because they share one gate (`scope` && `call_node`) and one call site.
|
|
383
|
+
#
|
|
384
|
+
# ADR-100 WD2/WD3 — the return-typing tier is where `void → top` widens, so it is the one place that
|
|
385
|
+
# still knows the RBS return was an author-declared `-> void` before the translator erases it to a
|
|
386
|
+
# plain `top`. The recovery lands on the scope's `void_origins` side-table, keyed by the call node,
|
|
387
|
+
# so the `static.value-use.void` check rule can fire when this `top` is used in value context. The
|
|
388
|
+
# gate naturally scopes it to the *direct*-RBS dispatch (the receiver's own resolvable class): the
|
|
389
|
+
# user-class / Object ancestor fallback nils both out (WD4 defers that, murkier, surface).
|
|
390
|
+
#
|
|
391
|
+
# Issue #286 — the same call site is the one place that still knows the selected overload spelled
|
|
392
|
+
# its miss as `%a{implicitly-returns-nil}` rather than as `?`; the translator reads the return type
|
|
393
|
+
# only, by the deliberate choice the spec records. Mark the result so a certainty judgment
|
|
394
|
+
# downstream can tell "nil-free because of its class" from "nil-free because we bet".
|
|
395
|
+
def record_dispatch_provenance(method_definition, method_type, scope, call_node, call_site)
|
|
396
|
+
record_void_recovery(method_type, scope, call_node, call_site)
|
|
397
|
+
record_optimistic_nil_free(method_definition, method_type, scope, call_node)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# The two method-level type-parameter binding positions, layered in precedence order over the
|
|
401
|
+
# receiver-derived `type_vars`: the block return type first, then the argument positions (which
|
|
402
|
+
# never displace an existing key). See {#compose_arg_type_vars} for the argument envelope.
|
|
403
|
+
def compose_type_vars(method_type, type_vars, args, block_type, scope, call_node, call_site)
|
|
404
|
+
vars = compose_block_type_vars(method_type, type_vars, block_type)
|
|
405
|
+
compose_arg_type_vars(method_type, vars, args, scope: scope, call_node: call_node,
|
|
406
|
+
call_site: call_site)
|
|
407
|
+
end
|
|
408
|
+
|
|
376
409
|
# Record the `void → top` recovery when the selected overload declares `-> void` and both `scope` and
|
|
377
410
|
# `call_node` are present (the direct-dispatch path). `void_site` is the `[class_name, method_name,
|
|
378
411
|
# kind]` triple the {VoidOrigin} carries.
|
|
@@ -386,6 +419,16 @@ module Rigor
|
|
|
386
419
|
)
|
|
387
420
|
end
|
|
388
421
|
|
|
422
|
+
# Issue #286 — record that this value's nil-freeness is a bet rather than a class property. Gated on
|
|
423
|
+
# `scope` && `call_node` exactly as {#record_void_recovery} is, which scopes it to the direct-RBS
|
|
424
|
+
# dispatch path.
|
|
425
|
+
def record_optimistic_nil_free(method_definition, method_type, scope, call_node)
|
|
426
|
+
return unless scope && call_node
|
|
427
|
+
return unless OptimisticOrigin.optimistic_overload?(method_definition, method_type)
|
|
428
|
+
|
|
429
|
+
scope.record_optimistic_origin(call_node, OptimisticOrigin::IMPLICITLY_RETURNS_NIL)
|
|
430
|
+
end
|
|
431
|
+
|
|
389
432
|
def void_return?(method_type)
|
|
390
433
|
fun = method_type.type
|
|
391
434
|
fun.respond_to?(:return_type) && fun.return_type.is_a?(RBS::Types::Bases::Void)
|
|
@@ -418,6 +461,109 @@ module Rigor
|
|
|
418
461
|
type_vars.merge(block_var_name => block_type)
|
|
419
462
|
end
|
|
420
463
|
|
|
464
|
+
# Issue #303 — bind method-level type parameters from ARGUMENT positions, layering on top of the
|
|
465
|
+
# receiver-derived and block-derived `type_vars` exactly as {#compose_block_type_vars} does, so
|
|
466
|
+
# `Ractor.make_shareable("x")` (`[T] (T) -> T`) answers `"x"` instead of `Dynamic[top]`.
|
|
467
|
+
#
|
|
468
|
+
# Envelope, deliberately the narrowest sound shape:
|
|
469
|
+
#
|
|
470
|
+
# * only a positional parameter (required or optional) whose declared type is EXACTLY
|
|
471
|
+
# `RBS::Types::Variable` — no container walk, so `(Array[T]) -> T` still degrades;
|
|
472
|
+
# * only names the SELECTED overload declares in its own `type_params` (a class-level variable
|
|
473
|
+
# keeps its receiver-derived binding);
|
|
474
|
+
# * an existing key wins, so the receiver and the block return type both outrank an argument;
|
|
475
|
+
# * a `Dynamic[Top]` argument carries no evidence and contributes nothing;
|
|
476
|
+
# * repeated occurrences of one variable union their arguments.
|
|
477
|
+
#
|
|
478
|
+
# See {#arg_binding_permitted?} for why the contribution is gated on the call site.
|
|
479
|
+
def compose_arg_type_vars(method_type, type_vars, args, scope:, call_node:, call_site:)
|
|
480
|
+
bindings = arg_type_var_bindings(method_type, type_vars, args)
|
|
481
|
+
return type_vars if bindings.empty?
|
|
482
|
+
return type_vars unless arg_binding_permitted?(scope, call_node, call_site)
|
|
483
|
+
|
|
484
|
+
type_vars.merge(bindings)
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# The candidate bindings, computed before the guard so a non-generic overload (the overwhelming
|
|
488
|
+
# majority) never pays for the `scope` probes.
|
|
489
|
+
def arg_type_var_bindings(method_type, type_vars, args)
|
|
490
|
+
declared = declared_type_param_names(method_type)
|
|
491
|
+
return EMPTY_TYPE_VARS if declared.empty? || args.empty?
|
|
492
|
+
|
|
493
|
+
fun = method_type.type
|
|
494
|
+
return EMPTY_TYPE_VARS unless fun.respond_to?(:required_positionals)
|
|
495
|
+
|
|
496
|
+
positionals = fun.required_positionals + fun.optional_positionals
|
|
497
|
+
positionals.zip(args).each_with_object({}) do |(param, arg), bindings|
|
|
498
|
+
next if arg.nil?
|
|
499
|
+
|
|
500
|
+
name = variable_param_name(param, declared, type_vars)
|
|
501
|
+
next if name.nil?
|
|
502
|
+
next if no_static_evidence?(arg)
|
|
503
|
+
|
|
504
|
+
bindings[name] = bindings.key?(name) ? Type::Combinator.union(bindings[name], arg) : arg
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def declared_type_param_names(method_type)
|
|
509
|
+
params = method_type.respond_to?(:type_params) ? method_type.type_params : nil
|
|
510
|
+
return EMPTY_TYPE_PARAM_NAMES if params.nil? || params.empty?
|
|
511
|
+
|
|
512
|
+
params.map(&:name)
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
# The parameter's binding name when it is spelled as a bare method-level type variable that is
|
|
516
|
+
# still unbound; nil for every other shape.
|
|
517
|
+
def variable_param_name(param, declared, type_vars)
|
|
518
|
+
declared_type = param.type
|
|
519
|
+
return nil unless declared_type.is_a?(RBS::Types::Variable)
|
|
520
|
+
|
|
521
|
+
name = declared_type.name
|
|
522
|
+
return nil unless declared.include?(name)
|
|
523
|
+
return nil if type_vars.key?(name)
|
|
524
|
+
|
|
525
|
+
name
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# `Dynamic[top]` is the engine's "we could not tell" answer, so binding a variable to it would
|
|
529
|
+
# dress up an absence of evidence as an inference. The variable stays unbound and degrades as it
|
|
530
|
+
# did before, which is the same value anyway.
|
|
531
|
+
def no_static_evidence?(arg)
|
|
532
|
+
arg.is_a?(Type::Dynamic) && arg.static_facet.is_a?(Type::Top)
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# Issue #303 — the FP guard on the argument-position binding.
|
|
536
|
+
#
|
|
537
|
+
# Binding an argument makes the RESULT precise, which turns a benign mis-resolution into a
|
|
538
|
+
# confidently wrong type. The shape that matters is a user method shadowing the RBS method this
|
|
539
|
+
# dispatch resolved: `spec/integration/fixtures/kernel_functions.rb`'s `def p(node)` self-send
|
|
540
|
+
# resolves through `Kernel#p: [T] (T) -> T`, and binding `T` would type `p(1)` as `1` when the
|
|
541
|
+
# real method returns a String.
|
|
542
|
+
#
|
|
543
|
+
# Two gates, cheapest first.
|
|
544
|
+
#
|
|
545
|
+
# 1. `scope` && `call_node` present, the same evidence surface {#record_void_recovery} reads.
|
|
546
|
+
# DIAGNOSED, not assumed: that fixture's `p(1)` reaches here through
|
|
547
|
+
# `MethodDispatcher#try_user_class_fallback`, which dispatches with `scope: nil, call_node:
|
|
548
|
+
# nil` — so the presence gate alone already declines every call routed through the Object /
|
|
549
|
+
# Kernel ancestor fallback, the path a class with no RBS of its own always takes.
|
|
550
|
+
# 2. The explicit redefinition probe, in the shape `KernelDispatch#user_redefined?` established.
|
|
551
|
+
# The presence gate does NOT cover the direct-dispatch spelling of the same hazard — a
|
|
552
|
+
# top-level `def p(x)` called from the top level resolves `Nominal[Object]` DIRECTLY, with
|
|
553
|
+
# scope and call node both live — so a discovered top-level def, or a discovered method on the
|
|
554
|
+
# resolved class itself, declines too. Over-wide by construction (a project class that also
|
|
555
|
+
# has RBS declines for its own self-sends, losing precision it could have kept), and
|
|
556
|
+
# deliberately so: the cost is a `Dynamic[top]` that was already there.
|
|
557
|
+
def arg_binding_permitted?(scope, call_node, call_site)
|
|
558
|
+
return false if scope.nil? || call_node.nil?
|
|
559
|
+
|
|
560
|
+
class_name, method_name, kind = call_site
|
|
561
|
+
return false if method_name.nil?
|
|
562
|
+
return false if scope.top_level_def_for(method_name)
|
|
563
|
+
|
|
564
|
+
!scope.discovered_method?(class_name, method_name, kind)
|
|
565
|
+
end
|
|
566
|
+
|
|
421
567
|
def method_type_block_return_variable(method_type)
|
|
422
568
|
return_variable = block_return_variable(method_type)
|
|
423
569
|
return nil if return_variable.nil?
|
|
@@ -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
|