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
|
@@ -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
|