rubocop-erb 0.7.1 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e1bcc90b702c7acd89b91a4f0e20852ba42aaf827a9facaf776a5bea6bc09254
4
- data.tar.gz: b470e1920d30f2f18f14964b5bd2927992f2d71d4ace4ee4d406c8d0e2466cce
3
+ metadata.gz: d829909cc60a55799ce1976340a7e82e7cb40f82e379de1ef8fd1b8cc3395ee0
4
+ data.tar.gz: 7712047a8cf2e03e4da145dd477f197a0697dbf300adc9728ab5d5fab26756cc
5
5
  SHA512:
6
- metadata.gz: 48de696c9afbb4eda39ffd31a740b26f22e5bd5199409518050c393ffc7546393bf287d48b2838dd43b2ae2ae4d2427dbc4db9749c852a8a3dca12d86978ec74
7
- data.tar.gz: eb2905a7113fd5a3e55b624c608c0968311f4019a1975a14d2f8db008b95cbf9b48a9cc9a6d0cc5fda0b37d0a14b54e8fb1e8714db3f5b3ab08fce5b71de5571
6
+ metadata.gz: 5a653d659c624405db7226d3501bab436656eeca3ed31d84e6cad8838141d6c9dd384af376e6bfe30307da0b8365dee24ee70d376f47e2da1ae44a2e160dd7cb
7
+ data.tar.gz: deb517d509c2e611f04a1555db766d076793a4e01f42c42697b95a5ed5a222ff6e6824711c6b613fc02a53149aafb487819378e39841ffeb1b0385662c097899
data/.rubocop.yml CHANGED
@@ -14,7 +14,7 @@ AllCops:
14
14
  Exclude:
15
15
  - spec/**/*.erb
16
16
  NewCops: enable
17
- TargetRubyVersion: 3.0
17
+ TargetRubyVersion: 3.3
18
18
 
19
19
  Gemspec/RequireMFA:
20
20
  Enabled: false
@@ -28,6 +28,9 @@ RSpec/ExampleLength:
28
28
  RSpec/MultipleExpectations:
29
29
  Enabled: false
30
30
 
31
+ RSpec/MultipleMemoizedHelpers:
32
+ Enabled: false
33
+
31
34
  RSpec/NamedSubject:
32
35
  Enabled: false
33
36
 
data/Gemfile.lock CHANGED
@@ -1,10 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rubocop-erb (0.7.1)
5
- herb (~> 0.7)
4
+ rubocop-erb (0.8.0)
5
+ herb (~> 0.9, >= 0.9.3)
6
6
  lint_roller (~> 1.1)
7
- rubocop (~> 1.72, >= 1.72.1)
7
+ rubocop (~> 1.84)
8
8
 
9
9
  GEM
10
10
  remote: https://rubygems.org/
@@ -30,21 +30,21 @@ GEM
30
30
  connection_pool (2.5.0)
31
31
  diff-lcs (1.6.0)
32
32
  drb (2.2.1)
33
- herb (0.9.2)
34
- herb (0.9.2-x86_64-linux-gnu)
33
+ herb (0.10.1)
34
+ herb (0.10.1-x86_64-linux-gnu)
35
35
  i18n (1.14.7)
36
36
  concurrent-ruby (~> 1.0)
37
37
  json (2.10.1)
38
38
  language_server-protocol (3.17.0.4)
39
39
  lint_roller (1.1.0)
40
40
  logger (1.6.6)
41
- minitest (5.25.4)
41
+ minitest (5.27.0)
42
42
  mutex_m (0.3.0)
43
43
  parallel (1.26.3)
44
- parser (3.3.7.1)
44
+ parser (3.3.11.1)
45
45
  ast (~> 2.4.1)
46
46
  racc
47
- prism (1.3.0)
47
+ prism (1.9.0)
48
48
  racc (1.8.1)
49
49
  rainbow (3.1.1)
50
50
  rake (13.2.1)
@@ -62,19 +62,20 @@ GEM
62
62
  diff-lcs (>= 1.2.0, < 2.0)
63
63
  rspec-support (~> 3.13.0)
64
64
  rspec-support (3.13.2)
65
- rubocop (1.72.2)
65
+ rubocop (1.87.0)
66
66
  json (~> 2.3)
67
67
  language_server-protocol (~> 3.17.0.2)
68
68
  lint_roller (~> 1.1.0)
69
- parallel (~> 1.10)
69
+ parallel (>= 1.10)
70
70
  parser (>= 3.3.0.2)
71
71
  rainbow (>= 2.2.2, < 4.0)
72
72
  regexp_parser (>= 2.9.3, < 3.0)
73
- rubocop-ast (>= 1.38.0, < 2.0)
73
+ rubocop-ast (>= 1.49.0, < 2.0)
74
74
  ruby-progressbar (~> 1.7)
75
75
  unicode-display_width (>= 2.4.0, < 4.0)
