haml_lint 0.44.0 → 0.46.0
Sign up to get free protection for your applications and to get access to all the features.
- 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.
|