haml_lint 0.44.0 → 0.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/bin/haml-lint +1 -1
- data/config/default.yml +6 -28
- data/config/forced_rubocop_config.yml +156 -0
- data/lib/haml_lint/adapter/haml_4.rb +18 -0
- data/lib/haml_lint/adapter/haml_5.rb +11 -0
- data/lib/haml_lint/adapter/haml_6.rb +11 -0
- data/lib/haml_lint/cli.rb +8 -3
- data/lib/haml_lint/configuration_loader.rb +13 -12
- data/lib/haml_lint/document.rb +89 -8
- data/lib/haml_lint/exceptions.rb +6 -0
- data/lib/haml_lint/extensions/haml_util_unescape_interpolation_tracking.rb +35 -0
- data/lib/haml_lint/file_finder.rb +2 -2
- data/lib/haml_lint/lint.rb +10 -1
- data/lib/haml_lint/linter/final_newline.rb +4 -3
- data/lib/haml_lint/linter/implicit_div.rb +1 -1
- data/lib/haml_lint/linter/indentation.rb +3 -3
- data/lib/haml_lint/linter/no_placeholders.rb +18 -0
- data/lib/haml_lint/linter/rubocop.rb +351 -59
- data/lib/haml_lint/linter/space_before_script.rb +8 -10
- data/lib/haml_lint/linter/unnecessary_string_output.rb +1 -1
- data/lib/haml_lint/linter/view_length.rb +1 -1
- data/lib/haml_lint/linter.rb +56 -9
- data/lib/haml_lint/linter_registry.rb +3 -5
- data/lib/haml_lint/logger.rb +2 -2
- data/lib/haml_lint/options.rb +26 -2
- data/lib/haml_lint/rake_task.rb +2 -2
- data/lib/haml_lint/reporter/hash_reporter.rb +1 -3
- data/lib/haml_lint/reporter/offense_count_reporter.rb +1 -1
- data/lib/haml_lint/reporter/utils.rb +33 -4
- data/lib/haml_lint/ruby_extraction/ad_hoc_chunk.rb +20 -0
- data/lib/haml_lint/ruby_extraction/base_chunk.rb +113 -0
- data/lib/haml_lint/ruby_extraction/chunk_extractor.rb +504 -0
- data/lib/haml_lint/ruby_extraction/coordinator.rb +181 -0
- data/lib/haml_lint/ruby_extraction/haml_comment_chunk.rb +54 -0
- data/lib/haml_lint/ruby_extraction/implicit_end_chunk.rb +17 -0
- data/lib/haml_lint/ruby_extraction/interpolation_chunk.rb +26 -0
- data/lib/haml_lint/ruby_extraction/non_ruby_filter_chunk.rb +32 -0
- data/lib/haml_lint/ruby_extraction/placeholder_marker_chunk.rb +40 -0
- data/lib/haml_lint/ruby_extraction/ruby_filter_chunk.rb +33 -0
- data/lib/haml_lint/ruby_extraction/ruby_source.rb +5 -0
- data/lib/haml_lint/ruby_extraction/script_chunk.rb +132 -0
- data/lib/haml_lint/ruby_extraction/tag_attributes_chunk.rb +39 -0
- data/lib/haml_lint/ruby_extraction/tag_script_chunk.rb +30 -0
- data/lib/haml_lint/ruby_extractor.rb +11 -10
- data/lib/haml_lint/runner.rb +35 -3
- data/lib/haml_lint/spec/matchers/report_lint.rb +22 -7
- data/lib/haml_lint/spec/normalize_indent.rb +2 -2
- data/lib/haml_lint/spec/shared_linter_context.rb +9 -1
- data/lib/haml_lint/spec/shared_rubocop_autocorrect_context.rb +143 -0
- data/lib/haml_lint/spec.rb +1 -0
- data/lib/haml_lint/tree/filter_node.rb +10 -0
- data/lib/haml_lint/tree/node.rb +13 -4
- data/lib/haml_lint/tree/tag_node.rb +5 -9
- data/lib/haml_lint/utils.rb +130 -5
- data/lib/haml_lint/version.rb +1 -1
- data/lib/haml_lint/version_comparer.rb +25 -0
- data/lib/haml_lint.rb +12 -0
- metadata +25 -6
@@ -7,8 +7,8 @@ module HamlLint
|
|
7
7
|
|
8
8
|
# Allowed leading indentation for each character type.
|
9
9
|
INDENT_REGEX = {
|
10
|
-
space: /^
|
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
|
@@ -2,73 +2,251 @@
|
|
2
2
|
|
3
3
|
require 'haml_lint/ruby_extractor'
|
4
4
|
require 'rubocop'
|
5
|
+
require 'tempfile'
|
5
6
|
|
6
7
|
module HamlLint
|
7
|
-
# Runs RuboCop on Ruby code contained within HAML templates.
|
8
|
+
# Runs RuboCop on the Ruby code contained within HAML templates.
|
9
|
+
#
|
10
|
+
# The processing is done by extracting a Ruby file that matches the content, including
|
11
|
+
# the indentation, of the HAML file. This way, we can run RuboCop with autocorrect
|
12
|
+
# and get new Ruby code which should be HAML compatible.
|
13
|
+
#
|
14
|
+
# The ruby extraction makes "Chunks" which wrap each HAML constructs. The Chunks can then
|
15
|
+
# use the corrected Ruby code to apply the corrections back in the HAML using logic specific
|
16
|
+
# to each type of Chunk.
|
17
|
+
#
|
18
|
+
# The work is spread across the classes in the HamlLint::RubyExtraction module.
|
8
19
|
class Linter::RuboCop < Linter
|
9
20
|
include LinterRegistry
|
10
21
|
|
22
|
+
supports_autocorrect(true)
|
23
|
+
|
11
24
|
# Maps the ::RuboCop::Cop::Severity levels to our own levels.
|
12
25
|
SEVERITY_MAP = {
|
13
26
|
error: :error,
|
14
27
|
fatal: :error,
|
15
|
-
|
16
28
|
convention: :warning,
|
17
29
|
refactor: :warning,
|
18
30
|
warning: :warning,
|
31
|
+
info: :info,
|
19
32
|
}.freeze
|
20
33
|
|
21
|
-
|
22
|
-
|
23
|
-
|
34
|
+
# Debug fields, also used in tests
|
35
|
+
attr_accessor :last_extracted_source
|
36
|
+
attr_accessor :last_new_ruby_source
|
37
|
+
|
38
|
+
def visit_root(_node) # rubocop:disable Metrics
|
39
|
+
# Need to call the received block to avoid Linter automatically visiting children
|
40
|
+
# Only important thing is that the argument is not ":children"
|
41
|
+
yield :skip_children
|
42
|
+
|
43
|
+
if document.indentation && document.indentation != ' '
|
44
|
+
@lints <<
|
45
|
+
HamlLint::Lint.new(
|
46
|
+
self,
|
47
|
+
document.file,
|
48
|
+
nil,
|
49
|
+
"Only supported indentation is 2 spaces, got: #{document.indentation.dump}",
|
50
|
+
:error
|
51
|
+
)
|
52
|
+
return
|
53
|
+
end
|
54
|
+
|
55
|
+
user_config_path = ENV['HAML_LINT_RUBOCOP_CONF'] || config['config_file']
|
56
|
+
user_config_path ||= self.class.rubocop_config_store.user_rubocop_config_path_for(document.file)
|
57
|
+
user_config_path = File.absolute_path(user_config_path)
|
58
|
+
@rubocop_config = self.class.rubocop_config_store.config_object_pointing_to(user_config_path)
|
59
|
+
|
60
|
+
@last_extracted_source = nil
|
61
|
+
@last_new_ruby_source = nil
|
62
|
+
|
63
|
+
coordinator = HamlLint::RubyExtraction::Coordinator.new(document)
|
64
|
+
|
65
|
+
extracted_source = coordinator.extract_ruby_source
|
66
|
+
if ENV['HAML_LINT_INTERNAL_DEBUG'] == 'true'
|
67
|
+
puts "------ Extracted ruby from #{@document.file}:"
|
68
|
+
puts extracted_source.source
|
69
|
+
puts '------'
|
70
|
+
end
|
71
|
+
|
72
|
+
@last_extracted_source = extracted_source
|
73
|
+
|
74
|
+
if extracted_source.source.empty?
|
75
|
+
@last_new_ruby_source = ''
|
76
|
+
return
|
77
|
+
end
|
24
78
|
|
25
|
-
|
79
|
+
new_ruby_code = process_ruby_source(extracted_source.source, extracted_source.source_map)
|
26
80
|
|
27
|
-
|
81
|
+
if @autocorrect && ENV['HAML_LINT_INTERNAL_DEBUG'] == 'true'
|
82
|
+
puts "------ Autocorrected extracted ruby from #{@document.file}:"
|
83
|
+
puts new_ruby_code
|
84
|
+
puts '------'
|
85
|
+
end
|
86
|
+
|
87
|
+
if @autocorrect && transfer_corrections?(extracted_source.source, new_ruby_code)
|
88
|
+
@last_new_ruby_source = new_ruby_code
|
89
|
+
transfer_corrections(coordinator, new_ruby_code)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.cops_names_not_supporting_autocorrect
|
94
|
+
return @cops_names_not_supporting_autocorrect if @cops_names_not_supporting_autocorrect
|
95
|
+
return [] unless ::RuboCop::Cop::Registry.respond_to?(:all)
|
96
|
+
|
97
|
+
cops_without_autocorrect = ::RuboCop::Cop::Registry.all.reject(&:support_autocorrect?)
|
98
|
+
# This cop cannot be disabled
|
99
|
+
cops_without_autocorrect.delete(::RuboCop::Cop::Lint::Syntax)
|
100
|
+
@cops_names_not_supporting_autocorrect = cops_without_autocorrect.map { |cop| cop.badge.to_s }.freeze
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
# Extracted here so that tests can stub this to always return true
|
106
|
+
def transfer_corrections?(initial_ruby_code, new_ruby_code)
|
107
|
+
initial_ruby_code != new_ruby_code
|
108
|
+
end
|
109
|
+
|
110
|
+
def transfer_corrections(coordinator, new_ruby_code)
|
111
|
+
begin
|
112
|
+
new_haml_lines = coordinator.haml_lines_with_corrections_applied(new_ruby_code)
|
113
|
+
rescue HamlLint::RubyExtraction::UnableToTransferCorrections => e
|
114
|
+
# Those are lints we couldn't correct. If haml-lint was called without the
|
115
|
+
# --auto-correct-only, then this linter will be called again without autocorrect,
|
116
|
+
# so the lints will be recorded then.
|
117
|
+
@lints = []
|
118
|
+
|
119
|
+
msg = "Corrections couldn't be transfered: #{e.message} - Consider linting the file " \
|
120
|
+
'without auto-correct and doing the changes manually.'
|
121
|
+
if ENV['HAML_LINT_DEBUG'] == 'true'
|
122
|
+
msg = "#{msg} DEBUG: Rubocop corrected Ruby code follows:\n#{new_ruby_code}\n------"
|
123
|
+
end
|
124
|
+
|
125
|
+
@lints << HamlLint::Lint.new(self, document.file, nil, msg, :error)
|
126
|
+
return
|
127
|
+
end
|
128
|
+
|
129
|
+
new_haml_string = new_haml_lines.join("\n")
|
130
|
+
|
131
|
+
if new_haml_validity_checks(new_haml_string)
|
132
|
+
document.change_source(new_haml_string)
|
133
|
+
true
|
134
|
+
else
|
135
|
+
false
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def new_haml_validity_checks(new_haml_string)
|
140
|
+
new_haml_error = HamlLint::Utils.check_error_when_compiling_haml(new_haml_string)
|
141
|
+
return true unless new_haml_error
|
142
|
+
|
143
|
+
error_message = if new_haml_error.is_a?(::SyntaxError)
|
144
|
+
'Corrections by haml-lint generate Haml that will have Ruby syntax error. Skipping.'
|
145
|
+
else
|
146
|
+
'Corrections by haml-lint generate invalid Haml. Skipping.'
|
147
|
+
end
|
148
|
+
|
149
|
+
if ENV['HAML_LINT_DEBUG'] == 'true'
|
150
|
+
error_message = error_message.dup
|
151
|
+
error_message << "\nDEBUG: Here is the exception:\n#{new_haml_error.full_message}"
|
152
|
+
|
153
|
+
error_message << "DEBUG: This is the (wrong) HAML after the corrections:\n"
|
154
|
+
if new_haml_error.respond_to?(:line)
|
155
|
+
error_message << "(DEBUG: Line number of error in the HAML: #{new_haml_error.line})\n"
|
156
|
+
end
|
157
|
+
error_message << new_haml_string
|
158
|
+
else
|
159
|
+
# Those are lints we couldn't correct. If haml-lint was called without the
|
160
|
+
# --auto-correct-only, then this linter will be called again without autocorrect,
|
161
|
+
# so the lints will be recorded then. If it was called with --auto-correct-only,
|
162
|
+
# then we did nothing so it makes sense not to show the lints.
|
163
|
+
@lints = []
|
164
|
+
end
|
165
|
+
|
166
|
+
@lints << HamlLint::Lint.new(self, document.file, nil, error_message, :error)
|
167
|
+
false
|
28
168
|
end
|
29
169
|
|
30
170
|
# A single CLI instance is shared between files to avoid RuboCop
|
31
171
|
# having to repeatedly reload .rubocop.yml.
|
32
|
-
def self.rubocop_cli
|
172
|
+
def self.rubocop_cli # rubocop:disable Lint/IneffectiveAccessModifier
|
33
173
|
# The ivar is stored on the class singleton rather than the Linter instance
|
34
174
|
# because it can't be Marshal.dump'd (as used by Parallel.map)
|
35
175
|
@rubocop_cli ||= ::RuboCop::CLI.new
|
36
176
|
end
|
37
177
|
|
38
|
-
|
178
|
+
def self.rubocop_config_store # rubocop:disable Lint/IneffectiveAccessModifier
|
179
|
+
@rubocop_config_store ||= RubocopConfigStore.new
|
180
|
+
end
|
39
181
|
|
40
|
-
# Executes RuboCop against the given Ruby code
|
41
|
-
# lints.
|
182
|
+
# Executes RuboCop against the given Ruby code, records the offenses as
|
183
|
+
# lints, runs autocorrect if requested and returns the corrected ruby.
|
42
184
|
#
|
43
|
-
# @param
|
185
|
+
# @param ruby_code [String] Ruby code
|
44
186
|
# @param source_map [Hash] map of Ruby code line numbers to original line
|
45
187
|
# numbers in the template
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
"#{document.file}.rb"
|
50
|
-
else
|
51
|
-
'ruby_script.rb'
|
52
|
-
end
|
188
|
+
# @return [String] The autocorrected Ruby source code
|
189
|
+
def process_ruby_source(ruby_code, source_map)
|
190
|
+
filename = document.file || 'ruby_script.rb'
|
53
191
|
|
54
|
-
|
55
|
-
|
56
|
-
|
192
|
+
offenses, corrected_ruby = run_rubocop(self.class.rubocop_cli, ruby_code, filename)
|
193
|
+
|
194
|
+
extract_lints_from_offenses(offenses, source_map)
|
195
|
+
corrected_ruby
|
57
196
|
end
|
58
197
|
|
59
|
-
#
|
198
|
+
# Runs RuboCop, returning the offenses and corrected code. Raises when RuboCop
|
199
|
+
# fails to run correctly.
|
60
200
|
#
|
61
|
-
# @param
|
62
|
-
# @param
|
63
|
-
# @
|
64
|
-
|
65
|
-
|
66
|
-
|
201
|
+
# @param rubocop_cli [RuboCop::CLI] There to simplify tests by using a stub
|
202
|
+
# @param ruby_code [String] The ruby code to run through RuboCop
|
203
|
+
# @param path [String] the path to tell RuboCop we are running
|
204
|
+
# @return [Array<RuboCop::Cop::Offense>, String]
|
205
|
+
def run_rubocop(rubocop_cli, ruby_code, path) # rubocop:disable Metrics
|
206
|
+
rubocop_status = nil
|
207
|
+
stdout_str, stderr_str = HamlLint::Utils.with_captured_streams(ruby_code) do
|
208
|
+
rubocop_cli.config_store.instance_variable_set(:@options_config, @rubocop_config)
|
209
|
+
rubocop_status = rubocop_cli.run(rubocop_flags + ['--stdin', path])
|
210
|
+
end
|
211
|
+
|
212
|
+
if ENV['HAML_LINT_INTERNAL_DEBUG'] == 'true'
|
213
|
+
if OffenseCollector.offenses.empty?
|
214
|
+
puts "------ No lints found by RuboCop in #{@document.file}"
|
215
|
+
else
|
216
|
+
puts "------ Raw lints found by RuboCop in #{@document.file}"
|
217
|
+
OffenseCollector.offenses.each do |offense|
|
218
|
+
puts offense
|
219
|
+
end
|
220
|
+
puts '------'
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
unless [::RuboCop::CLI::STATUS_SUCCESS, ::RuboCop::CLI::STATUS_OFFENSES].include?(rubocop_status)
|
225
|
+
if stderr_str.start_with?('Infinite loop')
|
226
|
+
msg = "RuboCop exited unsuccessfully with status #{rubocop_status}." \
|
227
|
+
' This appears to be due to an autocorrection infinite loop.'
|
228
|
+
if ENV['HAML_LINT_DEBUG'] == 'true'
|
229
|
+
msg += " DEBUG: RuboCop's output:\n"
|
230
|
+
msg += stderr_str.strip
|
231
|
+
else
|
232
|
+
msg += " First line of RuboCop's output (Use --debug mode to see more):\n"
|
233
|
+
msg += stderr_str.each_line.first.strip
|
234
|
+
end
|
235
|
+
|
236
|
+
raise HamlLint::Exceptions::InfiniteLoopError, msg
|
237
|
+
end
|
238
|
+
|
67
239
|
raise HamlLint::Exceptions::ConfigurationError,
|
68
|
-
"RuboCop exited unsuccessfully with status #{
|
69
|
-
'
|
240
|
+
"RuboCop exited unsuccessfully with status #{rubocop_status}." \
|
241
|
+
' Here is its output to check the stack trace or see if there was' \
|
242
|
+
" a misconfiguration:\n#{stderr_str}"
|
70
243
|
end
|
71
|
-
|
244
|
+
|
245
|
+
if @autocorrect
|
246
|
+
corrected_ruby = stdout_str.partition("#{'=' * 20}\n").last
|
247
|
+
end
|
248
|
+
|
249
|
+
[OffenseCollector.offenses, corrected_ruby]
|
72
250
|
end
|
73
251
|
|
74
252
|
# Aggregates RuboCop offenses and converts them to {HamlLint::Lint}s
|
@@ -76,51 +254,92 @@ module HamlLint
|
|
76
254
|
#
|
77
255
|
# @param offenses [Array<RuboCop::Cop::Offense>]
|
78
256
|
# @param source_map [Hash]
|
79
|
-
def extract_lints_from_offenses(offenses, source_map)
|
80
|
-
|
257
|
+
def extract_lints_from_offenses(offenses, source_map) # rubocop:disable Metrics
|
258
|
+
offenses.each do |offense|
|
259
|
+
next if Array(config['ignored_cops']).include?(offense.cop_name)
|
260
|
+
autocorrected = offense.status == :corrected
|
261
|
+
|
262
|
+
# There will be another execution to deal with not auto-corrected stuff unless
|
263
|
+
# we are in autocorrect-only mode, where we don't want not auto-corrected stuff.
|
264
|
+
next if @autocorrect && !autocorrected && offense.cop_name != 'Lint/Syntax'
|
81
265
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
266
|
+
if ENV['HAML_LINT_INTERNAL_DEBUG']
|
267
|
+
line = offense.line
|
268
|
+
else
|
269
|
+
line = source_map[offense.line]
|
270
|
+
|
271
|
+
if line.nil? && offense.line == source_map.keys.max + 1
|
272
|
+
# The sourcemap doesn't include an entry for the line just after the last line,
|
273
|
+
# but rubocop sometimes does place offenses there.
|
274
|
+
line = source_map[offense.line - 1]
|
275
|
+
end
|
276
|
+
end
|
277
|
+
record_lint(line, offense.message, offense.severity.name,
|
278
|
+
corrected: autocorrected)
|
86
279
|
end
|
87
280
|
end
|
88
281
|
|
89
282
|
# Record a lint for reporting back to the user.
|
90
283
|
#
|
91
|
-
# @param
|
284
|
+
# @param line [#line] line number of the lint
|
92
285
|
# @param message [String] error/warning to display to the user
|
93
286
|
# @param severity [Symbol] RuboCop severity level for the offense
|
94
|
-
def record_lint(
|
95
|
-
|
96
|
-
|
287
|
+
def record_lint(line, message, severity, corrected:)
|
288
|
+
# TODO: actual handling for RuboCop's new :info severity
|
289
|
+
return if severity == :info
|
290
|
+
|
291
|
+
@lints << HamlLint::Lint.new(self, @document.file, line, message,
|
292
|
+
SEVERITY_MAP.fetch(severity, :warning),
|
293
|
+
corrected: corrected)
|
97
294
|
end
|
98
295
|
|
99
296
|
# Returns flags that will be passed to RuboCop CLI.
|
100
297
|
#
|
101
298
|
# @return [Array<String>]
|
102
299
|
def rubocop_flags
|
103
|
-
config_file = ENV['HAML_LINT_RUBOCOP_CONF'] || config['config_file']
|
104
300
|
flags = %w[--format HamlLint::OffenseCollector]
|
105
|
-
flags +=
|
106
|
-
flags +=
|
301
|
+
flags += ignored_cops_flags
|
302
|
+
flags += rubocop_autocorrect_flags
|
107
303
|
flags
|
108
304
|
end
|
109
305
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
306
|
+
def rubocop_autocorrect_flags
|
307
|
+
return [] unless @autocorrect
|
308
|
+
|
309
|
+
rubocop_version = Gem::Version.new(::RuboCop::Version::STRING)
|
310
|
+
|
311
|
+
case @autocorrect
|
312
|
+
when :safe
|
313
|
+
if rubocop_version >= Gem::Version.new('1.30')
|
314
|
+
['--autocorrect']
|
315
|
+
else
|
316
|
+
['--auto-correct']
|
317
|
+
end
|
318
|
+
when :all
|
319
|
+
if rubocop_version >= Gem::Version.new('1.30')
|
320
|
+
['--autocorrect-all']
|
321
|
+
else
|
322
|
+
['--auto-correct-all']
|
323
|
+
end
|
324
|
+
else
|
325
|
+
raise "Unexpected autocorrect option: #{@autocorrect.inspect}"
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
# Because of autocorrect, we need to pass the ignored cops to RuboCop to
|
330
|
+
# prevent it from doing fixes we don't want.
|
331
|
+
# Because cop names changed names over time, we cleanup those that don't exist
|
332
|
+
# anymore or don't exist yet.
|
333
|
+
# This is not exhaustive, it's only for the cops that are in config/default.yml
|
334
|
+
def ignored_cops_flags
|
335
|
+
ignored_cops = config['ignored_cops']
|
336
|
+
|
337
|
+
if @autocorrect
|
338
|
+
ignored_cops += self.class.cops_names_not_supporting_autocorrect
|
339
|
+
end
|
340
|
+
|
341
|
+
return [] if ignored_cops.empty?
|
342
|
+
['--except', ignored_cops.uniq.join(',')]
|
124
343
|
end
|
125
344
|
end
|
126
345
|
|
@@ -147,4 +366,77 @@ module HamlLint
|
|
147
366
|
self.class.offenses += offenses
|
148
367
|
end
|
149
368
|
end
|
369
|
+
|
370
|
+
# To handle our need to force some configurations on RuboCop, while still allowing users
|
371
|
+
# to customize most of RuboCop using their own rubocop.yml config(s), we need to detect
|
372
|
+
# the effective RuboCop configuration for a specific file, and generate a new configuration
|
373
|
+
# containing our own "forced configuration" with a `inherit_from` that points on the
|
374
|
+
# user's configuration.
|
375
|
+
#
|
376
|
+
# This class handles all of this logic.
|
377
|
+
class RubocopConfigStore
|
378
|
+
def initialize
|
379
|
+
@dir_path_to_user_config_path = {}
|
380
|
+
@user_config_path_to_config_object = {}
|
381
|
+
end
|
382
|
+
|
383
|
+
# Build a RuboCop::Config from config/forced_rubocop_config.yml which inherits from the given
|
384
|
+
# user_config_path and return it's path.
|
385
|
+
def config_object_pointing_to(user_config_path)
|
386
|
+
if @user_config_path_to_config_object[user_config_path]
|
387
|
+
return @user_config_path_to_config_object[user_config_path]
|
388
|
+
end
|
389
|
+
|
390
|
+
final_config_hash = forced_rubocop_config_hash.dup
|
391
|
+
|
392
|
+
if user_config_path != ::RuboCop::ConfigLoader::DEFAULT_FILE
|
393
|
+
# If we manually inherit from the default RuboCop config, we may get warnings
|
394
|
+
# for deprecated stuff that is in it. We don't when we automatically
|
395
|
+
# inherit from it (which always happens)
|
396
|
+
final_config_hash['inherit_from'] = user_config_path
|
397
|
+
end
|
398
|
+
|
399
|
+
config_object = Tempfile.create(['.haml-lint-rubocop', '.yml']) do |tempfile|
|
400
|
+
tempfile.write(final_config_hash.to_yaml)
|
401
|
+
tempfile.close
|
402
|
+
::RuboCop::ConfigLoader.configuration_from_file(tempfile.path)
|
403
|
+
end
|
404
|
+
|
405
|
+
@user_config_path_to_config_object[user_config_path] = config_object
|
406
|
+
end
|
407
|
+
|
408
|
+
# Find the path to the effective RuboCop configuration for a path (file or dir)
|
409
|
+
def user_rubocop_config_path_for(path)
|
410
|
+
dir = if File.directory?(path)
|
411
|
+
path
|
412
|
+
else
|
413
|
+
File.dirname(path)
|
414
|
+
end
|
415
|
+
|
416
|
+
@dir_path_to_user_config_path[dir] ||= ::RuboCop::ConfigLoader.configuration_file_for(dir)
|
417
|
+
end
|
418
|
+
|
419
|
+
# Returns the content (Hash) of config/forced_rubocop_config.yml after processing it's ERB content.
|
420
|
+
# Cached since it doesn't change between files
|
421
|
+
def forced_rubocop_config_hash
|
422
|
+
return @forced_rubocop_config_hash if @forced_rubocop_config_hash
|
423
|
+
|
424
|
+
content = File.read(File.join(HamlLint::HOME, 'config', 'forced_rubocop_config.yml'))
|
425
|
+
processed_content = HamlLint::Utils.process_erb(content)
|
426
|
+
hash = YAML.safe_load(processed_content)
|
427
|
+
|
428
|
+
if ENV['HAML_LINT_TESTING']
|
429
|
+
# In newer RuboCop versions, new cops are not enabled by default, and instead
|
430
|
+
# show a message until they are used. We just want a default for them
|
431
|
+
# to avoid spamming STDOUT. Making it "disable" reduces the chances of having
|
432
|
+
# the test suite start failing after a new cop gets added.
|
433
|
+
hash['AllCops'] ||= {}
|
434
|
+
if Gem::Version.new(::RuboCop::Version::STRING) >= Gem::Version.new('1')
|
435
|
+
hash['AllCops']['NewCops'] = 'disable'
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
@forced_rubocop_config_hash = hash.freeze
|
440
|
+
end
|
441
|
+
end
|
150
442
|
end
|
@@ -9,7 +9,7 @@ module HamlLint
|
|
9
9
|
|
10
10
|
ALLOWED_SEPARATORS = [' ', '#'].freeze
|
11
11
|
|
12
|
-
def visit_tag(node) # rubocop:disable Metrics/CyclomaticComplexity
|
12
|
+
def visit_tag(node) # rubocop:disable Metrics/CyclomaticComplexity
|
13
13
|
# If this tag has inline script
|
14
14
|
return unless node.contains_script?
|
15
15
|
|
@@ -18,15 +18,13 @@ module HamlLint
|
|
18
18
|
|
19
19
|
tag_with_text = tag_with_inline_text(node)
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
return
|
29
|
-
end
|
21
|
+
# For tags with inline text that contain interpolation, the parser
|
22
|
+
# converts them to inline script by surrounding them in string quotes,
|
23
|
+
# e.g. `%p Hello #{name}` becomes `%p= "Hello #{name}"`, causing the
|
24
|
+
# above search to fail. Check for this case by removing added quotes.
|
25
|
+
if !(index = tag_with_text.rindex(text)) && !((text_without_quotes = strip_surrounding_quotes(text)) &&
|
26
|
+
(index = tag_with_text.rindex(text_without_quotes)))
|
27
|
+
return
|
30
28
|
end
|
31
29
|
|
32
30
|
return if tag_with_text[index] == '#' # Ignore code comments
|
@@ -35,7 +35,7 @@ module HamlLint
|
|
35
35
|
return unless tree = parse_ruby(script_node.script)
|
36
36
|
%i[str dstr].include?(tree.type) &&
|
37
37
|
!starts_with_reserved_character?(tree.children.first)
|
38
|
-
rescue ::Parser::SyntaxError
|
38
|
+
rescue ::Parser::SyntaxError
|
39
39
|
# Gracefully ignore syntax errors, as that's managed by a different linter
|
40
40
|
end
|
41
41
|
|
data/lib/haml_lint/linter.rb
CHANGED
@@ -24,11 +24,8 @@ module HamlLint
|
|
24
24
|
# Runs the linter against the given Haml document.
|
25
25
|
#
|
26
26
|
# @param document [HamlLint::Document]
|
27
|
-
def run(document)
|
28
|
-
|
29
|
-
@lints = []
|
30
|
-
visit(document.tree)
|
31
|
-
@lints
|
27
|
+
def run(document, autocorrect: nil) # rubocop:disable Metrics
|
28
|
+
run_or_raise(document, autocorrect: autocorrect)
|
32
29
|
rescue Parser::SyntaxError => e
|
33
30
|
location = e.diagnostic.location
|
34
31
|
@lints <<
|
@@ -39,6 +36,35 @@ module HamlLint
|
|
39
36
|
e.to_s,
|
40
37
|
:error
|
41
38
|
)
|
39
|
+
rescue StandardError => e
|
40
|
+
msg = "Couldn't process the file".dup
|
41
|
+
if @autocorrect
|
42
|
+
# Those lints related to auto-correction were not saved, so don't display them
|
43
|
+
@lints = []
|
44
|
+
msg << " for autocorrect '#{@autocorrect}'. "
|
45
|
+
else
|
46
|
+
msg << ' for linting. '
|
47
|
+
end
|
48
|
+
|
49
|
+
msg << "#{e.class.name}: #{e.message}"
|
50
|
+
if ENV['HAML_LINT_DEBUG'] == 'true'
|
51
|
+
msg << "(DEBUG: Backtrace follows)\n#{e.backtrace.join("\n")}\n------"
|
52
|
+
end
|
53
|
+
|
54
|
+
@lints << HamlLint::Lint.new(self, document.file, nil, msg, :error)
|
55
|
+
@lints
|
56
|
+
end
|
57
|
+
|
58
|
+
# Runs the linter against the given Haml document, raises if the file cannot be processed due to
|
59
|
+
# Syntax or HAML-Lint internal errors. (For testing purposes)
|
60
|
+
#
|
61
|
+
# @param document [HamlLint::Document]
|
62
|
+
def run_or_raise(document, autocorrect: nil)
|
63
|
+
@document = document
|
64
|
+
@lints = []
|
65
|
+
@autocorrect = autocorrect
|
66
|
+
visit(document.tree)
|
67
|
+
@lints
|
42
68
|
end
|
43
69
|
|
44
70
|
# Returns the simple name for this linter.
|
@@ -48,17 +74,38 @@ module HamlLint
|
|
48
74
|
self.class.name.to_s.split('::').last
|
49
75
|
end
|
50
76
|
|
77
|
+
# Returns true if this linter supports autocorrect, false otherwise
|
78
|
+
#
|
79
|
+
# @return [Boolean]
|
80
|
+
def self.supports_autocorrect?
|
81
|
+
@supports_autocorrect || false
|
82
|
+
end
|
83
|
+
|
84
|
+
def supports_autocorrect?
|
85
|
+
self.class.supports_autocorrect?
|
86
|
+
end
|
87
|
+
|
51
88
|
private
|
52
89
|
|
53
90
|
attr_reader :config, :document
|
54
91
|
|
92
|
+
# Linters can call supports_autocorrect(true) in their top-level scope to indicate that
|
93
|
+
# they supports autocorrect.
|
94
|
+
#
|
95
|
+
# @params value [Boolean] The new value for supports_autocorrect
|
96
|
+
private_class_method def self.supports_autocorrect(value)
|
97
|
+
@supports_autocorrect = value
|
98
|
+
end
|
99
|
+
|
55
100
|
# Record a lint for reporting back to the user.
|
56
101
|
#
|
57
|
-
# @param
|
102
|
+
# @param node_or_line [#line] line number or node to extract the line number from
|
58
103
|
# @param message [String] error/warning to display to the user
|
59
|
-
def record_lint(
|
60
|
-
|
61
|
-
|
104
|
+
def record_lint(node_or_line, message, corrected: false)
|
105
|
+
line = node_or_line.is_a?(Integer) ? node_or_line : node_or_line.line
|
106
|
+
@lints << HamlLint::Lint.new(self, @document.file, line, message,
|
107
|
+
config.fetch('severity', :warning).to_sym,
|
108
|
+
corrected: corrected)
|
62
109
|
end
|
63
110
|
|
64
111
|
# Parse Ruby code into an abstract syntax tree.
|