rubocop-instance_variable_access 1.0.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 944fafee55d6eae0c082e77a20ee605aee7ba1e35aa67e0b54c948d14c874bb9
4
+ data.tar.gz: dff9f511c5db8e20be45eb2dc6a14db0dd533590cd30f7fd62477e157643c5e9
5
+ SHA512:
6
+ metadata.gz: 85fd2668242673960b7ce52680d0f9ed788e8866dbe528c56b2ba6f89a83f0e6c234ebd7a15a6b45020355a573f7b1d802888677dd504175783867340dd7017e
7
+ data.tar.gz: cc2b85b5b008dfe3cbf8422f91c3725a0eee420790c091f1538943e8ac0ff17ee389f3ad61ff3193e1892cdab4167f059e052b40fc0102e3a0733d66258ff528
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ ## [Unreleased]
2
+
3
+ ## [1.0.0] - 2026-09-21
4
+
5
+ - Add `Style/InstanceVariableAccess` cop that enforces accessing instance
6
+ variables (including class instance variables) through reader methods,
7
+ even from inside the defining class.
@@ -0,0 +1,10 @@
1
+ # Code of Conduct
2
+
3
+ "rubocop-instance_variable_access" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
4
+
5
+ * Participants will be tolerant of opposing views.
6
+ * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
7
+ * When interpreting the words and actions of others, participants should always assume good intentions.
8
+ * Behaviour which can be reasonably considered harassment will not be tolerated.
9
+
10
+ If you have any concerns about behaviour within this project, please contact us at ["i.tkomiya@gmail.com"](mailto:"i.tkomiya@gmail.com").
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Takeshi KOMIYA
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # RuboCop::InstanceVariableAccess
2
+
3
+ A RuboCop extension that provides `Style/InstanceVariableAccess`, a cop that
4
+ enforces accessing instance variables (and class instance variables) through
5
+ reader methods, even from inside the class that defines them.
6
+
7
+ ## Why?
8
+
9
+ Wrapping instance variables in reader methods, even inside the class that
10
+ defines them, follows the
11
+ [Barewords Pattern](https://www.alchemists.io/articles/barewords_pattern):
12
+
13
+ - A typo in a method name raises `NameError` immediately, while a typo in
14
+ an instance variable name (`@nmae`) silently returns `nil`.
15
+ - A reader is easier to extend later (add memoization, a default value, or
16
+ validation) without touching every call site.
17
+ - Ruby's own hash value omission syntax (`{x:}`) ended up supporting
18
+ either a local variable or a method like `attr_reader`.
19
+
20
+ This does mean a reader becomes part of the class's public API unless it is
21
+ `private`; this cop doesn't care about visibility, so keep a reader private
22
+ when the attribute isn't meant to be exposed outside the class.
23
+
24
+ Further reading:
25
+
26
+ - [Barewords Pattern](https://www.alchemists.io/articles/barewords_pattern)
27
+ - [Feature #14579 - Hash value omission](https://bugs.ruby-lang.org/issues/14579)
28
+
29
+ ## Installation
30
+
31
+ Install the gem:
32
+
33
+ ```bash
34
+ bundle add rubocop-instance_variable_access --group development,test --require false
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ Add the following to your `.rubocop.yml`:
40
+
41
+ ```yaml
42
+ plugins:
43
+ - rubocop-instance_variable_access
44
+ ```
45
+
46
+ ## The cop
47
+
48
+ `Style/InstanceVariableAccess` flags:
49
+
50
+ ```ruby
51
+ # bad
52
+ class Person
53
+ def full_name
54
+ "#{@first_name} #{@last_name}"
55
+ end
56
+ end
57
+ ```
58
+
59
+ and suggests:
60
+
61
+ ```ruby
62
+ # good
63
+ class Person
64
+ attr_reader :first_name, :last_name
65
+
66
+ def full_name
67
+ "#{first_name} #{last_name}"
68
+ end
69
+ end
70
+ ```
71
+
72
+ Writing to an instance variable is always allowed, so a memoization idiom
73
+ such as `@memo ||= expensive_call` is unaffected. Reading that same
74
+ instance variable anywhere else while computing the value being assigned
75
+ to it is allowed too, whether the assignment is a plain `=`
76
+ (`@count = @count + 1`, `@count = compute(@count)`) or a compound
77
+ assignment that reads it again explicitly (`@count += compute(@count)`).
78
+
79
+ A method whose name matches the instance variable (`def first_name; ...;
80
+ @first_name; end`) is treated as that variable's own reader, so referencing
81
+ it there is not flagged either, no matter what else the method does first.
82
+ Class instance variables (e.g. `@total` inside `def self.total` or
83
+ `class << self`) are checked the same way as regular instance variables.
84
+
85
+ An instance variable read inside a block passed to `instance_eval`,
86
+ `instance_exec`, `class_eval`, or `module_eval` on anything other than
87
+ `self` isn't flagged, since `self` (and therefore whose instance variable
88
+ is actually being read) changes inside that block:
89
+
90
+ ```ruby
91
+ # good: `@first_name` belongs to `other`, not to `Person`
92
+ class Person
93
+ def borrow_name_from(other)
94
+ other.instance_eval { @first_name }
95
+ end
96
+ end
97
+ ```
98
+
99
+ Autocorrection only rewrites `@foo` to `foo` when an `attr_reader`/
100
+ `attr_accessor` for `foo` already exists in the same class body; a
101
+ hand-written reader doesn't count, since there's no way to tell whether
102
+ it's safe to call in place of `@foo` (it could have side effects, or
103
+ require arguments). Otherwise, only the offense is reported, since
104
+ generating an `attr_reader` automatically could unintentionally widen the
105
+ method's visibility.
106
+
107
+ ## Development
108
+
109
+ After checking out the repo, run `bin/setup` to install dependencies. Then,
110
+ run `rake spec` to run the tests. You can also run `bin/console` for an
111
+ interactive prompt that will allow you to experiment.
112
+
113
+ To install this gem onto your local machine, run `bundle exec rake install`.
114
+ To release a new version, update the version number in `version.rb` and
115
+ push it to `main`; the [release workflow](.github/workflows/release.yml)
116
+ then publishes the gem to [rubygems.org](https://rubygems.org) automatically.
117
+
118
+ ## Contributing
119
+
120
+ Bug reports and pull requests are welcome on GitHub at
121
+ https://github.com/tk0miya/rubocop-instance_variable_access. This project is
122
+ intended to be a safe, welcoming space for collaboration, and contributors
123
+ are expected to adhere to the
124
+ [code of conduct](https://github.com/tk0miya/rubocop-instance_variable_access/blob/main/CODE_OF_CONDUCT.md).
125
+
126
+ ## License
127
+
128
+ The gem is available as open source under the terms of the
129
+ [MIT License](https://opensource.org/licenses/MIT).
130
+
131
+ ## Code of Conduct
132
+
133
+ Everyone interacting in the RuboCop::InstanceVariableAccess project's
134
+ codebases, issue trackers, chat rooms and mailing lists is expected to
135
+ follow the
136
+ [code of conduct](https://github.com/tk0miya/rubocop-instance_variable_access/blob/main/CODE_OF_CONDUCT.md).
@@ -0,0 +1,4 @@
1
+ Style/InstanceVariableAccess:
2
+ Description: 'Checks that instance variables are accessed through reader methods.'
3
+ Enabled: true
4
+ VersionAdded: '1.0.0'
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "style/instance_variable_access"
@@ -0,0 +1,346 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # Checks that instance variables (including class instance variables) are
7
+ # accessed through reader methods rather than referenced directly, even
8
+ # from within the class that defines them.
9
+ #
10
+ # Writing to an instance variable (via `=`, `+=`, `||=`, `&&=`, or
11
+ # multiple assignment) is always allowed, so a memoization idiom such
12
+ # as `@memo ||= expensive_call` is unaffected, since it writes to the
13
+ # variable rather than reading it. Reading that same instance variable
14
+ # anywhere else while computing the value being assigned to it is
15
+ # allowed too, whether the assignment is a plain `=`
16
+ # (`@count = @count + 1`, `@count = compute(@count)`) or a compound
17
+ # assignment that reads it again explicitly
18
+ # (`@count += compute(@count)`).
19
+ #
20
+ # A method whose name matches the instance variable (`def name; ...;
21
+ # @name; end`, or its class-method equivalent `def self.name; ...;
22
+ # @name; end`) is treated as that variable's own reader, so referencing
23
+ # it there is not itself an offense, no matter what else the method's
24
+ # body does.
25
+ #
26
+ # An instance variable read inside a block passed to `instance_eval`,
27
+ # `instance_exec`, `class_eval`, or `module_eval` on anything other
28
+ # than `self` is not flagged, since `self` (and therefore whose
29
+ # instance variable is actually being read) changes inside that
30
+ # block.
31
+ #
32
+ # @safety
33
+ # Autocorrection only rewrites `@foo` to `foo` when an `attr_reader`/
34
+ # `attr_accessor` for the same instance/singleton context already
35
+ # exists in the same class body. A hand-written reader like the one
36
+ # above doesn't count, because there is no way to tell whether it is
37
+ # safe to call in place of `@foo` (it could have side effects, or
38
+ # require arguments). Otherwise, only the offense is reported; this
39
+ # cop never generates an `attr_reader`.
40
+ #
41
+ # @example
42
+ # # bad
43
+ # class Person
44
+ # def full_name
45
+ # "#{@first_name} #{@last_name}"
46
+ # end
47
+ # end
48
+ #
49
+ # # good
50
+ # class Person
51
+ # attr_reader :first_name, :last_name
52
+ #
53
+ # def full_name
54
+ # "#{first_name} #{last_name}"
55
+ # end
56
+ # end
57
+ #
58
+ # # good (the reader definition itself is not an offense)
59
+ # class Person
60
+ # private
61
+ #
62
+ # def first_name
63
+ # @first_name
64
+ # end
65
+ # end
66
+ #
67
+ # # good (still just the reader, whatever else it does first)
68
+ # class Person
69
+ # def first_name
70
+ # logger.debug("first_name accessed")
71
+ # @first_name
72
+ # end
73
+ # end
74
+ #
75
+ # # good (memoization writes to the instance variable rather than reading it)
76
+ # class Person
77
+ # def first_name
78
+ # @first_name ||= compute_first_name
79
+ # end
80
+ # end
81
+ #
82
+ # # good (reading `@count` anywhere while rewriting it is fine, `=` or `+=`)
83
+ # class Counter
84
+ # def increment
85
+ # @count = compute(@count)
86
+ # @count += compute(@count)
87
+ # end
88
+ # end
89
+ #
90
+ # # good (`self` changes inside the block, so this isn't Person's own ivar)
91
+ # class Person
92
+ # def borrow_name_from(other)
93
+ # other.instance_eval { @first_name }
94
+ # end
95
+ # end
96
+ class InstanceVariableAccess < Base
97
+ extend AutoCorrector
98
+
99
+ MSG = "Use a reader method instead of directly accessing `%<ivar>s`."
100
+
101
+ RESTRICT_ON_SEND = %i[attr_reader attr_accessor].freeze
102
+
103
+ # Methods that run their block with `self` rebound to something other
104
+ # than the block's lexical `self`, so an instance variable read inside
105
+ # such a block cannot be attributed to the enclosing class/module.
106
+ SELF_CHANGING_BLOCK_METHODS = %i[instance_eval instance_exec class_eval module_eval].freeze
107
+
108
+ # @rbs!
109
+ # def attr_reader_or_accessor?: (RuboCop::AST::Node node) -> bool
110
+
111
+ def_node_matcher :attr_reader_or_accessor?, <<~PATTERN
112
+ (send nil? {:attr_reader :attr_accessor} ...)
113
+ PATTERN
114
+
115
+ # A candidate offense: an instance variable read that isn't inside the
116
+ # body of a method of the same name. Resolved against the enclosing
117
+ # scope's readers once the whole class/module body has been seen.
118
+ Violation = Data.define(
119
+ :node, #: RuboCop::AST::Node -- the instance variable node that may be an offense
120
+ :singleton #: bool -- whether it's in a singleton (class-level) context
121
+ )
122
+
123
+ # The readers and pending violations for a single class/module body
124
+ # (or the top level, for the root scope).
125
+ Scope = Struct.new(
126
+ :instance_readers, #: Hash[Symbol, Symbol] -- known instance-level readers, by variable name
127
+ :class_readers, #: Hash[Symbol, Symbol] -- known class-level (singleton) readers, by variable name
128
+ :violations, #: Array[Violation] -- offense candidates collected so far in this scope
129
+ :singleton_depth, #: Integer -- nesting depth inside `class << self` blocks
130
+ keyword_init: true
131
+ )
132
+
133
+ def on_new_investigation #: void
134
+ @scope_stack = []
135
+ push_scope
136
+ end
137
+
138
+ def on_investigation_end #: void
139
+ report_violations
140
+ pop_scope
141
+ end
142
+
143
+ def on_class(_node) #: void
144
+ push_scope
145
+ end
146
+ alias on_module on_class
147
+
148
+ # @rbs _node: RuboCop::AST::Node
149
+ def after_class(_node) #: void
150
+ report_violations
151
+ pop_scope
152
+ end
153
+ alias after_module after_class
154
+
155
+ # @rbs _node: RuboCop::AST::Node
156
+ def on_sclass(_node) #: void
157
+ current_scope.singleton_depth += 1
158
+ end
159
+
160
+ # @rbs _node: RuboCop::AST::Node
161
+ def after_sclass(_node) #: void
162
+ current_scope.singleton_depth -= 1
163
+ end
164
+
165
+ # @rbs node: RuboCop::AST::SendNode
166
+ def on_send(node) #: void
167
+ return if top_level?
168
+ return unless attr_reader_or_accessor?(node)
169
+
170
+ registry = singleton_context? ? current_scope.class_readers : current_scope.instance_readers
171
+ node.arguments.each do |arg|
172
+ case arg
173
+ when RuboCop::AST::SymbolNode, RuboCop::AST::StrNode
174
+ name = arg.value.to_sym # steep:ignore
175
+ registry[name] = name
176
+ end
177
+ end
178
+ end
179
+
180
+ # @rbs node: RuboCop::AST::Node
181
+ def on_ivar(node) #: void
182
+ return if same_named_method_body?(node)
183
+ return if in_self_changing_block?(node)
184
+ return if self_referential_write?(node)
185
+
186
+ current_scope.violations << Violation.new(node:, singleton: civar?(node))
187
+ end
188
+
189
+ private
190
+
191
+ attr_reader :scope_stack #: Array[Scope] -- the scopes of the classes/modules currently being visited
192
+
193
+ def push_scope #: void
194
+ scope_stack.push(
195
+ Scope.new(instance_readers: {}, class_readers: {}, violations: [], singleton_depth: 0)
196
+ )
197
+ end
198
+
199
+ def pop_scope #: void
200
+ scope_stack.pop
201
+ end
202
+
203
+ def current_scope #: Scope
204
+ scope_stack.last or raise
205
+ end
206
+
207
+ def singleton_context? #: bool
208
+ current_scope.singleton_depth.positive?
209
+ end
210
+
211
+ # Whether we are outside of any class/module, at the top level of the
212
+ # file. Readers are never registered there, so that a reader defined
213
+ # at the top level cannot unexpectedly match an unrelated top-level
214
+ # instance variable.
215
+ def top_level? #: bool
216
+ scope_stack.size == 1
217
+ end
218
+
219
+ def report_violations #: void
220
+ current_scope.violations.each { report_violation(_1) }
221
+ end
222
+
223
+ # @rbs violation: Violation
224
+ def report_violation(violation) #: void
225
+ node = violation.node
226
+ registry = violation.singleton ? current_scope.class_readers : current_scope.instance_readers
227
+ reader = registry[bare_ivar_name(node)]
228
+
229
+ add_offense(node, message: format(MSG, ivar: node.source)) do |corrector|
230
+ corrector.replace(node, reader.to_s) if reader
231
+ end
232
+ end
233
+
234
+ # Whether the instance variable sits in a singleton (class-level)
235
+ # context: inside `def self.x`, or inside `def x` that is itself
236
+ # nested in `class << self`.
237
+ #
238
+ # @rbs node: RuboCop::AST::Node
239
+ def civar?(node) #: bool
240
+ method_node = enclosing_def(node)
241
+ return true unless method_node
242
+ return true if method_node.defs_type?
243
+
244
+ boundary = method_node.each_ancestor(:sclass, :class, :module).first
245
+ boundary&.sclass_type? || false
246
+ end
247
+
248
+ # Whether `node` is an instance variable read inside the body of a
249
+ # method of the same name, regardless of what else that method's
250
+ # body does.
251
+ #
252
+ # @rbs node: RuboCop::AST::Node
253
+ def same_named_method_body?(node) #: bool
254
+ method_node = enclosing_def(node)
255
+ return false unless method_node
256
+
257
+ bare_ivar_name(node) == bare_method_name(method_node)
258
+ end
259
+
260
+ # Whether `node` sits inside a block that rebinds `self` away from the
261
+ # enclosing class/module (`klass.instance_eval { @foo }`), making it
262
+ # impossible to tell whose ivar is actually being read. The search
263
+ # stops at the nearest enclosing `def`/`defs`, since a method
264
+ # definition always establishes its own `self` regardless of any
265
+ # block it happens to be lexically nested in.
266
+ #
267
+ # @rbs node: RuboCop::AST::Node
268
+ def in_self_changing_block?(node) #: bool
269
+ node.each_ancestor(:def, :defs, :block, :numblock).each do |ancestor|
270
+ case ancestor.type
271
+ when :def, :defs
272
+ return false
273
+ else
274
+ return true if self_changing_block?(ancestor)
275
+ end
276
+ end
277
+
278
+ false
279
+ end
280
+
281
+ # @rbs node: RuboCop::AST::Node
282
+ def self_changing_block?(node) #: bool
283
+ block = node #: RuboCop::AST::BlockNode
284
+ send_node = block.send_node
285
+ receiver = send_node.receiver
286
+
287
+ return false unless receiver
288
+ return false if receiver.self_type?
289
+
290
+ SELF_CHANGING_BLOCK_METHODS.include?(send_node.method_name)
291
+ end
292
+
293
+ # Whether `node` is an instance variable read that appears anywhere
294
+ # in the value being assigned to that very same instance variable —
295
+ # whether by a plain `=` (`@x = @x + 1`, `@x = process(@x)`) or a
296
+ # compound assignment (`@x += @x + 1`, `@x += process(@x)`,
297
+ # `@x ||= ...`, `@x &&= ...`). Rewriting an instance variable is
298
+ # always allowed to read that same instance variable freely while
299
+ # computing its new value, wherever in the expression it's used. The
300
+ # search stops at the nearest enclosing `def`/`defs`, so a variable
301
+ # of the same name assigned in an unrelated enclosing method doesn't
302
+ # accidentally match.
303
+ #
304
+ # @rbs node: RuboCop::AST::Node
305
+ def self_referential_write?(node) #: bool
306
+ ancestor = node.each_ancestor(:ivasgn, :op_asgn, :or_asgn, :and_asgn, :def, :defs).first
307
+ return false unless ancestor
308
+
309
+ target = assignment_target(ancestor)
310
+ return false unless target
311
+
312
+ target.children.first == node.children.first
313
+ end
314
+
315
+ # The instance variable being assigned by `node`, if `node` is a
316
+ # plain assignment to one (`ivasgn`) or a compound assignment whose
317
+ # target is one; `nil` otherwise, including when `node` is a
318
+ # `def`/`defs` boundary the search above stops at.
319
+ #
320
+ # @rbs node: RuboCop::AST::Node
321
+ def assignment_target(node) #: RuboCop::AST::Node?
322
+ return node if node.ivasgn_type?
323
+ return nil unless node.op_asgn_type? || node.or_asgn_type? || node.and_asgn_type?
324
+
325
+ lhs = node.children.first
326
+ lhs if lhs.ivasgn_type?
327
+ end
328
+
329
+ # @rbs node: RuboCop::AST::Node
330
+ def enclosing_def(node) #: RuboCop::AST::DefNode?
331
+ node.each_ancestor(:any_def).first #: RuboCop::AST::DefNode?
332
+ end
333
+
334
+ # @rbs node: RuboCop::AST::DefNode
335
+ def bare_method_name(node) #: Symbol
336
+ node.method_name.to_s.sub(/[=?]$/, "").to_sym
337
+ end
338
+
339
+ # @rbs node: RuboCop::AST::Node
340
+ def bare_ivar_name(node) #: Symbol
341
+ node.children.first.to_s.delete_prefix("@").to_sym
342
+ end
343
+ end
344
+ end
345
+ end
346
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lint_roller"
4
+ require "pathname"
5
+
6
+ module RuboCop
7
+ module InstanceVariableAccess
8
+ # A plugin that integrates rubocop-instance_variable_access with RuboCop's plugin system.
9
+ class Plugin < LintRoller::Plugin
10
+ def about #: LintRoller::About
11
+ LintRoller::About.new(
12
+ name: "rubocop-instance_variable_access",
13
+ version: VERSION,
14
+ homepage: "https://github.com/tk0miya/rubocop-instance_variable_access",
15
+ description: "A RuboCop extension that enforces accessing instance variables through reader methods."
16
+ )
17
+ end
18
+
19
+ # @rbs context: untyped
20
+ def supported?(context) #: bool
21
+ context.engine == :rubocop
22
+ end
23
+
24
+ # @rbs _context: untyped
25
+ def rules(_context) #: LintRoller::Rules
26
+ project_root = Pathname.new(__dir__.to_s).join("../../..")
27
+
28
+ LintRoller::Rules.new(
29
+ type: :path,
30
+ config_format: :rubocop,
31
+ value: project_root.join("config", "default.yml")
32
+ )
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module InstanceVariableAccess
5
+ VERSION = "1.0.0"
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "instance_variable_access/version"
4
+
5
+ module RuboCop
6
+ module InstanceVariableAccess
7
+ class Error < StandardError; end
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+
5
+ require_relative "rubocop/instance_variable_access"
6
+ require_relative "rubocop/instance_variable_access/plugin"
7
+
8
+ require_relative "rubocop/cop/instance_variable_access_cops"
@@ -0,0 +1,2 @@
1
+ # Generated from lib/rubocop/cop/instance_variable_access_cops.rb with RBS::Inline
2
+
@@ -0,0 +1,249 @@
1
+ # Generated from lib/rubocop/cop/style/instance_variable_access.rb with RBS::Inline
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # Checks that instance variables (including class instance variables) are
7
+ # accessed through reader methods rather than referenced directly, even
8
+ # from within the class that defines them.
9
+ #
10
+ # Writing to an instance variable (via `=`, `+=`, `||=`, `&&=`, or
11
+ # multiple assignment) is always allowed, so a memoization idiom such
12
+ # as `@memo ||= expensive_call` is unaffected, since it writes to the
13
+ # variable rather than reading it. Reading that same instance variable
14
+ # anywhere else while computing the value being assigned to it is
15
+ # allowed too, whether the assignment is a plain `=`
16
+ # (`@count = @count + 1`, `@count = compute(@count)`) or a compound
17
+ # assignment that reads it again explicitly
18
+ # (`@count += compute(@count)`).
19
+ #
20
+ # A method whose name matches the instance variable (`def name; ...;
21
+ # @name; end`, or its class-method equivalent `def self.name; ...;
22
+ # @name; end`) is treated as that variable's own reader, so referencing
23
+ # it there is not itself an offense, no matter what else the method's
24
+ # body does.
25
+ #
26
+ # An instance variable read inside a block passed to `instance_eval`,
27
+ # `instance_exec`, `class_eval`, or `module_eval` on anything other
28
+ # than `self` is not flagged, since `self` (and therefore whose
29
+ # instance variable is actually being read) changes inside that
30
+ # block.
31
+ #
32
+ # @safety
33
+ # Autocorrection only rewrites `@foo` to `foo` when an `attr_reader`/
34
+ # `attr_accessor` for the same instance/singleton context already
35
+ # exists in the same class body. A hand-written reader like the one
36
+ # above doesn't count, because there is no way to tell whether it is
37
+ # safe to call in place of `@foo` (it could have side effects, or
38
+ # require arguments). Otherwise, only the offense is reported; this
39
+ # cop never generates an `attr_reader`.
40
+ #
41
+ # @example
42
+ # # bad
43
+ # class Person
44
+ # def full_name
45
+ # "#{@first_name} #{@last_name}"
46
+ # end
47
+ # end
48
+ #
49
+ # # good
50
+ # class Person
51
+ # attr_reader :first_name, :last_name
52
+ #
53
+ # def full_name
54
+ # "#{first_name} #{last_name}"
55
+ # end
56
+ # end
57
+ #
58
+ # # good (the reader definition itself is not an offense)
59
+ # class Person
60
+ # private
61
+ #
62
+ # def first_name
63
+ # @first_name
64
+ # end
65
+ # end
66
+ #
67
+ # # good (still just the reader, whatever else it does first)
68
+ # class Person
69
+ # def first_name
70
+ # logger.debug("first_name accessed")
71
+ # @first_name
72
+ # end
73
+ # end
74
+ #
75
+ # # good (memoization writes to the instance variable rather than reading it)
76
+ # class Person
77
+ # def first_name
78
+ # @first_name ||= compute_first_name
79
+ # end
80
+ # end
81
+ #
82
+ # # good (reading `@count` anywhere while rewriting it is fine, `=` or `+=`)
83
+ # class Counter
84
+ # def increment
85
+ # @count = compute(@count)
86
+ # @count += compute(@count)
87
+ # end
88
+ # end
89
+ #
90
+ # # good (`self` changes inside the block, so this isn't Person's own ivar)
91
+ # class Person
92
+ # def borrow_name_from(other)
93
+ # other.instance_eval { @first_name }
94
+ # end
95
+ # end
96
+ class InstanceVariableAccess < Base
97
+ extend AutoCorrector
98
+
99
+ MSG: ::String
100
+
101
+ RESTRICT_ON_SEND: untyped
102
+
103
+ # Methods that run their block with `self` rebound to something other
104
+ # than the block's lexical `self`, so an instance variable read inside
105
+ # such a block cannot be attributed to the enclosing class/module.
106
+ SELF_CHANGING_BLOCK_METHODS: untyped
107
+
108
+ def attr_reader_or_accessor?: (RuboCop::AST::Node node) -> bool
109
+
110
+ # A candidate offense: an instance variable read that isn't inside the
111
+ # body of a method of the same name. Resolved against the enclosing
112
+ # scope's readers once the whole class/module body has been seen.
113
+ class Violation < Data
114
+ attr_reader node(): RuboCop::AST::Node
115
+
116
+ attr_reader singleton(): bool
117
+
118
+ def self.new: (RuboCop::AST::Node node, bool singleton) -> instance
119
+ | (node: RuboCop::AST::Node, singleton: bool) -> instance
120
+
121
+ def self.members: () -> [ :node, :singleton ]
122
+
123
+ def members: () -> [ :node, :singleton ]
124
+ end
125
+
126
+ # The readers and pending violations for a single class/module body
127
+ # (or the top level, for the root scope).
128
+ class Scope < Struct[Hash[Symbol, Symbol] | Array[Violation] | Integer]
129
+ attr_accessor instance_readers(): Hash[Symbol, Symbol]
130
+
131
+ attr_accessor class_readers(): Hash[Symbol, Symbol]
132
+
133
+ attr_accessor violations(): Array[Violation]
134
+
135
+ attr_accessor singleton_depth(): Integer
136
+
137
+ def self.new: (?instance_readers: Hash[Symbol, Symbol], ?class_readers: Hash[Symbol, Symbol], ?violations: Array[Violation], ?singleton_depth: Integer) -> instance
138
+ | ({ ?instance_readers: Hash[Symbol, Symbol], ?class_readers: Hash[Symbol, Symbol], ?violations: Array[Violation], ?singleton_depth: Integer }) -> instance
139
+ end
140
+
141
+ def on_new_investigation: () -> void
142
+
143
+ def on_investigation_end: () -> void
144
+
145
+ def on_class: (untyped _node) -> void
146
+
147
+ alias on_module on_class
148
+
149
+ # @rbs _node: RuboCop::AST::Node
150
+ def after_class: (untyped _node) -> void
151
+
152
+ alias after_module after_class
153
+
154
+ # @rbs _node: RuboCop::AST::Node
155
+ def on_sclass: (untyped _node) -> void
156
+
157
+ # @rbs _node: RuboCop::AST::Node
158
+ def after_sclass: (untyped _node) -> void
159
+
160
+ # @rbs node: RuboCop::AST::SendNode
161
+ def on_send: (RuboCop::AST::SendNode node) -> void
162
+
163
+ # @rbs node: RuboCop::AST::Node
164
+ def on_ivar: (RuboCop::AST::Node node) -> void
165
+
166
+ private
167
+
168
+ attr_reader scope_stack: Array[Scope]
169
+
170
+ def push_scope: () -> void
171
+
172
+ def pop_scope: () -> void
173
+
174
+ def current_scope: () -> Scope
175
+
176
+ def singleton_context?: () -> bool
177
+
178
+ # Whether we are outside of any class/module, at the top level of the
179
+ # file. Readers are never registered there, so that a reader defined
180
+ # at the top level cannot unexpectedly match an unrelated top-level
181
+ # instance variable.
182
+ def top_level?: () -> bool
183
+
184
+ def report_violations: () -> void
185
+
186
+ # @rbs violation: Violation
187
+ def report_violation: (Violation violation) -> void
188
+
189
+ # Whether the instance variable sits in a singleton (class-level)
190
+ # context: inside `def self.x`, or inside `def x` that is itself
191
+ # nested in `class << self`.
192
+ #
193
+ # @rbs node: RuboCop::AST::Node
194
+ def civar?: (RuboCop::AST::Node node) -> bool
195
+
196
+ # Whether `node` is an instance variable read inside the body of a
197
+ # method of the same name, regardless of what else that method's
198
+ # body does.
199
+ #
200
+ # @rbs node: RuboCop::AST::Node
201
+ def same_named_method_body?: (RuboCop::AST::Node node) -> bool
202
+
203
+ # Whether `node` sits inside a block that rebinds `self` away from the
204
+ # enclosing class/module (`klass.instance_eval { @foo }`), making it
205
+ # impossible to tell whose ivar is actually being read. The search
206
+ # stops at the nearest enclosing `def`/`defs`, since a method
207
+ # definition always establishes its own `self` regardless of any
208
+ # block it happens to be lexically nested in.
209
+ #
210
+ # @rbs node: RuboCop::AST::Node
211
+ def in_self_changing_block?: (RuboCop::AST::Node node) -> bool
212
+
213
+ # @rbs node: RuboCop::AST::Node
214
+ def self_changing_block?: (RuboCop::AST::Node node) -> bool
215
+
216
+ # Whether `node` is an instance variable read that appears anywhere
217
+ # in the value being assigned to that very same instance variable —
218
+ # whether by a plain `=` (`@x = @x + 1`, `@x = process(@x)`) or a
219
+ # compound assignment (`@x += @x + 1`, `@x += process(@x)`,
220
+ # `@x ||= ...`, `@x &&= ...`). Rewriting an instance variable is
221
+ # always allowed to read that same instance variable freely while
222
+ # computing its new value, wherever in the expression it's used. The
223
+ # search stops at the nearest enclosing `def`/`defs`, so a variable
224
+ # of the same name assigned in an unrelated enclosing method doesn't
225
+ # accidentally match.
226
+ #
227
+ # @rbs node: RuboCop::AST::Node
228
+ def self_referential_write?: (RuboCop::AST::Node node) -> bool
229
+
230
+ # The instance variable being assigned by `node`, if `node` is a
231
+ # plain assignment to one (`ivasgn`) or a compound assignment whose
232
+ # target is one; `nil` otherwise, including when `node` is a
233
+ # `def`/`defs` boundary the search above stops at.
234
+ #
235
+ # @rbs node: RuboCop::AST::Node
236
+ def assignment_target: (RuboCop::AST::Node node) -> RuboCop::AST::Node?
237
+
238
+ # @rbs node: RuboCop::AST::Node
239
+ def enclosing_def: (RuboCop::AST::Node node) -> RuboCop::AST::DefNode?
240
+
241
+ # @rbs node: RuboCop::AST::DefNode
242
+ def bare_method_name: (RuboCop::AST::DefNode node) -> Symbol
243
+
244
+ # @rbs node: RuboCop::AST::Node
245
+ def bare_ivar_name: (RuboCop::AST::Node node) -> Symbol
246
+ end
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,16 @@
1
+ # Generated from lib/rubocop/instance_variable_access/plugin.rb with RBS::Inline
2
+
3
+ module RuboCop
4
+ module InstanceVariableAccess
5
+ # A plugin that integrates rubocop-instance_variable_access with RuboCop's plugin system.
6
+ class Plugin < LintRoller::Plugin
7
+ def about: () -> LintRoller::About
8
+
9
+ # @rbs context: untyped
10
+ def supported?: (untyped context) -> bool
11
+
12
+ # @rbs _context: untyped
13
+ def rules: (untyped _context) -> LintRoller::Rules
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,7 @@
1
+ # Generated from lib/rubocop/instance_variable_access/version.rb with RBS::Inline
2
+
3
+ module RuboCop
4
+ module InstanceVariableAccess
5
+ VERSION: ::String
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ # Generated from lib/rubocop/instance_variable_access.rb with RBS::Inline
2
+
3
+ module RuboCop
4
+ module InstanceVariableAccess
5
+ class Error < StandardError
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,2 @@
1
+ # Generated from lib/rubocop-instance_variable_access.rb with RBS::Inline
2
+
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rubocop-instance_variable_access
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Takeshi KOMIYA
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: lint_roller
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rubocop
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '1.72'
33
+ - - "<"
34
+ - !ruby/object:Gem::Version
35
+ version: '2.0'
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '1.72'
43
+ - - "<"
44
+ - !ruby/object:Gem::Version
45
+ version: '2.0'
46
+ description: rubocop-instance_variable_access provides the Style/InstanceVariableAccess
47
+ cop, which flags direct reads of instance variables (including class instance variables)
48
+ outside of their reader method, encouraging access via attr_reader/attr_accessor
49
+ or a hand-written reader even from within the defining class.
50
+ email:
51
+ - i.tkomiya@gmail.com
52
+ executables: []
53
+ extensions: []
54
+ extra_rdoc_files: []
55
+ files:
56
+ - CHANGELOG.md
57
+ - CODE_OF_CONDUCT.md
58
+ - LICENSE.txt
59
+ - README.md
60
+ - config/default.yml
61
+ - lib/rubocop-instance_variable_access.rb
62
+ - lib/rubocop/cop/instance_variable_access_cops.rb
63
+ - lib/rubocop/cop/style/instance_variable_access.rb
64
+ - lib/rubocop/instance_variable_access.rb
65
+ - lib/rubocop/instance_variable_access/plugin.rb
66
+ - lib/rubocop/instance_variable_access/version.rb
67
+ - sig/rubocop-instance_variable_access.rbs
68
+ - sig/rubocop/cop/instance_variable_access_cops.rbs
69
+ - sig/rubocop/cop/style/instance_variable_access.rbs
70
+ - sig/rubocop/instance_variable_access.rbs
71
+ - sig/rubocop/instance_variable_access/plugin.rbs
72
+ - sig/rubocop/instance_variable_access/version.rbs
73
+ homepage: https://github.com/tk0miya/rubocop-instance_variable_access
74
+ licenses:
75
+ - MIT
76
+ metadata:
77
+ allowed_push_host: https://rubygems.org
78
+ homepage_uri: https://github.com/tk0miya/rubocop-instance_variable_access
79
+ source_code_uri: https://github.com/tk0miya/rubocop-instance_variable_access.git
80
+ changelog_uri: https://github.com/tk0miya/rubocop-instance_variable_access/blob/main/CHANGELOG.md
81
+ rubygems_mfa_required: 'true'
82
+ default_lint_roller_plugin: RuboCop::InstanceVariableAccess::Plugin
83
+ rdoc_options: []
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: 3.3.0
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubygems_version: 4.0.16
98
+ specification_version: 4
99
+ summary: A RuboCop extension that enforces accessing instance variables through reader
100
+ methods.
101
+ test_files: []