76
- rubocop-ast (1.38.0)
77
- parser (>= 3.3.1.0)
76
+ rubocop-ast (1.49.1)
77
+ parser (>= 3.3.7.2)
78
+ prism (~> 1.7)
78
79
  rubocop-rake (0.7.1)
79
80
  lint_roller (~> 1.1)
80
81
  rubocop (>= 1.72.1)
@@ -90,7 +91,7 @@ GEM
90
91
  concurrent-ruby (~> 1.0)
91
92
  unicode-display_width (3.1.4)
92
93
  unicode-emoji (~> 4.0, >= 4.0.4)
93
- unicode-emoji (4.0.4)
94
+ unicode-emoji (4.3.0)
94
95
 
95
96
  PLATFORMS
96
97
  ruby
data/config/default.yml CHANGED
@@ -27,10 +27,19 @@ Layout/CommentIndentation:
27
27
  Exclude:
28
28
  - '**/*.erb'
29
29
 
30
+ # Indentation follows the containing ERB tag, not the file as a whole.
30
31
  Layout/InitialIndentation:
31
32
  Exclude:
32
33
  - '**/*.erb'
33
34
 
35
+ Layout/ElseAlignment:
36
+ Exclude:
37
+ - '**/*.erb'
38
+
39
+ Layout/EmptyLinesAroundBlockBody:
40
+ Exclude:
41
+ - '**/*.erb'
42
+
34
43
  Layout/EmptyLineAfterGuardClause:
35
44
  Exclude:
36
45
  - '**/*.erb'
@@ -39,6 +48,10 @@ Layout/EndAlignment:
39
48
  Exclude:
40
49
  - '**/*.erb'
41
50
 
51
+ Layout/ExtraSpacing:
52
+ Exclude:
53
+ - '**/*.erb'
54
+
42
55
  Layout/FirstArgumentIndentation:
43
56
  Exclude:
44
57
  - '**/*.erb'
@@ -79,10 +92,7 @@ Layout/IndentationWidth:
79
92
  Exclude:
80
93
  - '**/*.erb'
81
94
 
82
- Layout/LeadingEmptyLine:
83
- Exclude:
84
- - '**/*.erb'
85
-
95
+ # Non-Ruby content gives false positives for this cop.
86
96
  Layout/LeadingEmptyLines:
87
97
  Exclude:
88
98
  - '**/*.erb'
@@ -127,46 +137,49 @@ Layout/ParameterAlignment:
127
137
  Exclude:
128
138
  - '**/*.erb'
129
139
 
130
- Layout/TrailingEmptyLines:
140
+ Layout/SpaceBeforeSemicolon:
131
141
  Exclude:
132
142
  - '**/*.erb'
133
143
 
134
- Layout/TrailingWhitespace:
144
+ # Non-Ruby content gives false positives for this cop.
145
+ Layout/TrailingEmptyLines:
135
146
  Exclude:
136
147
  - '**/*.erb'
137
148
 
138
- Lint/EmptyFile:
149
+ Layout/TrailingWhitespace:
139
150
  Exclude:
140
151
  - '**/*.erb'
141
152
 
142
- Lint/OutOfRangeRegexpRef:
153
+ Lint/DuplicateBranch:
143
154
  Exclude:
144
155
  - '**/*.erb'
145
156
 
146
- Lint/Syntax:
157
+ Lint/EmptyBlock:
147
158
  Exclude:
148
159
  - '**/*.erb'
149
160
 
150
- Lint/UselessAssignment:
161
+ Lint/EmptyConditionalBody:
151
162
  Exclude:
152
163
  - '**/*.erb'
153
164
 
154
- Lint/Void:
165
+ # Non-Ruby content gives false positives for this cop.
166
+ Lint/EmptyFile:
155
167
  Exclude:
156
168
  - '**/*.erb'
157
169
 
158
- Metrics/BlockLength:
170
+ Lint/EmptyWhen:
159
171
  Exclude:
160
172
  - '**/*.erb'
161
173
 
162
- Metrics/BlockNesting:
174
+ Lint/Void:
163
175
  Exclude:
164
176
  - '**/*.erb'
165
177
 
166
- Naming/FileName:
178
+ Style/EmptyElse:
167
179
  Exclude:
168
180
  - '**/*.erb'
169
181
 
182
+ # ERB doesn't support magic comments.
170
183
  Style/FrozenStringLiteralComment:
171
184
  Exclude:
172
185
  - '**/*.erb'
@@ -175,10 +188,18 @@ Style/IdenticalConditionalBranches:
175
188
  Exclude:
176
189
  - '**/*.erb'
177
190
 
191
+ Style/IfInsideElse:
192
+ Exclude:
193
+ - '**/*.erb'
194
+
178
195
  Style/IfUnlessModifier:
179
196
  Exclude:
180
197
  - '**/*.erb'
181
198
 
199
+ Style/IfWithSemicolon:
200
+ Exclude:
201
+ - '**/*.erb'
202
+
182
203
  Style/MultilineTernaryOperator:
