rigortype 0.2.9 → 0.3.0
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 +2 -2
- data/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +18 -9
- data/docs/handbook/08-understanding-errors.md +1 -0
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +98 -4
- data/docs/manual/02-cli-reference.md +49 -12
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +23 -0
- data/docs/manual/07-plugins.md +4 -4
- data/docs/manual/09-editor-integration.md +4 -4
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +93 -0
- data/lib/rigor/analysis/check_rules.rb +557 -95
- data/lib/rigor/analysis/dependency_recorder.rb +93 -9
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +456 -51
- data/lib/rigor/analysis/path_expansion.rb +42 -0
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
- data/lib/rigor/analysis/rule_catalog.rb +226 -1
- data/lib/rigor/analysis/run_cache_key.rb +68 -0
- data/lib/rigor/analysis/run_cache_probe.rb +72 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
- data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
- data/lib/rigor/analysis/runner.rb +343 -68
- data/lib/rigor/analysis/severity_stamp.rb +43 -0
- data/lib/rigor/analysis/worker_session.rb +30 -2
- data/lib/rigor/bleeding_edge.rb +37 -8
- data/lib/rigor/builtins/regex_refinement.rb +36 -1
- data/lib/rigor/cache/descriptor.rb +88 -28
- data/lib/rigor/cache/file_digest.rb +158 -0
- data/lib/rigor/cache/incremental_snapshot.rb +41 -5
- data/lib/rigor/cache/rbs_descriptor.rb +56 -10
- data/lib/rigor/cache/store.rb +33 -3
- data/lib/rigor/ci_detector.rb +90 -0
- data/lib/rigor/cli/annotate_command.rb +15 -9
- data/lib/rigor/cli/baseline_command.rb +4 -1
- data/lib/rigor/cli/check_command.rb +212 -27
- data/lib/rigor/cli/coverage_command.rb +6 -1
- data/lib/rigor/cli/docs_command.rb +0 -30
- data/lib/rigor/cli/doctor_command.rb +163 -23
- data/lib/rigor/cli/lsp_command.rb +5 -0
- data/lib/rigor/cli/mcp_command.rb +5 -0
- data/lib/rigor/cli/plugins_command.rb +36 -6
- data/lib/rigor/cli/plugins_renderer.rb +44 -12
- data/lib/rigor/cli/probe_environment.rb +85 -0
- data/lib/rigor/cli/sig_gen_command.rb +36 -9
- data/lib/rigor/cli/skill_command.rb +1 -32
- data/lib/rigor/cli/trace_command.rb +5 -6
- data/lib/rigor/cli/type_of_command.rb +9 -8
- data/lib/rigor/cli/type_scan_command.rb +8 -8
- data/lib/rigor/cli.rb +15 -4
- data/lib/rigor/config_audit.rb +48 -2
- data/lib/rigor/configuration/severity_profile.rb +31 -0
- data/lib/rigor/configuration.rb +156 -8
- data/lib/rigor/environment/default_libraries.rb +35 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +19 -27
- data/lib/rigor/inference/budget_trace.rb +77 -2
- data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
- data/lib/rigor/inference/def_handle.rb +23 -0
- data/lib/rigor/inference/def_node_resolver.rb +90 -0
- data/lib/rigor/inference/def_return_typer.rb +2 -1
- data/lib/rigor/inference/expression_typer.rb +361 -113
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +11 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
- data/lib/rigor/inference/method_dispatcher.rb +57 -1
- data/lib/rigor/inference/mutation_widening.rb +42 -8
- data/lib/rigor/inference/narrowing.rb +127 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +15 -0
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +8 -1
- data/lib/rigor/inference/scope_indexer.rb +451 -36
- data/lib/rigor/inference/statement_evaluator.rb +64 -15
- data/lib/rigor/inference/struct_fold_safety.rb +48 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
- data/lib/rigor/inference/void_origin.rb +25 -0
- data/lib/rigor/inference/void_tail_summary.rb +220 -0
- data/lib/rigor/language_server/completion_provider.rb +2 -1
- data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
- data/lib/rigor/language_server/folding_range_provider.rb +2 -1
- data/lib/rigor/language_server/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/plugin/base.rb +9 -22
- data/lib/rigor/plugin/inflector.rb +12 -3
- data/lib/rigor/plugin/io_boundary.rb +5 -1
- data/lib/rigor/plugin/isolation.rb +81 -11
- data/lib/rigor/plugin/load_error.rb +10 -1
- data/lib/rigor/plugin/loader.rb +96 -14
- data/lib/rigor/plugin/node_rule_walk.rb +5 -3
- data/lib/rigor/plugin/registry.rb +32 -23
- data/lib/rigor/plugin.rb +26 -0
- data/lib/rigor/protection/mutator.rb +3 -2
- data/lib/rigor/reflection.rb +64 -0
- data/lib/rigor/runtime/jit.rb +128 -0
- data/lib/rigor/scope/discovery_index.rb +12 -2
- data/lib/rigor/scope.rb +122 -28
- data/lib/rigor/sig_gen/classification.rb +5 -1
- data/lib/rigor/sig_gen/generator.rb +38 -6
- data/lib/rigor/sig_gen/observation_collector.rb +4 -3
- data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
- data/lib/rigor/sig_gen/renderer.rb +10 -0
- data/lib/rigor/sig_gen/write_result.rb +9 -4
- data/lib/rigor/sig_gen/writer.rb +24 -2
- data/lib/rigor/source/node_children.rb +116 -0
- data/lib/rigor/source/node_locator.rb +3 -1
- data/lib/rigor/source/node_walker.rb +4 -2
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/difference.rb +28 -24
- data/lib/rigor/type/hash_shape.rb +34 -10
- data/lib/rigor/version.rb +1 -1
- data/lib/rigortype.rb +24 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +3 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +7 -5
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
- data/sig/prism_node_children.rbs +9 -0
- data/sig/rigor/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +2 -3
- data/sig/rigor/reflection.rbs +1 -0
- data/sig/rigor/scope.rbs +12 -1
- data/skills/rigor-ci-setup/SKILL.md +10 -0
- data/skills/rigor-editor-setup/SKILL.md +11 -0
- data/skills/rigor-next-steps/SKILL.md +23 -2
- data/skills/rigor-plugin-author/SKILL.md +3 -3
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
- data/skills/rigor-plugin-review/SKILL.md +2 -2
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
- data/skills/rigor-project-init/SKILL.md +29 -3
- data/skills/rigor-project-init/references/01-detect.md +1 -1
- data/skills/rigor-project-init/references/02-configure.md +39 -5
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
- data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
- metadata +42 -24
- data/lib/rigor/cli/ci_detector.rb +0 -89
|
@@ -27,13 +27,14 @@ module Rigor
|
|
|
27
27
|
# the chain collapses to `Constant[nil]` so the carrier surfaces through downstream narrowing. A
|
|
28
28
|
# non-shape intermediate falls through to the projection answer.
|
|
29
29
|
# - HashShape#`size`/`length`: no-arg.
|
|
30
|
-
# - HashShape#`[]`, HashShape#`fetch`, HashShape#`dig` with a single `Constant
|
|
31
|
-
# argument
|
|
30
|
+
# - HashShape#`[]`, HashShape#`fetch`, HashShape#`dig` with a single `Constant` scalar-key
|
|
31
|
+
# argument (Symbol / String / Integer / Float / true / false / nil) matching one of the declared
|
|
32
|
+
# keys. `[]` and `dig` resolve missing keys to
|
|
32
33
|
# `Constant[nil]`; `fetch` (no default, no block) falls through on a miss because Ruby would raise
|
|
33
34
|
# `KeyError` and the analyzer prefers the conservative projection answer.
|
|
34
35
|
# - HashShape#`dig` with multi-arg chains (Slice 5 phase 2 sub-phase 2). Same chaining semantics as
|
|
35
36
|
# Tuple#`dig`.
|
|
36
|
-
# - HashShape#`values_at` with a list of `Constant
|
|
37
|
+
# - HashShape#`values_at` with a list of `Constant` scalar-key arguments (Slice 5 phase 2
|
|
37
38
|
# sub-phase 2). The result is a `Tuple` whose elements are the per-key values (`Constant[nil]` for
|
|
38
39
|
# missing keys, mirroring Ruby's runtime behaviour).
|
|
39
40
|
#
|
|
@@ -244,6 +245,14 @@ module Rigor
|
|
|
244
245
|
carrier
|
|
245
246
|
end
|
|
246
247
|
|
|
248
|
+
# True when `key` is a value the `HashShape` carrier can hold as a key (the value-pinned scalar
|
|
249
|
+
# set — Symbol, String, Integer, Float, true, false, nil). Key identity in every handler below
|
|
250
|
+
# is Ruby's own `eql?` via the shape's native `pairs` Hash, so `1` and `1.0` are distinct keys
|
|
251
|
+
# while `1.0` and `1.00` collide — exactly the runtime's `Hash` semantics.
|
|
252
|
+
def static_shape_key?(key)
|
|
253
|
+
Type::HashShape::ALLOWED_KEY_CLASSES.any? { |klass| key.is_a?(klass) }
|
|
254
|
+
end
|
|
255
|
+
|
|
247
256
|
def dispatch_nominal_size(nominal, method_name, args)
|
|
248
257
|
projection = nominal_projection(nominal, method_name, args)
|
|
249
258
|
return projection if projection
|
|
@@ -424,7 +433,7 @@ module Rigor
|
|
|
424
433
|
base = difference.base
|
|
425
434
|
return nil unless base.is_a?(Type::Nominal)
|
|
426
435
|
|
|
427
|
-
if removes_empty_witness?
|
|
436
|
+
if difference.removes_empty_witness?
|
|
428
437
|
precise = empty_removal_projection(difference, method_name, args)
|
|
429
438
|
return precise if precise
|
|
430
439
|
end
|
|
@@ -432,23 +441,6 @@ module Rigor
|
|
|
432
441
|
dispatch_nominal_size(base, method_name, args)
|
|
433
442
|
end
|
|
434
443
|
|
|
435
|
-
EMPTY_WITNESS_PREDICATES = {
|
|
436
|
-
"String" => ->(removed) { removed.is_a?(Type::Constant) && removed.value == "" },
|
|
437
|
-
"Integer" => lambda { |removed|
|
|
438
|
-
removed.is_a?(Type::Constant) && removed.value.is_a?(Integer) && removed.value.zero?
|
|
439
|
-
},
|
|
440
|
-
"Array" => ->(removed) { removed.is_a?(Type::Tuple) && removed.elements.empty? },
|
|
441
|
-
"Hash" => ->(removed) { removed.is_a?(Type::HashShape) && removed.pairs.empty? }
|
|
442
|
-
}.freeze
|
|
443
|
-
private_constant :EMPTY_WITNESS_PREDICATES
|
|
444
|
-
|
|
445
|
-
def removes_empty_witness?(difference)
|
|
446
|
-
return false unless difference.base.is_a?(Type::Nominal)
|
|
447
|
-
|
|
448
|
-
predicate = EMPTY_WITNESS_PREDICATES[difference.base.class_name]
|
|
449
|
-
!!(predicate && predicate.call(difference.removed))
|
|
450
|
-
end
|
|
451
|
-
|
|
452
444
|
# Methods on a non-empty String that preserve non-emptiness (they transform characters but never
|
|
453
445
|
# reduce the string to "").
|
|
454
446
|
NON_EMPTY_STRING_PRESERVING_UNARY = Set[:upcase, :downcase, :capitalize, :swapcase, :reverse].freeze
|
|
@@ -1308,7 +1300,7 @@ module Rigor
|
|
|
1308
1300
|
return nil unless arg.is_a?(Type::Constant)
|
|
1309
1301
|
|
|
1310
1302
|
key = arg.value
|
|
1311
|
-
return nil unless
|
|
1303
|
+
return nil unless static_shape_key?(key)
|
|
1312
1304
|
return nil unless shape.pairs.key?(key)
|
|
1313
1305
|
|
|
1314
1306
|
values << shape.pairs[key]
|
|
@@ -1327,7 +1319,7 @@ module Rigor
|
|
|
1327
1319
|
return nil unless arg.is_a?(Type::Constant)
|
|
1328
1320
|
|
|
1329
1321
|
key = arg.value
|
|
1330
|
-
return nil unless
|
|
1322
|
+
return nil unless static_shape_key?(key)
|
|
1331
1323
|
return Type::Combinator.constant_of(nil) unless shape.pairs.key?(key)
|
|
1332
1324
|
|
|
1333
1325
|
Type::Combinator.tuple_of(Type::Combinator.constant_of(key), shape.pairs[key])
|
|
@@ -1436,7 +1428,7 @@ module Rigor
|
|
|
1436
1428
|
end
|
|
1437
1429
|
|
|
1438
1430
|
# `shape.has_key?(k)` / `key?(k)` / `member?(k)` / `include?(k)` — folds to `Constant[true/false]`
|
|
1439
|
-
# when the argument is a `Constant
|
|
1431
|
+
# when the argument is a `Constant` scalar key and the shape is closed with no optional
|
|
1440
1432
|
# keys.
|
|
1441
1433
|
def hash_has_key?(shape, _method_name, args)
|
|
1442
1434
|
return nil unless args.size == 1
|
|
@@ -1447,7 +1439,7 @@ module Rigor
|
|
|
1447
1439
|
return nil unless arg.is_a?(Type::Constant)
|
|
1448
1440
|
|
|
1449
1441
|
key = arg.value
|
|
1450
|
-
return nil unless
|
|
1442
|
+
return nil unless static_shape_key?(key)
|
|
1451
1443
|
|
|
1452
1444
|
Type::Combinator.constant_of(shape.pairs.key?(key))
|
|
1453
1445
|
end
|
|
@@ -1496,7 +1488,8 @@ module Rigor
|
|
|
1496
1488
|
end
|
|
1497
1489
|
|
|
1498
1490
|
# `shape.invert` — swaps keys and values. Folds when every value is a `Constant` whose value is a
|
|
1499
|
-
# Symbol
|
|
1491
|
+
# scalar the carrier accepts as a key (Symbol / String / Integer / Float / true / false / nil;
|
|
1492
|
+
# see {#static_shape_key?}). Duplicate values
|
|
1500
1493
|
# would alias under inversion, so Rigor declines on collisions rather than silently dropping
|
|
1501
1494
|
# entries.
|
|
1502
1495
|
def hash_invert(shape, _method_name, args)
|
|
@@ -1504,7 +1497,7 @@ module Rigor
|
|
|
1504
1497
|
return nil unless shape.closed?
|
|
1505
1498
|
return nil unless shape.optional_keys.empty?
|
|
1506
1499
|
return nil unless shape.pairs.values.all?(Type::Constant)
|
|
1507
|
-
return nil unless shape.pairs.values.all? { |v|
|
|
1500
|
+
return nil unless shape.pairs.values.all? { |v| static_shape_key?(v.value) }
|
|
1508
1501
|
|
|
1509
1502
|
inverted = shape.pairs.each_with_object({}) do |(k, v), acc|
|
|
1510
1503
|
return nil if acc.key?(v.value)
|
|
@@ -1565,7 +1558,7 @@ module Rigor
|
|
|
1565
1558
|
Type::Combinator.hash_shape_of(shape.pairs.merge(other.pairs))
|
|
1566
1559
|
end
|
|
1567
1560
|
|
|
1568
|
-
# `shape[k]` and `shape.fetch(k)` for a
|
|
1561
|
+
# `shape[k]` and `shape.fetch(k)` for a value-pinned scalar key. Missing-key resolution depends
|
|
1569
1562
|
# on the method:
|
|
1570
1563
|
#
|
|
1571
1564
|
# - `[]` returns `nil` at runtime; we surface `Constant[nil]` so the carrier is visible to
|
|
@@ -1599,13 +1592,13 @@ module Rigor
|
|
|
1599
1592
|
end
|
|
1600
1593
|
|
|
1601
1594
|
# Returns the per-step value type for a HashShape lookup (or `Constant[nil]` for a known-missing
|
|
1602
|
-
# key). Returns `nil` when the argument is not a
|
|
1595
|
+
# key). Returns `nil` when the argument is not a value-pinned scalar key so the caller can fall
|
|
1603
1596
|
# through to the projection answer.
|
|
1604
1597
|
def hash_dig_step(shape, arg)
|
|
1605
1598
|
return nil unless arg.is_a?(Type::Constant)
|
|
1606
1599
|
|
|
1607
1600
|
key = arg.value
|
|
1608
|
-
return nil unless
|
|
1601
|
+
return nil unless static_shape_key?(key)
|
|
1609
1602
|
|
|
1610
1603
|
if shape.pairs.key?(key)
|
|
1611
1604
|
value = shape.pairs[key]
|
|
@@ -1631,7 +1624,7 @@ module Rigor
|
|
|
1631
1624
|
|
|
1632
1625
|
# `shape.values_at(:a, :b, ...)` with a list of static keys. Returns a `Tuple` whose per-position
|
|
1633
1626
|
# values are the per-key value types (`Constant[nil]` for missing keys, mirroring Ruby's runtime
|
|
1634
|
-
# behaviour). Falls through when any argument is not a
|
|
1627
|
+
# behaviour). Falls through when any argument is not a value-pinned scalar key.
|
|
1635
1628
|
def hash_values_at(shape, _method_name, args)
|
|
1636
1629
|
return nil if args.empty?
|
|
1637
1630
|
|
|
@@ -1647,7 +1640,7 @@ module Rigor
|
|
|
1647
1640
|
end
|
|
1648
1641
|
|
|
1649
1642
|
# `shape.slice(:a, :b, ...)` — returns a sub-HashShape containing only the specified keys. All
|
|
1650
|
-
# arguments must be `Constant
|
|
1643
|
+
# arguments must be `Constant` scalar keys. Keys not present in the shape are silently
|
|
1651
1644
|
# omitted (matching Ruby's runtime semantics — no nil padding). Declines on open shapes or when
|
|
1652
1645
|
# any argument is not a static key.
|
|
1653
1646
|
def hash_slice(shape, _method_name, args)
|
|
@@ -1660,7 +1653,7 @@ module Rigor
|
|
|
1660
1653
|
return nil unless arg.is_a?(Type::Constant)
|
|
1661
1654
|
|
|
1662
1655
|
key = arg.value
|
|
1663
|
-
return nil unless
|
|
1656
|
+
return nil unless static_shape_key?(key)
|
|
1664
1657
|
|
|
1665
1658
|
requested << key
|
|
1666
1659
|
end
|
|
@@ -1669,7 +1662,7 @@ module Rigor
|
|
|
1669
1662
|
end
|
|
1670
1663
|
|
|
1671
1664
|
# `shape.except(:a, :b, ...)` — returns a sub-HashShape with the specified keys removed. All
|
|
1672
|
-
# arguments must be `Constant
|
|
1665
|
+
# arguments must be `Constant` scalar keys. Keys not present in the shape are silently
|
|
1673
1666
|
# ignored. Declines on open shapes or when any argument is not a static key.
|
|
1674
1667
|
def hash_except(shape, _method_name, args)
|
|
1675
1668
|
return nil if args.empty?
|
|
@@ -1681,7 +1674,7 @@ module Rigor
|
|
|
1681
1674
|
return nil unless arg.is_a?(Type::Constant)
|
|
1682
1675
|
|
|
1683
1676
|
key = arg.value
|
|
1684
|
-
return nil unless
|
|
1677
|
+
return nil unless static_shape_key?(key)
|
|
1685
1678
|
|
|
1686
1679
|
excluded[key] = true
|
|
1687
1680
|
end
|
|
@@ -12,13 +12,12 @@ module Rigor
|
|
|
12
12
|
# `Data` path does not need: **mutation soundness.**
|
|
13
13
|
#
|
|
14
14
|
# A `Struct` instance is mutable (`s.x = v`, `s[:x] = v`, escape), so a member map bound to a
|
|
15
|
-
# variable can be invalidated by a later write.
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
# re-typing is slice 4.
|
|
15
|
+
# variable can be invalidated by a later write. A member read folds off a **fresh** instance (the
|
|
16
|
+
# transient receiver of a `.new(...).x` / `.with(...).x` chain, unmutatable between materialisation
|
|
17
|
+
# and the read — slices 1+2), off a **fold-safe stored local** the {Inference::StructFoldSafety} scan
|
|
18
|
+
# proved is never mutated / aliased / escaped (slice 3), or off a **setter-mutated fold-safe local**
|
|
19
|
+
# whose bindings {#apply_setter_writeback} keeps current (slice 4). Any other stored receiver degrades
|
|
20
|
+
# to `Dynamic[top]` rather than fold a possibly-stale value.
|
|
22
21
|
#
|
|
23
22
|
# Responsibilities:
|
|
24
23
|
#
|
|
@@ -272,7 +271,8 @@ module Rigor
|
|
|
272
271
|
end
|
|
273
272
|
|
|
274
273
|
# A fold-safe stored receiver is a local-variable read whose name the body's fold-safe set (on the
|
|
275
|
-
# scope) marks as never
|
|
274
|
+
# scope) marks as safe to fold — never aliased / escaped, and any mutation a straight-line member
|
|
275
|
+
# setter the write-back keeps the binding current for.
|
|
276
276
|
def fold_safe_local_receiver?(context)
|
|
277
277
|
node = context.call_node
|
|
278
278
|
receiver = node&.receiver
|
|
@@ -281,6 +281,38 @@ module Rigor
|
|
|
281
281
|
|
|
282
282
|
scope.struct_fold_safe?(receiver.name)
|
|
283
283
|
end
|
|
284
|
+
|
|
285
|
+
# ADR-48 slice 4 — precise mutated-member re-typing. After a `local.member = v` setter on a
|
|
286
|
+
# fold-safe `StructInstance` local, rebind the local to a `StructInstance` with that member replaced
|
|
287
|
+
# by the assigned type, so a later `local.member` read folds to the assigned value (and a sibling
|
|
288
|
+
# `local.other` stays precise). Called from `eval_call`'s post-call scope, mirroring
|
|
289
|
+
# `MutationWidening.widen_after_call`. Sound ONLY for a fold-safe local: the fold-safe scan proves it
|
|
290
|
+
# is never aliased / escaped and its setters are straight-line, so the binding this installs is the
|
|
291
|
+
# local's true member state at every later read on the path. A non-fold-safe local is left untouched
|
|
292
|
+
# (its reads do not fold, so the binding is never consulted for folding).
|
|
293
|
+
#
|
|
294
|
+
# @param call_node [Prism::CallNode] the `local.member = v` call
|
|
295
|
+
# @param assigned_type [Rigor::Type, nil] the setter's assigned value type (the call's own result)
|
|
296
|
+
# @param scope [Rigor::Scope, nil]
|
|
297
|
+
# @return [Rigor::Scope] the (possibly) rebound scope
|
|
298
|
+
def apply_setter_writeback(call_node:, assigned_type:, scope:)
|
|
299
|
+
return scope if scope.nil? || assigned_type.nil?
|
|
300
|
+
|
|
301
|
+
receiver = call_node.receiver
|
|
302
|
+
return scope unless receiver.is_a?(Prism::LocalVariableReadNode)
|
|
303
|
+
return scope unless scope.struct_fold_safe?(receiver.name)
|
|
304
|
+
|
|
305
|
+
current = scope.local(receiver.name)
|
|
306
|
+
return scope unless current.is_a?(Type::StructInstance)
|
|
307
|
+
|
|
308
|
+
member = member_setter_target(call_node.name, current.members)
|
|
309
|
+
return scope if member.nil?
|
|
310
|
+
|
|
311
|
+
rebound = Type::Combinator.struct_instance_of(
|
|
312
|
+
members: current.members.merge(member => assigned_type), class_name: current.class_name
|
|
313
|
+
)
|
|
314
|
+
scope.with_local(receiver.name, rebound)
|
|
315
|
+
end
|
|
284
316
|
end
|
|
285
317
|
end
|
|
286
318
|
end
|
|
@@ -8,6 +8,7 @@ require_relative "../builtins/hkt_builtins"
|
|
|
8
8
|
require_relative "../builtins/static_return_refinements"
|
|
9
9
|
require_relative "dynamic_origin"
|
|
10
10
|
require_relative "flow_tracer"
|
|
11
|
+
require_relative "void_tail_summary"
|
|
11
12
|
require_relative "method_dispatcher/call_context"
|
|
12
13
|
require_relative "method_dispatcher/constant_folding"
|
|
13
14
|
require_relative "method_dispatcher/literal_string_folding"
|
|
@@ -70,6 +71,14 @@ module Rigor
|
|
|
70
71
|
block_type: block_type, environment: environment,
|
|
71
72
|
call_node: call_node, scope: scope
|
|
72
73
|
)
|
|
74
|
+
# ADR-100 WD4 — the transitive-void consult, HERE in the wrapper rather than in any result
|
|
75
|
+
# tier: which tier answers the intermediate's call depends on where the leaf's RBS came from
|
|
76
|
+
# (`RbsDispatch` on the ADR-93 synthesized `untyped` skeleton, or the post-dispatch
|
|
77
|
+
# `ExpressionTyper` body-inference tiers on a partial hand-written `sig/`), and both answers
|
|
78
|
+
# are correct types that must not change. Recording is therefore result-independent and
|
|
79
|
+
# type-blind; a statement-position record stays inert because the collector only reads value
|
|
80
|
+
# positions.
|
|
81
|
+
record_transitive_void_origin(receiver_type, method_name, call_node, scope)
|
|
73
82
|
# `rigor trace` — record the dispatch outcome (resolved type, or the fail-soft `nil` the
|
|
74
83
|
# caller widens to `Dynamic[Top]`).
|
|
75
84
|
if FlowTracer.active?
|
|
@@ -225,6 +234,36 @@ module Rigor
|
|
|
225
234
|
fallback_result
|
|
226
235
|
end
|
|
227
236
|
|
|
237
|
+
# ADR-100 WD4 — records the transitive `-> void` provenance when the called method is a
|
|
238
|
+
# discovered project `def` whose {VoidTailSummary} chain ends in an author-declared `-> void`
|
|
239
|
+
# leaf on the same owner. Guards cheapest-first: the internal dispatcher callers nil `scope` /
|
|
240
|
+
# `call_node` out; the receiver must project through {#discovered_method_lookup}
|
|
241
|
+
# (`Nominal` / `Singleton` only — unions and `Dynamic` receivers stay out); and the
|
|
242
|
+
# `discovered_method?` pre-filter (two pure hash reads) keeps the hot path at ~zero cost —
|
|
243
|
+
# only a call that provably targets a discovered def of the right kind resolves the node and
|
|
244
|
+
# consults the summary. The `Prism::CallNode` guard mirrors `collect_plugin_contributions`:
|
|
245
|
+
# the `&:symbol` block path dispatches with the `Prism::BlockArgumentNode` itself, which is
|
|
246
|
+
# never a collector value position, so recording on it would be dead weight.
|
|
247
|
+
def record_transitive_void_origin(receiver_type, method_name, call_node, scope)
|
|
248
|
+
return if scope.nil? || call_node.nil?
|
|
249
|
+
return unless call_node.is_a?(Prism::CallNode)
|
|
250
|
+
|
|
251
|
+
class_name, kind = discovered_method_lookup(receiver_type)
|
|
252
|
+
return if class_name.nil?
|
|
253
|
+
return unless scope.discovered_method?(class_name, method_name, kind)
|
|
254
|
+
|
|
255
|
+
def_node =
|
|
256
|
+
if kind == :singleton
|
|
257
|
+
scope.singleton_def_for(class_name, method_name)
|
|
258
|
+
else
|
|
259
|
+
scope.user_def_for(class_name, method_name)
|
|
260
|
+
end
|
|
261
|
+
return if def_node.nil?
|
|
262
|
+
|
|
263
|
+
origin = VoidTailSummary.new(scope).origin_for(def_node, class_name, kind)
|
|
264
|
+
scope.record_void_origin(call_node, origin) if origin
|
|
265
|
+
end
|
|
266
|
+
|
|
228
267
|
# v0.1.3 — discovered-method dispatch tier. `scope` carries the `discovered_methods` table
|
|
229
268
|
# built once per program by `ScopeIndexer` (a `Hash[String, Hash[Symbol, :instance |
|
|
230
269
|
# :singleton]]`). When the receiver names a discovered class AND the requested method is
|
|
@@ -701,7 +740,7 @@ module Rigor
|
|
|
701
740
|
private_constant :STDLIB_SINGLETON_FOLDERS
|
|
702
741
|
|
|
703
742
|
PRECISE_TIERS_TAIL = Ractor.make_shareable([
|
|
704
|
-
|
|
743
|
+
MethodFolding, ReduceFolding, ArrayToHFolding, BlockFolding
|
|
705
744
|
].freeze)
|
|
706
745
|
private_constant :PRECISE_TIERS_TAIL
|
|
707
746
|
|
|
@@ -735,6 +774,9 @@ module Rigor
|
|
|
735
774
|
return result if result
|
|
736
775
|
end
|
|
737
776
|
|
|
777
|
+
kernel_result = dispatch_kernel_intrinsic(context)
|
|
778
|
+
return kernel_result if kernel_result
|
|
779
|
+
|
|
738
780
|
PRECISE_TIERS_TAIL.each do |tier|
|
|
739
781
|
result = tier.try_dispatch(context)
|
|
740
782
|
return result if result
|
|
@@ -742,6 +784,20 @@ module Rigor
|
|
|
742
784
|
nil
|
|
743
785
|
end
|
|
744
786
|
|
|
787
|
+
# ADR-91 WD1 — the single dispatcher-held ownership gate for the Kernel module-function surface
|
|
788
|
+
# (gate-by-held-key, the same move STDLIB_SINGLETON_FOLDERS made above). The tier runs only when
|
|
789
|
+
# the method name is in the compiled `INTRINSIC_NAMES` table AND the call is attributable to
|
|
790
|
+
# Kernel itself (implicit self / `Kernel.` receiver, no discovered user redefinition). Because the
|
|
791
|
+
# gate lives HERE and nowhere else, a fold inside KernelDispatch that "forgets" its ownership
|
|
792
|
+
# guard is unrepresentable — the whole tier is unreachable for a foreign receiver. Unit probes
|
|
793
|
+
# calling `KernelDispatch.try_dispatch` directly keep the caller-vouches-for-ownership contract.
|
|
794
|
+
def dispatch_kernel_intrinsic(context)
|
|
795
|
+
return nil unless KernelDispatch::INTRINSIC_NAMES.include?(context.method_name)
|
|
796
|
+
return nil unless KernelDispatch.kernel_owned_call?(context)
|
|
797
|
+
|
|
798
|
+
KernelDispatch.try_dispatch(context)
|
|
799
|
+
end
|
|
800
|
+
|
|
745
801
|
def try_user_class_fallback(receiver_type, environment, call_node, context)
|
|
746
802
|
return nil if environment.nil?
|
|
747
803
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../type"
|
|
4
|
+
require_relative "../source/node_children"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
module Inference
|
|
@@ -101,7 +102,8 @@ module Rigor
|
|
|
101
102
|
|
|
102
103
|
# Returns a scope with the call's receiver widened, when the receiver is a
|
|
103
104
|
# local-/instance-variable read whose current binding is a literal-shape carrier
|
|
104
|
-
# (`Tuple` / `HashShape`)
|
|
105
|
+
# (`Tuple` / `HashShape`) or an empty-witness refinement (`non-empty-array` /
|
|
106
|
+
# `non-empty-hash`) AND the call name is a known in-place mutator for that shape.
|
|
105
107
|
# Returns `current_scope` unchanged otherwise.
|
|
106
108
|
#
|
|
107
109
|
# @param call_node [Prism::CallNode]
|
|
@@ -160,7 +162,7 @@ module Rigor
|
|
|
160
162
|
# scope's locals) trigger widening, so descending into nested blocks is safe and
|
|
161
163
|
# necessary for the hkt_registry-shape case (an outer collection mutated inside an
|
|
162
164
|
# iterator block whose body is itself inside another block).
|
|
163
|
-
node.
|
|
165
|
+
node.rigor_each_child do |child|
|
|
164
166
|
scope = walk_for_outer_mutations(child, scope)
|
|
165
167
|
end
|
|
166
168
|
scope
|
|
@@ -216,6 +218,30 @@ module Rigor
|
|
|
216
218
|
return nil unless HASH_MUTATORS.include?(method_name)
|
|
217
219
|
|
|
218
220
|
widen_hash_shape(type)
|
|
221
|
+
when Type::Difference
|
|
222
|
+
widen_difference(type, method_name)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# `non-empty-array[T]` / `non-empty-hash[K, V]` → the bare base nominal. These refinement
|
|
227
|
+
# carriers are what `empty?` / `any?` narrowing writes (ADR-47 §4-4), and they are just as
|
|
228
|
+
# invalidated by an in-place mutator as a `Tuple` is: `arr.clear` makes `arr` empty, so a
|
|
229
|
+
# surviving `Difference[Array[T], Tuple[]]` would project `arr.size` to `positive-int` and
|
|
230
|
+
# fold `arr.size == 0` to `Constant[false]` — a false `flow.always-falsey-condition` on
|
|
231
|
+
# correct code.
|
|
232
|
+
#
|
|
233
|
+
# Only the EMPTY-witness differences over Array / Hash are widened, and only for that
|
|
234
|
+
# base's mutator table. The other catalogued refinements are unreachable from these tables
|
|
235
|
+
# by construction: `non-empty-string` and `non-zero-int` bind String / Integer receivers,
|
|
236
|
+
# whose mutators (`String#<<` and friends) appear in neither `ARRAY_MUTATORS` nor
|
|
237
|
+
# `HASH_MUTATORS` — and none of them can empty a non-empty string anyway.
|
|
238
|
+
def widen_difference(difference, method_name)
|
|
239
|
+
return nil unless difference.removes_empty_witness?
|
|
240
|
+
|
|
241
|
+
base = difference.base
|
|
242
|
+
case base.class_name
|
|
243
|
+
when "Array" then ARRAY_MUTATORS.include?(method_name) ? base : nil
|
|
244
|
+
when "Hash" then HASH_MUTATORS.include?(method_name) ? base : nil
|
|
219
245
|
end
|
|
220
246
|
end
|
|
221
247
|
|
|
@@ -248,16 +274,24 @@ module Rigor
|
|
|
248
274
|
Type::Combinator.nominal_of("Hash", type_args: [key_type, value_type])
|
|
249
275
|
end
|
|
250
276
|
|
|
251
|
-
# Maps the literal Ruby key set
|
|
252
|
-
#
|
|
253
|
-
#
|
|
254
|
-
#
|
|
277
|
+
# Maps the literal Ruby key set to a union of the corresponding type carriers. Symbol / String /
|
|
278
|
+
# Integer / Float keys widen to their class nominal; the `true` / `false` / `nil` singleton keys
|
|
279
|
+
# keep their constant carrier (the constant IS the class's whole value set, and `nil` reads better
|
|
280
|
+
# than `NilClass` in a widened `Hash[K, V]`). We deliberately do NOT fold the widenable kinds to a
|
|
281
|
+
# `Constant<:k1> | Constant<:k2>` union — that would be a precision improvement that complicates
|
|
282
|
+
# the widening contract; the goal here is to LOSE precision, not to record a new fact set.
|
|
255
283
|
def key_union_for(keys)
|
|
256
|
-
|
|
257
|
-
carriers = kinds.map { |name| Type::Combinator.nominal_of(name) }
|
|
284
|
+
carriers = keys.map { |k| key_widening_carrier(k) }.uniq
|
|
258
285
|
carriers.size == 1 ? carriers.first : Type::Combinator.union(*carriers)
|
|
259
286
|
end
|
|
260
287
|
|
|
288
|
+
def key_widening_carrier(key)
|
|
289
|
+
case key
|
|
290
|
+
when true, false, nil then Type::Combinator.constant_of(key)
|
|
291
|
+
else Type::Combinator.nominal_of(key.class.name)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
261
295
|
# ----------------------------------------------------------------
|
|
262
296
|
# ADR-56 slice C — receiver-content element-type JOIN.
|
|
263
297
|
#
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "../reflection"
|
|
6
|
+
require_relative "../source/constant_path"
|
|
6
7
|
require_relative "../type"
|
|
7
8
|
require_relative "../environment"
|
|
8
9
|
require_relative "../rbs_extended"
|
|
@@ -41,7 +42,13 @@ module Rigor
|
|
|
41
42
|
TRUSTED_EQUALITY_LITERAL_CLASSES = [String, Symbol, Integer, TrueClass, FalseClass, NilClass].freeze
|
|
42
43
|
SINGLETON_LITERAL_CLASSES = [TrueClass, FalseClass, NilClass].freeze
|
|
43
44
|
ClassNarrowingContext = Data.define(:exact, :polarity, :environment)
|
|
44
|
-
|
|
45
|
+
# A recognised `=~` regex pattern operand: the pattern `source` string and whether it was
|
|
46
|
+
# compiled in extended (`//x`) mode. Extraction from a literal `RegularExpressionNode` and from
|
|
47
|
+
# a value-pinned `Constant[Regexp]` share this carrier so the participation walk and the
|
|
48
|
+
# extended-mode bail read one shape.
|
|
49
|
+
RegexMatchPattern = Data.define(:source, :extended)
|
|
50
|
+
private_constant :TRUSTED_EQUALITY_LITERAL_CLASSES, :SINGLETON_LITERAL_CLASSES, :ClassNarrowingContext,
|
|
51
|
+
:RegexMatchPattern
|
|
45
52
|
|
|
46
53
|
module_function
|
|
47
54
|
|
|
@@ -962,8 +969,8 @@ module Rigor
|
|
|
962
969
|
end
|
|
963
970
|
|
|
964
971
|
def simple_dispatch_name?(name)
|
|
965
|
-
%i[nil? ! is_a? kind_of? instance_of? == != === =~ key? has_key? empty? any?
|
|
966
|
-
respond_to?].include?(name)
|
|
972
|
+
%i[nil? ! is_a? kind_of? instance_of? == != === =~ match? key? has_key? empty? any?
|
|
973
|
+
none? respond_to?].include?(name)
|
|
967
974
|
end
|
|
968
975
|
|
|
969
976
|
def dispatch_call_simple(node, scope, name)
|
|
@@ -974,6 +981,7 @@ module Rigor
|
|
|
974
981
|
when :==, :!= then analyse_equality_predicate(node, scope, equality: name)
|
|
975
982
|
when :=== then analyse_case_equality_predicate(node, scope)
|
|
976
983
|
when :=~ then analyse_regex_match_predicate(node, scope)
|
|
984
|
+
when :match? then analyse_whole_regex_match_predicate(node, scope)
|
|
977
985
|
when :key?, :has_key? then analyse_key_presence_predicate(node, scope)
|
|
978
986
|
when :empty?, :any?, :none? then analyse_array_emptiness_predicate(node, scope, name)
|
|
979
987
|
when :respond_to? then analyse_respond_to_predicate(node, scope)
|
|
@@ -1199,30 +1207,123 @@ module Rigor
|
|
|
1199
1207
|
# - Falsey edge (`=~` returned nil — no match): `$~` and every numbered / back-reference
|
|
1200
1208
|
# global bound to `Constant<nil>`.
|
|
1201
1209
|
#
|
|
1202
|
-
# Returns nil (no narrowing) when the receiver / argument pair does not
|
|
1203
|
-
#
|
|
1210
|
+
# Returns nil (no narrowing) when the receiver / argument pair does not resolve to exactly one
|
|
1211
|
+
# regex pattern we can count (see {#regex_match_pattern}).
|
|
1204
1212
|
def analyse_regex_match_predicate(node, scope)
|
|
1205
1213
|
return nil if node.arguments.nil?
|
|
1206
1214
|
return nil unless node.arguments.arguments.size == 1
|
|
1207
1215
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1216
|
+
arg = node.arguments.arguments.first
|
|
1217
|
+
pattern = regex_match_pattern(node.receiver, arg, scope)
|
|
1218
|
+
return nil if pattern.nil?
|
|
1219
|
+
# Extended mode (`//x`) lets the pattern carry free whitespace and `#` comments, and a
|
|
1220
|
+
# comment may contain a literal `(` — which the light char-scan walker would miscount as a
|
|
1221
|
+
# capturing group, shifting `$N` indices and narrowing the wrong global. That is a
|
|
1222
|
+
# false-negative-class misnarrowing, so bail rather than risk it. The literal path shares
|
|
1223
|
+
# this bail: `RegularExpressionNode` carries the same latent miscount.
|
|
1224
|
+
return nil if pattern.extended
|
|
1225
|
+
|
|
1226
|
+
unconditional = unconditional_capture_groups(pattern.source)
|
|
1227
|
+
truthy, falsey = regex_match_predicate_scopes(scope, unconditional)
|
|
1228
|
+
# #164 — layer whole-receiver refinement onto the truthy edge without disturbing the
|
|
1229
|
+
# match-global logic above. A successful `str =~ /\A\d+\z/` proves the WHOLE string
|
|
1230
|
+
# matched, so the string operand narrows to the imported refinement the anchored pattern
|
|
1231
|
+
# names. The falsey edge is untouched: a failed match proves nothing about the shape.
|
|
1232
|
+
truthy = apply_whole_receiver_refinement(truthy, node.receiver, arg, pattern.source, scope)
|
|
1233
|
+
[truthy, falsey]
|
|
1234
|
+
end
|
|
1235
|
+
|
|
1236
|
+
# `str.match?(/\A\d+\z/)` — `String#match?` is a pure boolean predicate whose truthy edge
|
|
1237
|
+
# proves a property of the ENTIRE receiver. When the argument is a fully `\A…\z`-anchored
|
|
1238
|
+
# single-char-class pattern recognised by {RegexRefinement.for_whole_pattern}, narrow the
|
|
1239
|
+
# receiver local to the matching imported refinement on the truthy edge; the falsey edge
|
|
1240
|
+
# passes through unchanged (failing one pattern proves nothing about the string's shape).
|
|
1241
|
+
# A non-literal / unanchored / `\Z` / line-anchored / `//x` pattern falls through to no
|
|
1242
|
+
# narrowing.
|
|
1243
|
+
def analyse_whole_regex_match_predicate(node, scope)
|
|
1244
|
+
return nil unless node.receiver.is_a?(Prism::LocalVariableReadNode)
|
|
1245
|
+
return nil if node.arguments.nil? || node.arguments.arguments.size != 1
|
|
1210
1246
|
|
|
1211
|
-
|
|
1212
|
-
|
|
1247
|
+
pattern = regex_operand_pattern(node.arguments.arguments.first, scope)
|
|
1248
|
+
return nil unless pattern.is_a?(RegexMatchPattern)
|
|
1249
|
+
return nil if pattern.extended
|
|
1250
|
+
|
|
1251
|
+
refinement = ::Rigor::Builtins::RegexRefinement.for_whole_pattern(pattern.source)
|
|
1252
|
+
return nil if refinement.nil?
|
|
1253
|
+
|
|
1254
|
+
local_name = node.receiver.name
|
|
1255
|
+
return nil if scope.local(local_name).nil?
|
|
1256
|
+
|
|
1257
|
+
[scope.with_local(local_name, refinement), scope]
|
|
1213
1258
|
end
|
|
1214
1259
|
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1260
|
+
# Narrows the string operand of a `=~` predicate to a whole-receiver refinement when the
|
|
1261
|
+
# regex source is a recognised `\A…\z`-anchored pattern. Returns the (possibly unchanged)
|
|
1262
|
+
# truthy scope; the falsey edge is never touched by this layer.
|
|
1263
|
+
def apply_whole_receiver_refinement(truthy, receiver, arg, source, scope)
|
|
1264
|
+
refinement = ::Rigor::Builtins::RegexRefinement.for_whole_pattern(source)
|
|
1265
|
+
return truthy if refinement.nil?
|
|
1218
1266
|
|
|
1219
|
-
|
|
1267
|
+
# `regex_match_pattern` already resolved the regex operand from a literal or a
|
|
1268
|
+
# `Constant[Regexp]` constant read — never a bare local — so the string subject is
|
|
1269
|
+
# unambiguously the local-read operand, if either operand is one.
|
|
1270
|
+
subject = [receiver, arg].find { |operand| operand.is_a?(Prism::LocalVariableReadNode) }
|
|
1271
|
+
return truthy if subject.nil? || scope.local(subject.name).nil?
|
|
1272
|
+
|
|
1273
|
+
truthy.with_local(subject.name, refinement)
|
|
1220
1274
|
end
|
|
1221
1275
|
|
|
1222
|
-
#
|
|
1223
|
-
#
|
|
1224
|
-
#
|
|
1225
|
-
|
|
1276
|
+
# Recognises the regex pattern operand of a `=~` predicate. An operand resolves to a regex when
|
|
1277
|
+
# it is either a syntactic `RegularExpressionNode` literal OR a constant read whose type is a
|
|
1278
|
+
# value-pinned `Constant[Regexp]` (`RE = /.../` and `RE = Regexp.new(...)` both fold to the
|
|
1279
|
+
# carrier in the whole-program constant pre-pass). Returns a single {RegexMatchPattern}, or nil
|
|
1280
|
+
# (no narrowing) when NEITHER operand resolves to a regex, when BOTH do (`/a/ =~ /b/` — no
|
|
1281
|
+
# string subject to bind), or when a constant operand is ambiguous — typed as a `Union` from a
|
|
1282
|
+
# twice-assigned constant, where no single source can be pinned.
|
|
1283
|
+
def regex_match_pattern(left, right, scope)
|
|
1284
|
+
left_pattern = regex_operand_pattern(left, scope)
|
|
1285
|
+
right_pattern = regex_operand_pattern(right, scope)
|
|
1286
|
+
return nil if left_pattern == :ambiguous || right_pattern == :ambiguous
|
|
1287
|
+
|
|
1288
|
+
patterns = [left_pattern, right_pattern].grep(RegexMatchPattern)
|
|
1289
|
+
patterns.size == 1 ? patterns.first : nil
|
|
1290
|
+
end
|
|
1291
|
+
|
|
1292
|
+
# Classifies one `=~` operand: a {RegexMatchPattern} when it resolves to a regex (literal or
|
|
1293
|
+
# `Constant[Regexp]`), `:ambiguous` when a constant operand types as a `Union` (a twice-assigned
|
|
1294
|
+
# constant), or nil when the operand is not a regex at all.
|
|
1295
|
+
def regex_operand_pattern(node, scope)
|
|
1296
|
+
case node
|
|
1297
|
+
when Prism::RegularExpressionNode
|
|
1298
|
+
RegexMatchPattern.new(source: node.unescaped, extended: node.extended?)
|
|
1299
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
1300
|
+
regex_operand_from_constant(node, scope)
|
|
1301
|
+
end
|
|
1302
|
+
end
|
|
1303
|
+
|
|
1304
|
+
# Resolves a constant-reference operand to a regex pattern via the shared lexical-constant
|
|
1305
|
+
# resolution (`Reflection.resolve_constant_type`). `Regexp#options` gives the real
|
|
1306
|
+
# extended-mode flag; `Regexp#source` the pattern. Returns `:ambiguous` for a `Union`-typed
|
|
1307
|
+
# constant so the caller bails, and nil when the constant does not resolve to a `Constant[Regexp]`.
|
|
1308
|
+
def regex_operand_from_constant(node, scope)
|
|
1309
|
+
name = Source::ConstantPath.qualified_name_or_nil(node)
|
|
1310
|
+
return nil if name.nil?
|
|
1311
|
+
|
|
1312
|
+
type = Reflection.resolve_constant_type(name, scope: scope)
|
|
1313
|
+
return :ambiguous if type.is_a?(Type::Union)
|
|
1314
|
+
return nil unless type.is_a?(Type::Constant)
|
|
1315
|
+
|
|
1316
|
+
value = type.value
|
|
1317
|
+
return nil unless value.is_a?(Regexp)
|
|
1318
|
+
|
|
1319
|
+
RegexMatchPattern.new(source: value.source, extended: value.options.anybits?(Regexp::EXTENDED))
|
|
1320
|
+
end
|
|
1321
|
+
|
|
1322
|
+
# Curated set of match globals bound non-nil on every successful `=~` regardless of grouping:
|
|
1323
|
+
# `$~` (the MatchData), `$&` (whole match), `` $` `` (pre-match), `$'` (post-match). Numbered
|
|
1324
|
+
# references (`$1..$N`) and `$+` (the last matched group) are handled separately because they
|
|
1325
|
+
# depend on the regex source — `$+` is gated on the same participation set as `$N` (#177).
|
|
1326
|
+
REGEX_MATCH_GLOBALS = %i[$~ $& $` $'].freeze
|
|
1226
1327
|
private_constant :REGEX_MATCH_GLOBALS
|
|
1227
1328
|
|
|
1228
1329
|
# `unconditional` is the Set of 1-based numbered-capture indices whose group is
|
|
@@ -1251,6 +1352,15 @@ module Rigor
|
|
|
1251
1352
|
truthy = truthy.with_global(name, string_t)
|
|
1252
1353
|
falsey = falsey.with_global(name, nil_t)
|
|
1253
1354
|
end
|
|
1355
|
+
# `$+` is the LAST matched group, so it is nil on a successful match whose groups are all
|
|
1356
|
+
# optional (`"b" =~ /(a)?b/` matches yet leaves `$+` nil) or absent (no capture group). It
|
|
1357
|
+
# narrows to `String` only when some group is guaranteed to participate — the same
|
|
1358
|
+
# participation gate `$N` uses (#177). With no unconditional group it is left unbound on both
|
|
1359
|
+
# edges (fall through to the RBS `String?` default), exactly as an optional `$N` is.
|
|
1360
|
+
unless unconditional.empty?
|
|
1361
|
+
truthy = truthy.with_global(:$+, string_t)
|
|
1362
|
+
falsey = falsey.with_global(:$+, nil_t)
|
|
1363
|
+
end
|
|
1254
1364
|
[truthy, falsey]
|
|
1255
1365
|
end
|
|
1256
1366
|
|