haml_lint 0.40.0 → 0.51.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/bin/haml-lint +1 -1
  3. data/config/default.yml +9 -27
  4. data/config/forced_rubocop_config.yml +180 -0
  5. data/lib/haml_lint/adapter/haml_4.rb +20 -0
  6. data/lib/haml_lint/adapter/haml_5.rb +11 -0
  7. data/lib/haml_lint/adapter/haml_6.rb +59 -0
  8. data/lib/haml_lint/adapter.rb +2 -0
  9. data/lib/haml_lint/cli.rb +8 -3
  10. data/lib/haml_lint/configuration_loader.rb +49 -13
  11. data/lib/haml_lint/document.rb +89 -8
  12. data/lib/haml_lint/exceptions.rb +6 -0
  13. data/lib/haml_lint/extensions/haml_util_unescape_interpolation_tracking.rb +35 -0
  14. data/lib/haml_lint/file_finder.rb +2 -2
  15. data/lib/haml_lint/lint.rb +10 -1
  16. data/lib/haml_lint/linter/final_newline.rb +4 -3
  17. data/lib/haml_lint/linter/implicit_div.rb +1 -1
  18. data/lib/haml_lint/linter/indentation.rb +3 -3
  19. data/lib/haml_lint/linter/no_placeholders.rb +18 -0
  20. data/lib/haml_lint/linter/repeated_id.rb +2 -1
  21. data/lib/haml_lint/linter/rubocop.rb +353 -59
  22. data/lib/haml_lint/linter/space_before_script.rb +8 -10
  23. data/lib/haml_lint/linter/trailing_empty_lines.rb +22 -0
  24. data/lib/haml_lint/linter/unnecessary_string_output.rb +1 -1
  25. data/lib/haml_lint/linter/view_length.rb +1 -1
  26. data/lib/haml_lint/linter.rb +60 -10
  27. data/lib/haml_lint/linter_registry.rb +3 -5
  28. data/lib/haml_lint/logger.rb +2 -2
  29. data/lib/haml_lint/options.rb +26 -2
  30. data/lib/haml_lint/rake_task.rb +2 -2
  31. data/lib/haml_lint/reporter/hash_reporter.rb +1 -3
  32. data/lib/haml_lint/reporter/offense_count_reporter.rb +1 -1
  33. data/lib/haml_lint/reporter/utils.rb +33 -4
  34. data/lib/haml_lint/ruby_extraction/ad_hoc_chunk.rb +24 -0
  35. data/lib/haml_lint/ruby_extraction/base_chunk.rb +114 -0
  36. data/lib/haml_lint/ruby_extraction/chunk_extractor.rb +672 -0
  37. data/lib/haml_lint/ruby_extraction/coordinator.rb +181 -0
  38. data/lib/haml_lint/ruby_extraction/haml_comment_chunk.rb +34 -0
  39. data/lib/haml_lint/ruby_extraction/implicit_end_chunk.rb +17 -0
  40. data/lib/haml_lint/ruby_extraction/interpolation_chunk.rb +26 -0
  41. data/lib/haml_lint/ruby_extraction/non_ruby_filter_chunk.rb +32 -0
  42. data/lib/haml_lint/ruby_extraction/placeholder_marker_chunk.rb +40 -0
  43. data/lib/haml_lint/ruby_extraction/ruby_filter_chunk.rb +33 -0
  44. data/lib/haml_lint/ruby_extraction/ruby_source.rb +5 -0
  45. data/lib/haml_lint/ruby_extraction/script_chunk.rb +251 -0
  46. data/lib/haml_lint/ruby_extraction/tag_attributes_chunk.rb +63 -0
  47. data/lib/haml_lint/ruby_extraction/tag_script_chunk.rb +30 -0
  48. data/lib/haml_lint/ruby_parser.rb +11 -1
  49. data/lib/haml_lint/runner.rb +35 -3
  50. data/lib/haml_lint/spec/matchers/report_lint.rb +22 -7
  51. data/lib/haml_lint/spec/normalize_indent.rb +2 -2
  52. data/lib/haml_lint/spec/shared_linter_context.rb +9 -1
  53. data/lib/haml_lint/spec/shared_rubocop_autocorrect_context.rb +158 -0
  54. data/lib/haml_lint/spec.rb +1 -0
  55. data/lib/haml_lint/tree/filter_node.rb +10 -0
  56. data/lib/haml_lint/tree/node.rb +13 -4
  57. data/lib/haml_lint/tree/script_node.rb +7 -1
  58. data/lib/haml_lint/tree/silent_script_node.rb +16 -1
  59. data/lib/haml_lint/tree/tag_node.rb +5 -9
  60. data/lib/haml_lint/utils.rb +135 -5
  61. data/lib/haml_lint/version.rb +1 -1
  62. data/lib/haml_lint/version_comparer.rb +25 -0
  63. data/lib/haml_lint.rb +12 -0
  64. metadata +29 -15
  65. data/lib/haml_lint/ruby_extractor.rb +0 -222