183
204
  Exclude:
184
205
  - '**/*.erb'
@@ -195,14 +216,30 @@ Style/ParallelAssignment:
195
216
  Exclude:
196
217
  - '**/*.erb'
197
218
 
219
+ Style/RedundantCondition:
220
+ Exclude:
221
+ - '**/*.erb'
222
+
198
223
  Style/RescueModifier:
199
224
  Exclude:
200
225
  - '**/*.erb'
201
226
 
227
+ Style/SafeNavigation:
228
+ Exclude:
229
+ - '**/*.erb'
230
+
202
231
  Style/Semicolon:
203
232
  Exclude:
204
233
  - '**/*.erb'
205
234
 
235
+ Style/SoleNestedConditional:
236
+ Exclude:
237
+ - '**/*.erb'
238
+
239
+ Style/SymbolProc:
240
+ Exclude:
241
+ - '**/*.erb'
242
+
206
243
  Style/TrailingCommaInArguments:
207
244
  Exclude:
208
245
  - '**/*.erb'
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Erb
5
+ module Corrector
6
+ def initialize(source)
7
+ @processed_source = source.processed_source if source.respond_to?(:processed_source)
8
+ @discard_non_ruby_corrections = @processed_source.is_a?(ERBSource)
9
+ super
10
+ end
11
+
12
+ # Every rewrite operation funnels through `combine`, which is the only place
13
+ # both the target range and the replacement text are available. We use it to
14
+ # recognize a benign re-indentation -- only adding or removing whitespace in
15
+ # the leading whitespace of a line that begins with an ERB tag -- so it is
16
+ # kept rather than discarded as a non-Ruby correction. When the correction
17
+ # reaches past the indentation into the opening tag itself (as
18
+ # `Layout/CaseIndentation` does), it is shrunk to cover only the leading
19
+ # whitespace so the tag is preserved.
20
+ def combine(
21
+ range,
22
+ attributes
23
+ )
24
+ if (adjustment = whitespace_indent_adjustment(range, attributes))
25
+ range, attributes = adjustment
26
+ @whitespace_only_indent = true
27
+ end
28
+ super
29
+ ensure
30
+ @whitespace_only_indent = false
31
+ end
32
+
33
+ # A correction that touches non-Ruby code can't be applied to the extracted
34
+ # Ruby without breaking the surrounding ERB. Rather than apply a broken fix,
35
+ # we discard the whole correction (every operation from the offense) by
36
+ # reporting the corrector as empty, leaving the offense flagged but not
37
+ # auto-correctable.
38
+ def empty?
39
+ return true if @intersects_non_ruby
40
+
41
+ super
42
+ end
43
+
44
+ def to_range(node_or_range)
45
+ node_or_range = @processed_source.to_range(node_or_range) if @processed_source.is_a?(ERBSource)
46
+ range = super
47
+ flag_non_ruby_intersection(range)
48
+ range
49
+ end
50
+
51
+ private
52
+
53
+ def flag_non_ruby_intersection(range)
54
+ return unless @discard_non_ruby_corrections
55
+ return unless @processed_source.is_a?(ERBSource)
56
+ return unless range.is_a?(::Parser::Source::Range)
57
+ return if @whitespace_only_indent
58
+ return if @processed_source.range_within_ruby_content?(range.begin_pos, range.end_pos)
59
+
60
+ @intersects_non_ruby = true
61
+ end
62
+
63
+ # @return [Array(Parser::Source::Range, Hash), nil] the (possibly rewritten)
64
+ # range and attributes to apply when the operation only adjusts whitespace
65
+ # in the leading indentation of an ERB-tag line, or nil when inapplicable
66
+ def whitespace_indent_adjustment(
67
+ range,
68
+ attributes
69
+ )
70
+ return unless @discard_non_ruby_corrections && @processed_source.is_a?(ERBSource)
71
+ return unless range.is_a?(::Parser::Source::Range)
72
+ return unless attributes.is_a?(Hash)
73
+
74
+ replacement = attributes[:replacement]
75
+ contents = attributes.values_at(:replacement, :insert_before, :insert_after).compact
76
+ # `attributes` comes from Parser's TreeRewriter internals. If none of the
77
+ # content keys we know about are present, the shape has changed from under
78
+ # us -- fail safe by treating this as a normal correction (discarded if it
79
+ # touches non-Ruby) rather than assuming it is whitespace-only.
80
+ return if contents.empty?
81
+ return unless contents.all? { |content| content.match?(/\A\s*\z/) }
82
+
83
+ source = @processed_source.template_source
84
+ line = @processed_source.buffer.line_range(@processed_source.buffer.line_for_position(range.begin_pos))
85
+ offset = source[line.begin_pos...line.end_pos].to_s.index(/\S/)
86
+ return unless offset
87
+
88
+ tag_start = line.begin_pos + offset
89
+ # Everything from the line start up to the range, and the first code on the
90
+ # line, must be whitespace then an ERB opening tag.
91
+ return unless source[tag_start, 2] == '<%'
92
+ return unless source[line.begin_pos...range.begin_pos].to_s.match?(/\A\s*\z/)
93
+ return unless range.begin_pos <= tag_start
94
+
95
+ # Wholly within the indentation: keep the operation untouched.
96
+ return [range, attributes] if range.end_pos <= tag_start
97
+
98
+ # The range crosses into the tag; only the opening delimiter and trailing
99
+ # whitespace may be swallowed, and only a replacement can be reshaped.
100
+ return unless replacement
101
+
102
+ preserved = source[tag_start...range.end_pos].to_s
103
+ return unless preserved.match?(/\A<%[-=]*\s*\z/)
104
+
105
+ new_length = replacement.length - preserved.length
106
+ return if new_length.negative?
107
+
108
+ new_range = ::Parser::Source::Range.new(range.source_buffer, range.begin_pos, tag_start)
109
+ [new_range, attributes.merge(replacement: ' ' * new_length)]
110
+ end
111
+ end
112
+ Cop::Corrector.prepend(Corrector)
113
+ end
114
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Erb
5
+ class ERBSource < ProcessedSource
6
+ attr_reader :herb_parse_result, :template_source
7
+
8
+ def initialize(
9
+ template_source,
10
+ ...
11
+ ) # rubocop:disable Layout/SpaceInsideParens
12
+ @template_source = template_source
13
+ @herb_parse_result = Herb.parse(template_source)
14
+
15
+ super(...)
16
+ end
17
+
18
+ # @param [Integer] byte_offset offset into the (original) source in bytes
19
+ # @return [Integer] the equivalent character offset
20
+ def byte_to_char_offset(byte_offset)
21
+ template_source.byteslice(0, byte_offset).length
22
+ end
23
+
24
+ # @param [Integer] position full position in the buffer
25
+ # @return [Herb::AST::Node, nil] the ERB node whose tag contains the position
26
+ def erb_node_for_pos(position)
27
+ erb_node_offsets.bsearch do |(range, _node)|
28
+ next -1 if position < range.begin
29
+ next 1 if position >= range.end
30
+
31
+ 0
32
+ end&.last
33
+ end
34
+
35
+ # Buffer ranges of every ERB tag (`<% … %>`), paired with their node.
36
+ #
37
+ # @return [Array<(Range, Herb::AST::Node)>]
38
+ def erb_node_offsets
39
+ @erb_node_offsets ||= [].tap { |offsets| ERBNodeOffsetVisitor.new(self, offsets).visit(erb_root) }
40
+ end
41
+
42
+ # @return [Herb::AST::DocumentNode]
43
+ def erb_root
44
+ herb_parse_result.value
45
+ end
46
+
47
+ # Prefer {#herb_range_to_parser_range}: Herb ranges are always byte offsets,
48
+ # while the unit of Herb position columns differs between herb versions
49
+ # (e.g. bytes in 0.9.x and characters in 0.11.x for multi-byte Ruby code).
50
+ #
51
+ # @param [Herb::Location] location
52
+ # @return [Parser::Source::Range]
53
+ def herb_location_to_parser_range(location)
54
+ Parser::Source::Range.new(buffer,
55
+ herb_position_to_buffer_pos(location.start),
56
+ herb_position_to_buffer_pos(location.end))
57
+ end
58
+
59
+ # @param [Herb::Position] position
60
+ # @return [Integer]
61
+ def herb_position_to_buffer_pos(position)
62
+ line_range = buffer.line_range(position.line)
63
+ # Herb reports columns as byte offsets, but the parser buffer uses
64
+ # character offsets; convert so multi-byte characters line up.
65
+ char_column = line_range.source.byteslice(0, position.column).length
66
+ line_range.begin.begin_pos + char_column
67
+ end
68
+
69
+ # @param [Herb::Position] position
70
+ # @return [Parser::Source::Range] zero-width range at the position
71
+ def herb_position_to_parser_range(position)
72
+ pos = herb_position_to_buffer_pos(position)
73
+ Parser::Source::Range.new(buffer, pos, pos)
74
+ end
75
+
76
+ # @param [Herb::Range] range
77
+ # @return [Parser::Source::Range]
78
+ def herb_range_to_parser_range(range)
79
+ # Herb ranges are byte offsets into the source; the parser buffer uses
80
+ # character offsets, so convert to keep multi-byte characters aligned.
81
+ Parser::Source::Range.new(buffer, byte_to_char_offset(range.from), byte_to_char_offset(range.to))
82
+ end
83
+
84
+ # @return [Boolean] whether the buffer range lies entirely within the Ruby
85
+ # content (between the delimiters) of a single ERB tag
86
+ def range_within_ruby_content?(
87
+ begin_pos,
88
+ end_pos
89
+ )
90
+ return false unless (node = erb_node_for_pos(begin_pos)) && (content = node.content)
91
+
92
+ content_range = herb_range_to_parser_range(content.range)
93
+ begin_pos >= content_range.begin_pos && end_pos <= content_range.end_pos
94
+ end
95
+
96
+ # Convert various Herb node or range types to a Parser::Source::Range
97
+ # Unrecognized values are passed through
98
+ #
99
+ # @param [#range, #location, Herb::Location, Herb::Range, Object] node_or_range
100
+ # @return [Parser::Source::Range, Object, nil]
101
+ def to_range(node_or_range)
102
+ node_or_range = node_or_range.range if node_or_range.respond_to?(:range)
103
+ node_or_range = herb_node_range(node_or_range) || node_or_range.location if node_or_range.is_a?(Herb::AST::Node)
104
+ node_or_range = node_or_range.location if node_or_range.is_a?(Herb::Token)
105
+ node_or_range = herb_location_to_parser_range(node_or_range) if node_or_range.is_a?(Herb::Location)
106
+ node_or_range = herb_range_to_parser_range(node_or_range) if node_or_range.is_a?(Herb::Range)
107
+ node_or_range = herb_position_to_parser_range(node_or_range) if node_or_range.is_a?(Herb::Position)
108
+
109
+ node_or_range
110
+ end
111
+
112
+ private
113
+
114
+ # Herb AST nodes have no byte range of their own, so derive one from the
115
+ # tokens of an ERB node when possible.
116
+ #
117
+ # @param [Herb::AST::Node] node
118
+ # @return [Herb::Range, nil]
119
+ def herb_node_range(node)
120
+ return unless node.respond_to?(:tag_opening) && node.respond_to?(:tag_closing)
121
+ return unless node.tag_opening && node.tag_closing
122
+
123
+ Herb::Range.new(node.tag_opening.range.from, node.tag_closing.range.to)
124
+ end
125
+
126
+ # Collects the buffer range of every ERB tag, paired with its node.
127
+ class ERBNodeOffsetVisitor < Herb::Visitor
128
+ def initialize(
129
+ source,
130
+ offsets
131
+ )
132
+ @source = source
133
+ @offsets = offsets
134
+ super()
135
+ end
136
+
137
+ def visit_erb_node(node)
138
+ @offsets << [
139
+ Range.new(@source.byte_to_char_offset(node.tag_opening.range.from),
140
+ @source.byte_to_char_offset(node.tag_closing.range.to),
141
+ true),
142
+ node
143
+ ]
144
+ super
145
+ end
146
+ end
147
+ private_constant :ERBNodeOffsetVisitor
148
+ end
149
+ end
150
+ end
@@ -5,7 +5,7 @@ require 'rubocop'
5
5
 
