haml_lint 0.45.0 → 0.48.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/bin/haml-lint +1 -1
  3. data/config/default.yml +6 -28
  4. data/config/forced_rubocop_config.yml +171 -0
  5. data/lib/haml_lint/adapter/haml_4.rb +18 -0
  6. data/lib/haml_lint/adapter/haml_5.rb +11 -0
  7. data/lib/haml_lint/adapter/haml_6.rb +11 -0
  8. data/lib/haml_lint/cli.rb +8 -3
  9. data/lib/haml_lint/configuration_loader.rb +9 -10
  10. data/lib/haml_lint/document.rb +89 -8
  11. data/lib/haml_lint/exceptions.rb +6 -0
  12. data/lib/haml_lint/extensions/haml_util_unescape_interpolation_tracking.rb +35 -0
  13. data/lib/haml_lint/file_finder.rb +2 -2
  14. data/lib/haml_lint/lint.rb +10 -1
  15. data/lib/haml_lint/linter/final_newline.rb +4 -3
  16. data/lib/haml_lint/linter/implicit_div.rb +1 -1
  17. data/lib/haml_lint/linter/indentation.rb +3 -3
  18. data/lib/haml_lint/linter/no_placeholders.rb +18 -0
  19. data/lib/haml_lint/linter/repeated_id.rb +2 -1
  20. data/lib/haml_lint/linter/rubocop.rb +353 -60
  21. data/lib/haml_lint/linter/space_before_script.rb +8 -10
  22. data/lib/haml_lint/linter/unnecessary_string_output.rb +1 -1
  23. data/lib/haml_lint/linter/view_length.rb +1 -1
  24. data/lib/haml_lint/linter.rb +60 -10
  25. data/lib/haml_lint/linter_registry.rb +3 -5
  26. data/lib/haml_lint/logger.rb +2 -2
  27. data/lib/haml_lint/options.rb +26 -2
  28. data/lib/haml_lint/rake_task.rb +2 -2
  29. data/lib/haml_lint/reporter/hash_reporter.rb +1 -3
  30. data/lib/haml_lint/reporter/offense_count_reporter.rb +1 -1
  31. data/lib/haml_lint/reporter/utils.rb +33 -4
  32. data/lib/haml_lint/ruby_extraction/ad_hoc_chunk.rb +24 -0
  33. data/lib/haml_lint/ruby_extraction/base_chunk.rb +114 -0
  34. data/lib/haml_lint/ruby_extraction/chunk_extractor.rb +630 -0
  35. data/lib/haml_lint/ruby_extraction/coordinator.rb +181 -0
  36. data/lib/haml_lint/ruby_extraction/haml_comment_chunk.rb +34 -0
  37. data/lib/haml_lint/ruby_extraction/implicit_end_chunk.rb +17 -0
  38. data/lib/haml_lint/ruby_extraction/interpolation_chunk.rb +26 -0
  39. data/lib/haml_lint/ruby_extraction/non_ruby_filter_chunk.rb +32 -0
  40. data/lib/haml_lint/ruby_extraction/placeholder_marker_chunk.rb +40 -0
  41. data/lib/haml_lint/ruby_extraction/ruby_filter_chunk.rb +33 -0
  42. data/lib/haml_lint/ruby_extraction/ruby_source.rb +5 -0
  43. data/lib/haml_lint/ruby_extraction/script_chunk.rb +244 -0
  44. data/lib/haml_lint/ruby_extraction/tag_attributes_chunk.rb +63 -0
  45. data/lib/haml_lint/ruby_extraction/tag_script_chunk.rb +30 -0
  46. data/lib/haml_lint/runner.rb +35 -3
  47. data/lib/haml_lint/spec/matchers/report_lint.rb +22 -7
  48. data/lib/haml_lint/spec/normalize_indent.rb +2 -2
  49. data/lib/haml_lint/spec/shared_linter_context.rb +9 -1
  50. data/lib/haml_lint/spec/shared_rubocop_autocorrect_context.rb +158 -0
  51. data/lib/haml_lint/spec.rb +1 -0
  52. data/lib/haml_lint/tree/filter_node.rb +10 -0
  53. data/lib/haml_lint/tree/node.rb +13 -4
  54. data/lib/haml_lint/tree/tag_node.rb +5 -9
  55. data/lib/haml_lint/utils.rb +135 -5
  56. data/lib/haml_lint/version.rb +1 -1
  57. data/lib/haml_lint/version_comparer.rb +25 -0
  58. data/lib/haml_lint.rb +12 -0
  59. metadata +24 -6
  60. data/lib/haml_lint/ruby_extractor.rb +0 -223
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HamlLint::RubyExtraction
4
+ # Chunk for handling outputting scripts after a tag, such as `%div= spam`
5
+ class TagScriptChunk < BaseChunk
6
+ def transfer_correction_logic(coordinator, to_ruby_lines, haml_lines) # rubocop:disable Metrics/AbcSize
7
+ # TODO: add checks that we have commas at the end of each line except the last one
8
+
9
+ from_ruby_line = @ruby_lines.first
10
+ to_ruby_line = to_ruby_lines.first
11
+
12
+ to_line_indent = to_ruby_line.index(/\S/)
13
+
14
+ from_ruby_line = from_ruby_line.sub(coordinator.script_output_prefix, '').sub(/^\s+/, '')
15
+ to_ruby_line = to_ruby_line.sub(coordinator.script_output_prefix, '').sub(/^\s+/, '')
16
+
17
+ affected_start_index = haml_lines[@haml_line_index].rindex(from_ruby_line)
18
+
19
+ haml_lines[@haml_line_index][affected_start_index..-1] = to_ruby_line
20
+
21
+ indent_delta = affected_start_index - coordinator.script_output_prefix.size - to_line_indent
22
+
23
+ HamlLint::Utils.map_after_first!(to_ruby_lines) do |line|
24
+ HamlLint::Utils.indent(line, indent_delta)
25
+ end
26
+
27
+ haml_lines[(@haml_line_index + 1)..haml_end_line_index] = to_ruby_lines[1..]
28
+ end
29
+ end
30
+ end
@@ -24,6 +24,8 @@ module HamlLint
24
24
  @linter_selector = HamlLint::LinterSelector.new(config, options)
