haml_lint 0.40.0 → 0.51.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.
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