6
6
  module RuboCop
7
7
  module Erb
8
- # Extract Ruby codes from Erb template.
8
+ # Extract Ruby codes from ERB template.
9
9
  class RubyExtractor
10
10
  class << self
11
11
  # @param [RuboCop::ProcessedSource] processed_source
@@ -24,54 +24,50 @@ module RuboCop
24
24
  def call
25
25
  return unless supported_file_path_pattern?
26
26
 
27
- ruby_clips.map do |ruby_clip|
28
- {
29
- offset: ruby_clip.offset,
30
- processed_source: ProcessedSourceBuilder.call(
31
- code: ruby_clip.code,
32
- processed_source: @processed_source
33
- )
34
- }
35
- end
27
+ new_processed_source = ERBSource.new(template_source,
28
+ extracted_ruby,
29
+ @processed_source.ruby_version,
30
+ @processed_source.path,
31
+ parser_engine: @processed_source.parser_engine)
32
+ new_processed_source.config = @processed_source.config
33
+ new_processed_source.registry = @processed_source.registry
34
+ [{
35
+ offset: 0,
36
+ processed_source: new_processed_source
37
+ }]
36
38
  end
37
39
 
38
40
  private
39
41
 
42
+ # The extracted Ruby, character-aligned with the template.
43
+ #
44
+ # `Herb.extract_ruby` blanks non-Ruby bytes to spaces, which expands every
45
+ # multi-byte character (e.g. an emoji in the HTML) into several spaces and
46
+ # shifts the position of all following characters. Since corrections map
47
+ # extracted positions back to the template 1:1, re-collapse those runs so a
48
+ # multi-byte source character stays a single character.
49
+ #
50
+ # @return [String]
51
+ def extracted_ruby
52
+ extracted = Herb.extract_ruby(template_source)
53
+ return extracted if extracted.length == template_source.length
54
+
55
+ realigned = +''
56
+ byte_index = 0
57
+ template_source.each_char do |char|
58
+ size = char.bytesize
59
+ chunk = extracted.byteslice(byte_index, size)
60
+ realigned << (size == 1 || chunk == char ? chunk : ' ')
61
+ byte_index += size
62
+ end
63
+ realigned
64
+ end
65
+
40
66
  # @return [String, nil]