25
25
  @fail_fast = options.fetch(:fail_fast, false)
26
26
  @cache = {}
27
+ @autocorrect = options[:autocorrect]
28
+ @autocorrect_only = options[:autocorrect_only]
27
29
 
28
30
  report(options)
29
31
  end
@@ -88,9 +90,39 @@ module HamlLint
88
90
  e.line, e.to_s, :error)]
89
91
  end
90
92
 
91
- linter_selector.linters_for_file(file).map do |linter|
92
- linter.run(document)
93
- end.flatten
93
+ linters = linter_selector.linters_for_file(file)
94
+ lint_arrays = []
95
+
96
+ if @autocorrect
97
+ lint_arrays << autocorrect_document(document, linters)
98
+ end
99
+
100
+ unless @autocorrect_only
101
+ lint_arrays << linters.map do |linter|
102
+ linter.run(document)
103
+ end
104
+ end
105
+ lint_arrays.flatten
106
+ end
107
+
108
+ # Out of the provided linters, runs those that support autocorrect
109
+ # against the specified document.
110
+ # Updates the document and returns the lints that were corrected.
111
+ #
112
+ # @param document [HamlLint::Document]
113
+ # @param linter_selector [HamlLint::LinterSelector]
114
+ # @return [Array<HamlLint::Lint>]
115
+ def autocorrect_document(document, linters)
116
+ lint_arrays = []
117
+
118
+ autocorrecting_linters = linters.select(&:supports_autocorrect?)
119
+ lint_arrays << autocorrecting_linters.map do |linter|
120
+ linter.run(document, autocorrect: @autocorrect)
121
+ end
122
+
123
+ document.write_to_disk!
124
+
125
+ lint_arrays
94
126
  end
95
127
 
