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
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Haml does heavy transformations to strings that contain interpolation without a way
4
+ # of perfectly inverting that transformation.
5
+ #
6
+ # We need this monkey patch to have a way of recovering the original strings as they
7
+ # are in the haml files, so that we can use them and then autocorrect them.
8
+ #
9
+ # The HamlLint::Document carries over a hash of interpolation to original string. The
10
+ # below patches are there to extract said information from Haml's parsing.
11
+ module Haml::Util
12
+ # The cache for the current Thread (technically Fiber)
13
+ def self.unescape_interpolation_to_original_cache
14
+ Thread.current[:haml_lint_unescape_interpolation_to_original_cache] ||= {}
15
+ end
16
+
17
+ # As soon as a HamlLint::Document has finished processing a HAML souce, this gets called to
18
+ # get a copy of this cache and clear up for the next HAML processing
19
+ def self.unescape_interpolation_to_original_cache_take_and_wipe
20
+ value = unescape_interpolation_to_original_cache.dup
21
+ unescape_interpolation_to_original_cache.clear
22
+ value
23
+ end
24
+
25
+ # Overriding the unescape_interpolation method to store the return and original string
26
+ # in the cache.
27
+ def unescape_interpolation_with_original_tracking(str, escape_html = nil)
28
+ value = unescape_interpolation_without_original_tracking(str, escape_html)
29
+ Haml::Util.unescape_interpolation_to_original_cache[value] = str
30
+ value
31
+ end
32
+
33
+ alias unescape_interpolation_without_original_tracking unescape_interpolation
34
+ alias unescape_interpolation unescape_interpolation_with_original_tracking
35
+ end
@@ -40,7 +40,7 @@ module HamlLint
40
40
  #
41
41
  # @param patterns [Array<String>]
42
42
  # @return [Array<String>]
43
- def extract_files_from(patterns) # rubocop:disable Metrics/MethodLength
43
+ def extract_files_from(patterns) # rubocop:disable Metrics
44
44
  files = []
45
45
 
46
46
  patterns.each do |pattern|
@@ -74,7 +74,7 @@ module HamlLint
74
74
  # @param path [String]
75
75
  # @return [String]
76
76
  def normalize_path(path)
77
- path.start_with?(".#{File::SEPARATOR}") ? path[2..-1] : path
77
+ path.start_with?(".#{File::SEPARATOR}") ? path[2..] : path
78
78
  end
79
79
 
80
80
  # Whether the given file should be treated as a Haml file.
@@ -3,6 +3,9 @@
3
3
  module HamlLint
4
4
  # Contains information about a problem or issue with a HAML document.
5
5
  class Lint
6
+ # @return [Boolean] If the error was corrected by auto-correct
7
+ attr_reader :corrected
8
+
6
9
  # @return [String] file path to which the lint applies
7
10
  attr_reader :filename
8
11
 
@@ -25,12 +28,13 @@ module HamlLint
25
28
  # @param line [Fixnum]
26
29
  # @param message [String]
27
30
  # @param severity [Symbol]
28
- def initialize(linter, filename, line, message, severity = :warning)
31
+ def initialize(linter, filename, line, message, severity = :warning, corrected: false) # rubocop:disable Metrics/ParameterLists
29
32
  @linter = linter
30
33
  @filename = filename
31
34
  @line = line || 0
32
35
  @message = message
33
36
  @severity = Severity.new(severity)
37
+ @corrected = corrected
34
38
  end
35
39
 
36
40
  # Return whether this lint has a severity of error.
@@ -39,5 +43,10 @@ module HamlLint
39
43
  def error?
40
44
  @severity.error?
41
45
  end
46
+
47
+ def inspect
48
+ "#{self.class.name}(corrected=#{corrected}, filename=#{filename}, line=#{line}, " \
49
+ "linter=#{linter.class.name}, message=#{message}, severity=#{severity})"
50
+ end
42
51
  end
43
52
  end
@@ -7,18 +7,19 @@ module HamlLint
7
7
 
8
8
  def visit_root(root)
9
9
  return if document.source.empty?
10
+ line_number = document.last_non_empty_line
10
11
 
11
- node = root.node_for_line(document.source_lines.count)
12
+ node = root.node_for_line(line_number)
12
13
  return if node.disabled?(self)