41
67
  def file_path
42
68
  @processed_source.path
43
69
  end
44
70
 
45
- # @return [Array<Herb::AST::Node>]
46
- def nodes
47
- visitor = ErbNodeVisitor.new
48
- visitor.visit(root)
49
- visitor.erb_nodes
50
- end
51
-
52
- # @return [Herb::AST::DocumentNode]
53
- def root
54
- ::Herb.parse(template_source).value
55
- end
56
-
57
- # @return [Array<RuboCop::Erb::RubyClip>]
58
- def ruby_clips
59
- nodes.map do |node|
60
- erb_start_location = node.content.location.start
61
- line_range = @processed_source.buffer.line_range(erb_start_location.line)
62
- RubyClip.new(
63
- code: node.content.value,
64
- offset: line_range.begin.begin_pos + erb_start_location.column
65
- )
66
- end.flat_map do |ruby_clip|
67
- WhenDecomposer.call(@processed_source, ruby_clip)
68
- end.map do |ruby_clip|
69
- KeywordRemover.call(ruby_clip)
70
- end.reject do |ruby_clip|
71
- ruby_clip.code.match?(/\A\s*\z/)
72
- end
73
- end
74
-
75
71
  # @return [Boolean]
76
72
  def supported_file_path_pattern?
77
73
  file_path&.end_with?('.erb')