96
128
  # Returns the list of files that should be linted given the specified
@@ -10,9 +10,11 @@ module HamlLint
10
10
  expected_line = options[:line]
11
11
  expected_message = options[:message]
12
12
  expected_severity = options[:severity]
13
+ expected_corrected = options[:corrected]
13
14
 
14
15
  match do |linter|
15
- has_lints?(linter, expected_line, count, expected_message, expected_severity)
16
+ has_lints?(linter, expected_line, count, expected_message, expected_severity,
17
+ expected_corrected)
16
18
  end
17
19
 
18
20
  failure_message do |linter|
@@ -63,13 +65,15 @@ module HamlLint
63
65
  (expected_severity ? " with severity '#{expected_severity}'" : '')
64
66
  end
65
67
 
66
- def has_lints?(linter, expected_line, count, expected_message, expected_severity)
68
+ def has_lints?(linter, expected_line, count, expected_message, expected_severity, # rubocop:disable Metrics/ParameterLists
69
+ expected_corrected)
67
70
  if expected_line
68
71
  has_expected_line_lints?(linter,
69
72
  expected_line,
70
73
  count,
71
74
  expected_message,
72
- expected_severity)
75
+ expected_severity,
76
+ expected_corrected)
73
77
  elsif count
74
78
  linter.lints.count == count
75
79
  elsif expected_message
@@ -79,17 +83,20 @@ module HamlLint
79
83
  end
80
84
  end
81
85
 
82
- def has_expected_line_lints?(linter,
86
+ def has_expected_line_lints?(linter, # rubocop:disable Metrics/ParameterLists
83
87
  expected_line,
84
88
  count,
85
89
  expected_message,
86
- expected_severity)
90
+ expected_severity,
91
+ expected_corrected)
87
92
  if count
88
93
  multiple_lints_match_line?(linter, expected_line, count)
89
94
  elsif expected_message
90
95
  lint_on_line_matches_message?(linter, expected_line, expected_message)
91
96
  elsif expected_severity
92
97
  lint_on_line_matches_severity?(linter, expected_line, expected_severity)
98
+ elsif !expected_corrected.nil?
99
+ lint_on_line_matches_corrected?(linter, expected_line, expected_corrected)
93
100
  else
94
101
  lint_lines(linter).include?(expected_line)
95
102
  end
@@ -101,9 +108,10 @@ module HamlLint
101
108
  end
102
109
 
103
110
  def lint_on_line_matches_message?(linter, expected_line, expected_message)
111
+ # Using === to support regex to match anywhere in the string
104
112
  linter
105
113
  .lints
106
- .any? { |lint| lint.line == expected_line && lint.message == expected_message }
114
+ .any? { |lint| lint.line == expected_line && expected_message === lint.message } # rubocop:disable Style/CaseEquality
107
115
  end
108
116
 
109
117
  def lint_on_line_matches_severity?(linter, expected_line, expected_severity)
@@ -112,8 +120,15 @@ module HamlLint
112
120
  .any? { |lint| lint.line == expected_line && lint.severity == expected_severity }
113
121
  end
114
122
 
123
+ def lint_on_line_matches_corrected?(linter, expected_line, expected_corrected)
124
+ linter
125
+ .lints
126
+ .any? { |lint| lint.line == expected_line && lint.corrected == expected_corrected }
127
+ end
128
+
115
129
  def lint_messages_match?(linter, expected_message)
116
- lint_messages(linter).all? { |message| message == expected_message }
130
+ # Using === to support regex to match anywhere in the string
131
+ lint_messages(linter).all? { |message| expected_message === message } # rubocop:disable Style/CaseEquality
117
132
  end
118
133
 
119
134
  def lint_lines(linter)
@@ -6,8 +6,8 @@ module HamlLint
6
6
  # for writing code without having the leading indentation count.
7
7
  module IndentNormalizer
8
8
  def normalize_indent(code)