@@ -4,7 +4,7 @@ require 'pathname'
4
4
 
5
5
  module HamlLint
6
6
  # A miscellaneous set of utility functions.
7
- module Utils
7
+ module Utils # rubocop:disable Metrics/ModuleLength
8
8
  module_function
9
9
 
10
10
  # Returns whether a glob pattern (or any of a list of patterns) matches the
@@ -52,12 +52,21 @@ module HamlLint
52
52
  # the text.
53
53
  # @yieldparam interpolated_code [String] code that was interpolated
54
54
  # @yieldparam line [Integer] line number code appears on in text
55
- def extract_interpolated_values(text)
55
+ def extract_interpolated_values(text) # rubocop:disable Metrics/AbcSize
56
56
  dumped_text = text.dump
57
- newline_positions = extract_substring_positions(dumped_text, '\\\n')
57
+
58
+ # Basically, match pairs of '\' and '\ followed by the letter 'n'
59
+ quoted_regex_s = "(#{Regexp.quote('\\\\')}|#{Regexp.quote('\\n')})"
60
+ newline_positions = extract_substring_positions(dumped_text, quoted_regex_s)
61
+
62
+ # Filter the matches to only keep those ending in 'n'.
63
+ # This way, escaped \n will not be considered
64
+ newline_positions.select! do |pos|
65
+ dumped_text[pos - 1] == 'n'
66
+ end
58
67
 
59
68
  Haml::Util.handle_interpolation(dumped_text) do |scan|