@@ -81,46 +77,6 @@ module RuboCop
81
77
  def template_source
82
78
  @processed_source.raw_source
83
79
  end
84
-
85
- class ErbNodeVisitor < Herb::Visitor
86
- # @return [Array<Symbol>]
87
- def self.erb_visitor_methods
88
- instance_methods.select { |method_name| method_name.to_s.start_with?('visit_erb_') }
89
- end
90
-
91
- attr_reader :erb_nodes
92
-
93
- def initialize
94
- @erb_nodes = []
95
- super
96
- end
97
-
98
- # @return [Boolean]
99
- def comment?(node)
100
- node.tag_opening&.value == '<%#'
101
- end
102
-
103
- # @return [Boolean]
104
- def escape?(node)
105
- node.tag_opening&.value == '<%%'
106
- end
107
-
108
- erb_visitor_methods.each do |method_name|
109
- class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
110
- def #{method_name}(node) # def visit_erb_content_node(node)
111
- record_node(node) # record_node(node)
112
- super # super
113
- end # end
114
- RUBY
115
- end
116
-
117
- def record_node(node)
118
- return unless node.respond_to?(:content) && node.content
119
- return if comment?(node) || escape?(node)
120
-
121
- @erb_nodes << node
122
- end
123
- end
124
80
  end
125
81
  end
126
82
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Erb
5
- VERSION = '0.7.1'
5
+ VERSION = '0.8.0'
6
6
  end
7
7
  end
data/lib/rubocop/erb.rb CHANGED
@@ -1,14 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'herb'
4
+ require 'rubocop'
5
+
3
6
  module RuboCop
4
7
  module Erb
5
- autoload :KeywordRemover, 'rubocop/erb/keyword_remover'
6
- autoload :ProcessedSourceBuilder, 'rubocop/erb/processed_source_builder'
7
- autoload :RubyClip, 'rubocop/erb/ruby_clip'
8
+ autoload :ERBSource, 'rubocop/erb/erb_source'
8
9
  autoload :RubyExtractor, 'rubocop/erb/ruby_extractor'
9
- autoload :WhenDecomposer, 'rubocop/erb/when_decomposer'
10
10
  end
11
11
  end
12
12
 
13
13
  require_relative 'erb/plugin'
14
14
  require_relative 'erb/version'
15
+ require_relative 'erb/corrector'
data/rubocop-erb.gemspec CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
11
11
  spec.summary = 'RuboCop plugin for ERB template.'
12
12
  spec.homepage = 'https://github.com/r7kamura/rubocop-erb'
13
13
  spec.license = 'MIT'
14
- spec.required_ruby_version = '>= 3.0'
14
+ spec.required_ruby_version = '>= 3.3'
15
15
 
16
16
  spec.metadata['homepage_uri'] = spec.homepage
17
17
  spec.metadata['source_code_uri'] = spec.homepage
@@ -29,7 +29,7 @@ Gem::Specification.new do |spec|
29
29
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
30
  spec.require_paths = ['lib']
31
31
 
32
- spec.add_dependency 'herb', '~> 0.7'
32
+ spec.add_dependency 'herb', '~> 0.9', '>= 0.9.3'
33
33
  spec.add_dependency 'lint_roller', '~> 1.1'
34
- spec.add_dependency 'rubocop', '~> 1.72', '>= 1.72.1'
34
+ spec.add_dependency 'rubocop', '~> 1.84'
35
35
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-erb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryo Nakamura
@@ -15,14 +15,20 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '0.7'
18
+ version: '0.9'
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: 0.9.3
19
22
  type: :runtime
20
23
  prerelease: false
21
24
  version_requirements: !ruby/object:Gem::Requirement
22
25
  requirements:
