rigortype 0.4.5 → 0.4.6
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 +1 -1
- data/docs/handbook/11-sig-gen.md +11 -2
- data/docs/manual/02-cli-reference.md +46 -7
- data/docs/manual/03-configuration.md +2 -1
- data/docs/manual/04-diagnostics.md +1 -1
- data/docs/manual/10-mcp-server.md +9 -1
- data/docs/manual/12-caching.md +8 -1
- data/docs/manual/plugins/rigor-actionmailer.md +30 -0
- data/docs/manual/plugins/rigor-actionpack.md +30 -0
- data/docs/manual/plugins/rigor-devise.md +15 -0
- data/docs/manual/plugins/rigor-rspec.md +2 -1
- data/lib/rigor/analysis/buffer_binding.rb +14 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -0
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
- data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +51 -0
- data/lib/rigor/analysis/check_rules.rb +93 -8
- data/lib/rigor/analysis/dependency_recorder.rb +11 -0
- data/lib/rigor/analysis/edit_replay.rb +67 -0
- data/lib/rigor/analysis/incremental.rb +37 -0
- data/lib/rigor/analysis/incremental_session.rb +175 -27
- data/lib/rigor/analysis/path_expansion.rb +41 -0
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +5 -1
- data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
- data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
- data/lib/rigor/analysis/runner/pool_coordinator.rb +17 -4
- data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
- data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
- data/lib/rigor/analysis/runner.rb +156 -49
- data/lib/rigor/analysis/worker_session.rb +14 -2
- data/lib/rigor/cache/incremental_snapshot.rb +30 -7
- data/lib/rigor/cli/check_command.rb +93 -4
- data/lib/rigor/cli/diff_command.rb +2 -1
- data/lib/rigor/cli/lens_command.rb +5 -5
- data/lib/rigor/cli/mcp_command.rb +2 -1
- data/lib/rigor/cli/options.rb +20 -3
- data/lib/rigor/cli/plugins_command.rb +5 -0
- data/lib/rigor/cli/plugins_renderer.rb +24 -2
- data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +46 -7
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/type_of_project_scope.rb +27 -0
- data/lib/rigor/config_audit.rb +16 -5
- data/lib/rigor/configuration.rb +14 -4
- data/lib/rigor/environment/rbs_loader.rb +115 -6
- data/lib/rigor/environment/required_features.rb +136 -29
- data/lib/rigor/environment/ruby_library_roots.rb +122 -0
- data/lib/rigor/environment.rb +4 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
- data/lib/rigor/inference/deep_erasure.rb +138 -0
- data/lib/rigor/inference/def_return_typer.rb +159 -9
- data/lib/rigor/inference/definer_resolution.rb +17 -3
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +62 -13
- data/lib/rigor/inference/global_write_census.rb +36 -1
- data/lib/rigor/inference/ivar_writer_census.rb +195 -0
- data/lib/rigor/inference/loop_value_record.rb +53 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +110 -9
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
- data/lib/rigor/inference/method_dispatcher.rb +46 -16
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +12 -0
- data/lib/rigor/inference/mutation_widening.rb +5 -2
- data/lib/rigor/inference/narrowing.rb +66 -5
- data/lib/rigor/inference/object_mixins.rb +13 -1
- data/lib/rigor/inference/project_seed_tables.rb +2 -1
- data/lib/rigor/inference/project_yielder.rb +128 -0
- data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
- data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
- data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
- data/lib/rigor/inference/scope_indexer.rb +480 -103
- data/lib/rigor/inference/self_call_ivar_writes.rb +18 -6
- data/lib/rigor/inference/statement_evaluator.rb +658 -90
- data/lib/rigor/inference/synthetic_method_index.rb +23 -0
- data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
- data/lib/rigor/language_server/buffer_table.rb +6 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
- data/lib/rigor/language_server/project_context.rb +59 -3
- data/lib/rigor/language_server/server.rb +5 -3
- data/lib/rigor/mcp/server.rb +9 -1
- data/lib/rigor/plugin/base.rb +26 -11
- data/lib/rigor/plugin/manifest.rb +29 -3
- data/lib/rigor/plugin/registry.rb +38 -0
- data/lib/rigor/protection/discovery_seed.rb +57 -2
- data/lib/rigor/rooted_glob.rb +10 -1
- data/lib/rigor/scope.rb +14 -5
- data/lib/rigor/sig_gen/generator.rb +308 -37
- data/lib/rigor/sig_gen/observation_collector.rb +4 -1
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +6 -3
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- data/lib/rigor/type/combinator.rb +4 -3
- data/lib/rigor/type/constant.rb +45 -3
- data/lib/rigor/type/hash_shape.rb +14 -9
- data/lib/rigor/type/struct_instance.rb +3 -2
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +60 -43
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +6 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +3 -2
- data/sig/rigor/effects/file_collection.rbs +5 -6
- data/sig/rigor/effects/registry.rbs +8 -6
- data/sig/rigor/effects/summary.rbs +6 -5
- data/sig/rigor/environment.rbs +5 -3
- data/sig/rigor/inference/optimistic_origin.rbs +11 -0
- data/sig/rigor/inference.rbs +6 -0
- data/sig/rigor/plugin/additional_initializer.rbs +5 -5
- data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
- data/sig/rigor/plugin/effect_attribution.rbs +25 -10
- data/sig/rigor/plugin/effect_edge.rbs +12 -5
- data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
- data/sig/rigor/plugin/io_boundary.rbs +4 -4
- data/sig/rigor/plugin/manifest.rbs +16 -18
- data/sig/rigor/plugin/protocol_contract.rbs +10 -9
- data/sig/rigor/plugin/registry.rbs +1 -1
- data/sig/rigor/scope.rbs +31 -50
- data/sig/rigor/type.rbs +0 -6
- metadata +20 -1
|
@@ -14,6 +14,7 @@ require_relative "../source/literals"
|
|
|
14
14
|
require_relative "../source/node_children"
|
|
15
15
|
require_relative "../inference/def_return_typer"
|
|
16
16
|
require_relative "../inference/scope_indexer"
|
|
17
|
+
require_relative "../inference/project_seed_tables"
|
|
17
18
|
require_relative "../inference/module_function_state"
|
|
18
19
|
require_relative "../inference/rbs_type_translator"
|
|
19
20
|
require_relative "alias_index"
|
|
@@ -99,7 +100,7 @@ module Rigor
|
|
|
99
100
|
# candidates from one file don't leak into another; initialising empty here gives downstream consumers
|
|
100
101
|
# (`build_candidate`, `method_def_prefix`) a never-nil invariant without per-call-site defensive guards.
|
|
101
102
|
@namespace_kinds = {}
|
|
102
|
-
@module_function_methods = Set.new
|
|
103
|
+
@module_function_methods = Set.new.compare_by_identity
|
|
103
104
|
@uncertain_module_function_defs = Set.new.compare_by_identity
|
|
104
105
|
@class_shells = Set.new
|
|
105
106
|
@class_superclasses = {}
|
|
@@ -126,6 +127,7 @@ module Rigor
|
|
|
126
127
|
|
|
127
128
|
def run
|
|
128
129
|
resolved = resolve_paths(@paths)
|
|
130
|
+
@resolved_paths = resolved
|
|
129
131
|
@environment = build_environment(resolved)
|
|
130
132
|
candidates = resolved.flat_map { |path| analyse_file(path, @environment) }
|
|
131
133
|
annotate_effects(
|
|
@@ -458,22 +460,41 @@ module Rigor
|
|
|
458
460
|
end.uniq
|
|
459
461
|
end
|
|
460
462
|
|
|
463
|
+
# #1154 — the project's discovery tables, read only by the declared-parameter ivar credit: it must see the ivar
|
|
464
|
+
# writers in other files (an ancestor, another object's `instance_variable_set`, a reopening) as `rigor check`
|
|
465
|
+
# does. Built once per run over the configured project paths as well as the files sig-gen reads, so a run over
|
|
466
|
+
# one file declines what a `check` of it declines; every other part of the generator keeps its per-file scope.
|
|
467
|
+
def ivar_writer_scope(base_scope)
|
|
468
|
+
@ivar_writer_tables ||= Inference::ProjectSeedTables.for_paths(
|
|
469
|
+
((@resolved_paths || []) + resolve_paths(Array(@configuration.paths))).uniq { |path| File.expand_path(path) }
|
|
470
|
+
)
|
|
471
|
+
base_scope.with_discovery(base_scope.discovery.with(**@ivar_writer_tables))
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# #1406 — sig-gen keeps the census's own answer: a declared ivar type read back would echo the declaration.
|
|
475
|
+
def index_file(root, base_scope)
|
|
476
|
+
Inference::ScopeIndexer.index(
|
|
477
|
+
root, default_scope: base_scope, ivar_writer_scope: ivar_writer_scope(base_scope), declared_ivar_types: false
|
|
478
|
+
)
|
|
479
|
+
end
|
|
480
|
+
|
|
461
481
|
def analyse_file(path, environment)
|
|
462
482
|
source = File.read(path)
|
|
463
483
|
parse_result = Prism.parse(source, filepath: path, version: @configuration.target_ruby)
|
|
464
484
|
return [] if parse_result.errors.any?
|
|
465
485
|
|
|
466
|
-
|
|
467
|
-
scope_index = Inference::ScopeIndexer.index(parse_result.value, default_scope: base_scope)
|
|
486
|
+
scope_index = index_file(parse_result.value, Scope.empty(environment: environment))
|
|
468
487
|
|
|
469
488
|
reset_file_maps
|
|
470
489
|
@meta_layouts = collect_meta_layouts(scope_index)
|
|
471
490
|
register_meta_classes
|
|
472
491
|
defs = collect_method_definitions(parse_result.value, scope_index)
|
|
492
|
+
definers = definer_table(parse_result.value, defs)
|
|
493
|
+
defs = live_definitions(defs, definers)
|
|
473
494
|
# Candidate construction freezes the per-file maps above (see {#build_candidate}), so every registration
|
|
474
495
|
# pass has to be finished before the first `build_candidate` call. Meta members lead the RETURNED order —
|
|
475
496
|
# a value class's members and constructors read first, ahead of the methods its block body defines.
|
|
476
|
-
meta_candidates =
|
|
497
|
+
meta_candidates = live_meta_candidates(path, scope_index, definers)
|
|
477
498
|
candidates_from_defs = defs.filter_map do |def_node, class_name, kind|
|
|
478
499
|
# An analyzer bug typing one def's body must cost only that def's candidate, never the whole
|
|
479
500
|
# `rigor sig-gen` run. The `check` path recovers each *file* this way (worker_session.rb); sig-gen
|
|
@@ -485,13 +506,108 @@ module Rigor
|
|
|
485
506
|
end
|
|
486
507
|
obs_ivar_map = build_observed_ivar_map(parse_result.value)
|
|
487
508
|
meta_candidates + candidates_from_defs +
|
|
488
|
-
collect_attr_candidates(parse_result.value, path, scope_index, obs_ivar_map)
|
|
509
|
+
collect_attr_candidates(parse_result.value, path, scope_index, obs_ivar_map, definers)
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
# Ruby keeps the LAST definition of a name: a `def x` followed by `attr_reader :x` (or the reverse) defines
|
|
513
|
+
# `x` once, and a second `def x` after a `module_function` reset replaces only the instance method. Writing
|
|
514
|
+
# one entry per definition gave RBS a duplicate method definition (#2008, #2023), so every definer of a
|
|
515
|
+
# `(class, kind, name)` slot is recorded with its source offset, and only the last one in a file is written.
|
|
516
|
+
# A module function defines two slots, its singleton copy and its instance method. A `Data.define` or
|
|
517
|
+
# `Struct.new` member is defined before any body that could redefine it, so it ranks first.
|
|
518
|
+
#
|
|
519
|
+
# Only a definer whose statement certainly runs can be last: a direct statement of a `class`, `module` or
|
|
520
|
+
# `class << self` body, of a `Data.define` / `Struct.new` block or of the file ({#walk_statements} records
|
|
521
|
+
# them). One under a condition, in a block or a lambda, passed to a call (`private def x`), or after an
|
|
522
|
+
# uncertain `module_function` reset may not run, as far as this reading tells. A slot such a definer could
|
|
523
|
+
# still take over from the last certain one is contested, and nothing is written for it.
|
|
524
|
+
#
|
|
525
|
+
# @return `{ [class_name, kind, method_name] => [[offset, certain], ...] }`.
|
|
526
|
+
MEMBER_OFFSET = -1
|
|
527
|
+
private_constant :MEMBER_OFFSET
|
|
528
|
+
|
|
529
|
+
def definer_table(root, defs)
|
|
530
|
+
certain = @certain_definers
|
|
531
|
+
table = Hash.new { |h, k| h[k] = [] }
|
|
532
|
+
record_def_definers(defs, certain, table)
|
|
533
|
+
meta_member_slots.each { |slot| table[slot] << [MEMBER_OFFSET, true] }
|
|
534
|
+
ctx = AttrWalkContext.new(definers: [])
|
|
535
|
+
walk_attr_calls(root, [], false, ctx)
|
|
536
|
+
ctx.definers.each do |slot, call_node|
|
|
537
|
+
table[slot] << [call_node.location.start_offset, certain.include?(call_node)]
|
|
538
|
+
end
|
|
539
|
+
table
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def record_def_definers(defs, certain, table)
|
|
543
|
+
defs.each do |def_node, class_name, kind|
|
|
544
|
+
next if class_name.nil?
|
|
545
|
+
|
|
546
|
+
definer_slots(def_node, class_name, kind).each do |slot, possible_only|
|
|
547
|
+
table[slot] << [def_node.location.start_offset, !possible_only && certain.include?(def_node)]
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
def live_meta_candidates(path, scope_index, table)
|
|
553
|
+
collect_meta_member_candidates(path, scope_index).select do |c|
|
|
554
|
+
live_definer?(table, [c.class_name, c.kind, c.method_name], MEMBER_OFFSET)
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
# `[slot, possible_only]` pairs. A `def` after an uncertain `module_function` reset may or may not install a
|
|
559
|
+
# singleton copy, so it is a possible definer of that slot only.
|
|
560
|
+
def definer_slots(def_node, class_name, kind)
|
|
561
|
+
slots = [[[class_name, kind, def_node.name], false]]
|
|
562
|
+
if @module_function_methods.include?(def_node)
|
|
563
|
+
slots << [[class_name, :singleton, def_node.name], false]
|
|
564
|
+
elsif @uncertain_module_function_defs.include?(def_node)
|
|
565
|
+
slots << [[class_name, :singleton, def_node.name], true]
|
|
566
|
+
end
|
|
567
|
+
slots
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def meta_member_slots
|
|
571
|
+
@meta_layouts.flat_map do |class_name, layout|
|
|
572
|
+
MetaClassShape.of(kind: layout.kind, members: layout.member_names, keyword_init: layout.keyword_init)
|
|
573
|
+
.member_decls.map { |member| [class_name, member.kind, member.method_name] }
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
# Whether the definer at `offset` is the one Ruby keeps for `slot`. A slot with a single definer keeps it,
|
|
578
|
+
# certain or not, as before.
|
|
579
|
+
def live_definer?(table, slot, offset)
|
|
580
|
+
entries = table.fetch(slot, [])
|
|
581
|
+
return true if entries.size <= 1
|
|
582
|
+
|
|
583
|
+
last_certain = entries.select { |_, certain| certain }.map(&:first).max
|
|
584
|
+
return false if last_certain.nil?
|
|
585
|
+
return false if entries.any? { |entry_offset, certain| !certain && entry_offset > last_certain }
|
|
586
|
+
|
|
587
|
+
offset == last_certain
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
# The `defs` whose definition Ruby keeps. A module function whose instance method a later definition
|
|
591
|
+
# replaced keeps only its singleton copy (`M.label` still returns the first body), so it is written as
|
|
592
|
+
# `def self.label`. One whose singleton copy was replaced keeps only a private instance method, which
|
|
593
|
+
# sig-gen does not write, so it is dropped.
|
|
594
|
+
def live_definitions(defs, table)
|
|
595
|
+
defs.filter_map do |def_node, class_name, kind|
|
|
596
|
+
next [def_node, class_name, kind] if class_name.nil?
|
|
597
|
+
|
|
598
|
+
slots = definer_slots(def_node, class_name, kind).reject(&:last).map(&:first)
|
|
599
|
+
live = slots.select { |slot| live_definer?(table, slot, def_node.location.start_offset) }
|
|
600
|
+
next [def_node, class_name, kind] if live.size == slots.size
|
|
601
|
+
next nil unless live == [[class_name, :singleton, def_node.name]]
|
|
602
|
+
|
|
603
|
+
[def_node, class_name, :singleton]
|
|
604
|
+
end
|
|
489
605
|
end
|
|
490
606
|
|
|
491
607
|
# The per-file scratch state {#analyse_file} starts each file with.
|
|
492
608
|
def reset_file_maps
|
|
493
609
|
@namespace_kinds = {}
|
|
494
|
-
@module_function_methods = Set.new
|
|
610
|
+
@module_function_methods = Set.new.compare_by_identity
|
|
495
611
|
@uncertain_module_function_defs = Set.new.compare_by_identity
|
|
496
612
|
@class_shells = Set.new
|
|
497
613
|
@class_superclasses = {}
|
|
@@ -509,15 +625,19 @@ module Rigor
|
|
|
509
625
|
# - `@namespace_kinds[qualified_name]` records whether each segment came from `class Foo` (`:class`) or
|
|
510
626
|
# `module Foo` (`:module`). Used by the writer's `wrap_in_modules` step to emit the right keyword for
|
|
511
627
|
# each intermediate segment AND the leaf.
|
|
512
|
-
# - `@module_function_methods` records
|
|
513
|
-
#
|
|
514
|
-
#
|
|
628
|
+
# - `@module_function_methods` records the `def` nodes a `module_function` (no args) call preceded inside
|
|
629
|
+
# a module body. The renderer emits `def self?.name` for these, the RBS spelling that matches the dual
|
|
630
|
+
# instance + singleton dispatch the runtime produces. It is keyed by node, not by name: a later `def` of
|
|
631
|
+
# the same name after a reset is an ordinary method (#2023).
|
|
515
632
|
def collect_method_definitions(root, scope_index)
|
|
516
633
|
@declared_types = scope_index[root].declared_types
|
|
517
634
|
@walk_root = root
|
|
518
635
|
@in_effect_refinements = nil
|
|
519
636
|
out = []
|
|
520
637
|
@block_depth = 0
|
|
638
|
+
@certain_bodies = Set.new.compare_by_identity
|
|
639
|
+
@certain_definers = Set.new.compare_by_identity
|
|
640
|
+
mark_certain_body(root.statements)
|
|
521
641
|
walk_defs(root, [], false, false, out)
|
|
522
642
|
out
|
|
523
643
|
end
|
|
@@ -530,6 +650,7 @@ module Rigor
|
|
|
530
650
|
return if descend_into_namespace?(node, prefix, out)
|
|
531
651
|
when Prism::SingletonClassNode
|
|
532
652
|
if node.expression.is_a?(Prism::SelfNode) && node.body
|
|
653
|
+
mark_certain_body(node.body)
|
|
533
654
|
walk_defs(node.body, prefix, true, false, out)
|
|
534
655
|
return
|
|
535
656
|
end
|
|
@@ -539,6 +660,7 @@ module Rigor
|
|
|
539
660
|
when Prism::ConstantWriteNode
|
|
540
661
|
body = meta_block_body(node, prefix)
|
|
541
662
|
if body
|
|
663
|
+
mark_certain_body(body)
|
|
542
664
|
walk_defs(body, prefix + [node.name.to_s], false, false, out)
|
|
543
665
|
return
|
|
544
666
|
end
|
|
@@ -717,13 +839,30 @@ module Rigor
|
|
|
717
839
|
def walk_namespace_body(namespace_node, prefix, out)
|
|
718
840
|
return if namespace_node.body.nil?
|
|
719
841
|
|
|
842
|
+
mark_certain_body(namespace_node.body)
|
|
720
843
|
walk_defs(namespace_node.body, prefix, false, false, out)
|
|
721
844
|
end
|
|
722
845
|
|
|
846
|
+
# A body whose direct statements certainly run when it does ({#definer_table}): its statement list unless the
|
|
847
|
+
# body has a `rescue` clause, and its `ensure` clause. A `rescue` or `else` clause may not run.
|
|
848
|
+
def mark_certain_body(body)
|
|
849
|
+
return @certain_bodies << body unless body.is_a?(Prism::BeginNode)
|
|
850
|
+
|
|
851
|
+
# With a `rescue`, a statement before a definer can raise and skip it, so the main list is not certain.
|
|
852
|
+
@certain_bodies << body.statements if body.statements && body.rescue_clause.nil?
|
|
853
|
+
@certain_bodies << body.ensure_clause.statements if body.ensure_clause&.statements
|
|
854
|
+
end
|
|
855
|
+
|
|
723
856
|
# The directive's reach is {Inference::ModuleFunctionState.each_sig_gen_statement}.
|
|
724
857
|
def walk_statements(stmts_node, prefix, in_singleton_class, module_function_active, out)
|
|
858
|
+
certain_body = @certain_bodies.include?(stmts_node)
|
|
725
859
|
Inference::ModuleFunctionState.each_sig_gen_statement(stmts_node.body,
|
|
726
860
|
module_function_active) do |stmt, active|
|
|
861
|
+
if certain_body
|
|
862
|
+
@certain_definers << stmt
|
|
863
|
+
# A `begin … end` statement's body (and `ensure`) runs whenever the statement does.
|
|
864
|
+
mark_certain_body(stmt) if stmt.is_a?(Prism::BeginNode)
|
|
865
|
+
end
|
|
727
866
|
walk_defs(stmt, prefix, in_singleton_class, active, out)
|
|
728
867
|
end
|
|
729
868
|
end
|
|
@@ -740,7 +879,7 @@ module Rigor
|
|
|
740
879
|
kind = node.receiver.is_a?(Prism::SelfNode) || in_singleton_class ? :singleton : :instance
|
|
741
880
|
class_name = prefix.join("::")
|
|
742
881
|
if Inference::ModuleFunctionState.sig_gen_module_function?(module_function_active, kind)
|
|
743
|
-
@module_function_methods <<
|
|
882
|
+
@module_function_methods << node
|
|
744
883
|
elsif Inference::ModuleFunctionState.sig_gen_uncertain?(module_function_active, kind)
|
|
745
884
|
@uncertain_module_function_defs << node
|
|
746
885
|
end
|
|
@@ -782,9 +921,9 @@ module Rigor
|
|
|
782
921
|
|
|
783
922
|
# Returns "def self." (kind: :singleton), "def self?." (instance method declared inside a
|
|
784
923
|
# `module_function` region — both instance + singleton dispatch at runtime), or "def " (plain instance).
|
|
785
|
-
def method_def_prefix(
|
|
924
|
+
def method_def_prefix(def_node, kind)
|
|
786
925
|
return "def self." if kind == :singleton
|
|
787
|
-
return "def self?." if @module_function_methods.include?(
|
|
926
|
+
return "def self?." if @module_function_methods.include?(def_node)
|
|
788
927
|
|
|
789
928
|
"def "
|
|
790
929
|
end
|
|
@@ -800,7 +939,7 @@ module Rigor
|
|
|
800
939
|
def visibility_excludes?(def_node, class_name, kind, scope_index)
|
|
801
940
|
return false if kind == :singleton
|
|
802
941
|
return false if @include_private
|
|
803
|
-
return false if @module_function_methods.include?(
|
|
942
|
+
return false if @module_function_methods.include?(def_node)
|
|
804
943
|
|
|
805
944
|
scope = scope_index[def_node] || scope_index.each_value.first
|
|
806
945
|
return false if scope.nil?
|
|
@@ -1038,7 +1177,7 @@ module Rigor
|
|
|
1038
1177
|
|
|
1039
1178
|
scope = scope_index[def_node]
|
|
1040
1179
|
method_def = lookup_existing_method(class_name, def_node.name, kind, scope&.environment, scope)
|
|
1041
|
-
head = "#{method_def_prefix(
|
|
1180
|
+
head = "#{method_def_prefix(def_node, kind)}#{def_node.name}: "
|
|
1042
1181
|
if return_defaulted
|
|
1043
1182
|
return mixed_provenance_candidate(path, def_node, class_name, kind, scope_index, inline, method_def, head)
|
|
1044
1183
|
end
|
|
@@ -1258,7 +1397,37 @@ module Rigor
|
|
|
1258
1397
|
# different method (issue #1382). Delegates to {Rigor::Inference::DefReturnTyper} — the same body-typing +
|
|
1259
1398
|
# explicit-return-union the `rigor annotate` def-line annotator uses.
|
|
1260
1399
|
def infer_return_type(def_node, scope_index)
|
|
1261
|
-
Inference::DefReturnTyper.call(def_node, scope_index)
|
|
1400
|
+
absorb_empty_literals(Inference::DefReturnTyper.call(def_node, scope_index, keep_lookup_nil: true))
|
|
1401
|
+
end
|
|
1402
|
+
|
|
1403
|
+
# Issue #1153 — an empty-collection early return (`return [] if table.empty?`) types its arm `[]`, which the
|
|
1404
|
+
# engine's union keeps beside a sibling `Array[E]` arm on purpose (the empty tuple is a distinct shape). A
|
|
1405
|
+
# signature has no use for the distinction: `[]` is an `Array[E]` for every `E`, so `Array[E] | []` is just
|
|
1406
|
+
# `Array[E]`, and the leftover `[]` arm made the proposal read as if the empty literal pinned the element.
|
|
1407
|
+
# Issue #1149 — the same holds for the empty closed hash shape `{}` (a frozen empty fallback,
|
|
1408
|
+
# `table[key] || EMPTY`) beside a `Hash[K, V]` nominal. Each empty literal is absorbed only beside its OWN
|
|
1409
|
+
# nominal: a `[]` beside a `Hash` arm, or a `{}` beside an `Array` arm, is untouched. A lone empty literal,
|
|
1410
|
+
# one beside another tuple / shape (`[1, 2] | []`), an open `{ ... }` shape, and a non-empty shape are
|
|
1411
|
+
# untouched too, since each of those is information the declaration should keep.
|
|
1412
|
+
def absorb_empty_literals(type)
|
|
1413
|
+
return type unless type.is_a?(Type::Union)
|
|
1414
|
+
|
|
1415
|
+
kept = type.members.reject { |member| absorbed_empty_literal?(member, type.members) }
|
|
1416
|
+
kept.size == type.members.size ? type : Type::Combinator.union(*kept)
|
|
1417
|
+
end
|
|
1418
|
+
|
|
1419
|
+
def absorbed_empty_literal?(member, members)
|
|
1420
|
+
if member.is_a?(Type::Tuple) && member.elements.empty?
|
|
1421
|
+
nominal_arm?(members, "Array")
|
|
1422
|
+
elsif member.is_a?(Type::HashShape) && member.pairs.empty? && member.closed?
|
|
1423
|
+
nominal_arm?(members, "Hash")
|
|
1424
|
+
else
|
|
1425
|
+
false
|
|
1426
|
+
end
|
|
1427
|
+
end
|
|
1428
|
+
|
|
1429
|
+
def nominal_arm?(members, class_name)
|
|
1430
|
+
members.any? { |member| member.is_a?(Type::Nominal) && member.class_name == class_name }
|
|
1262
1431
|
end
|
|
1263
1432
|
|
|
1264
1433
|
def dynamic_top?(type)
|
|
@@ -1305,7 +1474,7 @@ module Rigor
|
|
|
1305
1474
|
return equivalent(path, def_node, class_name, kind, inferred, VOID_RETURN_RBS) if declares_void?(method_def)
|
|
1306
1475
|
|
|
1307
1476
|
declared = build_declared_return(method_def)
|
|
1308
|
-
if declared_untyped?(declared)
|
|
1477
|
+
if declared_untyped?(declared, method_def)
|
|
1309
1478
|
return declared_untyped_candidate(path, def_node, class_name, kind, inferred, method_def, rendered: rendered)
|
|
1310
1479
|
end
|
|
1311
1480
|
|
|
@@ -1372,7 +1541,7 @@ module Rigor
|
|
|
1372
1541
|
returned = paren_wrap_union(elaborated_rbs(inferred, owner: class_name))
|
|
1373
1542
|
annotations = overload.annotations.map { |a| "#{a.location&.source || "%a{#{a.string}}"} " }.join
|
|
1374
1543
|
visibility = member.respond_to?(:visibility) && member.visibility ? "#{member.visibility} " : ""
|
|
1375
|
-
"#{visibility}#{method_def_prefix(
|
|
1544
|
+
"#{visibility}#{method_def_prefix(def_node, kind)}#{def_node.name}: " \
|
|
1376
1545
|
"#{annotations}#{spell_with_return(overload.method_type, returned)}"
|
|
1377
1546
|
end
|
|
1378
1547
|
|
|
@@ -1431,8 +1600,13 @@ module Rigor
|
|
|
1431
1600
|
# skips. ADR-93's inline `# @rbs param: Type` annotation (no return clause) synthesizes exactly this
|
|
1432
1601
|
# spelling for every method it documents, so the silent drop hit the parameter-annotated, return-bare
|
|
1433
1602
|
# shape ADR-14 exists to help.
|
|
1434
|
-
|
|
1435
|
-
|
|
1603
|
+
#
|
|
1604
|
+
# The translation alone cannot say the author wrote `untyped`: an interface return (`-> _Taker[1]`,
|
|
1605
|
+
# `-> _ToS`) also translates to `Dynamic`, and reading it as an absent declaration replaced it with an
|
|
1606
|
+
# unrelated nominal under `--overwrite` (#2178). So every overload's return must be `untyped` as written.
|
|
1607
|
+
def declared_untyped?(declared, method_def)
|
|
1608
|
+
declared.is_a?(Type::Dynamic) &&
|
|
1609
|
+
method_def.method_types.all? { |mt| mt.type.return_type.is_a?(RBS::Types::Bases::Any) }
|
|
1436
1610
|
end
|
|
1437
1611
|
|
|
1438
1612
|
# Proposed without {#tighter?}: a declared `untyped` return carries no information to weigh it against, the
|
|
@@ -1465,7 +1639,16 @@ module Rigor
|
|
|
1465
1639
|
VALUE_LITERAL_CLASSES = [Integer, String, Symbol].freeze
|
|
1466
1640
|
private_constant :VALUE_LITERAL_CLASSES
|
|
1467
1641
|
|
|
1468
|
-
#
|
|
1642
|
+
# Core generics whose RBS parameters are all `out` or `unchecked out`, so a widened argument is a supertype of
|
|
1643
|
+
# the body's type. An invariant or `in` parameter (a user `class Sink[in T]`) is not widened: `Sink[Integer]`
|
|
1644
|
+
# is no supertype of `Sink[1]` there.
|
|
1645
|
+
COVARIANT_CARRIERS = %w[Array Hash Set Range Enumerator].freeze
|
|
1646
|
+
private_constant :COVARIANT_CARRIERS
|
|
1647
|
+
|
|
1648
|
+
# The same object back when nothing needed widening. A literal nested in a tuple element, a record or hash
|
|
1649
|
+
# value, or a covariant generic argument (`[1, "a"]`, `{ a: 1 }`, `Hash[String, 1]`) widens the same way, while
|
|
1650
|
+
# the carrier's shape and a record's keys stay: a subclass override returning `[2, "b"]` is no widening of
|
|
1651
|
+
# `[Integer, String]`. A nested `nil` is an honest member there, as in a top-level union.
|
|
1469
1652
|
def widen_value_literals(type)
|
|
1470
1653
|
case type
|
|
1471
1654
|
when Type::Constant
|
|
@@ -1473,14 +1656,35 @@ module Rigor
|
|
|
1473
1656
|
return type unless VALUE_LITERAL_CLASSES.any? { |klass| type.value.instance_of?(klass) }
|
|
1474
1657
|
|
|
1475
1658
|
Type::Combinator.nominal_of(type.value.class.name)
|
|
1476
|
-
when Type::Union
|
|
1477
|
-
|
|
1478
|
-
|
|
1659
|
+
when Type::Union then widen_members(type.members, type) { |members| Type::Combinator.union(*members) }
|
|
1660
|
+
when Type::Tuple then widen_members(type.elements, type) { |elements| Type::Combinator.tuple_of(*elements) }
|
|
1661
|
+
when Type::HashShape then widen_hash_shape_values(type)
|
|
1662
|
+
when Type::Nominal
|
|
1663
|
+
return type unless COVARIANT_CARRIERS.include?(type.class_name)
|
|
1664
|
+
|
|
1665
|
+
widen_members(type.type_args, type) do |args|
|
|
1666
|
+
Type::Combinator.nominal_of(type.class_name, type_args: args)
|
|
1667
|
+
end
|
|
1479
1668
|
else
|
|
1480
1669
|
type
|
|
1481
1670
|
end
|
|
1482
1671
|
end
|
|
1483
1672
|
|
|
1673
|
+
def widen_members(members, type)
|
|
1674
|
+
widened = members.map { |member| widen_value_literals(member) }
|
|
1675
|
+
widened.each_with_index.all? { |m, i| m.equal?(members[i]) } ? type : yield(widened)
|
|
1676
|
+
end
|
|
1677
|
+
|
|
1678
|
+
def widen_hash_shape_values(shape)
|
|
1679
|
+
widen_members(shape.pairs.values, shape) do |values|
|
|
1680
|
+
Type::HashShape.new(
|
|
1681
|
+
shape.pairs.keys.zip(values).to_h,
|
|
1682
|
+
required_keys: shape.required_keys, optional_keys: shape.optional_keys,
|
|
1683
|
+
read_only_keys: shape.read_only_keys, extra_keys: shape.extra_keys
|
|
1684
|
+
)
|
|
1685
|
+
end
|
|
1686
|
+
end
|
|
1687
|
+
|
|
1484
1688
|
def widen_boolean_literal
|
|
1485
1689
|
Type::Combinator.union(Type::Combinator.constant_of(true), Type::Combinator.constant_of(false))
|
|
1486
1690
|
end
|
|
@@ -1624,11 +1828,12 @@ module Rigor
|
|
|
1624
1828
|
# `Type::Constant#erase_to_rbs` spells the values RBS writes as literal types (`true` / `false` /
|
|
1625
1829
|
# `nil`, an Integer, a Symbol, a String) as that literal and every other carrier as a nominal. Asking
|
|
1626
1830
|
# which of the two branches it took keeps the list of literal-spellable classes from forking a second
|
|
1627
|
-
# copy into this file. A Range's nominal carries
|
|
1628
|
-
# excluded by class rather than by comparing the erasure with the class
|
|
1629
|
-
# nominal, not a pinned literal.
|
|
1831
|
+
# copy into this file. A Range's and a Set's nominal carries a type argument (`Range[Integer]`, #1891;
|
|
1832
|
+
# `Set[untyped]`, #2177), so they are excluded by class rather than by comparing the erasure with the class
|
|
1833
|
+
# name: like a Float each is a nominal, not a pinned literal.
|
|
1630
1834
|
def rbs_literal?(type)
|
|
1631
|
-
type.is_a?(Type::Constant) && !type.value.is_a?(Range) && type.
|
|
1835
|
+
type.is_a?(Type::Constant) && !type.value.is_a?(Range) && !type.value.is_a?(::Set) &&
|
|
1836
|
+
type.erase_to_rbs != type.value.class.name
|
|
1632
1837
|
end
|
|
1633
1838
|
|
|
1634
1839
|
# Heuristic added after the third-round self-dogfood: `FallbackTracer#size` body is `@events.size`, where
|
|
@@ -1696,7 +1901,7 @@ module Rigor
|
|
|
1696
1901
|
def render_rbs_line(def_node, inferred, class_name, kind)
|
|
1697
1902
|
params = def_node.parameters
|
|
1698
1903
|
head = "(#{render_param_list(params, class_name, def_node.name)})#{block_signature_suffix(params)}"
|
|
1699
|
-
prefix = method_def_prefix(
|
|
1904
|
+
prefix = method_def_prefix(def_node, kind)
|
|
1700
1905
|
"#{prefix}#{def_node.name}: #{head} -> #{paren_wrap_union(elaborated_rbs(inferred, owner: class_name))}"
|
|
1701
1906
|
end
|
|
1702
1907
|
|
|
@@ -1800,12 +2005,17 @@ module Rigor
|
|
|
1800
2005
|
# Per-file context the attr_* walker threads through its recursive descent. Keeps parameter lists in
|
|
1801
2006
|
# check. `obs_ivar_map` carries the observation-derived fallback types built by
|
|
1802
2007
|
# {#build_observed_ivar_map}; it is empty when sig-gen is invoked without `--params=observed`.
|
|
1803
|
-
|
|
2008
|
+
# `definer_table` is {#definer_table}'s map, which drops an attribute another definer replaces. A context
|
|
2009
|
+
# with `definers` only records each attribute's slot and call node for that map, and builds nothing.
|
|
2010
|
+
AttrWalkContext = Struct.new(:path, :scope_index, :obs_ivar_map, :out, :definer_table, :definers,
|
|
2011
|
+
keyword_init: true)
|
|
1804
2012
|
private_constant :AttrWalkContext
|
|
1805
2013
|
|
|
1806
|
-
def collect_attr_candidates(root, path, scope_index, obs_ivar_map = {})
|
|
2014
|
+
def collect_attr_candidates(root, path, scope_index, obs_ivar_map = {}, definer_table = {})
|
|
2015
|
+
@lookup_ivars = Set.new
|
|
2016
|
+
collect_lookup_ivar_writes(root, scope_index)
|
|
1807
2017
|
ctx = AttrWalkContext.new(path: path, scope_index: scope_index,
|
|
1808
|
-
obs_ivar_map: obs_ivar_map, out: [])
|
|
2018
|
+
obs_ivar_map: obs_ivar_map, out: [], definer_table: definer_table)
|
|
1809
2019
|
walk_attr_calls(root, [], false, ctx)
|
|
1810
2020
|
ctx.out
|
|
1811
2021
|
end
|
|
@@ -1850,11 +2060,67 @@ module Rigor
|
|
|
1850
2060
|
class_name = prefix.join("::")
|
|
1851
2061
|
symbol_names = extract_symbol_arguments(call_node)
|
|
1852
2062
|
return if symbol_names.empty?
|
|
2063
|
+
return record_attr_definers(call_node, class_name, symbol_names, ctx) if ctx.definers
|
|
1853
2064
|
|
|
1854
2065
|
ivar_lookup = ivar_type_lookup(ctx.scope_index, class_name, ctx.obs_ivar_map)
|
|
2066
|
+
offset = call_node.location.start_offset
|
|
1855
2067
|
symbol_names.each do |attr_name|
|
|
1856
|
-
ivar_type = ivar_lookup.call(attr_name)
|
|
1857
|
-
|
|
2068
|
+
ivar_type = with_lookup_nil(ivar_lookup.call(attr_name), class_name, attr_name)
|
|
2069
|
+
candidates = build_attr_candidates(call_node.name, class_name, attr_name, ivar_type, ctx)
|
|
2070
|
+
ctx.out.concat(candidates.select do |c|
|
|
2071
|
+
live_definer?(ctx.definer_table, [class_name, :instance, c.method_name], offset)
|
|
2072
|
+
end)
|
|
2073
|
+
end
|
|
2074
|
+
end
|
|
2075
|
+
|
|
2076
|
+
# #2197 — an attribute whose ivar an instance method assigns from an optimistic lookup (`@x = h[k]`) reads
|
|
2077
|
+
# `nil` on a miss, as such a return does ({Inference::DefReturnTyper.optimistic_value?}), so the written
|
|
2078
|
+
# reader keeps the `nil` the engine's nil-free reading of the lookup drops.
|
|
2079
|
+
def with_lookup_nil(type, class_name, attr_name)
|
|
2080
|
+
return type if type.nil? || !@lookup_ivars.include?([class_name, :"@#{attr_name}"])
|
|
2081
|
+
|
|
2082
|
+
Type::Combinator.union(type, Type::Combinator.constant_of(nil))
|
|
2083
|
+
end
|
|
2084
|
+
|
|
2085
|
+
LOOKUP_IVAR_WRITES = [
|
|
2086
|
+
Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode
|
|
2087
|
+
].freeze
|
|
2088
|
+
private_constant :LOOKUP_IVAR_WRITES
|
|
2089
|
+
|
|
2090
|
+
# Records `[class_name, ivar]` for each ivar an instance `def` assigns an optimistic lookup to. The class
|
|
2091
|
+
# prefixes come from {#collect_other_ivar_writes}' walk, whose own result is not needed here.
|
|
2092
|
+
def collect_lookup_ivar_writes(root, scope_index)
|
|
2093
|
+
collect_other_ivar_writes(root, [], false, Hash.new { |h, k| h[k] = Set.new }) do |def_node, prefix|
|
|
2094
|
+
next if def_node.receiver || prefix.empty?
|
|
2095
|
+
|
|
2096
|
+
scan_lookup_ivar_writes(def_node.body, prefix, scope_index)
|
|
2097
|
+
end
|
|
2098
|
+
end
|
|
2099
|
+
|
|
2100
|
+
def scan_lookup_ivar_writes(node, prefix, scope_index)
|
|
2101
|
+
return unless node.is_a?(Prism::Node)
|
|
2102
|
+
|
|
2103
|
+
record_lookup_ivar_write(node, prefix, scope_index)
|
|
2104
|
+
node.rigor_each_child { |child| scan_lookup_ivar_writes(child, prefix, scope_index) }
|
|
2105
|
+
end
|
|
2106
|
+
|
|
2107
|
+
def record_lookup_ivar_write(node, prefix, scope_index)
|
|
2108
|
+
return unless LOOKUP_IVAR_WRITES.any? { |klass| node.is_a?(klass) }
|
|
2109
|
+
|
|
2110
|
+
scope = scope_index[node.value] || scope_index[node]
|
|
2111
|
+
return if scope.nil? || !Inference::DefReturnTyper.optimistic_value?(
|
|
2112
|
+
node.value, scope, Inference::DefReturnTyper::LookupContext.new(scope_index, {}.compare_by_identity)
|
|
2113
|
+
)
|
|
2114
|
+
|
|
2115
|
+
@lookup_ivars << [prefix.join("::"), node.name]
|
|
2116
|
+
end
|
|
2117
|
+
|
|
2118
|
+
def record_attr_definers(call_node, class_name, symbol_names, ctx)
|
|
2119
|
+
symbol_names.each do |attr_name|
|
|
2120
|
+
ATTR_KINDS.fetch(call_node.name).each do |variant|
|
|
2121
|
+
method_name = variant == :writer ? :"#{attr_name}=" : attr_name
|
|
2122
|
+
ctx.definers << [[class_name, :instance, method_name], call_node]
|
|
2123
|
+
end
|
|
1858
2124
|
end
|
|
1859
2125
|
end
|
|
1860
2126
|
|
|
@@ -1919,20 +2185,25 @@ module Rigor
|
|
|
1919
2185
|
# and every write inside an instance `def` or a block, an `instance_variable_set` on `self` included
|
|
1920
2186
|
# ({#written_ivar}). `def self.x`, a `class << self` body and a bare class-body write set the class
|
|
1921
2187
|
# object's ivars and do not count.
|
|
1922
|
-
|
|
2188
|
+
#
|
|
2189
|
+
# `on_def`, when given, is called with each instance `def` reached and its class prefix, so another
|
|
2190
|
+
# per-class reading of instance-method ivar writes shares this walk ({#collect_lookup_ivar_writes}).
|
|
2191
|
+
def collect_other_ivar_writes(node, prefix, in_block, other_writes, &on_def)
|
|
1923
2192
|
return unless node.is_a?(Prism::Node)
|
|
1924
2193
|
|
|
1925
2194
|
case node
|
|
1926
2195
|
when Prism::ClassNode, Prism::ModuleNode
|
|
1927
2196
|
child_prefix = Source::ConstantPath.declaration_prefix(prefix, node.constant_path)
|
|
1928
|
-
return collect_other_ivar_writes(node.body, child_prefix, false, other_writes) if child_prefix
|
|
2197
|
+
return collect_other_ivar_writes(node.body, child_prefix, false, other_writes, &on_def) if child_prefix
|
|
1929
2198
|
when Prism::SingletonClassNode then return
|
|
1930
|
-
when Prism::DefNode
|
|
2199
|
+
when Prism::DefNode
|
|
2200
|
+
on_def&.call(node, prefix)
|
|
2201
|
+
return record_def_ivar_writes(node, prefix, other_writes)
|
|
1931
2202
|
when Prism::BlockNode, Prism::LambdaNode then in_block = true
|
|
1932
2203
|
end
|
|
1933
2204
|
record_class_level_write(node, prefix, in_block, other_writes) unless prefix.empty?
|
|
1934
2205
|
|
|
1935
|
-
node.rigor_each_child { |child| collect_other_ivar_writes(child, prefix, in_block, other_writes) }
|
|
2206
|
+
node.rigor_each_child { |child| collect_other_ivar_writes(child, prefix, in_block, other_writes, &on_def) }
|
|
1936
2207
|
end
|
|
1937
2208
|
|
|
1938
2209
|
# The ivars an `attr_writer` / `attr_accessor` on `self` names, or the ivar a write in a block sets.
|
|
@@ -98,7 +98,10 @@ module Rigor
|
|
|
98
98
|
|
|
99
99
|
base_scope = Scope.empty(environment: environment)
|
|
100
100
|
base_scope = base_scope.with_discovery(base_scope.discovery.with(discovered_classes: discovered_classes))
|
|
101
|
-
|
|
101
|
+
# Guard: sig-gen never reads a declared ivar type back (#1406). No spec pins this flag here, because
|
|
102
|
+
# it only matters for an RBS-declared class defined in an observed spec file.
|
|
103
|
+
scope_index = Inference::ScopeIndexer.index(parse_result.value, default_scope: base_scope,
|
|
104
|
+
declared_ivar_types: false)
|
|
102
105
|
bindings = collect_rspec_bindings(parse_result.value, scope_index)
|
|
103
106
|
@before_ivars = collect_before_ivars(parse_result.value, scope_index)
|
|
104
107
|
|
|
@@ -7,7 +7,7 @@ module Rigor
|
|
|
7
7
|
# Parses what sig-gen is about to emit, before it is emitted.
|
|
8
8
|
#
|
|
9
9
|
# Two rendering bugs have shipped RBS that `rbs` itself rejects — a non-identifier record key
|
|
10
|
-
# (`{
|
|
10
|
+
# (`{ "data-contrast": T }`) and a `&block` constructor parameter rendered before the parens
|
|
11
11
|
# (`(**untyped, ?{ (?) -> void })`). Both were found downstream, as a poisoned `sig/` tree: the file is
|
|
12
12
|
# quarantined, its types vanish, and (for a project adopting `reject-unparseable-signatures`) the build
|
|
13
13
|
# fails. A generator that can poison its own consumer's build is worth guarding at the source.
|
|
@@ -29,9 +29,11 @@ module Rigor
|
|
|
29
29
|
# array means "all emittable classifications".
|
|
30
30
|
# @param observation — `{ paths:, missing: }` under `--params=observed`, else `nil`; JSON carries it
|
|
31
31
|
# as the top-level `observation` key (#1398).
|
|
32
|
-
|
|
32
|
+
# @param diagnostics — the path errors `rigor check` would report for the arguments (#1991); JSON carries
|
|
33
|
+
# them as the top-level `diagnostics` key, and only when there are some.
|
|
34
|
+
def render(candidates:, mode:, format:, selection:, observation: nil, diagnostics: [])
|
|
33
35
|
case format
|
|
34
|
-
when "json" then render_json(filter(candidates, selection, with_skipped: true), observation)
|
|
36
|
+
when "json" then render_json(filter(candidates, selection, with_skipped: true), observation, diagnostics)
|
|
35
37
|
when "text"
|
|
36
38
|
filtered = filter(candidates, selection)
|
|
37
39
|
mode == :diff ? render_diff(filtered) : render_print(filtered)
|
|
@@ -124,9 +126,10 @@ module Rigor
|
|
|
124
126
|
end
|
|
125
127
|
end
|
|
126
128
|
|
|
127
|
-
def render_json(candidates, observation = nil)
|
|
129
|
+
def render_json(candidates, observation = nil, diagnostics = [])
|
|
128
130
|
payload = { candidates: candidates.map(&:to_h) }
|
|
129
131
|
payload[:observation] = observation if observation
|
|
132
|
+
payload[:diagnostics] = diagnostics.map(&:to_h) if diagnostics.any?
|
|
130
133
|
@out.puts(JSON.pretty_generate(payload))
|
|
131
134
|
end
|
|
132
135
|
|
|
@@ -37,7 +37,17 @@ module Rigor
|
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
# A set is mutable, so the classes the engine reads off its construction (`Set[x]`, `Set.new(xs)`) are not
|
|
41
|
+
# a contract: `x = Set[1]; x << "a"; x` holds a String. A written `Set` therefore keeps `untyped` for its
|
|
42
|
+
# element, as a Set constant does (#2177), however precisely the engine typed it.
|
|
43
|
+
SET_ELEMENT_UNTYPED = "Set"
|
|
44
|
+
private_constant :SET_ELEMENT_UNTYPED
|
|
45
|
+
|
|
40
46
|
def self.elaborate_nominal(type, environment, arity_cache)
|
|
47
|
+
if type.class_name == SET_ELEMENT_UNTYPED && type.type_args.size == 1
|
|
48
|
+
return Type::Combinator.nominal_of(type.class_name, type_args: [Type::Combinator.untyped])
|
|
49
|
+
end
|
|
50
|
+
|
|
41
51
|
elaborated_args = type.type_args.map { |arg| walk(arg, environment, arity_cache) }
|
|
42
52
|
return Type::Combinator.nominal_of(type.class_name, type_args: elaborated_args) if elaborated_args.any?
|
|
43
53
|
|
|
@@ -57,11 +67,17 @@ module Rigor
|
|
|
57
67
|
Type::Tuple.new(elements)
|
|
58
68
|
end
|
|
59
69
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
type
|
|
70
|
+
# A bare generic in a value position (`{ k: Hash.new }`) fails `rbs validate` as a top-level one does
|
|
71
|
+
# (#2177), so the values are elaborated too. Only the pairs change; the key policy is carried over.
|
|
72
|
+
def self.elaborate_hash_shape(type, environment, arity_cache)
|
|
73
|
+
pairs = type.pairs.transform_values { |value| walk(value, environment, arity_cache) }
|
|
74
|
+
return type if pairs == type.pairs
|
|
75
|
+
|
|
76
|
+
Type::HashShape.new(
|
|
77
|
+
pairs,
|
|
78
|
+
required_keys: type.required_keys, optional_keys: type.optional_keys,
|
|
79
|
+
read_only_keys: type.read_only_keys, extra_keys: type.extra_keys
|
|
80
|
+
)
|
|
65
81
|
end
|
|
66
82
|
|
|
67
83
|
def self.generic_arity_for(class_name, environment, cache)
|