haml_lint 0.40.0 → 0.51.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 +9 -27
- data/config/forced_rubocop_config.yml +180 -0
- data/lib/haml_lint/adapter/haml_4.rb +20 -0
- data/lib/haml_lint/adapter/haml_5.rb +11 -0
- data/lib/haml_lint/adapter/haml_6.rb +59 -0
- data/lib/haml_lint/adapter.rb +2 -0
- data/lib/haml_lint/cli.rb +8 -3
- data/lib/haml_lint/configuration_loader.rb +49 -13
- 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/repeated_id.rb +2 -1
- data/lib/haml_lint/linter/rubocop.rb +353 -59
- data/lib/haml_lint/linter/space_before_script.rb +8 -10
- data/lib/haml_lint/linter/trailing_empty_lines.rb +22 -0
- 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 +60 -10
- 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 +24 -0
- data/lib/haml_lint/ruby_extraction/base_chunk.rb +114 -0
- data/lib/haml_lint/ruby_extraction/chunk_extractor.rb +672 -0
- data/lib/haml_lint/ruby_extraction/coordinator.rb +181 -0
- data/lib/haml_lint/ruby_extraction/haml_comment_chunk.rb +34 -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 +251 -0
- data/lib/haml_lint/ruby_extraction/tag_attributes_chunk.rb +63 -0
- data/lib/haml_lint/ruby_extraction/tag_script_chunk.rb +30 -0
- data/lib/haml_lint/ruby_parser.rb +11 -1
- 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 +158 -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/script_node.rb +7 -1
- data/lib/haml_lint/tree/silent_script_node.rb +16 -1
- data/lib/haml_lint/tree/tag_node.rb +5 -9
- data/lib/haml_lint/utils.rb +135 -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 +29 -15
- 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
|
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
|
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.
|
data/lib/haml_lint/lint.rb
CHANGED
@@ -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(
|
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(
|
19
|
+
record_lint(line_number, 'Files should end with a trailing newline')
|
19
20
|
end
|
20
21
|
elsif ends_with_newline
|
21
|
-
record_lint(
|
22
|
+
record_lint(line_number, 'Files should not end with a trailing newline')
|
22
23
|
end
|
23
24
|
end
|
24
25
|
end
|
@@ -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
|
@@ -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 =
|
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
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
130
|
+
new_haml_string = new_haml_lines.join("\n")
|
26
131
|
|
27
|
-
|
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
|
-
|
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
|
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
|
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
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
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
|
-
#
|
199
|
+
# Runs RuboCop, returning the offenses and corrected code. Raises when RuboCop
|
200
|
+
# fails to run correctly.
|
60
201
|
#
|
61
|
-
# @param
|
62
|
-
# @param
|
63
|
-
# @
|
64
|
-
|
65
|
-
|
66
|
-
|
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 #{
|
69
|
-
'
|
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
|
-
|
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
|
-
|
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
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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
|
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(
|
95
|
-
|
96
|
-
|
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 +=
|
105
|
-
flags +=
|
302
|
+
flags += ignored_cops_flags
|
303
|
+
flags += rubocop_autocorrect_flags
|
106
304
|
flags
|
107
305
|
end
|
108
306
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
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
|