haml_lint 0.40.0 → 0.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|