9
- leading_indent = code[/^(\s*)/, 1]
10
- code.lstrip.gsub(/\n#{leading_indent}/, "\n")
9
+ leading_indent = code[/([ \t]*)/, 1]
10
+ code.gsub(/^#{leading_indent}/, '')
11
11
  end
12
12
  end
13
13
  end
@@ -14,13 +14,21 @@ module HamlLint
14
14
  }
15
15
  end
16
16
 
17
+ let(:autocorrect) { nil }
18
+
17
19
  let(:config) { options[:config].for_linter(described_class) }
18
20
 
19
21
  let(:document) { HamlLint::Document.new(normalize_indent(haml), options) }
20
22
 
23
+ # :run_or_raise, :run, or nil to not auto-call something
24
+ let(:run_method_to_use) { :run_or_raise }
25
+
21
26
  subject { described_class.new(config) }
22
27
 
23
- before { subject.run(document) }
28
+ before do
29
+ next unless run_method_to_use
30
+ subject.send(run_method_to_use, document, autocorrect: autocorrect)
31
+ end
24
32
  end
25
33
  end
26
34
  end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Makes writing tests for linters a lot DRYer by taking any currently `haml`
4
+ # variable defined via `let` and normalizing it and running the linter against
5
+ # it, allowing specs to simply specify whether a lint was reported.
6
+
7
+ module HamlLint
8
+ module Spec
9
+ module SharedRubocopAutocorrectContext
10
+ RSpec.shared_context 'rubocop_autocorrect' do
11
+ # Setting ENV['STUB_RUBOCOP'] to 1 or true makes rubocop tests faster by not involving rubocop.
12
+ # It is sometimes automatically activated for tests which need a different Rubocop version
13
+ stub_rubocop_env_result = %w[1 true].include?(ENV['STUB_RUBOCOP'])
14
+
15
+ let(:stub_rubocop?) do |example|
16
+ # Tries to match `{% rubocop_version <= '1.2' %}`, extracting the operator and the "number"
17
+ rubocop_version_regex = /\{%\s*rubocop_version\s*([^\w\s]+?)\s*['"]?(\d+(\.\d+)*)['"]?\s*%\}/
18
+ requirements = example.metadata[:full_description].scan(rubocop_version_regex)
19
+
20
+ # This can be used by the requirements in eval
21
+ rubocop_version = HamlLint::VersionComparer.for_rubocop
22
+
23
+ accepted = requirements.all? do |(operator, version)|
24
+ rubocop_version.send(operator, version)
25
+ end
26
+
27
+ next true unless accepted
28
+
29
+ # Doing this last so that exceptions in the requirements always fail
30
+ next true if stub_rubocop_env_result
31
+
32
+ false
33
+ end
34
+
35
+ let(:supported_haml?) do |example|
36
+ # Tries to match `{% haml_version >= '5' %}`, extracting the operator and the "number"
37
+ haml_version_regex = /\{%\s*haml_version\s*([^\w\s]+?)\s*['"]?(\d+(\.\d+)*)['"]?\s*%\}/
38
+ requirements = example.metadata[:full_description].scan(haml_version_regex)
39
+
40
+ # This can be used by the requirements in eval
41
+ haml_version = HamlLint::VersionComparer.for_haml
42
+
43
+ requirements.all? do |(operator, version)|
44
+ haml_version.send(operator, version)
45
+ end
46
+ end
47
+
48
+ before do
49
+ if stub_rubocop?
50
+ skip if end_ruby.include?('SKIP')
51
+ subject.stub(:process_ruby_source).and_return(end_ruby)
52
+ end
53
+ subject.stub(:transfer_corrections?).and_return(true)
54
+ end
55
+
56
+ include_context 'linter'
57
+ # The goal is not to test rubocop the gem, so no need to test the details using both
58
+ # :safe and :all
59
+ let(:autocorrect) { :all }
60
+
61
+ # We want want to do error handling ourself
62
+ let(:run_method_to_use) { nil }
63
+
64
+ let(:steps_parts) do
65
+ parts = steps_string.split(/^[ \t]*---[ \t]*\n/, -1)
66
+ raise "Expected 4 steps, got: #{parts.size}" if parts.size != 4
67
+ parts
68
+ end
69
+
70
+ let(:start_haml) { steps_parts[0] }
71
+
72
+ let(:start_ruby) do
73
+ lines = steps_parts[1].split("\n", -1)
74
+ current_matching_line = 1
75
+ @source_map = {}
76
+ lines.each.with_index do |line, i|
77
+ next unless line =~ /\S/
78
+ mo = line.match(/^(.*?)\$?\s*\$\$(\d+)$/)
79
+ if mo
80
+ lines[i] = mo[1]
81
+ current_matching_line = Integer(mo[2])
82
+ end
83
+ @source_map[i + 1] = current_matching_line
84
+ end
85
+ lines.join("\n")
86
+ end
87
+
88
+ let(:source_map) do
89
+ start_ruby
90
+ @source_map
91
+ end
92
+
93
+ let(:end_ruby) { steps_parts[2] }
94
+
95
+ let(:end_haml) { steps_parts[3] }
96
+
97
+ # Used by the 'linter' context
98
+ let(:haml) { start_haml }
99
+
100
+ # steps_string is string of multiple lines describing the steps that
101
+ # the code will take:
102
+ # 1) input haml
103
+ # 2) extracted ruby
104
+ # 3) the corrected ruby
105
+ # 4) the corrected haml
106
+ # Each steps is delimited by a line with ---
107
+ def follows_steps # rubocop:disable Metrics
108
+ skip unless supported_haml?
109
+
110
+ begin
111
+ subject.run_or_raise(document, autocorrect: autocorrect)
112
+ rescue StandardError => e
113
+ exception_while_running = e
114
+ end
115
+
116
+ syntax_lints = subject.lints.select { |lint| lint.message =~ %r{Lint/Syntax} }
117
+
118
+ if start_ruby.strip != 'SKIP' && subject.last_extracted_source
119
+ matcher = eq(start_ruby)
120
+ subject.last_extracted_source.source.should(
121
+ matcher,
122
+ -> { "Extracted Ruby is different from expected. #{matcher.failure_message}\n#{format_lints}" }
123
+ )
124
+ end
125
+
126
+ syntax_lints.should(be_empty, "Generated Ruby has Syntax Lints:\n#{format_lints(syntax_lints)}")
127
+
128
+ if end_ruby.strip != 'SKIP' && subject.last_new_ruby_source
129
+ matcher = eq(end_ruby)
130
+ subject.last_new_ruby_source.should(
131
+ matcher,
132
+ -> { "Ruby generated by RuboCop is different from expected. #{matcher.failure_message}\n#{format_lints}" }
133
+ )
134
+ end
135
+
136
+ raise exception_while_running if exception_while_running
137
+
138
+ matcher = eq(end_haml)
139
+ document.source.should(
140
+ matcher,
141
+ -> { "Final HAML is different from expected. #{matcher.failure_message}\n#{format_lints}" }
142
+ )
143
+
144
+ if subject.last_extracted_source && start_ruby.strip != 'SKIP'
145
+ subject.last_extracted_source.source_map.should == source_map
146
+ end
147
+
148
+ haml_different = start_haml != end_haml
149
+ document.source_was_changed.should == haml_different
150
+ end
151
+
152
+ def format_lints(lints = subject.lints)
153
+ lints.map { |lint| "#{lint.line}:#{lint.message}" }.join("\n")
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -2,4 +2,5 @@
2
2
 
3
3
  require 'haml_lint/spec/normalize_indent'
4
4
  require 'haml_lint/spec/shared_linter_context'
5
+ require 'haml_lint/spec/shared_rubocop_autocorrect_context'
5
6
  require 'haml_lint/spec/matchers/report_lint'
@@ -7,5 +7,15 @@ module HamlLint::Tree
7
7
  def filter_type
8
8
  @value[:name]
9
9
  end
10
+
11
+ def text
12
+ # Seems HAML strips the starting blank lines... without them, line numbers become offset,
13
+ # breaking the source_map and auto-correct
14
+
15
+ nb_blank_lines = 0
16
+ nb_blank_lines += 1 while @document.source_lines[line + nb_blank_lines]&.empty?
17
+
18
+ "#{"\n" * nb_blank_lines}#{super}"
19
+ end
10
20
  end
11
21
  end
@@ -104,8 +104,13 @@ module HamlLint::Tree
104
104
  def line_numbers
105
105
  return (line..line) unless @value && text
106
106
 
107
- end_line = line + lines.count
108
- end_line = nontrivial_end_line if line == end_line && children.empty?
107
+ end_line = if !lines.empty?
108
+ line + lines.count - 1
109
+ elsif children.empty?
110
+ nontrivial_end_line
111
+ else
112
+ line
113
+ end
109
114
 
110
115
  (line..end_line)
111
116
  end
@@ -155,6 +160,10 @@ module HamlLint::Tree
155
160
  @value[:text].to_s
156
161
  end
157
162
 
163
+ def keyword
164
+ @value[:keyword]
165
+ end
166
+
158
167
  private
159
168
 
160
169
  # Discovers the end line of the node when there are no lines.
@@ -164,7 +173,7 @@ module HamlLint::Tree
164
173
  if successor
165
174
  successor.line_numbers.begin - 1
166
175
  else
167
- @document.source_lines.count
176
+ @document.last_non_empty_line
168
177
  end
169
178
  end
170
179
 
@@ -212,7 +221,7 @@ module HamlLint::Tree
212
221
  # @param node [HamlLint::Tree::Node]
213
222
  # @return [Array<HamlLint::Tree::Node>]
214
223
  def subsequents(node)
215
- siblings[(position(node) + 1)..-1]
224
+ siblings[(position(node) + 1)..]
216
225
  end
217
226
 
218
227
  private
@@ -2,7 +2,7 @@
2
2
 
3
3
  module HamlLint::Tree
4
4
  # Represents a tag node in a HAML document.
5
- class TagNode < Node # rubocop:disable Metrics/ClassLength
5
+ class TagNode < Node
6
6
  # Computed set of attribute hashes code.
7
7
  #
8
8
  # This is a combination of all dynamically calculated attributes from the
@@ -49,9 +49,7 @@ module HamlLint::Tree
49
49
  # dot removed
50
50
  def static_classes
51
51
  @static_classes ||=
52
- begin
53
- static_attributes_source.scan(/\.([-:\w]+)/)
54
- end
52
+ static_attributes_source.scan(/\.([-:\w]+)/)
55
53
  end
56
54
 
57
55
  # List of ids statically defined for this tag.
@@ -63,9 +61,7 @@ module HamlLint::Tree
63
61
  # removed
64
62
  def static_ids
65
63
  @static_ids ||=
66
- begin
67
- static_attributes_source.scan(/#([-:\w]+)/)
68
- end
64
+ static_attributes_source.scan(/#([-:\w]+)/)
69
65
  end
70
66
 
71
67
  # Static element attributes defined after the tag name.
@@ -101,7 +97,7 @@ module HamlLint::Tree
101
97
  @attributes_source ||=
102
98
  begin
103
99
  _explicit_tag, static_attrs, rest =
104
- source_code.scan(/\A\s*(%[-:\w]+)?([-:\w\.\#]*)(.*)/m)[0]
100
+ source_code.scan(/\A\s*(%[-:\w]+)?([-:\w.\#]*)(.*)/m)[0]
105
101
 
106
102
  attr_types = {
107
103
  '{' => [:hash, %w[{ }]],
@@ -220,7 +216,7 @@ module HamlLint::Tree
220
216
  #
221
217
  # @return [true,false]
222
218
  def remove_outer_whitespace?
223
- !!@value[:nuke_outer_whitespace] # rubocop:disable Style/DoubleNegation
219
+ !!@value[:nuke_outer_whitespace]
224
220
  end
225
221
 
226
222
  # Returns the script source that will be evaluated to produce this tag's
@@ -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.45.0'
5
+ VERSION = '0.48.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