23
26
  - - "~>"
24
27
  - !ruby/object:Gem::Version
25
- version: '0.7'
28
+ version: '0.9'
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: 0.9.3
26
32
  - !ruby/object:Gem::Dependency
27
33
  name: lint_roller
28
34
  requirement: !ruby/object:Gem::Requirement
@@ -43,20 +49,14 @@ dependencies:
43
49
  requirements:
44
50
  - - "~>"
45
51
  - !ruby/object:Gem::Version
46
- version: '1.72'
47
- - - ">="
48
- - !ruby/object:Gem::Version
49
- version: 1.72.1
52
+ version: '1.84'
50
53
  type: :runtime
51
54
  prerelease: false
52
55
  version_requirements: !ruby/object:Gem::Requirement
53
56
  requirements:
54
57
  - - "~>"
55
58
  - !ruby/object:Gem::Version
56
- version: '1.72'
57
- - - ">="
58
- - !ruby/object:Gem::Version
59
- version: 1.72.1
59
+ version: '1.84'
60
60
  email:
61
61
  - r7kamura@gmail.com
62
62
  executables: []
@@ -73,13 +73,11 @@ files:
73
73
  - Rakefile
74
74
  - config/default.yml
75
75
  - lib/rubocop/erb.rb
76
- - lib/rubocop/erb/keyword_remover.rb
76
+ - lib/rubocop/erb/corrector.rb
77
+ - lib/rubocop/erb/erb_source.rb
77
78
  - lib/rubocop/erb/plugin.rb
78
- - lib/rubocop/erb/processed_source_builder.rb
79
- - lib/rubocop/erb/ruby_clip.rb
80
79
  - lib/rubocop/erb/ruby_extractor.rb
81
80
  - lib/rubocop/erb/version.rb
82
- - lib/rubocop/erb/when_decomposer.rb
83
81
  - rubocop-erb.gemspec
84
82
  homepage: https://github.com/r7kamura/rubocop-erb
85
83
  licenses:
@@ -96,14 +94,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
96
94
  requirements:
97
95
  - - ">="
98
96
  - !ruby/object:Gem::Version
99
- version: '3.0'
97
+ version: '3.3'
100
98
  required_rubygems_version: !ruby/object:Gem::Requirement
101
99
  requirements:
102
100
  - - ">="
103
101
  - !ruby/object:Gem::Version
104
102
  version: '0'
105
103
  requirements: []
106
- rubygems_version: 3.6.9
104
+ rubygems_version: 4.0.20
107
105
  specification_version: 4
108
106
  summary: RuboCop plugin for ERB template.
109
107
  test_files: []
