rubocop-metz 0.5.3

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: 38eb7e859b0bf45c9f80e8838678713d2aaedb5f18409eb802f53c0489fec544
4
+ data.tar.gz: 2972b51623d4c6027735b8a3cd9a1b85bb575a1778dd444f661e7f1bc7c37bf1
5
+ SHA512:
6
+ metadata.gz: 68746f49ee4469e0f23e22eb8020ac3a6cdfbc5e117d6f524fcb6f1c88c95e6f6362bed07d6bf2e5f8896c4e3ea6f6b0ad8f02461366bd045b83348aadb54140
7
+ data.tar.gz: 7a242eacde2feca2b19fb116391f42923b59e64b0273432cfc7022eb33e94c5164cb8e1a3e0a122faca561c086d94aabc10cd3f2e12bd7848fa9e47252d2e302
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Salvador Fuentes Jr
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 all
13
+ 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 THE
21
+ SOFTWARE.
@@ -0,0 +1,76 @@
1
+ # Default configuration for the rubocop-metz plugin.
2
+
3
+ Metrics/ClassLength:
4
+ Enabled: false
5
+
6
+ Metrics/MethodLength:
7
+ Enabled: false
8
+
9
+ Metrics/ParameterLists:
10
+ Enabled: false
11
+
12
+ Metz/ClassesTooLong:
13
+ Description: 'Flags classes longer than the configured Max line count; the Metz default is stricter than core RuboCop.'
14
+ Enabled: true
15
+ Severity: refactor
16
+ Max: 100
17
+ CountComments: false
18
+ CountAsOne: []
19
+
20
+ Metz/MethodsTooLong:
21
+ Description: 'Flags methods longer than the configured Max line count; the Metz default is stricter than core RuboCop.'
22
+ Enabled: true
23
+ Severity: refactor
24
+ Max: 5
25
+ CountComments: false
26
+ CountAsOne: []
27
+ AllowedMethods: []
28
+ AllowedPatterns: []
29
+
30
+ Metz/MethodsTooManyParameters:
31
+ Description: 'Flags methods whose parameter list exceeds Max; the Metz default is stricter than core RuboCop.'
32
+ Enabled: true
33
+ Severity: refactor
34
+ Max: 4
35
+
36
+ Metz/DemeterTrainWreck:
37
+ Description: 'Detects Law-of-Demeter-violating object-graph traversal chains.'
38
+ Enabled: true
39
+ Severity: refactor
40
+ Max: 4
41
+ AllowedReceivers:
42
+ - Rails
43
+ - Arel
44
+ - Time
45
+ - Date
46
+ - DateTime
47
+ AllowedValueObjects:
48
+ - String
49
+ - Integer
50
+ - Float
51
+ - Symbol
52
+ - Array
53
+ - Hash
54
+ - Set
55
+ Exclude:
56
+ - 'spec/**/*'
57
+ - 'db/migrate/**/*'
58
+ VersionAdded: '0.1'
59
+
60
+ Metz/ControllersTooManyDirectCollaborators:
61
+ Description: 'Flags Rails controller actions that reach into more than MaxCollaborators distinct collaborator constants.'
62
+ Enabled: true
63
+ Severity: refactor
64
+ MaxCollaborators: 1
65
+ Include:
66
+ - 'app/controllers/**/*.rb'
67
+ VersionAdded: '0.1'
68
+
69
+ Metz/ViewsDeepNavigation:
70
+ Description: 'Flags Rails view templates whose ERB/HAML/SLIM expressions traverse the object graph deeper than MaxChainLength.'
71
+ Enabled: true
72
+ Severity: refactor
73
+ MaxChainLength: 3
74
+ Include:
75
+ - 'app/views/**/*.{erb,haml,slim}'
76
+ VersionAdded: '0.1'
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metz
4
+ # Metadata DSL for Metz cops. Provides class-level setter/reader macros for
5
+ # `why_it_matters`, `fix_safety`, and `suggested_next_moves`. Works whether
6
+ # `include`d (instance-style sub-modules) or `extend`ed onto the class.
7
+ #
8
+ # Public contract for a cop class that never invokes any setter:
9
+ # - why_it_matters -> "" (empty string)
10
+ # - fix_safety -> :manual (the safest, no-autocorrect tier)
11
+ # - suggested_next_moves -> [] (empty, frozen array)
12
+ module CopMetadata
13
+ UNSET = Object.new.freeze
14
+ private_constant :UNSET
15
+
16
+ DEFAULT_WHY_IT_MATTERS = ""
17
+ DEFAULT_FIX_SAFETY = :manual
18
+ DEFAULT_SUGGESTED_NEXT_MOVES = [].freeze
19
+
20
+ def self.included(base)
21
+ base.extend(self)
22
+ end
23
+
24
+ def why_it_matters(value = UNSET)
25
+ return @metz_why_it_matters = value unless UNSET.equal?(value)
26
+
27
+ defined?(@metz_why_it_matters) ? @metz_why_it_matters : DEFAULT_WHY_IT_MATTERS
28
+ end
29
+
30
+ def fix_safety(value = UNSET)
31
+ return @metz_fix_safety = value unless UNSET.equal?(value)
32
+
33
+ defined?(@metz_fix_safety) ? @metz_fix_safety : DEFAULT_FIX_SAFETY
34
+ end
35
+
36
+ def suggested_next_moves(value = UNSET)
37
+ return @metz_suggested_next_moves = value unless UNSET.equal?(value)
38
+
39
+ defined?(@metz_suggested_next_moves) ? @metz_suggested_next_moves : DEFAULT_SUGGESTED_NEXT_MOVES
40
+ end
41
+
42
+ def metz_metadata
43
+ {
44
+ why_it_matters: why_it_matters,
45
+ fix_safety: fix_safety,
46
+ suggested_next_moves: suggested_next_moves
47
+ }
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "file_classifier"
4
+
5
+ module Metz
6
+ # Ruby extractor for ERB / HAML / SLIM view templates. Registered with
7
+ # `RuboCop::Runner.ruby_extractors` so that view-aware Metz cops (currently
8
+ # `Metz/ViewsDeepNavigation`) can run against ERB content. RuboCop's parser
9
+ # would otherwise emit `Lint/Syntax` for any `.erb` file because the raw
10
+ # template is not a valid Ruby program.
11
+ #
12
+ # The extractor scans for ERB tags, hands each `<% ... %>` / `<%= ... %>`
13
+ # body to RuboCop as its own `ProcessedSource`, and records the byte offset
14
+ # of the body inside the original template. RuboCop adds that offset back
15
+ # to every offense location, so reported line / column numbers point at
16
+ # the original `.erb` file -- not the synthetic snippet.
17
+ #
18
+ # Control-flow openers such as `<% if ... %>` are reduced to the condition
19
+ # expression. Other invalid fragments are skipped to avoid `Lint/Syntax`
20
+ # noise. ERB comments (`<%# ... %>`) are skipped as well.
21
+ module ErbRubyExtractor
22
+ ERB_TAG = /<%(?<kind>[-=#]?)(?<body>.*?)-?%>/m
23
+ CONTROL_CONDITION = /\A(?<prefix>\s*(?:if|unless|while|until|elsif|case)\b\s*)(?<expr>.*?)(?:\s+then)?\s*\z/m
24
+ TRAILING_DO_BLOCK = /\A(?<expr>.*?)(?<suffix>\s+do\b(?:\s*\|[^|]*\|)?\s*)\z/m
25
+ MAGIC_COMMENT = "# frozen_string_literal: true\n\n"
26
+
27
+ module_function
28
+
29
+ def call(processed_source)
30
+ path = processed_source.path
31
+ return nil unless path && handles?(path)
32
+
33
+ snippets = extract_snippets(processed_source)
34
+ snippets << empty_snippet(processed_source) if snippets.empty?
35
+ snippets
36
+ end
37
+
38
+ def handles?(path)
39
+ File.extname(path.to_s) == ".erb" && ::Metz::FileClassifier.view?(path)
40
+ end
41
+
42
+ def install!
43
+ extractors = RuboCop::Runner.ruby_extractors
44
+ extractor = method(:call)
45
+ extractors.unshift(extractor) unless extractors.include?(extractor)
46
+ end
47
+
48
+ def extract_snippets(processed_source)
49
+ enumerate_matches(processed_source.raw_source).filter_map do |match|
50
+ snippet_for(match, processed_source)
51
+ end
52
+ end
53
+
54
+ def enumerate_matches(raw, pos: 0, matches: [])
55
+ while (match = raw.match(ERB_TAG, pos))
56
+ matches << match
57
+ pos = match.end(0)
58
+ end
59
+ matches
60
+ end
61
+
62
+ def snippet_for(match, processed_source)
63
+ return if match[:kind] == "#"
64
+
65
+ ruby_version = processed_source.ruby_version || RUBY_VERSION.to_f
66
+ ps, offset = source_and_offset(match, ruby_version, processed_source.path)
67
+ return unless ps
68
+
69
+ { offset: offset, processed_source: ps }
70
+ end
71
+
72
+ def source_and_offset(match, ruby_version, path)
73
+ body = match[:body]
74
+ ps = RuboCop::ProcessedSource.new(body, ruby_version, path)
75
+ return [ps, match.begin(:body)] if ps.valid_syntax?
76
+ return unless code_tag?(match[:kind])
77
+
78
+ expression_source(body, ruby_version, path, match.begin(:body))
79
+ end
80
+
81
+ def code_tag?(kind)
82
+ kind.empty? || kind == "-"
83
+ end
84
+
85
+ def expression_source(body, ruby_version, path, body_offset)
86
+ match = body.match(CONTROL_CONDITION) || body.match(TRAILING_DO_BLOCK)
87
+ return unless match
88
+
89
+ ps = RuboCop::ProcessedSource.new(control_expression(match[:expr]), ruby_version, path)
90
+ [ps, expression_offset(body_offset, match)] if ps.valid_syntax?
91
+ end
92
+
93
+ def control_expression(expr)
94
+ "#{MAGIC_COMMENT}#{expr}\n"
95
+ end
96
+
97
+ def expression_offset(body_offset, match)
98
+ body_offset + match.begin(:expr) - MAGIC_COMMENT.bytesize
99
+ end
100
+
101
+ def empty_snippet(processed_source)
102
+ ruby_version = processed_source.ruby_version || RUBY_VERSION.to_f
103
+ ps = RuboCop::ProcessedSource.new("", ruby_version, processed_source.path)
104
+ { offset: 0, processed_source: ps }
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metz
4
+ # Path-based classifier for Rails-shaped source files. Used by the
5
+ # Rails-aware cops (`Metz/ControllersTooManyDirectCollaborators`,
6
+ # `Metz/ViewsDeepNavigation`) to decide whether a file is in scope for
7
+ # those cops. Predicates accept either a relative or an absolute path and
8
+ # always return a strict boolean.
9
+ #
10
+ # The matching rules are deliberately syntactic: a path is a controller
11
+ # iff some path segment chain ends in `app/controllers/<...>.rb`, a view
12
+ # iff `app/views/<...>.{erb,haml,slim}`, a model iff `app/models/<...>.rb`.
13
+ # We never touch the filesystem from here -- the inputs are already paths
14
+ # that RuboCop or a caller has handed us.
15
+ #
16
+ # Phase 3 reuse: future cross-file analyzers (project index, service-soup
17
+ # detector, repeated-branching detector) need the same path-shape question
18
+ # answered. Keep predicates pure, allocation-light, and dependency-free
19
+ # so they can be reused as-is from the wrapper or future analyzers.
20
+ module FileClassifier
21
+ CONTROLLER_PATTERN = %r{(?:\A|/)app/controllers/[^\0]+\.rb\z}
22
+ VIEW_PATTERN = %r{(?:\A|/)app/views/[^\0]+\.(?:erb|haml|slim)\z}
23
+ MODEL_PATTERN = %r{(?:\A|/)app/models/[^\0]+\.rb\z}
24
+
25
+ module_function
26
+
27
+ def controller?(path)
28
+ normalize(path).match?(CONTROLLER_PATTERN)
29
+ end
30
+
31
+ def view?(path)
32
+ normalize(path).match?(VIEW_PATTERN)
33
+ end
34
+
35
+ def model?(path)
36
+ normalize(path).match?(MODEL_PATTERN)
37
+ end
38
+
39
+ # Windows-style paths arrive with backslash separators when the caller is
40
+ # an editor running on Windows or a CI step that shells out via `cmd.exe`.
41
+ # Normalize to forward slashes so the regexes below stay POSIX-shaped.
42
+ def normalize(path)
43
+ path.to_s.tr("\\", "/")
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "template_ruby_extractor"
4
+
5
+ module Metz
6
+ # Ruby extractor for HAML view templates. Mirrors `Metz::ErbRubyExtractor`:
7
+ # registered with `RuboCop::Runner.ruby_extractors` so view-aware Metz cops
8
+ # (currently `Metz/ViewsDeepNavigation`) can run against HAML content.
9
+ #
10
+ # Recognises three HAML constructs that carry Ruby:
11
+ #
12
+ # * `= ruby_expr` - output script (escaped)
13
+ # * `- ruby_stmt` - silent script
14
+ # * `:ruby` filter - indented block of plain Ruby
15
+ #
16
+ # Each tag's body is handed to RuboCop as its own `ProcessedSource` whose
17
+ # offset points at the body's column in the original template. Tags whose
18
+ # body is not valid Ruby on its own are silently skipped (we never want to
19
+ # surface `Lint/Syntax` from a partial HAML fragment).
20
+ module HamlRubyExtractor
21
+ extend TemplateRubyExtractor
22
+
23
+ EXPR_LINE = /\A(?<lead>[ \t]*(?:[%.#][^\s=]*)?)=\s(?<body>.*)$/
24
+ STMT_LINE = /\A(?<lead>[ \t]*)-(?!#)\s(?<body>.*)$/
25
+ FILTER_LINE = /\A(?<indent>[ \t]*):ruby[ \t]*$/
26
+
27
+ module_function
28
+
29
+ def handles?(path)
30
+ File.extname(path.to_s) == ".haml" && ::Metz::FileClassifier.view?(path)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "template_ruby_extractor"
4
+
5
+ module Metz
6
+ # Ruby extractor for Slim view templates. Mirrors `Metz::ErbRubyExtractor`:
7
+ # registered with `RuboCop::Runner.ruby_extractors` so view-aware Metz cops
8
+ # (currently `Metz/ViewsDeepNavigation`) can run against Slim content.
9
+ #
10
+ # Recognises four Slim constructs that carry Ruby:
11
+ #
12
+ # * `= ruby_expr` - output script (escaped)
13
+ # * `== ruby_expr` - output script (raw / unescaped)
14
+ # * `- ruby_stmt` - silent script
15
+ # * `ruby:` filter - indented block of plain Ruby
16
+ #
17
+ # Each tag's body is handed to RuboCop as its own `ProcessedSource` whose
18
+ # offset points at the body's column in the original template. Tags whose
19
+ # body is not valid Ruby on its own are silently skipped (we never want to
20
+ # surface `Lint/Syntax` from a partial Slim fragment).
21
+ module SlimRubyExtractor
22
+ extend TemplateRubyExtractor
23
+
24
+ EXPR_LINE = /\A(?<lead>[ \t]*(?:[a-zA-Z][\w-]*|[.#][^\s=]*)?)==?\s(?<body>.*)$/
25
+ STMT_LINE = /\A(?<lead>[ \t]*)-\s(?<body>.*)$/
26
+ FILTER_LINE = /\A(?<indent>[ \t]*)ruby:[ \t]*$/
27
+
28
+ module_function
29
+
30
+ def handles?(path)
31
+ File.extname(path.to_s) == ".slim" && ::Metz::FileClassifier.view?(path)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "file_classifier"
4
+
5
+ module Metz
6
+ # Shared iteration backbone for line-based Ruby extractors that target
7
+ # indentation-driven template languages (HAML, Slim). Concrete extractors
8
+ # define three patterns -- an expression line, a statement line, and a
9
+ # filter-block start -- and this module walks the raw source line by
10
+ # line, hands every matching body to RuboCop as its own ProcessedSource,
11
+ # and tracks the byte offset of each body inside the original template
12
+ # so offense locations refer back to the source file rather than the
13
+ # synthetic snippet.
14
+ module TemplateRubyExtractor
15
+ def call(processed_source)
16
+ path = processed_source.path
17
+ return nil unless path && handles?(path)
18
+
19
+ snippets = Walker.new(self, processed_source).snippets
20
+ snippets << empty_snippet(processed_source) if snippets.empty?
21
+ snippets
22
+ end
23
+
24
+ def install!
25
+ extractors = RuboCop::Runner.ruby_extractors
26
+ entrypoint = method(:call)
27
+ extractors.unshift(entrypoint) unless extractors.include?(entrypoint)
28
+ end
29
+
30
+ def empty_snippet(processed_source)
31
+ ps = build_processed_source("", processed_source)
32
+ { offset: 0, processed_source: ps }
33
+ end
34
+
35
+ def build_snippet(body, body_offset, processed_source)
36
+ return nil if body.strip.empty?
37
+
38
+ ps = build_processed_source(body, processed_source)
39
+ return nil unless ps.valid_syntax?
40
+
41
+ { offset: body_offset, processed_source: ps }
42
+ end
43
+
44
+ def build_processed_source(body, processed_source)
45
+ ruby_version = processed_source.ruby_version || RUBY_VERSION.to_f
46
+ RuboCop::ProcessedSource.new(body, ruby_version, processed_source.path)
47
+ end
48
+
49
+ # Walks lines of a template, dispatching each line to the owner module's
50
+ # match patterns. Holds the running byte offset, the line index, and the
51
+ # accumulating snippet list so the owner module can stay stateless.
52
+ class Walker
53
+ def initialize(owner, processed_source)
54
+ @owner = owner
55
+ @processed_source = processed_source
56
+ @cursor = Cursor.new(processed_source.raw_source.lines)
57
+ walk
58
+ end
59
+
60
+ attr_reader :snippets
61
+
62
+ def walk
63
+ @snippets = []
64
+ @snippets << next_snippet until @cursor.done?
65
+ @snippets.compact!
66
+ end
67
+
68
+ def next_snippet
69
+ line = @cursor.line
70
+ filter = line.match(@owner::FILTER_LINE)
71
+ return consume_filter(filter) if filter
72
+
73
+ consume_inline(line.match(@owner::EXPR_LINE) || line.match(@owner::STMT_LINE))
74
+ end
75
+
76
+ def consume_inline(match)
77
+ snippet = inline_snippet(match)
78
+ @cursor.advance(1)
79
+ snippet
80
+ end
81
+
82
+ def inline_snippet(match)
83
+ return nil if match.nil?
84
+
85
+ body_offset = @cursor.offset + match.begin(:body)
86
+ @owner.build_snippet(match[:body], body_offset, @processed_source)
87
+ end
88
+
89
+ def consume_filter(match)
90
+ body_offset = @cursor.offset + @cursor.line.length
91
+ body_lines = collect_filter_body(match[:indent].length)
92
+ snippet = build_filter_snippet(body_lines, body_offset)
93
+ @cursor.advance(1 + body_lines.size)
94
+ snippet
95
+ end
96
+
97
+ def collect_filter_body(base_indent)
98
+ body = []
99
+ body << @cursor.peek(body.size + 1) while filter_member?(body.size + 1, base_indent)
100
+ body
101
+ end
102
+
103
+ def filter_member?(lookahead, base_indent)
104
+ line = @cursor.peek(lookahead)
105
+ return false if line.nil?
106
+
107
+ line.match?(/\A\s*\z/) || leading_indent(line) > base_indent
108
+ end
109
+
110
+ def leading_indent(line)
111
+ line[/\A[ \t]*/].length
112
+ end
113
+
114
+ def build_filter_snippet(body_lines, body_offset)
115
+ return nil if body_lines.empty?
116
+
117
+ @owner.build_snippet(body_lines.join, body_offset, @processed_source)
118
+ end
119
+ end
120
+
121
+ # Tracks the running byte offset and line index while walking a template.
122
+ class Cursor
123
+ def initialize(lines)
124
+ @lines = lines
125
+ @index = 0
126
+ @offset = 0
127
+ end
128
+
129
+ attr_reader :offset
130
+
131
+ def done?
132
+ @index >= @lines.size
133
+ end
134
+
135
+ def line
136
+ @lines[@index]
137
+ end
138
+
139
+ def peek(lookahead)
140
+ @lines[@index + lookahead]
141
+ end
142
+
143
+ def advance(count)
144
+ count.times do
145
+ break if done?
146
+
147
+ @offset += @lines[@index].length
148
+ @index += 1
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+ require_relative "../../../metz/cop_metadata"
5
+
6
+ module RuboCop
7
+ module Cop
8
+ module Metz
9
+ # Flags classes whose body exceeds the configured `Max` line count.
10
+ # Wraps the semantics of core's `Metrics/ClassLength` with a stricter
11
+ # default of 100 lines.
12
+ class ClassesTooLong < RuboCop::Cop::Base
13
+ include RuboCop::Cop::CodeLength
14
+ extend ::Metz::CopMetadata
15
+
16
+ why_it_matters "Long classes accumulate responsibilities and become hard to change safely."
17
+ fix_safety :manual
18
+ suggested_next_moves [
19
+ "Extract collaborating objects that own a coherent slice of the behavior.",
20
+ "Move data-shaping helpers onto value objects.",
21
+ "Split persistence, presentation, and orchestration concerns into separate classes."
22
+ ]
23
+
24
+ MSG = "Class has too many lines. [%<length>d/%<max>d]"
25
+
26
+ def on_class(node)
27
+ check_code_length(node)
28
+ end
29
+
30
+ def on_sclass(node)
31
+ return if node.each_ancestor(:class).any?
32
+
33
+ on_class(node)
34
+ end
35
+
36
+ def on_casgn(node)
37
+ block_node = node.expression || find_expression_within_parent(node.parent)
38
+ return unless block_node.respond_to?(:class_definition?) && block_node.class_definition?
39
+
40
+ check_code_length(block_node)
41
+ end
42
+
43
+ private
44
+
45
+ def message(length, max_length)
46
+ format(MSG, length: length, max: max_length)
47
+ end
48
+
49
+ def find_expression_within_parent(parent)
50
+ if parent&.assignment?
51
+ parent.expression
52
+ elsif parent&.parent&.masgn_type?
53
+ parent.parent.expression
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end