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.
Files changed (136) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/docs/handbook/11-sig-gen.md +11 -2
  4. data/docs/manual/02-cli-reference.md +46 -7
  5. data/docs/manual/03-configuration.md +2 -1
  6. data/docs/manual/04-diagnostics.md +1 -1
  7. data/docs/manual/10-mcp-server.md +9 -1
  8. data/docs/manual/12-caching.md +8 -1
  9. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  10. data/docs/manual/plugins/rigor-actionpack.md +30 -0
  11. data/docs/manual/plugins/rigor-devise.md +15 -0
  12. data/docs/manual/plugins/rigor-rspec.md +2 -1
  13. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  14. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
  15. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
  16. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -0
  17. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
  18. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  19. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  20. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  21. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +51 -0
  22. data/lib/rigor/analysis/check_rules.rb +93 -8
  23. data/lib/rigor/analysis/dependency_recorder.rb +11 -0
  24. data/lib/rigor/analysis/edit_replay.rb +67 -0
  25. data/lib/rigor/analysis/incremental.rb +37 -0
  26. data/lib/rigor/analysis/incremental_session.rb +175 -27
  27. data/lib/rigor/analysis/path_expansion.rb +41 -0
  28. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  29. data/lib/rigor/analysis/rule_catalog.rb +5 -1
  30. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  31. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  32. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  33. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
  34. data/lib/rigor/analysis/runner/pool_coordinator.rb +17 -4
  35. data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
  36. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  37. data/lib/rigor/analysis/runner.rb +156 -49
  38. data/lib/rigor/analysis/worker_session.rb +14 -2
  39. data/lib/rigor/cache/incremental_snapshot.rb +30 -7
  40. data/lib/rigor/cli/check_command.rb +93 -4
  41. data/lib/rigor/cli/diff_command.rb +2 -1
  42. data/lib/rigor/cli/lens_command.rb +5 -5
  43. data/lib/rigor/cli/mcp_command.rb +2 -1
  44. data/lib/rigor/cli/options.rb +20 -3
  45. data/lib/rigor/cli/plugins_command.rb +5 -0
  46. data/lib/rigor/cli/plugins_renderer.rb +24 -2
  47. data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
  48. data/lib/rigor/cli/sig_gen_command.rb +46 -7
  49. data/lib/rigor/cli/type_of_command.rb +2 -1
  50. data/lib/rigor/cli/type_of_project_scope.rb +27 -0
  51. data/lib/rigor/config_audit.rb +16 -5
  52. data/lib/rigor/configuration.rb +14 -4
  53. data/lib/rigor/environment/rbs_loader.rb +115 -6
  54. data/lib/rigor/environment/required_features.rb +136 -29
  55. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  56. data/lib/rigor/environment.rb +4 -2
  57. data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
  58. data/lib/rigor/inference/deep_erasure.rb +138 -0
  59. data/lib/rigor/inference/def_return_typer.rb +159 -9
  60. data/lib/rigor/inference/definer_resolution.rb +17 -3
  61. data/lib/rigor/inference/error_info.rb +17 -3
  62. data/lib/rigor/inference/expression_typer.rb +62 -13
  63. data/lib/rigor/inference/global_write_census.rb +36 -1
  64. data/lib/rigor/inference/ivar_writer_census.rb +195 -0
  65. data/lib/rigor/inference/loop_value_record.rb +53 -0
  66. data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
  67. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +110 -9
  68. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
  69. data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
  70. data/lib/rigor/inference/method_dispatcher.rb +46 -16
  71. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  72. data/lib/rigor/inference/module_function_state.rb +12 -0
  73. data/lib/rigor/inference/mutation_widening.rb +5 -2
  74. data/lib/rigor/inference/narrowing.rb +66 -5
  75. data/lib/rigor/inference/object_mixins.rb +13 -1
  76. data/lib/rigor/inference/project_seed_tables.rb +2 -1
  77. data/lib/rigor/inference/project_yielder.rb +128 -0
  78. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
  79. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  80. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
  81. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  82. data/lib/rigor/inference/scope_indexer.rb +480 -103
  83. data/lib/rigor/inference/self_call_ivar_writes.rb +18 -6
  84. data/lib/rigor/inference/statement_evaluator.rb +658 -90
  85. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  86. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  87. data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
  88. data/lib/rigor/language_server/buffer_table.rb +6 -0
  89. data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
  90. data/lib/rigor/language_server/project_context.rb +59 -3
  91. data/lib/rigor/language_server/server.rb +5 -3
  92. data/lib/rigor/mcp/server.rb +9 -1
  93. data/lib/rigor/plugin/base.rb +26 -11
  94. data/lib/rigor/plugin/manifest.rb +29 -3
  95. data/lib/rigor/plugin/registry.rb +38 -0
  96. data/lib/rigor/protection/discovery_seed.rb +57 -2
  97. data/lib/rigor/rooted_glob.rb +10 -1
  98. data/lib/rigor/scope.rb +14 -5
  99. data/lib/rigor/sig_gen/generator.rb +308 -37
  100. data/lib/rigor/sig_gen/observation_collector.rb +4 -1
  101. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  102. data/lib/rigor/sig_gen/renderer.rb +6 -3
  103. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  104. data/lib/rigor/type/combinator.rb +4 -3
  105. data/lib/rigor/type/constant.rb +45 -3
  106. data/lib/rigor/type/hash_shape.rb +14 -9
  107. data/lib/rigor/type/struct_instance.rb +3 -2
  108. data/lib/rigor/version.rb +1 -1
  109. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +60 -43
  110. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
  111. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
  112. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
  113. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  114. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +6 -5
  115. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  116. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
  117. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  118. data/sig/rigor/analysis/baseline.rbs +3 -2
  119. data/sig/rigor/effects/file_collection.rbs +5 -6
  120. data/sig/rigor/effects/registry.rbs +8 -6
  121. data/sig/rigor/effects/summary.rbs +6 -5
  122. data/sig/rigor/environment.rbs +5 -3
  123. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  124. data/sig/rigor/inference.rbs +6 -0
  125. data/sig/rigor/plugin/additional_initializer.rbs +5 -5
  126. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  127. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  128. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  129. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  130. data/sig/rigor/plugin/io_boundary.rbs +4 -4
  131. data/sig/rigor/plugin/manifest.rbs +16 -18
  132. data/sig/rigor/plugin/protocol_contract.rbs +10 -9
  133. data/sig/rigor/plugin/registry.rbs +1 -1
  134. data/sig/rigor/scope.rbs +31 -50
  135. data/sig/rigor/type.rbs +0 -6
  136. 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