@@ -1,160 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RuboCop
4
- module Erb
5
- # Remove unnecessary keyword part (e.g. `if`, `unless`, `do`, ...) from Ruby-ish code.
6
- class KeywordRemover
7
- class << self
8
- # @param [RuboCop::Erb::RubyClip] ruby_clip
9
- # @return [RuboCop::Erb::RubyClip]
10
- def call(ruby_clip)
11
- new(ruby_clip).call
12
- end
13
- end
14
-
15
- # @param [RuboCop::Erb::RubyClip] ruby_clip
16
- def initialize(ruby_clip)
17
- @ruby_clip = ruby_clip
18
- end
19
-
20
- # @return [RuboCop::Erb::RubyClip]
21
- def call
22
- [
23
- PrecedingKeywordRemover,
24
- PrecedingBraceRemover,
25
- TrailingBraceRemover,
26
- TrailingThenRemover,
27
- TrailingDoRemover
28
- ].reduce(@ruby_clip) do |previous, callable|
29
- result = callable.call(previous.code)
30
- RubyClip.new(
31
- code: result.code,
32
- offset: previous.offset + result.offset
33
- )
34
- end
35
- end
36
-
37
- class PrecedingSourceRemover
38
- class << self
39
- # @param [String] code
40
- # @return [RubyClip]
41
- def call(code)
42
- new(code).call
43
- end
44
- end
45
-
46
- # @param [String] code
47
- def initialize(code)
48
- @code = code
49
- end
50
-
51
- # @return [Hash]
52
- def call
53
- data = @code.match(self.class::REGEXP)
54
- if data
55
- offset = data[0].length
56
- RubyClip.new(
57
- code: @code[offset..],
58
- offset: offset
59
- )
60
- else
61
- RubyClip.new(
62
- code: @code,
63
- offset: 0
64
- )
65
- end
66
- end
67
- end
68
-
69
- # Remove preceding keyword.
70
- class PrecedingKeywordRemover < PrecedingSourceRemover
71
- REGEXP = /
72
- \A
73
- \s*
74
- (?:
75
- begin
76
- | case
77
- | else
78
- | elsif
79
- | end
80
- | ensure
81
- | if
82
- | rescue
83
- | unless
84
- | until
85
- | when
86
- | while
87
- | for[ \t]+\w+[ \t]+in
88
- )
89
- \b[ \t]*
90
- /x
91
- end
92
-
93
- # Remove preceding `}`.
94
- class PrecedingBraceRemover < PrecedingSourceRemover
95
- REGEXP = /
96
- \A
97
- \s*
98
- }
99
- /x
100
- end
101
-
102
- class TrailingSourceRemover
103
- class << self
104
- # @param [String] code
105
- # @return [RubyClip]
106
- def call(code)
107
- new(code).call
108
- end
109
- end
110
-
111
- # @param [String] code
112
- def initialize(code)
113
- @code = code
114
- end
115
-
116
- # @return [Hash]
117
- def call
118
- RubyClip.new(
119
- code: @code.sub(self.class::REGEXP, ''),
120
- offset: 0
121
- )
122
- end
123
- end
124
-
125
- # Remove trailing `{`.
126
- class TrailingBraceRemover < TrailingSourceRemover
127
- REGEXP = /
128
- {
129
- [ \t]*
130
- (?:\|[^|]*\|)?
131
- \s*
132
- \z
133
- /x
134
- end
135
-
136
- # Remove trailing `then`.
137
- class TrailingThenRemover < TrailingSourceRemover
138
- REGEXP = /
139
- [ \t]*\b
140
- then
141
- \s*
142
- \z
143
- /x
144
- end
145
-
146
- # Remove trailing `do`.
147
- class TrailingDoRemover < TrailingSourceRemover
148
- REGEXP = /
149
- (?:\b[ \t]*|[ \t])
150
- do
151
- [ \t]*
152
- (?:\|[^|]*\|)?
153
- \s*
154
- (\#.*)?
155
- \z
156
- /x
157
- end
158
- end
159
- end
160
- end
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RuboCop
4
- module Erb
5
- class ProcessedSourceBuilder
6
- class << self
7
- # Creates a new ProcessedSource, inheriting state from a donor.
8
- #
9
- # @param [String] code
10
- # @param [RuboCop::ProcessedSource] processed_source
11
- # @return [RuboCop::ProcessedSource]
12
- def call(
13
- code:,
14
- processed_source:
15
- )
16
- new(
17
- code: code,
18
- processed_source: processed_source
19
- ).call
20
- end
21
- end
22
-
23
- def initialize(
24
- code:,
25
- processed_source:
26
- )
27
- @code = code
28
- @processed_source = processed_source
29
- end
30
-
31
- def call
32
- new_processed_source = ::RuboCop::ProcessedSource.new(
33
- @code,
34
- @processed_source.ruby_version,
35
- @processed_source.path,
36
- parser_engine: @processed_source.parser_engine
37
- )
38
- new_processed_source.config = @processed_source.config
39
- new_processed_source.registry = @processed_source.registry
40
- new_processed_source
41
- end
42
- end
43
- end
44
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RuboCop
4
- module Erb
5
- RubyClip = ::Struct.new(
6
- :code,
7
- :offset,
8
- keyword_init: true
9
- )
10
- end
11
- end
@@ -1,71 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RuboCop
4
- module Erb
5
- class WhenDecomposer
6
- REGEXP = /
7
- \A
8
- \s*
9
- when[ \t]
10
- /x
11
-
12
- class << self
13
- # @param [RuboCop::ProcessedSource] processed_source
14
- # @param [RuboCop::Erb::RubyClip] ruby_clip
15
- # @return [Array<RuboCop::Erb::RubyClip>]
16
- def call(
17
- processed_source,
18
- ruby_clip
19
- )
20
- new(processed_source, ruby_clip).call
21
- end
22
- end
23
-
24
- # @param [RuboCop::ProcessedSource] processed_source
25
- # @param [RuboCop::Erb::RubyClip] ruby_clip
26
- def initialize(
27
- processed_source,
28
- ruby_clip
29
- )
30
- @processed_source = processed_source
31
- @ruby_clip = ruby_clip
32
- end
33
-
34
- # @return [Array<RuboCop::Erb::RubyClip>]
35
- def call
36
- match_data = @ruby_clip.code.match(REGEXP)
37
- if match_data
38
- offset = match_data[0].length
39
- condition = @ruby_clip.code[offset..].sub(/[ \t]then(?:[ \t].*)?/, '')
40
- nodes = parse(
41
- <<~RUBY
42
- [
43
- #{condition}
44
- ]
45
- RUBY
46
- )&.children || []
47
-
48
- nodes.map do |child|
49
- RubyClip.new(
50
- code: child.location.expression.source,
51
- offset: @ruby_clip.offset + offset + child.location.expression.begin_pos - 4
52
- )
53
- end
54
- else
55
- [@ruby_clip]
56
- end
57
- end
58
-
59
- private
60
-
61
- # @param [String] source
62
- # @return [RuboCop::AST::Node]
63
- def parse(source)
64
- ProcessedSourceBuilder.call(
65
- code: source,
66
- processed_source: @processed_source
67
- ).ast
68
- end
69
- end
70
- end
71
- end