rigortype 0.4.4 → 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 +5 -5
- data/docs/handbook/11-sig-gen.md +100 -18
- data/docs/manual/02-cli-reference.md +85 -16
- data/docs/manual/03-configuration.md +12 -9
- data/docs/manual/04-diagnostics.md +17 -6
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +2 -1
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +17 -2
- data/docs/manual/12-caching.md +8 -1
- data/docs/manual/18-removing-dead-code.md +18 -7
- 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 +102 -1
- data/lib/rigor/analysis/buffer_binding.rb +14 -0
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -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 +115 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +253 -0
- 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/rule_ids.rb +3 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +64 -4
- data/lib/rigor/analysis/check_rules.rb +319 -21
- data/lib/rigor/analysis/dependency_recorder.rb +11 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/edit_replay.rb +67 -0
- data/lib/rigor/analysis/incremental.rb +86 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +336 -46
- data/lib/rigor/analysis/path_expansion.rb +44 -1
- data/lib/rigor/analysis/reachability/graph.rb +90 -13
- data/lib/rigor/analysis/reachability/scan.rb +279 -39
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +22 -5
- 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 +82 -16
- 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 +222 -111
- data/lib/rigor/analysis/severity_stamp.rb +11 -0
- data/lib/rigor/analysis/worker_session.rb +24 -1
- data/lib/rigor/cache/descriptor.rb +40 -7
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +49 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/check_command.rb +93 -4
- data/lib/rigor/cli/command.rb +3 -1
- data/lib/rigor/cli/diff_command.rb +2 -1
- data/lib/rigor/cli/docs_command.rb +3 -2
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +3 -2
- 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/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +7 -1
- 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 +73 -13
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/type_of_project_scope.rb +27 -0
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +72 -16
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +43 -7
- data/lib/rigor/effects/signature_sources.rb +3 -1
- data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
- data/lib/rigor/environment/gated_signature_guard.rb +2 -1
- data/lib/rigor/environment/gated_signature_plan.rb +2 -1
- data/lib/rigor/environment/installed_gem_set.rb +2 -1
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
- data/lib/rigor/environment/rbs_loader.rb +136 -7
- 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/block_call_timing.rb +7 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
- data/lib/rigor/inference/captured_locals.rb +44 -13
- data/lib/rigor/inference/clause_widening.rb +90 -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 +101 -20
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +97 -18
- data/lib/rigor/inference/global_write_census.rb +36 -1
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/ivar_writer_census.rb +195 -0
- data/lib/rigor/inference/loop_value_record.rb +53 -0
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +278 -42
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
- data/lib/rigor/inference/method_dispatcher.rb +113 -19
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +149 -42
- data/lib/rigor/inference/mutation_widening.rb +5 -2
- data/lib/rigor/inference/narrowing.rb +116 -11
- data/lib/rigor/inference/object_mixins.rb +13 -1
- data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
- data/lib/rigor/inference/project_patched_methods.rb +168 -4
- data/lib/rigor/inference/project_patched_mixins.rb +162 -0
- data/lib/rigor/inference/project_patched_scanner.rb +51 -12
- data/lib/rigor/inference/project_patched_visibility.rb +214 -0
- data/lib/rigor/inference/project_seed_tables.rb +121 -0
- data/lib/rigor/inference/project_yielder.rb +128 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
- data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
- data/lib/rigor/inference/return_barrier.rb +16 -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 +1144 -197
- data/lib/rigor/inference/self_call_ivar_writes.rb +29 -12
- data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
- data/lib/rigor/inference/statement_evaluator.rb +1088 -213
- 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/inference/unthreaded_rebinds.rb +3 -1
- 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/lens/contract_reader.rb +45 -2
- data/lib/rigor/lens/document.rb +2 -1
- data/lib/rigor/lens/file_reader.rb +12 -2
- data/lib/rigor/lens/members.rb +16 -2
- data/lib/rigor/lens/run.rb +34 -7
- data/lib/rigor/mcp/server.rb +11 -3
- data/lib/rigor/plugin/base.rb +29 -13
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/plugin/manifest.rb +29 -3
- data/lib/rigor/plugin/registry.rb +38 -0
- data/lib/rigor/protection/discovery_seed.rb +64 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +36 -0
- data/lib/rigor/scope/discovery_index.rb +5 -0
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
- data/lib/rigor/scope/resolution_chain.rb +40 -5
- data/lib/rigor/scope.rb +59 -7
- data/lib/rigor/sig_gen/alias_index.rb +2 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +465 -49
- data/lib/rigor/sig_gen/layout_index.rb +5 -2
- data/lib/rigor/sig_gen/observation_collector.rb +141 -12
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +16 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- data/lib/rigor/sig_gen/write_reporter.rb +203 -0
- data/lib/rigor/sig_gen/write_result.rb +3 -1
- data/lib/rigor/sig_gen/writer.rb +5 -3
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/combinator.rb +4 -3
- data/lib/rigor/type/constant.rb +54 -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-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +61 -44
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -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 +5 -2
- 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/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +34 -52
- data/sig/rigor/type.rbs +0 -6
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +32 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Inference
|
|
5
|
+
module RefinedDispatch
|
|
6
|
+
# Issue #1979 — whether a class whose superclass the tables do not know is `class K < <expr>` with `<expr>` a
|
|
7
|
+
# core constructor call (`Data.define(...)`, `Struct.new(...)`, `Class.new(<constant>)`) that cannot answer a
|
|
8
|
+
# given class or one of its subclasses, so a subclass walk may leave it out (`OwnMethod.each_subclass` with
|
|
9
|
+
# `possible_only:`).
|
|
10
|
+
#
|
|
11
|
+
# Each constant the expression names (the indexer's `[DiscoveryIndex::ENVELOPE_CONSTRUCTED_KIND, name]` entries)
|
|
12
|
+
# must be a class the environment knows that is neither the given class nor a subclass of it, and the project
|
|
13
|
+
# must not touch the name: no project class or module whose last segment is the name (Ruby resolves a constant
|
|
14
|
+
# through the enclosing classes' ancestors and mixins too, so a `Base::StandardError` a class inherits may be
|
|
15
|
+
# what `Class.new(StandardError)` names), no reopening, singleton `def`, refinement or method table keyed by
|
|
16
|
+
# the name itself, and no constant write of it. Any other shape answers false, as does a class a
|
|
17
|
+
# `K = Class.new(...)` write names.
|
|
18
|
+
module ConstructedClass
|
|
19
|
+
MEMO_KEY = :__rigor_constructed_class__
|
|
20
|
+
EMPTY = [].freeze
|
|
21
|
+
private_constant :MEMO_KEY, :EMPTY
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# True when `name` cannot be `class_name` or a subclass of it. Files `name`'s class edge and a `constant:`
|
|
26
|
+
# edge per named constant on every call; a class declaration appearing fires the walk's `subclass_of:*`.
|
|
27
|
+
def elsewhere?(scope, name, class_name)
|
|
28
|
+
table = memo(scope)
|
|
29
|
+
named = constructed_names(scope, name, table)
|
|
30
|
+
if Analysis::DependencyRecorder.active?
|
|
31
|
+
Scope::ResolutionChain.record_class(scope, name)
|
|
32
|
+
named.each { |constant| Analysis::DependencyRecorder.read_name(:constant, constant) }
|
|
33
|
+
end
|
|
34
|
+
return false if named.empty?
|
|
35
|
+
|
|
36
|
+
by_class = (table[:verdicts][class_name] ||= {})
|
|
37
|
+
return by_class[named] if by_class.key?(named)
|
|
38
|
+
|
|
39
|
+
by_class[named] = named.all? { |constant| core_class_apart?(scope, constant, class_name.to_s, table) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def core_class_apart?(scope, constant, class_name, table)
|
|
43
|
+
environment = scope.environment
|
|
44
|
+
return false unless environment&.class_known?(constant)
|
|
45
|
+
return false if project_name?(scope, constant, table)
|
|
46
|
+
return false unless scope.shadowing_constant_names(constant).empty?
|
|
47
|
+
|
|
48
|
+
ordering = environment.class_ordering(constant, class_name)
|
|
49
|
+
%i[disjoint superclass].include?(ordering) ||
|
|
50
|
+
(ordering == :unknown && !environment.class_known?(class_name))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Whether the project touches `constant` by name: a declared or reopened class or module whose last segment
|
|
54
|
+
# it is, or a top-level table keyed by it (a singleton `def Data.define`, methods, envelope entries or marks,
|
|
55
|
+
# mixins, a refinement).
|
|
56
|
+
def project_name?(scope, constant, table)
|
|
57
|
+
return true if declared_segments(scope, table).include?(constant)
|
|
58
|
+
|
|
59
|
+
discovery = scope.discovery
|
|
60
|
+
[discovery.discovered_classes, discovery.discovered_superclasses, discovery.discovered_includes,
|
|
61
|
+
discovery.discovered_prepends, discovery.discovered_extends, discovery.discovered_methods,
|
|
62
|
+
discovery.discovered_parameter_envelopes, discovery.discovered_refinements].any? do |by_name|
|
|
63
|
+
by_name.key?(constant)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The last segment of every class or module the project declares, reopenings included.
|
|
68
|
+
def declared_segments(scope, table)
|
|
69
|
+
table[:segments] ||= declared_names(scope).each_key.to_set { |key| key.to_s.split("::").last }.freeze
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The constants `name`'s superclass expression names, or an empty Array; memoised per envelope bucket,
|
|
73
|
+
# which a file's own discovery shares with the run's seed for every class it does not declare.
|
|
74
|
+
def constructed_names(scope, name, table)
|
|
75
|
+
bucket = scope.discovered_parameter_envelopes[name]
|
|
76
|
+
return EMPTY if bucket.nil?
|
|
77
|
+
|
|
78
|
+
names = table[:names]
|
|
79
|
+
return names[bucket] if names.key?(bucket)
|
|
80
|
+
|
|
81
|
+
names[bucket] =
|
|
82
|
+
if bucket.key?(Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK)
|
|
83
|
+
EMPTY
|
|
84
|
+
else
|
|
85
|
+
kind = Scope::DiscoveryIndex::ENVELOPE_CONSTRUCTED_KIND
|
|
86
|
+
bucket.each_key.filter_map { |key| key[1] if key.is_a?(Array) && key[0] == kind }.freeze
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The run-wide table of declared classes: the seeded `discovered_class_sources`, or a single-file run's
|
|
91
|
+
# `discovered_classes` where nothing seeded it.
|
|
92
|
+
def declared_names(scope)
|
|
93
|
+
sources = scope.discovery.discovered_class_sources
|
|
94
|
+
sources.empty? ? scope.discovery.discovered_classes : sources
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# One thread-local slot keyed on the run-generation token the runner seeds (or, unseeded, the file's merged
|
|
98
|
+
# `discovered_def_nodes`, as `ExpressionTyper` falls back) and the environment: every table read here is
|
|
99
|
+
# fixed for a run, so each answer is computed once a run, and a new run replaces the slot.
|
|
100
|
+
def memo(scope)
|
|
101
|
+
slot = Thread.current[MEMO_KEY]
|
|
102
|
+
generation = scope.run_generation || scope.discovered_def_nodes
|
|
103
|
+
environment = scope.environment
|
|
104
|
+
unless slot && slot[0].equal?(generation) && slot[1].equal?(environment)
|
|
105
|
+
slot = [generation, environment, { names: {}.compare_by_identity, verdicts: {} }]
|
|
106
|
+
Thread.current[MEMO_KEY] = slot
|
|
107
|
+
end
|
|
108
|
+
slot[2]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../builtins/cruby_definers"
|
|
4
|
+
require_relative "constructed_class"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
module Inference
|
|
@@ -193,8 +194,7 @@ module Rigor
|
|
|
193
194
|
# A module some code `prepend`s to an object's singleton class (or `extend`s an object with in a method
|
|
194
195
|
# body), which puts it ahead of the object's class even where the class already includes it.
|
|
195
196
|
def object_prepended?(scope, name)
|
|
196
|
-
scope.
|
|
197
|
-
false
|
|
197
|
+
scope.parameter_envelopes_of(name).key?(Scope::DiscoveryIndex::ENVELOPE_OBJECT_EXTENDED_MARK)
|
|
198
198
|
end
|
|
199
199
|
|
|
200
200
|
# Is some in-effect row under a class no table declares by any spelling of it (the keys sharing its last
|
|
@@ -282,10 +282,16 @@ module Rigor
|
|
|
282
282
|
# declaration with that superclass appearing or vanishing fires (`IncrementalSession#negative_affected`).
|
|
283
283
|
# A class whose superclass the tables do not know may be a subclass of any class, so it joins the walk too
|
|
284
284
|
# (`subclass_of:*`, which any class declaration appearing or vanishing fires).
|
|
285
|
-
|
|
285
|
+
#
|
|
286
|
+
# Issue #1979 — with `possible_only:`, a class whose superclass expression is a core constructor call that
|
|
287
|
+
# cannot answer `class_name` or a subclass of it (`ConstructedClass.elsewhere?`) is left out, with its
|
|
288
|
+
# subclasses.
|
|
289
|
+
def each_subclass(scope, class_name, possible_only: false)
|
|
286
290
|
children = children_by_parent(scope)
|
|
287
291
|
record_subclasses_read(class_name)
|
|
288
|
-
|
|
292
|
+
unknown = unknown_superclass_classes(scope, class_name)
|
|
293
|
+
unknown = unknown.reject { |name| ConstructedClass.elsewhere?(scope, name, class_name) } if possible_only
|
|
294
|
+
queue = children.fetch(class_name.to_s, []) + unknown
|
|
289
295
|
seen = {}
|
|
290
296
|
until queue.empty?
|
|
291
297
|
subclass = queue.shift
|
|
@@ -300,10 +306,32 @@ module Rigor
|
|
|
300
306
|
|
|
301
307
|
def unknown_superclass_classes(scope, class_name)
|
|
302
308
|
Analysis::DependencyRecorder.read_name(:subclass_of, "*") if Analysis::DependencyRecorder.active?
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
309
|
+
table = subclass_memo(scope)
|
|
310
|
+
all = table[:unknown] ||= begin
|
|
311
|
+
mark = Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK
|
|
312
|
+
computed = scope.discovered_superclasses.filter_map { |child, parent| child if parent.nil? }
|
|
313
|
+
meta = scope.discovered_parameter_envelopes.filter_map { |name, bucket| name if bucket.key?(mark) }
|
|
314
|
+
(computed + meta).uniq.freeze
|
|
315
|
+
end
|
|
316
|
+
name = class_name.to_s
|
|
317
|
+
all.include?(name) ? all - [name] : all
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
SUBCLASS_MEMO_KEY = :__rigor_own_method_subclasses__
|
|
321
|
+
private_constant :SUBCLASS_MEMO_KEY
|
|
322
|
+
|
|
323
|
+
# One thread-local slot keyed on the scope's discovery index (a frozen `Data`) and environment, the only
|
|
324
|
+
# inputs `children_by_parent` and the unknown-superclass list read; neither files a dependency edge
|
|
325
|
+
# (`each_subclass` files its own `subclass_of:` edges on every call), so a hit drops none.
|
|
326
|
+
def subclass_memo(scope)
|
|
327
|
+
slot = Thread.current[SUBCLASS_MEMO_KEY]
|
|
328
|
+
discovery = scope.discovery
|
|
329
|
+
environment = scope.environment
|
|
330
|
+
unless slot && slot[0].equal?(discovery) && slot[1].equal?(environment)
|
|
331
|
+
slot = [discovery, environment, {}]
|
|
332
|
+
Thread.current[SUBCLASS_MEMO_KEY] = slot
|
|
333
|
+
end
|
|
334
|
+
slot[2]
|
|
307
335
|
end
|
|
308
336
|
|
|
309
337
|
def record_subclasses_read(class_name)
|
|
@@ -313,10 +341,11 @@ module Rigor
|
|
|
313
341
|
end
|
|
314
342
|
|
|
315
343
|
def children_by_parent(scope)
|
|
316
|
-
scope
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
344
|
+
subclass_memo(scope)[:children] ||=
|
|
345
|
+
scope.discovered_superclasses.each_with_object({}) do |(child, raw_parent), children|
|
|
346
|
+
parent = scope.ancestor_name_candidates(child, raw_parent).find { |name| known_class?(scope, name) }
|
|
347
|
+
(children[parent] ||= []) << child if parent
|
|
348
|
+
end.each_value(&:freeze).freeze
|
|
320
349
|
end
|
|
321
350
|
|
|
322
351
|
def known_class?(scope, name)
|
|
@@ -65,9 +65,10 @@ module Rigor
|
|
|
65
65
|
node.arguments.arguments[1]
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
-
# Runs the block with the evaluator inside a `while` / `until`
|
|
69
|
-
#
|
|
70
|
-
# ({.apply}).
|
|
68
|
+
# Runs the block with the evaluator inside a `for` body or a `while` / `until` predicate. A `for` loop has no
|
|
69
|
+
# rebind fixpoint, and a predicate runs before every iteration but is walked once, so a literal-name write there
|
|
70
|
+
# must not replace the binding with one pass's value ({.apply}). A `while` / `until` body does not enter it: its
|
|
71
|
+
# fixpoint widens the ivar (issue #1930).
|
|
71
72
|
def in_repeating_body
|
|
72
73
|
depth = Thread.current[REPEATING_KEY] || 0
|
|
73
74
|
Thread.current[REPEATING_KEY] = depth + 1
|
|
@@ -83,9 +84,8 @@ module Rigor
|
|
|
83
84
|
end
|
|
84
85
|
|
|
85
86
|
# `scope` after `node` runs. A literal name binds that ivar to the value's type (yielded), as `@n = v` does.
|
|
86
|
-
# Inside a
|
|
87
|
-
# instead:
|
|
88
|
-
# iteration's locals (`instance_variable_set(:@n, i + 1)` stored `1`) and would leave that. Any other call joins
|
|
87
|
+
# Inside a `for` body or a loop predicate ({.in_repeating_body}) the value and `Dynamic[top]` are joined into the
|
|
88
|
+
# current binding instead: nothing widens the single pass's value across iterations there. Any other call joins
|
|
89
89
|
# `Dynamic[top]` into every ivar `scope` binds: any one of them may now hold the value, and joining the
|
|
90
90
|
# value's own type would report a method the old value has and the new one lacks. An ivar on its ADR-58
|
|
91
91
|
# declaration seed keeps its mark, which stops a nil in the seed from fuelling a nil-receiver diagnostic.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
|
+
require_relative "../source/node_children"
|
|
6
|
+
|
|
5
7
|
module Rigor
|
|
6
8
|
module Inference
|
|
7
9
|
# The constructs whose body `return` leaves only that body, not the enclosing method. A `return` anywhere else —
|
|
@@ -39,6 +41,20 @@ module Rigor
|
|
|
39
41
|
DEFINE_CALLS.include?(name) || (name == :lambda && kernel_receiver?(call_node.receiver))
|
|
40
42
|
end
|
|
41
43
|
|
|
44
|
+
# True when a `return` under `node` exits the enclosing method: one reachable without crossing a barrier node
|
|
45
|
+
# or a barrier call's block. Allocation-free and early-exiting, for a caller that asks it of every block body.
|
|
46
|
+
def exits_method?(node)
|
|
47
|
+
return false if node.nil? || node?(node)
|
|
48
|
+
return true if node.is_a?(Prism::ReturnNode)
|
|
49
|
+
|
|
50
|
+
barrier_block = node.block if node.is_a?(Prism::CallNode) && block_call?(node)
|
|
51
|
+
node.rigor_each_child do |child|
|
|
52
|
+
next if child.equal?(barrier_block)
|
|
53
|
+
return true if exits_method?(child)
|
|
54
|
+
end
|
|
55
|
+
false
|
|
56
|
+
end
|
|
57
|
+
|
|
42
58
|
# True when a `lambda` call on `receiver` reaches `Kernel#lambda`: no receiver, `self`, `Kernel` or `::Kernel`.
|
|
43
59
|
def kernel_receiver?(receiver)
|
|
44
60
|
case receiver
|
|
@@ -66,6 +66,15 @@ module Rigor
|
|
|
66
66
|
classifier.result
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
+
# Issue #2021 — every contribution of the tree under `root`, for a file discovery reached only through a
|
|
70
|
+
# `require_relative` walk: another file the walk does not see may redefine what it defines, so none of its
|
|
71
|
+
# contributions is certain.
|
|
72
|
+
def every_contribution(root)
|
|
73
|
+
classifier = Classifier.new
|
|
74
|
+
classifier.possible(root)
|
|
75
|
+
classifier.result
|
|
76
|
+
end
|
|
77
|
+
|
|
69
78
|
# Whether `node` was classified possible in `possible` (a {possible_nodes} answer).
|
|
70
79
|
def possible?(possible, node)
|
|
71
80
|
!possible.empty? && possible.include?(node)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../reflection"
|
|
4
|
+
require_relative "../rbs_type_translator"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Inference
|
|
8
|
+
module ScopeIndexer
|
|
9
|
+
# Issue #1406 — an ivar the class's RBS declares, as `@x: T` or through an `attr_reader x: T` /
|
|
10
|
+
# `attr_writer` / `attr_accessor` member (in `sig/`, or inline `attr_reader :x #: T`), reads the declared type
|
|
11
|
+
# in place of the census's join of the writes. The declaration is the author's contract for the field, so it
|
|
12
|
+
# wins over every write the census saw, #541's untyped `attr_writer` write included; whether the writes
|
|
13
|
+
# honour it is ADR-112 WD5's question (#1075), not this one's. It serves
|
|
14
|
+
# checking only: `rigor sig-gen` indexes with `declared_ivar_types: false` and keeps the census's answer.
|
|
15
|
+
#
|
|
16
|
+
# Only an ivar the census already holds is replaced, so a declaration adds no entry the file's writes did not.
|
|
17
|
+
# A declaration is not taken when it is `untyped` (what rbs-inline supplies for a bare `attr_reader :x`, which
|
|
18
|
+
# the type specification reads as a placeholder) or mentions a type variable (`@x: T` in `class Box[T]` is
|
|
19
|
+
# unsolved at a read), and the census entry stands.
|
|
20
|
+
module DeclaredIvarTypes
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
def apply!(accumulator, scope)
|
|
24
|
+
environment = scope&.environment
|
|
25
|
+
return if environment.nil?
|
|
26
|
+
|
|
27
|
+
accumulator.each do |class_name, entries|
|
|
28
|
+
next unless class_name.is_a?(String) # a guard: the side-table keys are deleted before this runs
|
|
29
|
+
|
|
30
|
+
declared = declared_ivars(class_name, environment)
|
|
31
|
+
next if declared.empty?
|
|
32
|
+
|
|
33
|
+
accumulator[class_name] = entries.to_h do |ivar, type|
|
|
34
|
+
[ivar, declared.fetch(ivar, type)]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# `{ivar name => Rigor::Type}` for the ivars `class_name`'s RBS instance definition declares with a usable
|
|
40
|
+
# type, or `{}`.
|
|
41
|
+
def declared_ivars(class_name, environment)
|
|
42
|
+
definition = Reflection.instance_definition(class_name, environment: environment)
|
|
43
|
+
return {} if definition.nil?
|
|
44
|
+
|
|
45
|
+
instance = Type::Combinator.nominal_of(class_name)
|
|
46
|
+
definition.instance_variables.each_with_object({}) do |(name, variable), acc|
|
|
47
|
+
type = translate(variable.type, instance, environment)
|
|
48
|
+
acc[name] = type if type
|
|
49
|
+
end
|
|
50
|
+
rescue StandardError # a guard: a definition that fails to build declares nothing
|
|
51
|
+
{}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def translate(rbs_type, instance, environment)
|
|
55
|
+
return nil unless rbs_type.free_variables.empty?
|
|
56
|
+
|
|
57
|
+
type = RbsTypeTranslator.translate(rbs_type, self_type: instance, instance_type: instance,
|
|
58
|
+
alias_expander: environment.rbs_loader)
|
|
59
|
+
type.is_a?(Type::Dynamic) ? nil : type
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../../reflection"
|
|
6
|
+
require_relative "../method_parameter_binder"
|
|
7
|
+
require_relative "../global_write_census"
|
|
8
|
+
require_relative "unseen_ivar_writers"
|
|
9
|
+
|
|
10
|
+
module Rigor
|
|
11
|
+
module Inference
|
|
12
|
+
module ScopeIndexer
|
|
13
|
+
# Issue #1154 — the class-ivar census types an `@x = x` write whose right-hand side reads a parameter of the
|
|
14
|
+
# enclosing instance `def` as that parameter's DECLARED type: the method's RBS signature, from `sig/` or from
|
|
15
|
+
# an rbs-inline `# @rbs` / `#:` annotation, read through {MethodParameterBinder}, the binder the method body's
|
|
16
|
+
# own entry scope uses. Without it the census typed the rvalue with no local bindings, so a constructor field
|
|
17
|
+
# read `Dynamic[top]` in every sibling method even where `sig/` declared `initialize: (String x) -> void`.
|
|
18
|
+
#
|
|
19
|
+
# The census is flow-free, so a write is credited only where the parameter can hold nothing but its
|
|
20
|
+
# argument. {.collect} answers `{write node => declared type}` (an identity Hash) for the writes that pass
|
|
21
|
+
# every gate, and {EMPTY} otherwise; an absent write keeps the census's ordinary rvalue typing, so a declined
|
|
22
|
+
# case reads exactly what it read before:
|
|
23
|
+
#
|
|
24
|
+
# - the write's value is a bare read of the parameter (`@x = x`), not an expression over it;
|
|
25
|
+
# - the write is not inside a block or a lambda, so a block parameter of the same name and a block that runs
|
|
26
|
+
# under another `self` (`instance_eval`, `Class.new`, `define_method`) never borrow the method's parameter;
|
|
27
|
+
# - the method body never writes the parameter, in any form and at any depth (`x = Integer(x)`, `x ||= …`,
|
|
28
|
+
# a `rescue => x` or pattern target);
|
|
29
|
+
# - the owner is a class, not a module (a module's ivar belongs to whichever object includes it), and its
|
|
30
|
+
# RBS declares the method itself (`defined_in`), so an inherited signature never types a redefinition;
|
|
31
|
+
# - the signature has exactly one overload, whose parameter lists are typed (`(?)` declines) and mention no
|
|
32
|
+
# type variable (a class `Box[T]` or a method `[T]` variable is unsolved at the write);
|
|
33
|
+
# - the class's primary RBS declaration is the project's (`sig/` or rbs-inline), not core, stdlib or a gem's:
|
|
34
|
+
# a project reopening of a library class (`Thread::SizedQueue#initialize`) would otherwise borrow the
|
|
35
|
+
# library's parameter type for its own `def`;
|
|
36
|
+
# - the body neither calls `binding` nor passes code to `eval` / `instance_eval` / `class_eval` /
|
|
37
|
+
# `module_eval` / `instance_exec` as an argument, any of which can rewrite the parameter unseen;
|
|
38
|
+
# - nothing outside the class's own census may write the ivar ({UnseenIvarWriters}): no project ancestor, and
|
|
39
|
+
# no other object's code (`o.instance_variable_set(:@x, v)`);
|
|
40
|
+
# - the declared type is not `untyped`, which is what the write already records.
|
|
41
|
+
module DeclaredParamWrites
|
|
42
|
+
EMPTY = {}.compare_by_identity.freeze
|
|
43
|
+
|
|
44
|
+
SCOPE_BARRIERS = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
45
|
+
private_constant :SCOPE_BARRIERS
|
|
46
|
+
|
|
47
|
+
BLOCK_NODES = [Prism::BlockNode, Prism::LambdaNode].freeze
|
|
48
|
+
private_constant :BLOCK_NODES
|
|
49
|
+
|
|
50
|
+
OPAQUE_EVAL_CALLS = %i[eval instance_eval class_eval module_eval instance_exec class_exec
|
|
51
|
+
module_exec].to_set.freeze
|
|
52
|
+
private_constant :OPAQUE_EVAL_CALLS
|
|
53
|
+
|
|
54
|
+
WRITER_SCOPE_KEY = :__rigor_declared_param_writer_scope__
|
|
55
|
+
private_constant :WRITER_SCOPE_KEY
|
|
56
|
+
|
|
57
|
+
module_function
|
|
58
|
+
|
|
59
|
+
# Runs the block with `scope` as the one whose project tables the unseen-writer and single-file gates read, in
|
|
60
|
+
# place of the census's own scope. `rigor sig-gen` indexes each file under a scope that carries no project
|
|
61
|
+
# tables, so without it another file's writer, ancestor or reopening would go unseen.
|
|
62
|
+
def with_writer_scope(scope)
|
|
63
|
+
return yield if scope.nil?
|
|
64
|
+
|
|
65
|
+
previous = Thread.current[WRITER_SCOPE_KEY]
|
|
66
|
+
Thread.current[WRITER_SCOPE_KEY] = scope
|
|
67
|
+
begin
|
|
68
|
+
yield
|
|
69
|
+
ensure
|
|
70
|
+
Thread.current[WRITER_SCOPE_KEY] = previous
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def writer_scope(scope) = Thread.current[WRITER_SCOPE_KEY] || scope
|
|
75
|
+
|
|
76
|
+
# `{InstanceVariableWriteNode => Rigor::Type}` for `def_node`'s creditable writes, or {EMPTY}. The caller
|
|
77
|
+
# passes only an instance `def`: a singleton body's `@x` is the class object's.
|
|
78
|
+
def collect(def_node, class_name, scope)
|
|
79
|
+
return EMPTY if def_node.body.nil? || def_node.parameters.nil?
|
|
80
|
+
|
|
81
|
+
written = Set.new
|
|
82
|
+
writes = []
|
|
83
|
+
return EMPTY if scan(def_node.body, written, writes, in_block: false) == :opaque
|
|
84
|
+
|
|
85
|
+
writes.reject! { |write| written.include?(write.value.name) }
|
|
86
|
+
return EMPTY if writes.empty?
|
|
87
|
+
|
|
88
|
+
types = declared_types(def_node, class_name, scope)
|
|
89
|
+
writers = writer_scope(scope)
|
|
90
|
+
credited = writes.each_with_object({}.compare_by_identity) do |write, acc|
|
|
91
|
+
type = types[write.value.name]
|
|
92
|
+
acc[write] = type if type && !UnseenIvarWriters.possible?(writers, class_name, write.name)
|
|
93
|
+
end
|
|
94
|
+
credited.empty? ? EMPTY : credited
|
|
95
|
+
rescue StandardError
|
|
96
|
+
EMPTY
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Collects the `@x = local` writes outside every block, and the name of every local the body writes in
|
|
100
|
+
# any form at any depth. A read at depth zero outside a block names the method's own local, which is the
|
|
101
|
+
# parameter when the body never writes the name.
|
|
102
|
+
#
|
|
103
|
+
# Answers `:opaque` when the body can rewrite a local through code the scan cannot read: a `binding` call, or
|
|
104
|
+
# an `eval`-family call given an argument ({OPAQUE_EVAL_CALLS}).
|
|
105
|
+
def scan(node, written, writes, in_block:)
|
|
106
|
+
return unless node.is_a?(Prism::Node)
|
|
107
|
+
return if SCOPE_BARRIERS.any? { |klass| node.is_a?(klass) }
|
|
108
|
+
|
|
109
|
+
case node
|
|
110
|
+
when Prism::LocalVariableWriteNode, Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode,
|
|
111
|
+
Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableTargetNode
|
|
112
|
+
written << node.name
|
|
113
|
+
when Prism::CallNode
|
|
114
|
+
return :opaque if opaque_call?(node)
|
|
115
|
+
end
|
|
116
|
+
writes << node if !in_block && local_read_write?(node)
|
|
117
|
+
inner = in_block || BLOCK_NODES.any? { |klass| node.is_a?(klass) }
|
|
118
|
+
node.rigor_each_child do |child|
|
|
119
|
+
return :opaque if scan(child, written, writes, in_block: inner) == :opaque
|
|
120
|
+
end
|
|
121
|
+
nil
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# `binding` hands the frame's locals to code that may write them (`binding.eval("x = 's'")`,
|
|
125
|
+
# `Helper.rw(binding)`); a string passed to an `eval`-family call is code the scan cannot read.
|
|
126
|
+
def opaque_call?(node)
|
|
127
|
+
return true if node.name == :binding
|
|
128
|
+
|
|
129
|
+
OPAQUE_EVAL_CALLS.include?(node.name) && !(node.arguments&.arguments || []).empty?
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def local_read_write?(node)
|
|
133
|
+
node.is_a?(Prism::InstanceVariableWriteNode) && node.value.is_a?(Prism::LocalVariableReadNode)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# The declared, non-`untyped` type of each parameter, or `{}` when a gate declines the method.
|
|
137
|
+
def declared_types(def_node, class_name, scope)
|
|
138
|
+
environment = scope&.environment
|
|
139
|
+
return {} if environment.nil?
|
|
140
|
+
return {} if environment.rbs_module?(class_name)
|
|
141
|
+
return {} unless Reflection.project_declared_class?(class_name, environment: environment)
|
|
142
|
+
|
|
143
|
+
rbs_method = Reflection.instance_method_definition(class_name, def_node.name, environment: environment)
|
|
144
|
+
return {} unless sole_typed_overload?(rbs_method, class_name)
|
|
145
|
+
|
|
146
|
+
binder = MethodParameterBinder.new(environment: environment, class_path: class_name, singleton: false)
|
|
147
|
+
binder.bind(def_node).reject { |_name, type| type.is_a?(Type::Dynamic) }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def sole_typed_overload?(rbs_method, class_name)
|
|
151
|
+
return false if rbs_method.nil? || rbs_method.defined_in.to_s.delete_prefix("::") != class_name
|
|
152
|
+
|
|
153
|
+
method_types = rbs_method.method_types
|
|
154
|
+
return false unless method_types.size == 1
|
|
155
|
+
|
|
156
|
+
function = method_types.first.type
|
|
157
|
+
function.respond_to?(:required_positionals) && function.free_variables.empty?
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../global_write_census"
|
|
6
|
+
require_relative "../reflective_ivar_write"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Inference
|
|
10
|
+
module ScopeIndexer
|
|
11
|
+
# Issue #1154 — whether code the class-ivar census of `class_name` never walks may write the instance variable
|
|
12
|
+
# `ivar` on its instances, where a literal write in the same place would answer for it too. The census joins
|
|
13
|
+
# only the writes in the class's own bodies in one file, so a declared-parameter write ({DeclaredParamWrites}) is
|
|
14
|
+
# credited only when this answers false. The standard is parity with a literal write: what an unknown object's
|
|
15
|
+
# code may do is accepted, as it is for `@x = 1` (the {IvarWriterCensus} policy), and what the class or its
|
|
16
|
+
# ancestors do is not:
|
|
17
|
+
#
|
|
18
|
+
# - a project ancestor on the class's instance resolution chain (a superclass, or a module it or an ancestor
|
|
19
|
+
# includes or prepends) defines the writer `ivar=` (an `attr_writer` / `attr_accessor` or a `def`), or has an
|
|
20
|
+
# instance `def` whose body writes the ivar, in any form, or calls `instance_variable_set` on `self` (`super`
|
|
21
|
+
# from the class's `initialize`, or an inherited setter, runs it on the instance);
|
|
22
|
+
# - an ancestor the project does not declare is reached through a superclass edge and is not a core class (a
|
|
23
|
+
# gem's or a Ruby-written stdlib class's `initialize` may write any ivar), or the chain was cut;
|
|
24
|
+
# - the class or a project ancestor holds an open-world self writer in an instance body (`[:ivar_set_class, …]`,
|
|
25
|
+
# `def configure(&b) = instance_eval(&b)`);
|
|
26
|
+
# - a file other than this one writes the same ivar in an instance `def` of the class (`[:own_write, …]`): the
|
|
27
|
+
# census is per file, so a reopening's write would not be joined;
|
|
28
|
+
# - the project's {GlobalWriteCensus} records a named write to the ivar from outside the object
|
|
29
|
+
# (`o.instance_variable_set(:@x, v)`, `o.instance_eval { @x = v }`).
|
|
30
|
+
#
|
|
31
|
+
# An ancestor's `define_method` body is not read: the ancestor's own file census sees it, this file does not.
|
|
32
|
+
#
|
|
33
|
+
# Every read files its ADR-46 edge: each project ancestor's declaring files ({Scope::ResolutionChain.record_entry})
|
|
34
|
+
# and each of its `def`s read ({Scope#user_def_for}), and the `ivar_set:`, `ivar_set_class:` and `own_write:`
|
|
35
|
+
# name keys the incremental session fires when a file's entries move.
|
|
36
|
+
module UnseenIvarWriters
|
|
37
|
+
BARRIERS = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
38
|
+
private_constant :BARRIERS
|
|
39
|
+
|
|
40
|
+
module_function
|
|
41
|
+
|
|
42
|
+
def possible?(scope, class_name, ivar)
|
|
43
|
+
census = census(scope)
|
|
44
|
+
foreign_write?(census, ivar) || class_open_world?(census, class_name) ||
|
|
45
|
+
other_file_write?(census, class_name, ivar) || ancestor_write?(scope, census, class_name, ivar)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def census(scope)
|
|
49
|
+
census = scope.discovered_global_write_census
|
|
50
|
+
pre_eval = scope.environment&.project_patched_methods&.write_census
|
|
51
|
+
pre_eval.nil? || pre_eval.empty? ? census : census | pre_eval
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def foreign_write?(census, ivar)
|
|
55
|
+
Analysis::DependencyRecorder.read_name(:ivar_set, ivar.to_s) if Analysis::DependencyRecorder.active?
|
|
56
|
+
census.include?([:ivar_set, ivar])
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def class_open_world?(census, class_name)
|
|
60
|
+
# A guard: the entry comes from an instance body of the class (or an ancestor), whose declaring files the
|
|
61
|
+
# class's own resolution already re-checks; kept so the read states its own dependency.
|
|
62
|
+
Analysis::DependencyRecorder.read_name(:ivar_set_class, class_name) if Analysis::DependencyRecorder.active?
|
|
63
|
+
census.include?([:ivar_set_class, class_name])
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# More than one file records an instance-`def` write of the ivar for the class: this file's census sees its
|
|
67
|
+
# own only. An entry no path tags (a census built outside the project pre-pass) counts as this file.
|
|
68
|
+
def other_file_write?(census, class_name, ivar)
|
|
69
|
+
if Analysis::DependencyRecorder.active?
|
|
70
|
+
Analysis::DependencyRecorder.read_name(:own_write, "#{class_name}##{ivar}")
|
|
71
|
+
end
|
|
72
|
+
paths = census.filter_map do |entry|
|
|
73
|
+
entry[3] if entry.size == 4 && entry[0] == :own_write && entry[1] == class_name && entry[2] == ivar
|
|
74
|
+
end
|
|
75
|
+
paths.uniq.size > 1
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def ancestor_write?(scope, census, class_name, ivar)
|
|
79
|
+
chain = Scope::ResolutionChain.for(scope, class_name, :instance, :constants)
|
|
80
|
+
return true if chain.truncated?
|
|
81
|
+
|
|
82
|
+
setter = :"#{ivar.to_s.delete_prefix('@')}="
|
|
83
|
+
chain.entries.any? do |entry|
|
|
84
|
+
next false if entry.side != :instance || entry.name == class_name
|
|
85
|
+
|
|
86
|
+
# A guard: the class body's own `include` / `<` resolution already files this edge; kept so the read
|
|
87
|
+
# states its own dependency.
|
|
88
|
+
Scope::ResolutionChain.record_entry(scope, entry, :instance) if Analysis::DependencyRecorder.active?
|
|
89
|
+
next foreign_superclass?(scope, entry) if entry.external?
|
|
90
|
+
|
|
91
|
+
class_open_world?(census, entry.name) || scope.discovered_method?(entry.name, setter, :instance) ||
|
|
92
|
+
defs_write?(scope, entry.name, ivar)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# A superclass the project does not declare that the interpreter does not implement: a gem's class, or a
|
|
97
|
+
# stdlib one written in Ruby (`Logger` sets `@level` in `initialize`), may write any ivar of its subclass's
|
|
98
|
+
# instances. A module the project does not declare is left alone.
|
|
99
|
+
def foreign_superclass?(scope, entry)
|
|
100
|
+
return false unless entry.superclass_edge
|
|
101
|
+
|
|
102
|
+
loader = scope.environment&.rbs_loader
|
|
103
|
+
entry.candidates.empty? || entry.candidates.none? { |name| loader&.core_class?(name) }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def defs_write?(scope, owner, ivar)
|
|
107
|
+
(scope.discovered_def_nodes[owner] || {}).each_key.any? do |method_name|
|
|
108
|
+
node = scope.user_def_for(owner, method_name)
|
|
109
|
+
node.is_a?(Prism::DefNode) && writes?(node.body, ivar)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def reflective_name(node) = ReflectiveIvarWrite.literal_name(node)
|
|
114
|
+
|
|
115
|
+
def writes?(node, ivar)
|
|
116
|
+
return false unless node.is_a?(Prism::Node)
|
|
117
|
+
return false if BARRIERS.any? { |klass| node.is_a?(klass) }
|
|
118
|
+
|
|
119
|
+
case node
|
|
120
|
+
when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
|
|
121
|
+
Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableTargetNode
|
|
122
|
+
return true if node.name == ivar
|
|
123
|
+
end
|
|
124
|
+
# A literal `instance_variable_set` of the ivar, or one whose name no literal spells.
|
|
125
|
+
return true if ReflectiveIvarWrite.self_write?(node) && [ivar, nil].include?(reflective_name(node))
|
|
126
|
+
|
|
127
|
+
node.rigor_each_child { |child| return true if writes?(child, ivar) }
|
|
128
|
+
false
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|