13
14
 
14
15
  ends_with_newline = document.source.end_with?("\n")
15
16
 
16
17
  if config['present']
17
18
  unless ends_with_newline
18
- record_lint(node, 'Files should end with a trailing newline')
19
+ record_lint(line_number, 'Files should end with a trailing newline')
19
20
  end
20
21
  elsif ends_with_newline
21
- record_lint(node, 'Files should not end with a trailing newline')
22
+ record_lint(line_number, 'Files should not end with a trailing newline')
22
23
  end
23
24
  end
24
25
  end
@@ -11,7 +11,7 @@ module HamlLint
11
11
 
12
12
  return unless node.static_classes.any? || node.static_ids.any?
13
13
 
14
- tag = node.source_code[/\s*([^\s={\(\[]+)/, 1]
14
+ tag = node.source_code[/\s*([^\s={(\[]+)/, 1]
15
15
  return unless tag.start_with?('%div')
16
16
 
17
17
  record_lint(node,
@@ -7,8 +7,8 @@ module HamlLint
7
7
 
8
8
  # Allowed leading indentation for each character type.
9
9
  INDENT_REGEX = {
10
- space: /^[ ]*(?!\t)/,
11
- tab: /^\t*(?![ ])/,
10
+ space: /^ *(?!\t)/,
11
+ tab: /^\t*(?! )/,
12
12
  }.freeze
13
13
 
14
14
  LEADING_SPACES_REGEX = /^( +)(?! )/.freeze
@@ -45,7 +45,7 @@ module HamlLint
45
45
  root.children.each do |top_node|
46
46
  # once we've found one line with leading space, there's no need to check any more lines
47
47
  # `haml` will check indenting_at_start, deeper_indenting, inconsistent_indentation
48
- break if top_node.children.find do |node|
48
+ break if top_node.children.find do |node| # rubocop:disable Lint/UnreachableLoop
49
49
  line = node.source_code
50
50
  leading_space = LEADING_SPACES_REGEX.match(line)
51
51
 
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HamlLint
4
+ # Checks that placeholder attributes are not used.
5
+ class Linter::NoPlaceholders < Linter
6
+ include LinterRegistry
7
+
8
+ MSG = 'Placeholders attributes should not be used.'
9
+ HASH_REGEXP = /:?['"]?placeholder['"]?(?::| *=>)/.freeze
10
+ HTML_REGEXP = /placeholder=/.freeze
11
+
12
+ def visit_tag(node)
13
+ return unless node.hash_attributes_source =~ HASH_REGEXP || node.html_attributes_source =~ HTML_REGEXP
14
+
15
+ record_lint(node, MSG)
16
+ end
17
+ end
18
+ end
@@ -8,13 +8,14 @@ module HamlLint
8
8
  MESSAGE_FORMAT = %{Do not repeat id "#%s" on the page}
9
9
 
10
10
  def visit_root(_node)
11
- @id_map = Hash.new { |hash, key| hash[key] = [] }
11
+ @id_map = {}
12
12
  end
13
13
 
14
14
  def visit_tag(node)
15
15
  id = node.tag_id
16
16
  return unless id && !id.empty?
17
17
 
18
+ id_map[id] ||= []
18
19
  nodes = (id_map[id] << node)
19
20
  case nodes.size
20
21
  when 1 then nil
@@ -1,74 +1,253 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'haml_lint/ruby_extractor'
4
3
  require 'rubocop'
4
+ require 'tempfile'
5
5
 
6
6
  module HamlLint
7
- # Runs RuboCop on Ruby code contained within HAML templates.
7
+ # Runs RuboCop on the Ruby code contained within HAML templates.
8
+ #
9
+ # The processing is done by extracting a Ruby file that matches the content, including
10
+ # the indentation, of the HAML file. This way, we can run RuboCop with autocorrect
11
+ # and get new Ruby code which should be HAML compatible.
12
+ #
13
+ # The ruby extraction makes "Chunks" which wrap each HAML constructs. The Chunks can then
14
+ # use the corrected Ruby code to apply the corrections back in the HAML using logic specific
15
+ # to each type of Chunk.
16
+ #
17
+ # The work is spread across the classes in the HamlLint::RubyExtraction module.
8
18
  class Linter::RuboCop < Linter
9
19
  include LinterRegistry
10
20
 
21
+ supports_autocorrect(true)
22
+
11
23
  # Maps the ::RuboCop::Cop::Severity levels to our own levels.
12
24
  SEVERITY_MAP = {
13
25
  error: :error,
14
26
  fatal: :error,
15
-
16
27
  convention: :warning,
17
28
  refactor: :warning,
18
29
  warning: :warning,
30
+ info: :info,
19
31
  }.freeze
20
32
 
21
- def visit_root(_node)
22
- extractor = HamlLint::RubyExtractor.new
23
- extracted_source = extractor.extract(document)
33
+ # Debug fields, also used in tests
34
+ attr_accessor :last_extracted_source
35
+ attr_accessor :last_new_ruby_source
36
+
37
+ def visit_root(_node) # rubocop:disable Metrics
38
+ # Need to call the received block to avoid Linter automatically visiting children
39
+ # Only important thing is that the argument is not ":children"
40
+ yield :skip_children
41
+
42
+ if document.indentation && document.indentation != ' '
43
+ @lints <<
44
+ HamlLint::Lint.new(
45
+ self,
46
+ document.file,
47
+ nil,
48
+ "Only supported indentation is 2 spaces, got: #{document.indentation.dump}",
49
+ :error
50
+ )
51
+ return
52
+ end
53
+
54
+ @last_extracted_source = nil
55
+ @last_new_ruby_source = nil
56
+
57
+ coordinator = HamlLint::RubyExtraction::Coordinator.new(document)
58
+
59
+ extracted_source = coordinator.extract_ruby_source
60
+ if ENV['HAML_LINT_INTERNAL_DEBUG'] == 'true'
61
+ puts "------ Extracted ruby from #{@document.file}:"
62
+ puts extracted_source.source
63
+ puts '------'
64
+ end
65
+
66
+ @last_extracted_source = extracted_source
67
+
68
+ if extracted_source.source.empty?
69
+ @last_new_ruby_source = ''
70
+ return
71
+ end
72
+
73
+ new_ruby_code = process_ruby_source(extracted_source.source, extracted_source.source_map)
74
+
75
+ if @autocorrect && ENV['HAML_LINT_INTERNAL_DEBUG'] == 'true'
76
+ puts "------ Autocorrected extracted ruby from #{@document.file}:"
77
+ puts new_ruby_code
78
+ puts '------'
79
+ end
80
+
81
+ if @autocorrect && transfer_corrections?(extracted_source.source, new_ruby_code)
82
+ @last_new_ruby_source = new_ruby_code
83
+ transfer_corrections(coordinator, new_ruby_code)
84
+ end
85
+ end
86
+
87
+ def self.cops_names_not_supporting_autocorrect
88
+ return @cops_names_not_supporting_autocorrect if @cops_names_not_supporting_autocorrect
89
+ return [] unless ::RuboCop::Cop::Registry.respond_to?(:all)
90
+
91
+ cops_without_autocorrect = ::RuboCop::Cop::Registry.all.reject(&:support_autocorrect?)
92
+ # This cop cannot be disabled
93
+ cops_without_autocorrect.delete(::RuboCop::Cop::Lint::Syntax)
94
+ @cops_names_not_supporting_autocorrect = cops_without_autocorrect.map { |cop| cop.badge.to_s }.freeze
95
+ end
96
+
97
+ private
98
+
99
+ def rubocop_config_for(path)
100
+ user_config_path = ENV['HAML_LINT_RUBOCOP_CONF'] || config['config_file']
101
+ user_config_path ||= self.class.rubocop_config_store.user_rubocop_config_path_for(path)
102
+ user_config_path = File.absolute_path(user_config_path)
103
+ self.class.rubocop_config_store.config_object_pointing_to(user_config_path)
104
+ end
105
+
106
+ # Extracted here so that tests can stub this to always return true
107
+ def transfer_corrections?(initial_ruby_code, new_ruby_code)
108
+ initial_ruby_code != new_ruby_code
109
+ end
110
+
111
+ def transfer_corrections(coordinator, new_ruby_code)
112
+ begin
113
+ new_haml_lines = coordinator.haml_lines_with_corrections_applied(new_ruby_code)
114
+ rescue HamlLint::RubyExtraction::UnableToTransferCorrections => e
115
+ # Those are lints we couldn't correct. If haml-lint was called without the
116
+ # --auto-correct-only, then this linter will be called again without autocorrect,
117
+ # so the lints will be recorded then.
118
+ @lints = []
119
+
120
+ msg = "Corrections couldn't be transfered: #{e.message} - Consider linting the file " \
121
+ 'without auto-correct and doing the changes manually.'
122
+ if ENV['HAML_LINT_DEBUG'] == 'true'
123
+ msg = "#{msg} DEBUG: Rubocop corrected Ruby code follows:\n#{new_ruby_code}\n------"
124
+ end
125
+
126
+ @lints << HamlLint::Lint.new(self, document.file, nil, msg, :error)
127
+ return
128
+ end
24
129
 
25
- return if extracted_source.source.empty?
130
+ new_haml_string = new_haml_lines.join("\n")
26
131
 
27
- find_lints(extracted_source.source, extracted_source.source_map)
132
+ if new_haml_validity_checks(new_haml_string)
133
+ document.change_source(new_haml_string)
134
+ true
135
+ else
136
+ false
137
+ end
138
+ end
139
+
140
+ def new_haml_validity_checks(new_haml_string)
141
+ new_haml_error = HamlLint::Utils.check_error_when_compiling_haml(new_haml_string)
142
+ return true unless new_haml_error
143
+
144
+ error_message = if new_haml_error.is_a?(::SyntaxError)
145
+ 'Corrections by haml-lint generate Haml that will have Ruby syntax error. Skipping.'
146
+ else
147
+ 'Corrections by haml-lint generate invalid Haml. Skipping.'
148
+ end
149
+
150
+ if ENV['HAML_LINT_DEBUG'] == 'true'
151
+ error_message = error_message.dup
152
+ error_message << "\nDEBUG: Here is the exception:\n#{new_haml_error.full_message}"
153
+
154
+ error_message << "DEBUG: This is the (wrong) HAML after the corrections:\n"
155
+ if new_haml_error.respond_to?(:line)
156
+ error_message << "(DEBUG: Line number of error in the HAML: #{new_haml_error.line})\n"
157
+ end
158
+ error_message << new_haml_string
159
+ else
160
+ # Those are lints we couldn't correct. If haml-lint was called without the
161
+ # --auto-correct-only, then this linter will be called again without autocorrect,
162
+ # so the lints will be recorded then. If it was called with --auto-correct-only,
163
+ # then we did nothing so it makes sense not to show the lints.
164
+ @lints = []
165
+ end
166
+
167
+ @lints << HamlLint::Lint.new(self, document.file, nil, error_message, :error)
168
+ false
28
169
  end
29
170
 
30
171
  # A single CLI instance is shared between files to avoid RuboCop
31
172
  # having to repeatedly reload .rubocop.yml.
32
- def self.rubocop_cli
173
+ def self.rubocop_cli # rubocop:disable Lint/IneffectiveAccessModifier
33
174
  # The ivar is stored on the class singleton rather than the Linter instance
34
175
  # because it can't be Marshal.dump'd (as used by Parallel.map)
35
176
  @rubocop_cli ||= ::RuboCop::CLI.new
36
177
  end
37
178
 
38
- private
179
+ def self.rubocop_config_store # rubocop:disable Lint/IneffectiveAccessModifier
180
+ @rubocop_config_store ||= RubocopConfigStore.new
181
+ end
39
182
 
40
- # Executes RuboCop against the given Ruby code and records the offenses as
41
- # lints.
183
+ # Executes RuboCop against the given Ruby code, records the offenses as
184
+ # lints, runs autocorrect if requested and returns the corrected ruby.
42
185
  #
43
- # @param ruby [String] Ruby code
186
+ # @param ruby_code [String] Ruby code
44
187
  # @param source_map [Hash] map of Ruby code line numbers to original line
45
188
  # numbers in the template
46
- def find_lints(ruby, source_map)
47
- filename =
48
- if document.file
49
- "#{document.file}.rb"
50
- else
51
- 'ruby_script.rb'
52
- end
189
+ # @return [String] The autocorrected Ruby source code
190
+ def process_ruby_source(ruby_code, source_map)
191
+ filename = document.file || 'ruby_script.rb'
53
192
 
54
- with_ruby_from_stdin(ruby) do
55
- extract_lints_from_offenses(lint_file(self.class.rubocop_cli, filename), source_map)
56
- end
193
+ offenses, corrected_ruby = run_rubocop(self.class.rubocop_cli, ruby_code, filename)
194
+
195
+ extract_lints_from_offenses(offenses, source_map)
196
+ corrected_ruby
57
197
  end
58
198
 
59
- # Defined so we can stub the results in tests
199
+ # Runs RuboCop, returning the offenses and corrected code. Raises when RuboCop
200
+ # fails to run correctly.
60
201
  #
61
- # @param rubocop [RuboCop::CLI]
62
- # @param file [String]
63
- # @return [Array<RuboCop::Cop::Offense>]
64
- def lint_file(rubocop, file)
65
- status = rubocop.run(rubocop_flags << file)
66
- unless [::RuboCop::CLI::STATUS_SUCCESS, ::RuboCop::CLI::STATUS_OFFENSES].include?(status)
202
+ # @param rubocop_cli [RuboCop::CLI] There to simplify tests by using a stub
203
+ # @param ruby_code [String] The ruby code to run through RuboCop
204
+ # @param path [String] the path to tell RuboCop we are running
205
+ # @return [Array<RuboCop::Cop::Offense>, String]
206
+ def run_rubocop(rubocop_cli, ruby_code, path) # rubocop:disable Metrics
207
+ rubocop_status = nil
208
+ stdout_str, stderr_str = HamlLint::Utils.with_captured_streams(ruby_code) do
209
+ rubocop_cli.config_store.instance_variable_set(:@options_config, rubocop_config_for(path))
210
+ rubocop_status = rubocop_cli.run(rubocop_flags + ['--stdin', path])
211
+ end
212
+
213
+ if ENV['HAML_LINT_INTERNAL_DEBUG'] == 'true'
214
+ if OffenseCollector.offenses.empty?
215
+ puts "------ No lints found by RuboCop in #{@document.file}"
216
+ else
217
+ puts "------ Raw lints found by RuboCop in #{@document.file}"
218
+ OffenseCollector.offenses.each do |offense|
219
+ puts offense
220
+ end
221
+ puts '------'
222
+ end
223
+ end
224
+
225
+ unless [::RuboCop::CLI::STATUS_SUCCESS, ::RuboCop::CLI::STATUS_OFFENSES].include?(rubocop_status)
226
+ if stderr_str.start_with?('Infinite loop')
227
+ msg = "RuboCop exited unsuccessfully with status #{rubocop_status}." \
228
+ ' This appears to be due to an autocorrection infinite loop.'
229
+ if ENV['HAML_LINT_DEBUG'] == 'true'
230
+ msg += " DEBUG: RuboCop's output:\n"
231
+ msg += stderr_str.strip
232
+ else
233
+ msg += " First line of RuboCop's output (Use --debug mode to see more):\n"
234
+ msg += stderr_str.each_line.first.strip
235
+ end
236
+
237
+ raise HamlLint::Exceptions::InfiniteLoopError, msg
238
+ end
239
+
67
240
  raise HamlLint::Exceptions::ConfigurationError,
68
- "RuboCop exited unsuccessfully with status #{status}." \
69
- ' Check the stack trace to see if there was a misconfiguration.'
241
+ "RuboCop exited unsuccessfully with status #{rubocop_status}." \
242
+ ' Here is its output to check the stack trace or see if there was' \
243
+ " a misconfiguration:\n#{stderr_str}"
70
244
  end
71
- OffenseCollector.offenses
245
+
246
+ if @autocorrect
247
+ corrected_ruby = stdout_str.partition("#{'=' * 20}\n").last
248
+ end
249
+
250
+ [OffenseCollector.offenses, corrected_ruby]
72
251
  end
73
252
 
74
253
  # Aggregates RuboCop offenses and converts them to {HamlLint::Lint}s
@@ -76,24 +255,43 @@ module HamlLint
76
255
  #
77
256
  # @param offenses [Array<RuboCop::Cop::Offense>]
78
257
  # @param source_map [Hash]
79
- def extract_lints_from_offenses(offenses, source_map)
80
- dummy_node = Struct.new(:line)
258
+ def extract_lints_from_offenses(offenses, source_map) # rubocop:disable Metrics
259
+ offenses.each do |offense|
260
+ next if Array(config['ignored_cops']).include?(offense.cop_name)
261
+ autocorrected = offense.status == :corrected
262
+
263
+ # There will be another execution to deal with not auto-corrected stuff unless
264
+ # we are in autocorrect-only mode, where we don't want not auto-corrected stuff.
265
+ next if @autocorrect && !autocorrected && offense.cop_name != 'Lint/Syntax'
81
266
 
82
- offenses.reject { |offense| Array(config['ignored_cops']).include?(offense.cop_name) }
83
- .each do |offense|
84
- record_lint(dummy_node.new(source_map[offense.line]), offense.message,
85
- offense.severity.name)
267
+ if ENV['HAML_LINT_INTERNAL_DEBUG']
268
+ line = offense.line
269
+ else
270
+ line = source_map[offense.line]
271
+
272
+ if line.nil? && offense.line == source_map.keys.max + 1
273
+ # The sourcemap doesn't include an entry for the line just after the last line,
274
+ # but rubocop sometimes does place offenses there.
275
+ line = source_map[offense.line - 1]
276
+ end
277
+ end
278
+ record_lint(line, offense.message, offense.severity.name,
279
+ corrected: autocorrected)
86
280
  end
87
281
  end
88
282
 
89
283
  # Record a lint for reporting back to the user.
90
284
  #
91
- # @param node [#line] node to extract the line number from
285
+ # @param line [#line] line number of the lint
92
286
  # @param message [String] error/warning to display to the user
93
287
  # @param severity [Symbol] RuboCop severity level for the offense
94
- def record_lint(node, message, severity)
95
- @lints << HamlLint::Lint.new(self, @document.file, node.line, message,
96
- SEVERITY_MAP.fetch(severity, :warning))
288
+ def record_lint(line, message, severity, corrected:)
289
+ # TODO: actual handling for RuboCop's new :info severity
290
+ return if severity == :info
291
+
292
+ @lints << HamlLint::Lint.new(self, @document.file, line, message,
293
+ SEVERITY_MAP.fetch(severity, :warning),
294
+ corrected: corrected)
97
295
  end
98
296
 
99
297
  # Returns flags that will be passed to RuboCop CLI.
@@ -101,25 +299,48 @@ module HamlLint
101
299
  # @return [Array<String>]
102
300
  def rubocop_flags
103
301
  flags = %w[--format HamlLint::OffenseCollector]
104
- flags += ['--config', ENV['HAML_LINT_RUBOCOP_CONF']] if ENV['HAML_LINT_RUBOCOP_CONF']
105
- flags += ['--stdin']
302
+ flags += ignored_cops_flags
303
+ flags += rubocop_autocorrect_flags
106
304
  flags
107
305
  end
108
306
 
109
- # Overrides the global stdin to allow RuboCop to read Ruby code from it.
110
- #
111
- # @param ruby [String] the Ruby code to write to the overridden stdin
112
- # @param _block [Block] the block to perform with the overridden stdin
113
- # @return [void]
114
- def with_ruby_from_stdin(ruby, &_block)
115
- original_stdin = $stdin
116
- stdin = StringIO.new
117
- stdin.write(ruby)
118
- stdin.rewind
119
- $stdin = stdin
120
- yield
121
- ensure
122
- $stdin = original_stdin
307
+ def rubocop_autocorrect_flags
308
+ return [] unless @autocorrect
309
+
310
+ rubocop_version = Gem::Version.new(::RuboCop::Version::STRING)
311
+
312
+ case @autocorrect
313
+ when :safe
314
+ if rubocop_version >= Gem::Version.new('1.30')
315
+ ['--autocorrect']
316
+ else
317
+ ['--auto-correct']
318
+ end
319
+ when :all
320
+ if rubocop_version >= Gem::Version.new('1.30')
321
+ ['--autocorrect-all']
322
+ else
323
+ ['--auto-correct-all']
324
+ end
325
+ else
326
+ raise "Unexpected autocorrect option: #{@autocorrect.inspect}"
327
+ end
328
+ end
329
+
330
+ # Because of autocorrect, we need to pass the ignored cops to RuboCop to
331
+ # prevent it from doing fixes we don't want.
332
+ # Because cop names changed names over time, we cleanup those that don't exist
333
+ # anymore or don't exist yet.
334
+ # This is not exhaustive, it's only for the cops that are in config/default.yml
335
+ def ignored_cops_flags
336
+ ignored_cops = config.fetch('ignored_cops', [])
337
+
338
+ if @autocorrect
339
+ ignored_cops += self.class.cops_names_not_supporting_autocorrect
340
+ end
341
+
342
+ return [] if ignored_cops.empty?
343
+ ['--except', ignored_cops.uniq.join(',')]
123
344
  end
124
345
  end
125
346
 
@@ -146,4 +367,77 @@ module HamlLint
146
367
  self.class.offenses += offenses
147
368
  end
148
369
  end
370
+
371
+ # To handle our need to force some configurations on RuboCop, while still allowing users
372
+ # to customize most of RuboCop using their own rubocop.yml config(s), we need to detect
373
+ # the effective RuboCop configuration for a specific file, and generate a new configuration
374
+ # containing our own "forced configuration" with a `inherit_from` that points on the
375
+ # user's configuration.
376
+ #
377
+ # This class handles all of this logic.
378
+ class RubocopConfigStore
379
+ def initialize
380
+ @dir_path_to_user_config_path = {}
381
+ @user_config_path_to_config_object = {}
382
+ end
383
+
384
+ # Build a RuboCop::Config from config/forced_rubocop_config.yml which inherits from the given
385
+ # user_config_path and return it's path.
386
+ def config_object_pointing_to(user_config_path)
387
+ if @user_config_path_to_config_object[user_config_path]
388
+ return @user_config_path_to_config_object[user_config_path]
389
+ end
390
+
391
+ final_config_hash = forced_rubocop_config_hash.dup
392
+
393
+ if user_config_path != ::RuboCop::ConfigLoader::DEFAULT_FILE
394
+ # If we manually inherit from the default RuboCop config, we may get warnings
395
+ # for deprecated stuff that is in it. We don't when we automatically
396
+ # inherit from it (which always happens)
397
+ final_config_hash['inherit_from'] = user_config_path
398
+ end
399
+
400
+ config_object = Tempfile.create(['.haml-lint-rubocop', '.yml']) do |tempfile|
401
+ tempfile.write(final_config_hash.to_yaml)
402
+ tempfile.close
403
+ ::RuboCop::ConfigLoader.configuration_from_file(tempfile.path)
404
+ end
405
+
406
+ @user_config_path_to_config_object[user_config_path] = config_object
407
+ end
408
+
409
+ # Find the path to the effective RuboCop configuration for a path (file or dir)
410
+ def user_rubocop_config_path_for(path)
411
+ dir = if File.directory?(path)
412
+ path
413
+ else
414
+ File.dirname(path)
415
+ end
416
+
417
+ @dir_path_to_user_config_path[dir] ||= ::RuboCop::ConfigLoader.configuration_file_for(dir)
418
+ end
419
+
420
+ # Returns the content (Hash) of config/forced_rubocop_config.yml after processing it's ERB content.
421
+ # Cached since it doesn't change between files
422
+ def forced_rubocop_config_hash
423
+ return @forced_rubocop_config_hash if @forced_rubocop_config_hash
424
+
425
+ content = File.read(File.join(HamlLint::HOME, 'config', 'forced_rubocop_config.yml'))
426
+ processed_content = HamlLint::Utils.process_erb(content)
427
+ hash = YAML.safe_load(processed_content)
428
+
429
+ if ENV['HAML_LINT_TESTING']
430
+ # In newer RuboCop versions, new cops are not enabled by default, and instead
431
+ # show a message until they are used. We just want a default for them
432
+ # to avoid spamming STDOUT. Making it "disable" reduces the chances of having
433
+ # the test suite start failing after a new cop gets added.
434
+ hash['AllCops'] ||= {}
435
+ if Gem::Version.new(::RuboCop::Version::STRING) >= Gem::Version.new('1')
436
+ hash['AllCops']['NewCops'] = 'disable'
437
+ end
438
+ end
439
+
440
+ @forced_rubocop_config_hash = hash.freeze
441
+ end
442
+ end
149
443
  end