haml_lint 0.53.0 → 0.76.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/config/default.yml +17 -5
- data/config/forced_rubocop_config.yml +25 -13
- data/lib/haml_lint/adapter.rb +5 -4
- data/lib/haml_lint/cli.rb +7 -3
- data/lib/haml_lint/constants.rb +1 -1
- data/lib/haml_lint/directive.rb +2 -2
- data/lib/haml_lint/document.rb +14 -5
- data/lib/haml_lint/exceptions.rb +2 -2
- data/lib/haml_lint/extensions/haml_util_unescape_interpolation_tracking.rb +2 -2
- data/lib/haml_lint/haml_visitor.rb +1 -1
- data/lib/haml_lint/lint.rb +8 -3
- data/lib/haml_lint/linter/alignment_tabs.rb +21 -4
- data/lib/haml_lint/linter/class_attribute_with_static_value.rb +52 -12
- data/lib/haml_lint/linter/classes_before_ids.rb +15 -6
- data/lib/haml_lint/linter/consecutive_comments.rb +20 -1
- data/lib/haml_lint/linter/consecutive_silent_scripts.rb +68 -1
- data/lib/haml_lint/linter/empty_object_reference.rb +16 -1
- data/lib/haml_lint/linter/empty_script.rb +32 -2
- data/lib/haml_lint/linter/final_newline.rb +31 -7
- data/lib/haml_lint/linter/html_attributes.rb +71 -2
- data/lib/haml_lint/linter/id_names.rb +1 -1
- data/lib/haml_lint/linter/implicit_div.rb +14 -1
- data/lib/haml_lint/linter/indentation.rb +2 -2
- data/lib/haml_lint/linter/instance_variables.rb +28 -3
- data/lib/haml_lint/linter/leading_comment_space.rb +14 -2
- data/lib/haml_lint/linter/multiline_pipe.rb +3 -3
- data/lib/haml_lint/linter/multiline_script.rb +65 -5
- data/lib/haml_lint/linter/no_placeholders.rb +2 -2
- data/lib/haml_lint/linter/rubocop.rb +87 -76
- data/lib/haml_lint/linter/ruby_comments.rb +14 -4
- data/lib/haml_lint/linter/space_before_script.rb +37 -4
- data/lib/haml_lint/linter/space_inside_hash_attributes.rb +41 -3
- data/lib/haml_lint/linter/strict_locals.rb +69 -0
- data/lib/haml_lint/linter/tag_name.rb +15 -2
- data/lib/haml_lint/linter/trailing_empty_lines.rb +13 -1
- data/lib/haml_lint/linter/trailing_whitespace.rb +16 -3
- data/lib/haml_lint/linter/unescaped_html.rb +27 -0
- data/lib/haml_lint/linter/unnecessary_interpolation.rb +30 -3
- data/lib/haml_lint/linter/unnecessary_string_output.rb +108 -12
- data/lib/haml_lint/linter.rb +119 -5
- data/lib/haml_lint/node_transformer.rb +2 -2
- data/lib/haml_lint/options.rb +17 -3
- data/lib/haml_lint/rake_task.rb +3 -2
- data/lib/haml_lint/reporter/default_reporter.rb +1 -1
- data/lib/haml_lint/reporter/disabled_config_reporter.rb +29 -8
- data/lib/haml_lint/reporter/github_reporter.rb +50 -0
- data/lib/haml_lint/reporter/hash_reporter.rb +2 -0
- data/lib/haml_lint/reporter/json_reporter.rb +1 -1
- data/lib/haml_lint/reporter/progress_reporter.rb +2 -1
- data/lib/haml_lint/reporter/utils.rb +4 -2
- data/lib/haml_lint/reporter.rb +1 -1
- data/lib/haml_lint/ruby_extraction/base_chunk.rb +4 -4
- data/lib/haml_lint/ruby_extraction/chunk_extractor.rb +73 -32
- data/lib/haml_lint/ruby_extraction/coordinator.rb +9 -4
- data/lib/haml_lint/ruby_extraction/implicit_end_chunk.rb +1 -1
- data/lib/haml_lint/ruby_extraction/non_ruby_filter_chunk.rb +2 -2
- data/lib/haml_lint/ruby_extraction/placeholder_marker_chunk.rb +1 -1
- data/lib/haml_lint/ruby_extraction/ruby_filter_chunk.rb +1 -1
- data/lib/haml_lint/ruby_extraction/script_chunk.rb +2 -2
- data/lib/haml_lint/runner.rb +70 -30
- data/lib/haml_lint/source.rb +25 -0
- data/lib/haml_lint/spec/matchers/report_lint.rb +17 -6
- data/lib/haml_lint/spec/shared_rubocop_autocorrect_context.rb +34 -4
- data/lib/haml_lint/spec.rb +4 -4
- data/lib/haml_lint/tree/comment_node.rb +1 -1
- data/lib/haml_lint/tree/doctype_node.rb +1 -1
- data/lib/haml_lint/tree/filter_node.rb +14 -1
- data/lib/haml_lint/tree/haml_comment_node.rb +9 -2
- data/lib/haml_lint/tree/node.rb +4 -4
- data/lib/haml_lint/tree/root_node.rb +4 -4
- data/lib/haml_lint/tree/script_node.rb +1 -1
- data/lib/haml_lint/tree/silent_script_node.rb +1 -1
- data/lib/haml_lint/tree/tag_node.rb +69 -25
- data/lib/haml_lint/utils.rb +4 -31
- data/lib/haml_lint/version.rb +1 -1
- data/lib/haml_lint.rb +23 -23
- metadata +14 -10
|
@@ -4,18 +4,46 @@ require 'rubocop'
|
|
|
4
4
|
require 'tempfile'
|
|
5
5
|
|
|
6
6
|
module HamlLint
|
|
7
|
-
# Runs RuboCop on the Ruby code contained within
|
|
7
|
+
# Runs RuboCop on the Ruby code contained within Haml templates.
|
|
8
8
|
#
|
|
9
9
|
# The processing is done by extracting a Ruby file that matches the content, including
|
|
10
|
-
# the indentation, of the
|
|
11
|
-
# and get new Ruby code which should be
|
|
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
12
|
#
|
|
13
|
-
# The ruby extraction makes "Chunks" which wrap each
|
|
14
|
-
# use the corrected Ruby code to apply the corrections back in the
|
|
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
15
|
# to each type of Chunk.
|
|
16
16
|
#
|
|
17
17
|
# The work is spread across the classes in the HamlLint::RubyExtraction module.
|
|
18
18
|
class Linter::RuboCop < Linter
|
|
19
|
+
# Processes a ruby file and reports RuboCop offenses
|
|
20
|
+
class Runner < ::RuboCop::Runner
|
|
21
|
+
attr_reader :offenses
|
|
22
|
+
|
|
23
|
+
def run(haml_path, ruby_code, config:)
|
|
24
|
+
@offenses = []
|
|
25
|
+
@config_store.instance_variable_set(:@options_config, config)
|
|
26
|
+
# Using stdin also disables RuboCop's result cache, which is intentional:
|
|
27
|
+
# it reconstructs offense positions from the on-disk Haml, not the Ruby we
|
|
28
|
+
# inspected, so reusing it misreports lines (sds/haml-lint#593).
|
|
29
|
+
@options[:stdin] = ruby_code
|
|
30
|
+
super([haml_path])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def corrected_code
|
|
34
|
+
@options[:stdin]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Executed when a file has been scanned by RuboCop, adding the reported
|
|
38
|
+
# offenses to our collection.
|
|
39
|
+
#
|
|
40
|
+
# @param _file [String]
|
|
41
|
+
# @param offenses [Array<RuboCop::Cop::Offense>]
|
|
42
|
+
def file_finished(_file, offenses)
|
|
43
|
+
@offenses = offenses
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
19
47
|
include LinterRegistry
|
|
20
48
|
|
|
21
49
|
supports_autocorrect(true)
|
|
@@ -54,7 +82,7 @@ module HamlLint
|
|
|
54
82
|
@last_extracted_source = nil
|
|
55
83
|
@last_new_ruby_source = nil
|
|
56
84
|
|
|
57
|
-
coordinator = HamlLint::RubyExtraction::Coordinator.new(document)
|
|
85
|
+
coordinator = HamlLint::RubyExtraction::Coordinator.new(document, autocorrect: @autocorrect)
|
|
58
86
|
|
|
59
87
|
extracted_source = coordinator.extract_ruby_source
|
|
60
88
|
if ENV['HAML_LINT_INTERNAL_DEBUG'] == 'true'
|
|
@@ -98,9 +126,9 @@ module HamlLint
|
|
|
98
126
|
|
|
99
127
|
def rubocop_config_for(path)
|
|
100
128
|
user_config_path = ENV['HAML_LINT_RUBOCOP_CONF'] || config['config_file']
|
|
101
|
-
user_config_path ||=
|
|
129
|
+
user_config_path ||= rubocop_config_store.user_rubocop_config_path_for(path)
|
|
102
130
|
user_config_path = File.absolute_path(user_config_path)
|
|
103
|
-
|
|
131
|
+
rubocop_config_store.config_object_pointing_to(user_config_path)
|
|
104
132
|
end
|
|
105
133
|
|
|
106
134
|
# Extracted here so that tests can stub this to always return true
|
|
@@ -117,7 +145,7 @@ module HamlLint
|
|
|
117
145
|
# so the lints will be recorded then.
|
|
118
146
|
@lints = []
|
|
119
147
|
|
|
120
|
-
msg = "Corrections couldn't be
|
|
148
|
+
msg = "Corrections couldn't be transferred: #{e.message} - Consider linting the file " \
|
|
121
149
|
'without auto-correct and doing the changes manually.'
|
|
122
150
|
if ENV['HAML_LINT_DEBUG'] == 'true'
|
|
123
151
|
msg = "#{msg} DEBUG: Rubocop corrected Ruby code follows:\n#{new_ruby_code}\n------"
|
|
@@ -168,15 +196,13 @@ module HamlLint
|
|
|
168
196
|
false
|
|
169
197
|
end
|
|
170
198
|
|
|
171
|
-
# A single
|
|
199
|
+
# A single RuboCop runner is shared between files to avoid RuboCop
|
|
172
200
|
# having to repeatedly reload .rubocop.yml.
|
|
173
|
-
def
|
|
174
|
-
|
|
175
|
-
# because it can't be Marshal.dump'd (as used by Parallel.map)
|
|
176
|
-
@rubocop_cli ||= ::RuboCop::CLI.new
|
|
201
|
+
def rubocop_runner
|
|
202
|
+
@rubocop_runner ||= Runner.new(rubocop_options, ::RuboCop::ConfigStore.new)
|
|
177
203
|
end
|
|
178
204
|
|
|
179
|
-
def
|
|
205
|
+
def rubocop_config_store
|
|
180
206
|
@rubocop_config_store ||= RubocopConfigStore.new
|
|
181
207
|
end
|
|
182
208
|
|
|
@@ -190,7 +216,7 @@ module HamlLint
|
|
|
190
216
|
def process_ruby_source(ruby_code, source_map)
|
|
191
217
|
filename = document.file || 'ruby_script.rb'
|
|
192
218
|
|
|
193
|
-
offenses, corrected_ruby = run_rubocop(
|
|
219
|
+
offenses, corrected_ruby = run_rubocop(rubocop_runner, ruby_code, filename)
|
|
194
220
|
|
|
195
221
|
extract_lints_from_offenses(offenses, source_map)
|
|
196
222
|
corrected_ruby
|
|
@@ -199,55 +225,35 @@ module HamlLint
|
|
|
199
225
|
# Runs RuboCop, returning the offenses and corrected code. Raises when RuboCop
|
|
200
226
|
# fails to run correctly.
|
|
201
227
|
#
|
|
202
|
-
# @param
|
|
228
|
+
# @param rubocop_runner [HamlLint::Linter::RuboCop::Runner] There to simplify tests by using a stub
|
|
203
229
|
# @param ruby_code [String] The ruby code to run through RuboCop
|
|
204
230
|
# @param path [String] the path to tell RuboCop we are running
|
|
205
231
|
# @return [Array<RuboCop::Cop::Offense>, String]
|
|
206
|
-
def run_rubocop(
|
|
207
|
-
|
|
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
|
|
232
|
+
def run_rubocop(rubocop_runner, ruby_code, path) # rubocop:disable Metrics
|
|
233
|
+
rubocop_runner.run(path, ruby_code, config: rubocop_config_for(path))
|
|
212
234
|
|
|
213
235
|
if ENV['HAML_LINT_INTERNAL_DEBUG'] == 'true'
|
|
214
|
-
if
|
|
236
|
+
if rubocop_runner.offenses.empty?
|
|
215
237
|
puts "------ No lints found by RuboCop in #{@document.file}"
|
|
216
238
|
else
|
|
217
239
|
puts "------ Raw lints found by RuboCop in #{@document.file}"
|
|
218
|
-
|
|
240
|
+
rubocop_runner.offenses.each do |offense|
|
|
219
241
|
puts offense
|
|
220
242
|
end
|
|
221
243
|
puts '------'
|
|
222
244
|
end
|
|
223
245
|
end
|
|
224
246
|
|
|
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
|
-
|
|
240
|
-
raise HamlLint::Exceptions::ConfigurationError,
|
|
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}"
|
|
244
|
-
end
|
|
245
|
-
|
|
246
247
|
if @autocorrect
|
|
247
|
-
corrected_ruby =
|
|
248
|
+
corrected_ruby = rubocop_runner.corrected_code
|
|
248
249
|
end
|
|
249
250
|
|
|
250
|
-
[
|
|
251
|
+
[rubocop_runner.offenses, corrected_ruby]
|
|
252
|
+
rescue ::RuboCop::Error => e
|
|
253
|
+
raise HamlLint::Exceptions::ConfigurationError,
|
|
254
|
+
"RuboCop raised #{e}." \
|
|
255
|
+
' Here is its output to check the stack trace or see if there was' \
|
|
256
|
+
" a misconfiguration:\n#{e.message}\n#{e.backtrace}"
|
|
251
257
|
end
|
|
252
258
|
|
|
253
259
|
# Aggregates RuboCop offenses and converts them to {HamlLint::Lint}s
|
|
@@ -276,7 +282,7 @@ module HamlLint
|
|
|
276
282
|
end
|
|
277
283
|
end
|
|
278
284
|
record_lint(line, offense.message, offense.severity.name,
|
|
279
|
-
corrected: autocorrected)
|
|
285
|
+
corrected: autocorrected, correctable: offense.correctable?)
|
|
280
286
|
end
|
|
281
287
|
end
|
|
282
288
|
|
|
@@ -285,23 +291,38 @@ module HamlLint
|
|
|
285
291
|
# @param line [#line] line number of the lint
|
|
286
292
|
# @param message [String] error/warning to display to the user
|
|
287
293
|
# @param severity [Symbol] RuboCop severity level for the offense
|
|
288
|
-
|
|
294
|
+
# @param corrected [Boolean] whether RuboCop auto-corrected the offense
|
|
295
|
+
# @param correctable [Boolean] whether RuboCop is able to auto-correct the offense
|
|
296
|
+
def record_lint(line, message, severity, corrected:, correctable: false)
|
|
289
297
|
# TODO: actual handling for RuboCop's new :info severity
|
|
290
298
|
return if severity == :info
|
|
291
299
|
|
|
292
300
|
@lints << HamlLint::Lint.new(self, @document.file, line, message,
|
|
293
301
|
SEVERITY_MAP.fetch(severity, :warning),
|
|
294
|
-
corrected: corrected)
|
|
302
|
+
corrected: corrected, correctable: correctable)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# rubocop:disable Style/MutableConstant
|
|
306
|
+
# Using BaseFormatter suppresses any default output
|
|
307
|
+
DEFAULT_FLAGS = %w[--format RuboCop::Formatter::BaseFormatter]
|
|
308
|
+
begin
|
|
309
|
+
::RuboCop::Options.new.parse(['--raise-cop-error'])
|
|
310
|
+
DEFAULT_FLAGS << '--raise-cop-error'
|
|
311
|
+
rescue OptionParser::InvalidOption
|
|
312
|
+
# older versions of RuboCop don't support this flag
|
|
295
313
|
end
|
|
314
|
+
DEFAULT_FLAGS.freeze
|
|
315
|
+
# rubocop:enable Style/MutableConstant
|
|
296
316
|
|
|
297
|
-
# Returns
|
|
317
|
+
# Returns options that will be passed to the RuboCop runner.
|
|
298
318
|
#
|
|
299
|
-
# @return [
|
|
300
|
-
def
|
|
301
|
-
flags =
|
|
319
|
+
# @return [Hash]
|
|
320
|
+
def rubocop_options
|
|
321
|
+
flags = DEFAULT_FLAGS
|
|
302
322
|
flags += ignored_cops_flags
|
|
303
323
|
flags += rubocop_autocorrect_flags
|
|
304
|
-
flags
|
|
324
|
+
options, _args = ::RuboCop::Options.new.parse(flags)
|
|
325
|
+
options
|
|
305
326
|
end
|
|
306
327
|
|
|
307
328
|
def rubocop_autocorrect_flags
|
|
@@ -342,29 +363,19 @@ module HamlLint
|
|
|
342
363
|
return [] if ignored_cops.empty?
|
|
343
364
|
['--except', ignored_cops.uniq.join(',')]
|
|
344
365
|
end
|
|
345
|
-
end
|
|
346
|
-
|
|
347
|
-
# Collects offenses detected by RuboCop.
|
|
348
|
-
class OffenseCollector < ::RuboCop::Formatter::BaseFormatter
|
|
349
|
-
class << self
|
|
350
|
-
# List of offenses reported by RuboCop.
|
|
351
|
-
attr_accessor :offenses
|
|
352
|
-
end
|
|
353
366
|
|
|
354
|
-
#
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
367
|
+
# Exclude ivars that don't marshal properly
|
|
368
|
+
def marshal_dump
|
|
369
|
+
excluded_ivars = %i[@rubocop_runner @rubocop_config_store @user_config_path_to_config_object]
|
|
370
|
+
(instance_variables - excluded_ivars).to_h do |ivar|
|
|
371
|
+
[ivar, instance_variable_get(ivar)]
|
|
372
|
+
end
|
|
359
373
|
end
|
|
360
374
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
# @param offenses [Array<RuboCop::Cop::Offense>]
|
|
366
|
-
def file_finished(_file, offenses)
|
|
367
|
-
self.class.offenses += offenses
|
|
375
|
+
def marshal_load(ivars)
|
|
376
|
+
ivars.each do |k, v|
|
|
377
|
+
instance_variable_set(k, v)
|
|
378
|
+
end
|
|
368
379
|
end
|
|
369
380
|
end
|
|
370
381
|
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module HamlLint
|
|
4
|
-
# Checks for Ruby comments that can be written as
|
|
4
|
+
# Checks for Ruby comments that can be written as Haml comments.
|
|
5
5
|
class Linter::RubyComments < Linter
|
|
6
6
|
include LinterRegistry
|
|
7
7
|
|
|
8
|
+
supports_autocorrect(true)
|
|
9
|
+
|
|
8
10
|
def visit_silent_script(node)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
return unless code_comment?(node)
|
|
12
|
+
|
|
13
|
+
corrected = correct_comment(node)
|
|
14
|
+
record_lint(node, 'Use `-#` for comments instead of `- #`', corrected: corrected)
|
|
12
15
|
end
|
|
13
16
|
|
|
14
17
|
private
|
|
@@ -16,5 +19,12 @@ module HamlLint
|
|
|
16
19
|
def code_comment?(node)
|
|
17
20
|
node.script =~ /\A\s+#/
|
|
18
21
|
end
|
|
22
|
+
|
|
23
|
+
# @return [Boolean]
|
|
24
|
+
def correct_comment(node)
|
|
25
|
+
index = node.line - 1
|
|
26
|
+
line = autocorrected_lines[index]
|
|
27
|
+
correct_line(index, line.sub(/\A(\s*)-\s+#/, '\1-#'))
|
|
28
|
+
end
|
|
19
29
|
end
|
|
20
30
|
end
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module HamlLint
|
|
4
|
-
# Checks for Ruby script in
|
|
4
|
+
# Checks for Ruby script in Haml templates with no space after the `=`/`-`.
|
|
5
5
|
class Linter::SpaceBeforeScript < Linter
|
|
6
6
|
include LinterRegistry
|
|
7
7
|
|
|
8
|
+
supports_autocorrect(true)
|
|
9
|
+
|
|
8
10
|
MESSAGE_FORMAT = 'The %s symbol should have one space separating it from code'
|
|
9
11
|
|
|
10
12
|
ALLOWED_SEPARATORS = [' ', '#'].freeze
|
|
@@ -33,18 +35,23 @@ module HamlLint
|
|
|
33
35
|
# (need to do it this way as the parser strips whitespace from node)
|
|
34
36
|
return unless tag_with_text[index - 1] != ' '
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
corrected = correct_inline_script(node, text)
|
|
39
|
+
record_lint(node, MESSAGE_FORMAT % '=', corrected: corrected)
|
|
37
40
|
end
|
|
38
41
|
|
|
39
42
|
def visit_script(node)
|
|
40
43
|
# Plain text nodes with interpolation are converted to script nodes, so we
|
|
41
44
|
# need to ignore them here.
|
|
42
45
|
return unless document.source_lines[node.line - 1].lstrip.start_with?('=')
|
|
43
|
-
|
|
46
|
+
return unless missing_space?(node)
|
|
47
|
+
|
|
48
|
+
record_lint(node, MESSAGE_FORMAT % '=', corrected: correct_leading_marker(node, '='))
|
|
44
49
|
end
|
|
45
50
|
|
|
46
51
|
def visit_silent_script(node)
|
|
47
|
-
|
|
52
|
+
return unless missing_space?(node)
|
|
53
|
+
|
|
54
|
+
record_lint(node, MESSAGE_FORMAT % '-', corrected: correct_leading_marker(node, '-'))
|
|
48
55
|
end
|
|
49
56
|
|
|
50
57
|
private
|
|
@@ -53,5 +60,31 @@ module HamlLint
|
|
|
53
60
|
text = node.script
|
|
54
61
|
!ALLOWED_SEPARATORS.include?(text[0]) if text
|
|
55
62
|
end
|
|
63
|
+
|
|
64
|
+
# Inserts one space after a leading `=`/`-` marker.
|
|
65
|
+
#
|
|
66
|
+
# @return [Boolean]
|
|
67
|
+
def correct_leading_marker(node, marker)
|
|
68
|
+
index = node.line - 1
|
|
69
|
+
line = autocorrected_lines[index]
|
|
70
|
+
escaped = Regexp.escape(marker)
|
|
71
|
+
correct_line(index, line.sub(/\A(\s*#{escaped})(?=[^\s#{escaped}])/, '\1 '))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Inserts a space after the `=` marker introducing a tag's inline script.
|
|
75
|
+
#
|
|
76
|
+
# @return [Boolean]
|
|
77
|
+
def correct_inline_script(node, text)
|
|
78
|
+
index = node.line - 1
|
|
79
|
+
line = autocorrected_lines[index]
|
|
80
|
+
|
|
81
|
+
pos = line.rindex("=#{text}")
|
|
82
|
+
if pos.nil? && (unquoted = strip_surrounding_quotes(text))
|
|
83
|
+
pos = line.rindex("=#{unquoted}")
|
|
84
|
+
end
|
|
85
|
+
return false unless pos
|
|
86
|
+
|
|
87
|
+
correct_line(index, "#{line[0..pos]} #{line[(pos + 1)..]}")
|
|
88
|
+
end
|
|
56
89
|
end
|
|
57
90
|
end
|
|
@@ -6,6 +6,8 @@ module HamlLint
|
|
|
6
6
|
class Linter::SpaceInsideHashAttributes < Linter
|
|
7
7
|
include LinterRegistry
|
|
8
8
|
|
|
9
|
+
supports_autocorrect(true)
|
|
10
|
+
|
|
9
11
|
STYLE = {
|
|
10
12
|
'no_space' => {
|
|
11
13
|
start_regex: /\A\{[^ ]/,
|
|
@@ -24,11 +26,47 @@ module HamlLint
|
|
|
24
26
|
def visit_tag(node)
|
|
25
27
|
return unless node.hash_attributes?
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
style_name = config['style'] == 'no_space' ? 'no_space' : 'space'
|
|
30
|
+
style = STYLE[style_name]
|
|
28
31
|
source = node.hash_attributes_source
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
start_ok = source.match?(style[:start_regex])
|
|
34
|
+
end_ok = source.match?(style[:end_regex])
|
|
35
|
+
return if start_ok && end_ok
|
|
36
|
+
|
|
37
|
+
corrected = correct_hash_spacing(node, source, style_name)
|
|
38
|
+
record_lint(node, style[:start_message], corrected: corrected) unless start_ok
|
|
39
|
+
record_lint(node, style[:end_message], corrected: corrected) unless end_ok
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# @return [Boolean]
|
|
45
|
+
def correct_hash_spacing(node, source, style_name)
|
|
46
|
+
return false unless source
|
|
47
|
+
return false if source.include?("\n") # multi-line hash: detection-only
|
|
48
|
+
|
|
49
|
+
index = node.line - 1
|
|
50
|
+
line = autocorrected_lines[index]
|
|
51
|
+
return false unless line.include?(source)
|
|
52
|
+
|
|
53
|
+
fixed = corrected_hash_source(source, style_name)
|
|
54
|
+
return false if fixed == source
|
|
55
|
+
|
|
56
|
+
correct_line(index, line.sub(source) { fixed })
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @return [String]
|
|
60
|
+
def corrected_hash_source(source, style_name)
|
|
61
|
+
inner = source[1...-1].strip
|
|
62
|
+
|
|
63
|
+
if style_name == 'no_space'
|
|
64
|
+
"{#{inner}}"
|
|
65
|
+
elsif inner.empty?
|
|
66
|
+
'{ }'
|
|
67
|
+
else
|
|
68
|
+
"{ #{inner} }"
|
|
69
|
+
end
|
|
32
70
|
end
|
|
33
71
|
end
|
|
34
72
|
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HamlLint
|
|
4
|
+
# Checks for the presence of a `locals` magic comment at the beginning of a partial.
|
|
5
|
+
class Linter::StrictLocals < Linter
|
|
6
|
+
include LinterRegistry
|
|
7
|
+
|
|
8
|
+
DummyNode = Struct.new(:line)
|
|
9
|
+
|
|
10
|
+
# Enables the linter if the tree is for the right file type.
|
|
11
|
+
#
|
|
12
|
+
# @param [HamlLint::Tree::RootNode] the root of a syntax tree
|
|
13
|
+
# @return [true, false] whether the linter is enabled for the tree
|
|
14
|
+
def visit_root(root)
|
|
15
|
+
return unless enabled?(root)
|
|
16
|
+
|
|
17
|
+
# Rails technically allows the comment to be anywhere in the file,
|
|
18
|
+
# but as a best practice they should be at the top of the file.
|
|
19
|
+
# https://guides.rubyonrails.org/action_view_overview.html#strict-locals
|
|
20
|
+
# https://github.com/rails/rails/blob/v8.0.2/actionview/lib/action_view/template.rb#L368
|
|
21
|
+
found =
|
|
22
|
+
root.children
|
|
23
|
+
.take_while { |child| child.is_a?(HamlLint::Tree::HamlCommentNode) }
|
|
24
|
+
.any?(&:is_strict_locals?)
|
|
25
|
+
|
|
26
|
+
return if found
|
|
27
|
+
|
|
28
|
+
record_lint(DummyNode.new(1), failure_message)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# Checks whether the linter is enabled for the file.
|
|
34
|
+
#
|
|
35
|
+
# @api private
|
|
36
|
+
# @return [true, false]
|
|
37
|
+
def enabled?(root)
|
|
38
|
+
return false unless matcher.match?(File.basename(root.file))
|
|
39
|
+
|
|
40
|
+
# This linter can also be disabled by a comment at the top of the file
|
|
41
|
+
first_child = root.children.first
|
|
42
|
+
first_child.nil? || !first_child.disabled?(self)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The type of files the linter is configured to check.
|
|
46
|
+
#
|
|
47
|
+
# @api private
|
|
48
|
+
# @return [String]
|
|
49
|
+
def file_types
|
|
50
|
+
@file_types ||= config['file_types'] || 'partial'
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The matcher to use for testing whether to check a file by file name.
|
|
54
|
+
#
|
|
55
|
+
# @api private
|
|
56
|
+
# @return [Regexp]
|
|
57
|
+
def matcher
|
|
58
|
+
@matcher ||= Regexp.new(config['matchers'][file_types] || '\A_.*\.haml\z')
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The error message when an `locals` comment is not found.
|
|
62
|
+
#
|
|
63
|
+
# @api private
|
|
64
|
+
# @return [String]
|
|
65
|
+
def failure_message
|
|
66
|
+
'Expected a strict `-# locals: ()` comment at the beginning of the file'
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -5,11 +5,24 @@ module HamlLint
|
|
|
5
5
|
class Linter::TagName < Linter
|
|
6
6
|
include LinterRegistry
|
|
7
7
|
|
|
8
|
+
supports_autocorrect(true)
|
|
9
|
+
|
|
8
10
|
def visit_tag(node)
|
|
9
11
|
tag = node.tag_name
|
|
10
|
-
return unless
|
|
12
|
+
return unless /[A-Z]/.match?(tag)
|
|
13
|
+
|
|
14
|
+
corrected = correct_tag_name(node, tag)
|
|
15
|
+
record_lint(node, "`#{tag}` should be written in lowercase as `#{tag.downcase}`",
|
|
16
|
+
corrected: corrected)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
11
20
|
|
|
12
|
-
|
|
21
|
+
# @return [Boolean]
|
|
22
|
+
def correct_tag_name(node, tag)
|
|
23
|
+
index = node.line - 1
|
|
24
|
+
line = autocorrected_lines[index]
|
|
25
|
+
correct_line(index, line.sub(/(%)#{Regexp.escape(tag)}/, "\\1#{tag.downcase}"))
|
|
13
26
|
end
|
|
14
27
|
end
|
|
15
28
|
end
|
|
@@ -5,6 +5,8 @@ module HamlLint
|
|
|
5
5
|
class Linter::TrailingEmptyLines < Linter
|
|
6
6
|
include LinterRegistry
|
|
7
7
|
|
|
8
|
+
supports_autocorrect(true)
|
|
9
|
+
|
|
8
10
|
DummyNode = Struct.new(:line)
|
|
9
11
|
|
|
10
12
|
def visit_root(root)
|
|
@@ -16,7 +18,17 @@ module HamlLint
|
|
|
16
18
|
|
|
17
19
|
return unless document.source.end_with?("\n\n")
|
|
18
20
|
|
|
19
|
-
record_lint(line_number, 'Files should not end with trailing empty lines'
|
|
21
|
+
record_lint(line_number, 'Files should not end with trailing empty lines',
|
|
22
|
+
corrected: autocorrect?)
|
|
23
|
+
|
|
24
|
+
apply_autocorrect(corrected_source)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def corrected_source
|
|
30
|
+
last_non_empty_index = document.source_lines.rindex { |line| !line.empty? } || 0
|
|
31
|
+
"#{document.source_lines[0..last_non_empty_index].join("\n")}\n"
|
|
20
32
|
end
|
|
21
33
|
end
|
|
22
34
|
end
|
|
@@ -5,17 +5,30 @@ module HamlLint
|
|
|
5
5
|
class Linter::TrailingWhitespace < Linter
|
|
6
6
|
include LinterRegistry
|
|
7
7
|
|
|
8
|
+
supports_autocorrect(true)
|
|
9
|
+
|
|
8
10
|
DummyNode = Struct.new(:line)
|
|
9
11
|
|
|
10
12
|
def visit_root(root)
|
|
13
|
+
new_lines = document.source_lines.dup
|
|
14
|
+
changed = false
|
|
15
|
+
|
|
11
16
|
document.source_lines.each_with_index do |line, index|
|
|
12
|
-
next unless
|
|
17
|
+
next unless /\s+$/.match?(line)
|
|
13
18
|
|
|
14
19
|
node = root.node_for_line(index + 1)
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
next if node.disabled?(self)
|
|
21
|
+
|
|
22
|
+
if autocorrect?
|
|
23
|
+
new_lines[index] = line.sub(/\s+$/, '')
|
|
24
|
+
changed = true
|
|
17
25
|
end
|
|
26
|
+
|
|
27
|
+
record_lint(DummyNode.new(index + 1), 'Line contains trailing whitespace',
|
|
28
|
+
corrected: autocorrect?)
|
|
18
29
|
end
|
|
30
|
+
|
|
31
|
+
apply_autocorrect(new_lines.join("\n")) if changed
|
|
19
32
|
end
|
|
20
33
|
end
|
|
21
34
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HamlLint
|
|
4
|
+
# Flags Haml's unescaped-output markers (`!=`, `!~`, and the unescaped
|
|
5
|
+
# plain-text `!`), which bypass HTML escaping.
|
|
6
|
+
#
|
|
7
|
+
# Like `raw`, `html_safe`, and `h()` in Rails, these make it easy to
|
|
8
|
+
# accidentally introduce XSS vulnerabilities when the output includes
|
|
9
|
+
# user-controlled data, e.g.:
|
|
10
|
+
#
|
|
11
|
+
# != "Username: <strong>#{user.name}</strong>"
|
|
12
|
+
class Linter::UnescapedHtml < Linter
|
|
13
|
+
include LinterRegistry
|
|
14
|
+
|
|
15
|
+
MESSAGE =
|
|
16
|
+
'Avoid outputting unescaped HTML with `!`; it bypasses HTML escaping and ' \
|
|
17
|
+
'can introduce XSS vulnerabilities. Sanitize the value instead.'
|
|
18
|
+
|
|
19
|
+
def visit_script(node)
|
|
20
|
+
record_lint(node, MESSAGE) if /\A\s*!/.match?(node.source_code)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def visit_tag(node)
|
|
24
|
+
record_lint(node, MESSAGE) if node.unescape_html?
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -11,21 +11,48 @@ module HamlLint
|
|
|
11
11
|
class Linter::UnnecessaryInterpolation < Linter
|
|
12
12
|
include LinterRegistry
|
|
13
13
|
|
|
14
|
+
supports_autocorrect(true)
|
|
15
|
+
|
|
14
16
|
def visit_tag(node)
|
|
15
17
|
return if node.script.length <= 2
|
|
16
18
|
|
|
17
19
|
count = 0
|
|
18
20
|
chars = 2 # Include surrounding quote chars
|
|
19
|
-
|
|
21
|
+
interpolated_code = nil
|
|
22
|
+
HamlLint::Utils.extract_interpolated_values(node.script) do |code, _line|
|
|
20
23
|
count += 1
|
|
21
24
|
return if count > 1 # rubocop:disable Lint/NonLocalExitFromIterator
|
|
22
|
-
chars +=
|
|
25
|
+
chars += code.length + 3
|
|
26
|
+
interpolated_code = code
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
if chars == node.script.length
|
|
30
|
+
corrected = correct_interpolation(node, interpolated_code)
|
|
26
31
|
record_lint(node, '`%... \#{expression}` can be written without ' \
|
|
27
|
-
'interpolation as `%...= expression`')
|
|
32
|
+
'interpolation as `%...= expression`', corrected: corrected)
|
|
28
33
|
end
|
|
29
34
|
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# @return [Boolean]
|
|
39
|
+
def correct_interpolation(node, interpolated_code)
|
|
40
|
+
return false unless interpolated_code
|
|
41
|
+
|
|
42
|
+
index = node.line - 1
|
|
43
|
+
line = autocorrected_lines[index]
|
|
44
|
+
escaped = Regexp.escape(interpolated_code)
|
|
45
|
+
|
|
46
|
+
new_line =
|
|
47
|
+
if inline_content_is_string?(node)
|
|
48
|
+
line.sub(/=\s*"\#\{#{escaped}\}"\s*\z/, "= #{interpolated_code}")
|
|
49
|
+
else
|
|
50
|
+
line.sub(/\s+\#\{#{escaped}\}\s*\z/, "= #{interpolated_code}")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
return false if new_line == line
|
|
54
|
+
|
|
55
|
+
correct_line(index, new_line)
|
|
56
|
+
end
|
|
30
57
|
end
|
|
31
58
|
end
|