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
|
@@ -29,6 +29,7 @@ require_relative "check_rules/void_value_use_collector"
|
|
|
29
29
|
require_relative "check_rules/self_closedness_scanner"
|
|
30
30
|
require_relative "check_rules/source_arity"
|
|
31
31
|
require_relative "check_rules/lexical_method_sites"
|
|
32
|
+
require_relative "check_rules/rebinding_block_callees"
|
|
32
33
|
require_relative "check_rules/rbs_receiver_surface"
|
|
33
34
|
require_relative "check_rules/special_global_setters"
|
|
34
35
|
require_relative "check_rules/ruby_deprecations"
|
|
@@ -450,11 +451,31 @@ module Rigor
|
|
|
450
451
|
other_class = ivar_class_for(write[:type])
|
|
451
452
|
next nil if other_class.nil? || other_class == "NilClass" || other_class == first_class
|
|
452
453
|
next nil if declaration_admits?(write[:declared], canonical[:type], write[:type])
|
|
454
|
+
next nil if separate_invocations?(write, other_class, writes)
|
|
453
455
|
|
|
454
456
|
build_ivar_write_mismatch_diagnostic(path, write[:node], class_name, ivar_name, first_class, other_class)
|
|
455
457
|
end
|
|
456
458
|
end
|
|
457
459
|
|
|
460
|
+
# ADR-125 — a divergent write is never live together with the writes it conflicts with when the class
|
|
461
|
+
# serves one entry point per instance (a plugin-declared per-invocation class, e.g. a Rails controller:
|
|
462
|
+
# one action per request) and every conflicting write — non-nil, of a concrete class other than this
|
|
463
|
+
# write's — sits in a DIFFERENT action of the same class. An action is a public method no declared
|
|
464
|
+
# non-action macro names (`before_action :x`, `helper_method :y`); {IvarWriteCollector} decides it. A
|
|
465
|
+
# conflicting write in a private or protected helper, a callback target, a `helper_method`,
|
|
466
|
+
# `initialize`, or the same method keeps the finding. Which helper each action reaches is not proved
|
|
467
|
+
# here.
|
|
468
|
+
def separate_invocations?(write, write_class, writes)
|
|
469
|
+
return false unless write[:action]
|
|
470
|
+
|
|
471
|
+
writes.all? do |other|
|
|
472
|
+
other_class = ivar_class_for(other[:type])
|
|
473
|
+
next true if other_class.nil? || other_class == "NilClass" || other_class == write_class
|
|
474
|
+
|
|
475
|
+
other[:action] && other[:method_name] != write[:method_name]
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
458
479
|
def declaration_admits?(declared, *write_types)
|
|
459
480
|
return false if declared.nil?
|
|
460
481
|
|
|
@@ -969,7 +990,8 @@ module Rigor
|
|
|
969
990
|
# ({RbsReceiverSurface}). Last for {#ancestry_declares_method?}'s reason: it walks the class graph.
|
|
970
991
|
def rbs_ancestor_may_answer?(class_name, method_name, scope, kind)
|
|
971
992
|
if kind == :singleton
|
|
972
|
-
RbsReceiverSurface.singleton_hook?(scope, class_name)
|
|
993
|
+
RbsReceiverSurface.singleton_hook?(scope, class_name) ||
|
|
994
|
+
RbsReceiverSurface.singleton_mixin_may_answer?(scope, class_name, method_name)
|
|
973
995
|
else
|
|
974
996
|
RbsReceiverSurface.ancestor_may_answer?(scope, class_name, method_name)
|
|
975
997
|
end
|
|
@@ -1225,12 +1247,22 @@ module Rigor
|
|
|
1225
1247
|
# body is morally a class body, so ADR-34 stays silent there — including on a genuinely
|
|
1226
1248
|
# undefined name inside the block. Ranges are computed once per file.
|
|
1227
1249
|
return nil if call_inside_receiver_eval_ranges?(eval_ranges, call_node)
|
|
1228
|
-
|
|
1229
|
-
return nil if refined_method_in_effect?("Object", call_node, scope, :instance, lexical_sites)
|
|
1250
|
+
return nil if lexically_declined_toplevel_call?(call_node, scope, lexical_sites, scope_index)
|
|
1230
1251
|
|
|
1231
1252
|
build_unresolved_toplevel_diagnostic(path, call_node)
|
|
1232
1253
|
end
|
|
1233
1254
|
|
|
1255
|
+
# The `call.unresolved-toplevel` declines that read the file's syntax, asked last because they walk it.
|
|
1256
|
+
def lexically_declined_toplevel_call?(call_node, scope, lexical_sites, scope_index = nil)
|
|
1257
|
+
# ADR-121 WD7 — a refinement of `Object` (or one Rigor cannot read) in effect here may define the name.
|
|
1258
|
+
return true if refined_method_in_effect?("Object", call_node, scope, :instance, lexical_sites)
|
|
1259
|
+
return false if lexical_sites.nil?
|
|
1260
|
+
|
|
1261
|
+
# Issue #1817 — a block handed to a project method that may `module_eval` / `instance_exec` it runs under
|
|
1262
|
+
# another `self`, so a call in it is not a top-level one ({RebindingBlockCallees}).
|
|
1263
|
+
RebindingBlockCallees.inside_rebound_block?(call_node, scope, lexical_sites.block_calls, scope_index)
|
|
1264
|
+
end
|
|
1265
|
+
|
|
1234
1266
|
# Issue #1383 — RBS core declares `main`'s private singleton methods on
|
|
1235
1267
|
# `RBS::Unnamed::TopLevelSelfClass`, so the set follows RBS rather than a hand-kept
|
|
1236
1268
|
# list. CRuby 4.0's `main` also has `ruby2_keywords`, which RBS does not declare.
|
|
@@ -1487,12 +1519,27 @@ module Rigor
|
|
|
1487
1519
|
# overlay open receiver ({GEM_OVERLAY_OPEN_RECEIVERS}) whose
|
|
1488
1520
|
# bundled directory this run actually loaded, a core-overlay
|
|
1489
1521
|
# open receiver ({CORE_OVERLAY_OPEN_RECEIVERS}), or a
|
|
1490
|
-
# Rigor-synthesized stub type (see `RbsLoader#synthesized_type_names`)
|
|
1522
|
+
# Rigor-synthesized stub type (see `RbsLoader#synthesized_type_names`),
|
|
1523
|
+
# or a project-only reopen of a Ruby library class whose own RBS
|
|
1524
|
+
# did not load ({#ruby_library_reopen?}).
|
|
1491
1525
|
def unbounded_receiver_surface?(class_name, scope)
|
|
1492
1526
|
bare_name = class_name.to_s.sub(/\A::/, "")
|
|
1493
1527
|
open_receiver?(class_name, scope) || synthesized_stub_receiver?(class_name, scope) ||
|
|
1494
1528
|
CORE_OVERLAY_OPEN_RECEIVERS.include?(bare_name) ||
|
|
1495
|
-
(GEM_OVERLAY_OPEN_RECEIVERS.include?(bare_name) && gem_overlay_loaded?(scope))
|
|
1529
|
+
(GEM_OVERLAY_OPEN_RECEIVERS.include?(bare_name) && gem_overlay_loaded?(scope)) ||
|
|
1530
|
+
ruby_library_reopen?(class_name, scope)
|
|
1531
|
+
end
|
|
1532
|
+
|
|
1533
|
+
# Issue #2174 — `class Matrix` reopened with one `#:`-annotated method, and no `matrix` signatures
|
|
1534
|
+
# loaded, gives Matrix a declaration holding only that method ({RbsLoader#ruby_library_reopen?}).
|
|
1535
|
+
# `Matrix.identity(2)` is correct Ruby, so the class's surface is treated as open: `call.undefined-method`
|
|
1536
|
+
# declines on it, and a signature it only inherits (`Object#initialize` for `Matrix.new(rows)`) stops
|
|
1537
|
+
# deciding arity and argument types. Methods the project's own declaration lists stay checked.
|
|
1538
|
+
def ruby_library_reopen?(class_name, scope)
|
|
1539
|
+
loader = scope.environment&.rbs_loader
|
|
1540
|
+
return false if loader.nil? || !loader.respond_to?(:ruby_library_reopen?)
|
|
1541
|
+
|
|
1542
|
+
loader.ruby_library_reopen?(class_name)
|
|
1496
1543
|
end
|
|
1497
1544
|
|
|
1498
1545
|
def open_receiver?(class_name, scope)
|
|
@@ -2780,9 +2827,37 @@ module Rigor
|
|
|
2780
2827
|
|
|
2781
2828
|
# Whether any project or `pre_eval:` file aliases `name`, on either side, which can make it another variable.
|
|
2782
2829
|
def global_aliased?(scope, name)
|
|
2830
|
+
record_global_census(Inference::GlobalWriteCensus.alias_entry(name))
|
|
2783
2831
|
Inference::GlobalWriteCensus.aliased?(global_write_census(scope), name)
|
|
2784
2832
|
end
|
|
2785
2833
|
|
|
2834
|
+
# Issue #2158 — a read of the whole-program census records the `global_census:` key of each entry it asks
|
|
2835
|
+
# about ({Inference::GlobalWriteCensus.dependency_key}): a file that starts or stops writing the entry (a `def
|
|
2836
|
+
# write`, an `alias $stdout $x`, a computed `define_method`) re-checks the write. A `pre_eval:` file moves the
|
|
2837
|
+
# snapshot's own fingerprint.
|
|
2838
|
+
def record_global_census(*entries)
|
|
2839
|
+
return unless DependencyRecorder.active?
|
|
2840
|
+
|
|
2841
|
+
entries.each do |entry|
|
|
2842
|
+
DependencyRecorder.read_name(:global_census, Inference::GlobalWriteCensus.dependency_key(entry))
|
|
2843
|
+
end
|
|
2844
|
+
end
|
|
2845
|
+
|
|
2846
|
+
# Whether the census holds any of `entries`, recording only what can flip the answer: the entries present
|
|
2847
|
+
# when it holds one (the answer stays true until every one of them vanishes, and another appearing changes
|
|
2848
|
+
# nothing), every entry when it holds none.
|
|
2849
|
+
#
|
|
2850
|
+
# This rests on an invariant: every census entry can only move a write toward being silenced, and a reported
|
|
2851
|
+
# write is always re-checked through `IncrementalSession#global_write_reporters`, so recording only the
|
|
2852
|
+
# present entries on a yes is sound. A future census entry that makes a write reported would break it, and
|
|
2853
|
+
# its reader would have to record every entry. Recording every entry on a no is defence in depth, since the
|
|
2854
|
+
# write that answer leaves reported re-checks through `global_write_reporters` anyway.
|
|
2855
|
+
def census_holds_any?(census, entries)
|
|
2856
|
+
present = entries.select { |entry| census.include?(entry) }
|
|
2857
|
+
record_global_census(*(present.empty? ? entries : present))
|
|
2858
|
+
!present.empty?
|
|
2859
|
+
end
|
|
2860
|
+
|
|
2786
2861
|
# A literal is rejected when its class is none the setter takes and, for a setter that also converts
|
|
2787
2862
|
# (`to_str`, `to_int`) or asks for `write`, the literal's object cannot answer that method.
|
|
2788
2863
|
def literal_rejected?(class_name, contract, node, scope, lexical_sites)
|
|
@@ -2830,11 +2905,15 @@ module Rigor
|
|
|
2830
2905
|
end
|
|
2831
2906
|
|
|
2832
2907
|
def program_may_answer?(ancestors, names, census, scope)
|
|
2833
|
-
|
|
2908
|
+
defines = [Inference::GlobalWriteCensus::DEFINES_ANY, *names.map { |name| [:defines, name] }]
|
|
2909
|
+
return true if census_holds_any?(census, defines)
|
|
2834
2910
|
|
|
2835
2911
|
dynamic_mark = Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK
|
|
2836
2912
|
return true if ancestors.any? { |owner| scope.parameter_envelopes_of(owner).key?(dynamic_mark) }
|
|
2837
2913
|
|
|
2914
|
+
# One key for every top-level mixin, a deliberate trade-off: the reader asks about each, so a mixin
|
|
2915
|
+
# appearing or vanishing anywhere re-checks every write that reached this step.
|
|
2916
|
+
record_global_census(%i[main_mixin *])
|
|
2838
2917
|
main_mixins = Inference::GlobalWriteCensus.main_mixins(census)
|
|
2839
2918
|
main_mixins.any? { |mixin| rbs_mixin_may_answer?(mixin, names, scope) } ||
|
|
2840
2919
|
mixin_may_answer?(ancestors, names, scope)
|
|
@@ -2876,7 +2955,7 @@ module Rigor
|
|
|
2876
2955
|
# active refinement on Ruby 4.0.5; a refined `respond_to_missing?` or `respond_to?`, and every implicit
|
|
2877
2956
|
# conversion, do not, so a refinement that adds only those leaves the write reported.
|
|
2878
2957
|
def refined_writer_in_effect?(census, node, scope, lexical_sites)
|
|
2879
|
-
Inference::GlobalWriteCensus
|
|
2958
|
+
census_holds_any?(census, [Inference::GlobalWriteCensus::REFINES_ANY, %i[refines write]]) &&
|
|
2880
2959
|
(lexical_sites.nil? || !scope.declared_refinements.empty? ||
|
|
2881
2960
|
lexical_sites.refinements.any_at?(node.location.start_offset))
|
|
2882
2961
|
end
|
|
@@ -3032,6 +3111,7 @@ module Rigor
|
|
|
3032
3111
|
# `Widget.new` would read, and file the dependency edges of, the whole singleton chain. The set is memoised per
|
|
3033
3112
|
# discovery index.
|
|
3034
3113
|
def private_singleton_candidate?(scope, name)
|
|
3114
|
+
DependencyRecorder.read_visibility("*", name) if DependencyRecorder.active?
|
|
3035
3115
|
discovery = scope.discovery
|
|
3036
3116
|
slot = Thread.current[PRIVATE_NAMES_MEMO]
|
|
3037
3117
|
unless slot && slot[0].equal?(discovery)
|
|
@@ -3058,7 +3138,8 @@ module Rigor
|
|
|
3058
3138
|
return true if rows.nil?
|
|
3059
3139
|
|
|
3060
3140
|
offset = call_node.location.start_offset
|
|
3061
|
-
|
|
3141
|
+
# A row with no side (a `def` in a block, a `define_method` name) is a block's range or no range at all.
|
|
3142
|
+
rows.none? { |(start, finish, _name, kind, _owner)| !kind.nil? && start <= offset && offset < finish }
|
|
3062
3143
|
end
|
|
3063
3144
|
|
|
3064
3145
|
SINGLETON_WILDCARD_SLOT = %i[singleton *].freeze
|
|
@@ -3070,6 +3151,7 @@ module Rigor
|
|
|
3070
3151
|
discovery = scope.discovery
|
|
3071
3152
|
chain = Scope::ResolutionChain.for(scope, class_name.to_s, :singleton, :methods)
|
|
3072
3153
|
chain.entries.any? do |entry|
|
|
3154
|
+
DependencyRecorder.read_visibility(entry.name, SINGLETON_WILDCARD_SLOT.last) unless entry.external?
|
|
3073
3155
|
!entry.external? && discovery.contested?(:discovered_method_visibilities,
|
|
3074
3156
|
[entry.name, SINGLETON_WILDCARD_SLOT])
|
|
3075
3157
|
end
|
|
@@ -4202,6 +4284,9 @@ module Rigor
|
|
|
4202
4284
|
|
|
4203
4285
|
override_visibility = scope.discovered_method_visibility(class_name, method_name)
|
|
4204
4286
|
return nil if override_visibility.nil?
|
|
4287
|
+
# Issue #2013 — a contested slot (a later `define_method` or alias gave the name another visibility, or a
|
|
4288
|
+
# write the walk cannot place) proves no reduction.
|
|
4289
|
+
return nil if scope.discovery.contested?(:discovered_method_visibilities, [class_name, method_name])
|
|
4205
4290
|
|
|
4206
4291
|
parent = nearest_ancestor_visibility(scope, class_name, method_name)
|
|
4207
4292
|
return nil if parent.nil?
|
|
@@ -406,6 +406,17 @@ module Rigor
|
|
|
406
406
|
end
|
|
407
407
|
end
|
|
408
408
|
|
|
409
|
+
# Issue #2134 — {read_name} for a read of `class_name`'s visibility slots for `method_name`, on either side, or
|
|
410
|
+
# of their contest: `visibility:<class>#<method>`. `IncrementalSession#visibility_name_affected` fires the key
|
|
411
|
+
# when a file's visibility entry or contest for the slot moves. A reader that asks whether ANY class records
|
|
412
|
+
# the name private passes `"*"` as the class; the key fires when any entry of the name moves. A wildcard
|
|
413
|
+
# slot (`[:singleton, :*]`) is `*` as the method name.
|
|
414
|
+
def read_visibility(class_name, method_name)
|
|
415
|
+
return if Thread.current[KEY].nil?
|
|
416
|
+
|
|
417
|
+
read_name(:visibility, "#{class_name}##{method_name}")
|
|
418
|
+
end
|
|
419
|
+
|
|
409
420
|
# The miss-only spelling of {read_name}: a cross-file lookup of `name` (kind `:method` / `:class` /
|
|
410
421
|
# `:toplevel` / …) that resolved to nothing. Kept as the name the internal spec's negative-edge
|
|
411
422
|
# contract uses.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Analysis
|
|
7
|
+
# Issue #1967 — the edits `rigor check --verify-incremental-edits` replays on each sampled file, as the text an
|
|
8
|
+
# editor buffer stands in for it with ({IncrementalSession#replay_edit}).
|
|
9
|
+
#
|
|
10
|
+
# - `emptied` — the file with nothing in it. Every declaration and definition it makes disappears, so the replay
|
|
11
|
+
# exercises the edges a removed declaration fires.
|
|
12
|
+
# - `skeleton` — the file's `class` / `module` declarations, nested as written, with every body statement dropped
|
|
13
|
+
# (methods, `refine` blocks, visibility calls, mixins, constants). The declarations stay, so the class-level keys
|
|
14
|
+
# a removed declaration fires stay quiet, and a finer edge such as #2163's `refinement:` is what puts a reader
|
|
15
|
+
# into the closure. Emptying alone does not exercise that one: the broader key it fires re-checks the reader
|
|
16
|
+
# anyway. Not every finer edge is reached: dropping a body still moves a broader key for some of them. A
|
|
17
|
+
# `visibility:` edge is one (#2161's `module_function :two` to `private :two` swap passes both edits with the
|
|
18
|
+
# edge removed), and only an edit that changes a body while keeping it (#2277) would test it.
|
|
19
|
+
#
|
|
20
|
+
# Neither replays an addition or a body-changing edit. A skeleton identical to the file, or with no declaration
|
|
21
|
+
# (the emptied file again, or a file that does not parse), is not replayed.
|
|
22
|
+
module EditReplay
|
|
23
|
+
Edit = Data.define(:label, :source)
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# The edits to replay for `source`, in order.
|
|
28
|
+
def edits(source)
|
|
29
|
+
emptied = Edit.new(label: "emptying", source: "")
|
|
30
|
+
reduced = skeleton(source)
|
|
31
|
+
return [emptied] if reduced.empty? || reduced == source
|
|
32
|
+
|
|
33
|
+
[emptied, Edit.new(label: "reducing to its class/module skeleton", source: reduced)]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# `source`'s `class` / `module` declarations, nested, with their bodies dropped; "" when it does not parse or
|
|
37
|
+
# declares nothing.
|
|
38
|
+
def skeleton(source)
|
|
39
|
+
result = Prism.parse(source)
|
|
40
|
+
return "" unless result.success?
|
|
41
|
+
|
|
42
|
+
lines = declarations(result.value.statements, 0)
|
|
43
|
+
lines.empty? ? "" : "#{lines.join("\n")}\n"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The skeleton lines of the declarations directly in `statements`, indented `depth` levels.
|
|
47
|
+
def declarations(statements, depth)
|
|
48
|
+
return [] unless statements.is_a?(Prism::StatementsNode)
|
|
49
|
+
|
|
50
|
+
statements.body.flat_map do |node|
|
|
51
|
+
next [] unless node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode)
|
|
52
|
+
|
|
53
|
+
indent = " " * depth
|
|
54
|
+
["#{indent}#{header_of(node)}", *declarations(node.body, depth + 1), "#{indent}end"]
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def header_of(node)
|
|
59
|
+
return "module #{node.constant_path.slice}" if node.is_a?(Prism::ModuleNode)
|
|
60
|
+
|
|
61
|
+
superclass = node.superclass
|
|
62
|
+
superclass ? "class #{node.constant_path.slice} < #{superclass.slice}" : "class #{node.constant_path.slice}"
|
|
63
|
+
end
|
|
64
|
+
private_class_method :declarations, :header_of
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../inference/global_write_census"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
module Analysis
|
|
5
7
|
# ADR-46 slice 2 — the pure set-algebra core of the incremental step, kept side-effect-free and
|
|
@@ -132,6 +134,41 @@ module Rigor
|
|
|
132
134
|
moved.freeze
|
|
133
135
|
end
|
|
134
136
|
|
|
137
|
+
# Issue #2158 — the `global_census:` keys whose entries moved in this edit: one for every entry that appeared in
|
|
138
|
+
# or vanished from a file's `global_write_census` ({Inference::GlobalWriteCensus.dependency_key}). `paths` MUST
|
|
139
|
+
# include removed files, whose whole census vanished. `before` / `after` map a path to that file's census Set (nil
|
|
140
|
+
# when it records nothing).
|
|
141
|
+
def changed_global_census_keys(paths, before, after)
|
|
142
|
+
moved = Set.new
|
|
143
|
+
paths.each do |path|
|
|
144
|
+
((before[path] || Set.new) ^ (after[path] || Set.new)).each do |entry|
|
|
145
|
+
moved << "global_census:#{Inference::GlobalWriteCensus.dependency_key(entry)}"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
moved.freeze
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# The {Inference::ScopeIndexer.visibility_entries} entries that carry a leading marker before `class, slot`.
|
|
152
|
+
MARKED_VISIBILITY_ENTRIES = %i[contested module_function].freeze
|
|
153
|
+
|
|
154
|
+
# Issue #2134 — the `<class>#<method>` slots whose visibility entries or contests moved in this edit: one for
|
|
155
|
+
# every entry that appeared in or vanished from a file's {Inference::ScopeIndexer.visibility_entries}, a
|
|
156
|
+
# singleton slot's `[:singleton, name]` read as `name` (the wildcard slot as `*`), plus `*#<method>`, which the
|
|
157
|
+
# "is the name private anywhere" readers record. That key fires for an entry of any visibility: the merge is
|
|
158
|
+
# last-writer-wins, so dropping a `public :foo` can uncover another file's `private :foo`. `paths` MUST include
|
|
159
|
+
# removed files. Issue #2161 — a `[:module_function, owner, name]` entry fires `<owner>#<name>` the same way.
|
|
160
|
+
def changed_visibility_slots(paths, before, after)
|
|
161
|
+
moved = Set.new
|
|
162
|
+
paths.each do |path|
|
|
163
|
+
((before[path] || Set.new) ^ (after[path] || Set.new)).each do |entry|
|
|
164
|
+
class_name, slot = MARKED_VISIBILITY_ENTRIES.include?(entry.first) ? entry.drop(1) : entry
|
|
165
|
+
name = slot.is_a?(Array) ? slot.last : slot
|
|
166
|
+
moved << "#{class_name}##{name}" << "*##{name}"
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
moved.freeze
|
|
170
|
+
end
|
|
171
|
+
|
|
135
172
|
# Issue #1120 — the method names whose refinements moved in this edit. A consumer whose answer consulted the
|
|
136
173
|
# refinement table recorded a `refinement:<method>` edge whichever way it answered, because the answer is a
|
|
137
174
|
# function of the whole project's refinements of that name. `paths` MUST include removed files, whose whole
|