60
- line = (newline_positions.find_index { |marker| scan.pos <= marker } ||
69
+ line = (newline_positions.find_index { |marker| scan.charpos <= marker } ||
61
70
  newline_positions.size) + 1
62
71
 
63
72
  escape_count = (scan[2].size - 1) / 2
@@ -70,6 +79,41 @@ module HamlLint
70
79
  end
71
80
  end
72
81
 
82
+ def handle_interpolation_with_indexes(text)
83
+ newline_indexes = extract_substring_positions(text, "\n")
84
+
85
+ handle_interpolation_with_newline(text) do |scan|
86
+ line_index = newline_indexes.find_index { |index| scan.charpos <= index }
87
+ line_index ||= newline_indexes.size
88
+
89
+ line_start_char_index = if line_index == 0
90
+ 0
91
+ else
92
+ newline_indexes[line_index - 1]
93
+ end
94
+
95
+ char_index = scan.charpos - line_start_char_index
96
+
97
+ yield scan, line_index, char_index
98
+ end
99
+ end
100
+
101
+ if Gem::Version.new(Haml::VERSION) >= Gem::Version.new('5')
102
+ # Same as Haml::Util.handle_interpolation, but enables multiline mode on the regex
103
+ def handle_interpolation_with_newline(str)
104
+ scan = StringScanner.new(str)
105
+ yield scan while scan.scan(/(.*?)(\\*)#([{@$])/m)
106
+ scan.rest
107
+ end
108
+ else
109
+ # Same as Haml::Util.handle_interpolation, but enables multiline mode on the regex
110
+ def handle_interpolation_with_newline(str)
111
+ scan = StringScanner.new(str)
112
+ yield scan while scan.scan(/(.*?)(\\*)\#\{/m)
113
+ scan.rest
114
+ end
115
+ end
116
+
73
117
  # Returns indexes of all occurrences of a substring within a string.
74
118
  #
75
119
  # Note, this will not return overlaping substrings, so searching for "aa"
@@ -81,7 +125,7 @@ module HamlLint
81
125
  def extract_substring_positions(text, substr)
82
126
  positions = []
83
127
  scanner = StringScanner.new(text)
84
- positions << scanner.pos while scanner.scan(/(.*?)#{substr}/)
128
+ positions << scanner.charpos while scanner.scan(/(.*?)#{substr}/)
85
129
  positions
86
130
  end
87
131
 
@@ -136,6 +180,22 @@ module HamlLint
136
180
  count
137
181
  end
138
182
 
183
+ # Process ERB, providing some values for for versions to it
184
+ #
185
+ # @param content [String] the (usually yaml) content to process
186
+ # @return [String]
187
+ def process_erb(content)
188
+ # Variables for use in the ERB's post-processing
189
+ rubocop_version = HamlLint::VersionComparer.for_rubocop
190
+
191
+ ERB.new(content).result(binding)
192
+ end
193
+
194
+ def insert_after_indentation(code, insert)
195
+ index = code.index(/\S/)
196
+ "#{code[0...index]}#{insert}#{code[index..]}"
197
+ end
198
+
139
199
  # Calls a block of code with a modified set of environment variables,
140
200
  # restoring them once the code has executed.
141
201
  #
@@ -151,5 +211,75 @@ module HamlLint
151
211
  ensure
152
212
  old_env.each { |var, value| ENV[var.to_s] = value }
153
213
  end
214
+
215
+ def indent(string, nb_indent)
216
+ if nb_indent < 0
217
+ string.gsub(/^ {1,#{-nb_indent}}/, '')
218
+ else
219
+ string.gsub(/^/, ' ' * nb_indent)
220
+ end
221
+ end
222
+
223
+ def map_subset!(array, range, &block)
224
+ subset = array[range]
225
+ return if subset.nil? || subset.empty?
226
+
227
+ array[range] = subset.map(&block)
228
+ end
229
+
230
+ def map_after_first!(array, &block)
231
+ map_subset!(array, 1..-1, &block)
232
+ end
233
+
234
+ # Returns true if line is only whitespace.
235
+ # Note, this is not like blank? is rails. For nil, this returns false.
236
+ def is_blank_line?(line)
237
+ line && line.index(/\S/).nil?
238
+ end
239
+
240
+ def check_error_when_compiling_haml(haml_string)
241
+ begin
242
+ ruby_code = ::HamlLint::Adapter.detect_class.new(haml_string).precompile
243
+ rescue StandardError => e
244
+ return e
245
+ end
246
+ eval("BEGIN {return nil}; #{ruby_code}", binding, __FILE__, __LINE__) # rubocop:disable Security/Eval
247
+ # The eval will return nil
248
+ rescue ::SyntaxError
249
+ $!
250
+ end
251
+
252
+ # Overrides the global stdin, stdout and stderr while within the block, to
253
+ # push a string in stdin, and capture both stdout and stderr which are returned.
254
+ #
255
+ # @param stdin_str [String] the string to push in as stdin
256
+ # @param _block [Block] the block to perform with the overridden std streams
257
+ # @return [String, String]
258
+ def with_captured_streams(stdin_str, &_block)
259
+ original_stdin = $stdin
260
+ # The dup is needed so that stdin_data isn't altered (encoding-wise at least)
261
+ $stdin = StringIO.new(stdin_str.dup)
262
+ begin
263
+ original_stdout = $stdout
264
+ $stdout = StringIO.new
265
+ begin
266
+ original_stderr = $stderr
267
+ $stderr = StringIO.new
268
+ yield
269
+ [$stdout.string, $stderr.string]
270
+ ensure
271
+ $stderr = original_stderr
272
+ end
273
+ ensure
274
+ $stdout = original_stdout
275
+ end
276
+ ensure
277
+ $stdin = original_stdin
278
+ end
279
+
280
+ def regexp_for_parts(parts, join_regexp)
281
+ regexp_code = parts.map { |c| Regexp.quote(c) }.join(join_regexp)
282
+ Regexp.new(regexp_code)
283
+ end
154
284
  end
155
285
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Defines the gem version.
4
4
  module HamlLint
5
- VERSION = '0.40.0'
5
+ VERSION = '0.51.0'
6
6
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HamlLint
4
+ # A simple wrapper around Gem::Version to allow comparison with String instances
5
+ # This makes code shorter in some places
6
+ class VersionComparer
7
+ def initialize(version)
8
+ @version = Gem::Version.new(version)
9
+ end
10
+
11
+ include Comparable
12
+ def <=>(other)
13
+ @version <=> Gem::Version.new(other)
14
+ end
15
+
16
+ # Shortcut to create a version comparer for the current RuboCop's version
17
+ def self.for_rubocop
18
+ new(RuboCop::Version::STRING)
19
+ end
20
+
21
+ def self.for_haml
22
+ new(Haml::VERSION)
23
+ end
24
+ end
25
+ end
data/lib/haml_lint.rb CHANGED
@@ -21,8 +21,14 @@ require 'haml_lint/file_finder'
21
21
  require 'haml_lint/runner'
22
22
  require 'haml_lint/utils'
23
23
  require 'haml_lint/version'
24
+ require 'haml_lint/version_comparer'
24
25
  require 'haml_lint/severity'
25
26
 
27
+ # Lead all extensions to external source code
28
+ Dir[File.expand_path('haml_lint/extensions/*.rb', File.dirname(__FILE__))].sort.each do |file|
29
+ require file
30
+ end
31
+
26
32
  # Load all parse tree node classes
27
33
  require 'haml_lint/tree/node'
28
34
  require 'haml_lint/node_transformer'
@@ -39,3 +45,9 @@ end
39
45
  Dir[File.expand_path('haml_lint/reporter/*.rb', File.dirname(__FILE__))].sort.each do |file|
40
46
  require file
41
47
  end
48
+
49
+ # Load all the chunks for RubyExtraction
50
+ require 'haml_lint/ruby_extraction/base_chunk'
51
+ Dir[File.expand_path('haml_lint/ruby_extraction/*.rb', File.dirname(__FILE__))].sort.each do |file|
52
+ require file
53
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: haml_lint
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.40.0
4
+ version: 0.51.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shane da Silva
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-02-27 00:00:00.000000000 Z
11
+ date: 2023-10-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: haml
@@ -17,9 +17,6 @@ dependencies:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '4.0'
20
- - - "<"
21
- - !ruby/object:Gem::Version
22
- version: '5.3'
23
20
  type: :runtime
24
21
  prerelease: false
25
22
  version_requirements: !ruby/object:Gem::Requirement
@@ -27,9 +24,6 @@ dependencies:
27
24
  - - ">="
28
25
  - !ruby/object:Gem::Version
29
26
  version: '4.0'
30
- - - "<"
31
- - !ruby/object:Gem::Version
32
- version: '5.3'
33
27
  - !ruby/object:Gem::Dependency
34
28
  name: parallel
35
29
  requirement: !ruby/object:Gem::Requirement
@@ -64,14 +58,14 @@ dependencies:
64
58
  requirements:
65
59
  - - ">="
66
60
  - !ruby/object:Gem::Version
67
- version: 0.50.0
61
+ version: '1.0'
68
62
  type: :runtime
69
63
  prerelease: false
70
64
  version_requirements: !ruby/object:Gem::Requirement
71
65
  requirements:
72
66
  - - ">="
73
67
  - !ruby/object:Gem::Version
74
- version: 0.50.0
68
+ version: '1.0'
75
69
  - !ruby/object:Gem::Dependency
76
70
  name: sysexits
77
71
  requirement: !ruby/object:Gem::Requirement
@@ -96,10 +90,12 @@ extra_rdoc_files: []
96
90
  files:
97
91
  - bin/haml-lint
98
92
  - config/default.yml
93
+ - config/forced_rubocop_config.yml
99
94
  - lib/haml_lint.rb
100
95
  - lib/haml_lint/adapter.rb
101
96
  - lib/haml_lint/adapter/haml_4.rb
102
97
  - lib/haml_lint/adapter/haml_5.rb
98
+ - lib/haml_lint/adapter/haml_6.rb
103
99
  - lib/haml_lint/cli.rb
104
100
  - lib/haml_lint/comment_configuration.rb
105
101
  - lib/haml_lint/configuration.rb
@@ -108,6 +104,7 @@ files:
108
104
  - lib/haml_lint/directive.rb
109
105
  - lib/haml_lint/document.rb
110
106
  - lib/haml_lint/exceptions.rb
107
+ - lib/haml_lint/extensions/haml_util_unescape_interpolation_tracking.rb
111
108
  - lib/haml_lint/file_finder.rb
112
109
  - lib/haml_lint/haml_visitor.rb
113
110
  - lib/haml_lint/lint.rb
@@ -131,6 +128,7 @@ files:
131
128
  - lib/haml_lint/linter/line_length.rb
132
129
  - lib/haml_lint/linter/multiline_pipe.rb
133
130
  - lib/haml_lint/linter/multiline_script.rb
131
+ - lib/haml_lint/linter/no_placeholders.rb
134
132
  - lib/haml_lint/linter/object_reference_attributes.rb
135
133
  - lib/haml_lint/linter/repeated_id.rb
136
134
  - lib/haml_lint/linter/rubocop.rb
@@ -139,6 +137,7 @@ files:
139
137
  - lib/haml_lint/linter/space_inside_hash_attributes.rb
140
138
  - lib/haml_lint/linter/syntax.rb
141
139
  - lib/haml_lint/linter/tag_name.rb
140
+ - lib/haml_lint/linter/trailing_empty_lines.rb
142
141
  - lib/haml_lint/linter/trailing_whitespace.rb
143
142
  - lib/haml_lint/linter/unnecessary_interpolation.rb
144
143
  - lib/haml_lint/linter/unnecessary_string_output.rb
@@ -161,7 +160,20 @@ files:
161
160
  - lib/haml_lint/reporter/offense_count_reporter.rb
162
161
  - lib/haml_lint/reporter/progress_reporter.rb
163
162
  - lib/haml_lint/reporter/utils.rb
164
- - lib/haml_lint/ruby_extractor.rb
163
+ - lib/haml_lint/ruby_extraction/ad_hoc_chunk.rb
164
+ - lib/haml_lint/ruby_extraction/base_chunk.rb
165
+ - lib/haml_lint/ruby_extraction/chunk_extractor.rb
166
+ - lib/haml_lint/ruby_extraction/coordinator.rb
167
+ - lib/haml_lint/ruby_extraction/haml_comment_chunk.rb
168
+ - lib/haml_lint/ruby_extraction/implicit_end_chunk.rb
169
+ - lib/haml_lint/ruby_extraction/interpolation_chunk.rb
170
+ - lib/haml_lint/ruby_extraction/non_ruby_filter_chunk.rb
171
+ - lib/haml_lint/ruby_extraction/placeholder_marker_chunk.rb
172
+ - lib/haml_lint/ruby_extraction/ruby_filter_chunk.rb
173
+ - lib/haml_lint/ruby_extraction/ruby_source.rb
174
+ - lib/haml_lint/ruby_extraction/script_chunk.rb
175
+ - lib/haml_lint/ruby_extraction/tag_attributes_chunk.rb
176
+ - lib/haml_lint/ruby_extraction/tag_script_chunk.rb
165
177
  - lib/haml_lint/ruby_parser.rb
166
178
  - lib/haml_lint/runner.rb
167
179
  - lib/haml_lint/severity.rb
@@ -169,6 +181,7 @@ files:
169
181
  - lib/haml_lint/spec/matchers/report_lint.rb
170
182
  - lib/haml_lint/spec/normalize_indent.rb
171
183
  - lib/haml_lint/spec/shared_linter_context.rb
184
+ - lib/haml_lint/spec/shared_rubocop_autocorrect_context.rb
172
185
  - lib/haml_lint/tree/comment_node.rb
173
186
  - lib/haml_lint/tree/doctype_node.rb
174
187
  - lib/haml_lint/tree/filter_node.rb
@@ -182,11 +195,12 @@ files:
182
195
  - lib/haml_lint/tree/tag_node.rb
183
196
  - lib/haml_lint/utils.rb
184
197
  - lib/haml_lint/version.rb
198
+ - lib/haml_lint/version_comparer.rb
185
199
  homepage: https://github.com/sds/haml-lint
186
200
  licenses:
187
201
  - MIT
188
202
  metadata: {}
189
- post_install_message:
203
+ post_install_message:
190
204
  rdoc_options: []
191
205
  require_paths:
192
206
  - lib
@@ -194,7 +208,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
194
208
  requirements:
195
209
  - - ">="
196
210
  - !ruby/object:Gem::Version
197
- version: 2.4.0
211
+ version: 2.7.0
198
212
  required_rubygems_version: !ruby/object:Gem::Requirement
199
213
  requirements:
200
214
  - - ">="
@@ -202,7 +216,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
202
216
  version: '0'
203
217
  requirements: []
204
218
  rubygems_version: 3.1.6
205
- signing_key:
219
+ signing_key:
206
220
  specification_version: 4
207
221
  summary: HAML lint tool
208
222
  test_files: []
@@ -1,222 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module HamlLint
4
- # Utility class for extracting Ruby script from a HAML file that can then be
5
- # linted with a Ruby linter (i.e. is "legal" Ruby). The goal is to turn this:
6
- #
7
- # - if signed_in?(viewer)
8
- # %span Stuff
9
- # = link_to 'Sign Out', sign_out_path
10
- # - else
11
- # .some-class{ class: my_method }= my_method
12
- # = link_to 'Sign In', sign_in_path
13
- #
14
- # into this:
15
- #
16
- # if signed_in?(viewer)
17
- # link_to 'Sign Out', sign_out_path
18
- # else
19
- # { class: my_method }
20
- # my_method
21
- # link_to 'Sign In', sign_in_path
22
- # end
23
- #
24
- # The translation won't be perfect, and won't make any real sense, but the
25
- # relationship between variable declarations/uses and the flow control graph
26
- # will remain intact.
27
- class RubyExtractor # rubocop:disable Metrics/ClassLength
28
- include HamlVisitor
29
-
30
- # Stores the extracted source and a map of lines of generated source to the
31
- # original source that created them.
32
- #
33
- # @attr_reader source [String] generated source code
34
- # @attr_reader source_map [Hash] map of line numbers from generated source
35
- # to original source line number
36
- RubySource = Struct.new(:source, :source_map)
37
-
38
- # Extracts Ruby code from Sexp representing a Slim document.
39
- #
40
- # @param document [HamlLint::Document]
41
- # @return [HamlLint::RubyExtractor::RubySource]
42
- def extract(document)
43
- visit(document.tree)
44
- RubySource.new(@source_lines.join("\n"), @source_map)
45
- end
46
-
47
- def visit_root(_node)
48
- @source_lines = []
49
- @source_map = {}
50
- @line_count = 0
51
- @indent_level = 0
52
- @output_count = 0
53
-
54
- yield # Collect lines of code from children
55
- end
56
-
57
- def visit_plain(node)
58
- # Don't output the text, as we don't want to have to deal with any RuboCop
59
- # cops regarding StringQuotes or AsciiComments, and it's not important to
60
- # overall document anyway.
61
- add_dummy_puts(node)
62
- end
63
-
64
- def visit_tag(node)
65
- additional_attributes = node.dynamic_attributes_sources
66
-
67
- # Include dummy references to code executed in attributes list
68
- # (this forces a "use" of a variable to prevent "assigned but unused
69
- # variable" lints)
70
- additional_attributes.each do |attributes_code|
71
- # Normalize by removing excess whitespace to avoid format lints
72
- attributes_code = attributes_code.gsub(/\s*\n\s*/, ' ').strip
73
-
74
- # Attributes can either be a method call or a literal hash, so wrap it
75
- # in a method call itself in order to avoid having to differentiate the
76
- # two.
77
- add_line("{}.merge(#{attributes_code.strip})", node)
78
- end
79
-
80
- check_tag_static_hash_source(node)
81
-
82
- # We add a dummy puts statement to represent the tag name being output.
83
- # This prevents some erroneous RuboCop warnings.
84
- add_dummy_puts(node, node.tag_name)
85
-
86
- code = node.script.strip
87
- add_line(code, node) unless code.empty?
88
- end
89
-
90
- def after_visit_tag(node)
91
- # We add a dummy puts statement for closing tag.
92
- add_dummy_puts(node, "#{node.tag_name}/")
93
- end
94
-
95
- def visit_script(node)
96
- code = node.text
97
- add_line(code.strip, node)
98
-
99
- start_block = anonymous_block?(code) || start_block_keyword?(code)
100
-
101
- if start_block
102
- @indent_level += 1
103
- end
104
-
105
- yield # Continue extracting code from children
106
-
107
- if start_block
108
- @indent_level -= 1
109
- add_line('end', node)
110
- end
111
- end
112
-
113
- def visit_haml_comment(node)
114
- # We want to preseve leading whitespace if it exists, but include leading
115
- # whitespace if it doesn't exist so that RuboCop's LeadingCommentSpace
116
- # doesn't complain
117
- comment = node.text
118
- .gsub(/\n(\S)/, "\n# \\1")
119
- .gsub(/\n(\s)/, "\n#\\1")
120
- add_line("##{comment}", node)
121
- end
122
-
123
- def visit_silent_script(node, &block)
124
- visit_script(node, &block)
125
- end
126
-
127
- def visit_filter(node)
128
- if node.filter_type == 'ruby'
129
- node.text.split("\n").each_with_index do |line, index|
130
- add_line(line, node.line + index + 1, false)
131
- end
132
- else
133
- add_dummy_puts(node, ":#{node.filter_type}")
134
- HamlLint::Utils.extract_interpolated_values(node.text) do |interpolated_code, line|
135
- add_line(interpolated_code, node.line + line)
136
- end
137
- end
138
- end
139
-
140
- private
141
-
142
- def check_tag_static_hash_source(node)
143
- # Haml::Parser converts hashrocket-style hash attributes of strings and symbols
144
- # to static attributes, and excludes them from the dynamic attribute sources:
145
- # https://github.com/haml/haml/blob/08f97ec4dc8f59fe3d7f6ab8f8807f86f2a15b68/lib/haml/parser.rb#L400-L404
146
- # https://github.com/haml/haml/blob/08f97ec4dc8f59fe3d7f6ab8f8807f86f2a15b68/lib/haml/parser.rb#L540-L554
147
- # Here, we add the hash source back in so it can be inspected by rubocop.
148
- if node.hash_attributes? && node.dynamic_attributes_sources.empty?
149
- normalized_attr_source = node.dynamic_attributes_source[:hash].gsub(/\s*\n\s*/, ' ')
150
-
151
- add_line(normalized_attr_source, node)
152
- end
153
- end
154
-
155
- # Adds a dummy method call with a unique name so we don't get
156
- # Style/IdenticalConditionalBranches RuboCop warnings
157
- def add_dummy_puts(node, annotation = nil)
158
- annotation = " # #{annotation}" if annotation
159
- add_line("_haml_lint_puts_#{@output_count}#{annotation}", node)
160
- @output_count += 1
161
- end
162
-
163
- def add_line(code, node_or_line, discard_blanks = true)
164
- return if code.empty? && discard_blanks
165
-
166
- indent_level = @indent_level
167
-
168
- if node_or_line.respond_to?(:line)
169
- # Since mid-block keywords are children of the corresponding start block
170
- # keyword, we need to reduce their indentation level by 1. However, we
171
- # don't do this unless this is an actual tag node (a raw line number
172
- # means this came from a `:ruby` filter).
173
- indent_level -= 1 if mid_block_keyword?(code)
174
- end
175
-
176
- indent = (' ' * 2 * indent_level)
177
-
178
- @source_lines << indent_code(code, indent)
179
-
180
- original_line =
181
- node_or_line.respond_to?(:line) ? node_or_line.line : node_or_line
182
-
183
- # For interpolated code in filters that spans multiple lines, the
184
- # resulting code will span multiple lines, so we need to create a
185
- # mapping for each line.
186
- (code.count("\n") + 1).times do
187
- @line_count += 1
188
- @source_map[@line_count] = original_line
189
- end
190
- end
191
-
192
- def indent_code(code, indent)
193
- codes = code.split("\n")
194
- codes.map { |c| indent + c }.join("\n")
195
- end
196
-
197
- def anonymous_block?(text)
198
- text =~ /\bdo\s*(\|\s*[^\|]*\s*\|)?(\s*#.*)?\z/
199
- end
200
-
201
- START_BLOCK_KEYWORDS = %w[if unless case begin for until while].freeze
202
- def start_block_keyword?(text)
203
- START_BLOCK_KEYWORDS.include?(block_keyword(text))
204
- end
205
-
206
- MID_BLOCK_KEYWORDS = %w[else elsif when rescue ensure].freeze
207
- def mid_block_keyword?(text)
208
- MID_BLOCK_KEYWORDS.include?(block_keyword(text))
209
- end
210
-
211
- LOOP_KEYWORDS = %w[for until while].freeze
212
- def block_keyword(text)
213
- # Need to handle 'for'/'while' since regex stolen from HAML parser doesn't
214
- if keyword = text[/\A\s*([^\s]+)\s+/, 1]
215
- return keyword if LOOP_KEYWORDS.include?(keyword)
216
- end
217
-
218
- return unless keyword = text.scan(Haml::Parser::BLOCK_KEYWORD_REGEX)[0]
219
- keyword[0] || keyword[1]
220
- end
221
- end
222
- end