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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +17 -5
  3. data/config/forced_rubocop_config.yml +25 -13
  4. data/lib/haml_lint/adapter.rb +5 -4
  5. data/lib/haml_lint/cli.rb +7 -3
  6. data/lib/haml_lint/constants.rb +1 -1
  7. data/lib/haml_lint/directive.rb +2 -2
  8. data/lib/haml_lint/document.rb +14 -5
  9. data/lib/haml_lint/exceptions.rb +2 -2
  10. data/lib/haml_lint/extensions/haml_util_unescape_interpolation_tracking.rb +2 -2
  11. data/lib/haml_lint/haml_visitor.rb +1 -1
  12. data/lib/haml_lint/lint.rb +8 -3
  13. data/lib/haml_lint/linter/alignment_tabs.rb +21 -4
  14. data/lib/haml_lint/linter/class_attribute_with_static_value.rb +52 -12
  15. data/lib/haml_lint/linter/classes_before_ids.rb +15 -6
  16. data/lib/haml_lint/linter/consecutive_comments.rb +20 -1
  17. data/lib/haml_lint/linter/consecutive_silent_scripts.rb +68 -1
  18. data/lib/haml_lint/linter/empty_object_reference.rb +16 -1
  19. data/lib/haml_lint/linter/empty_script.rb +32 -2
  20. data/lib/haml_lint/linter/final_newline.rb +31 -7
  21. data/lib/haml_lint/linter/html_attributes.rb +71 -2
  22. data/lib/haml_lint/linter/id_names.rb +1 -1
  23. data/lib/haml_lint/linter/implicit_div.rb +14 -1
  24. data/lib/haml_lint/linter/indentation.rb +2 -2
  25. data/lib/haml_lint/linter/instance_variables.rb +28 -3
  26. data/lib/haml_lint/linter/leading_comment_space.rb +14 -2
  27. data/lib/haml_lint/linter/multiline_pipe.rb +3 -3
  28. data/lib/haml_lint/linter/multiline_script.rb +65 -5
  29. data/lib/haml_lint/linter/no_placeholders.rb +2 -2
  30. data/lib/haml_lint/linter/rubocop.rb +87 -76
  31. data/lib/haml_lint/linter/ruby_comments.rb +14 -4
  32. data/lib/haml_lint/linter/space_before_script.rb +37 -4
  33. data/lib/haml_lint/linter/space_inside_hash_attributes.rb +41 -3
  34. data/lib/haml_lint/linter/strict_locals.rb +69 -0
  35. data/lib/haml_lint/linter/tag_name.rb +15 -2
  36. data/lib/haml_lint/linter/trailing_empty_lines.rb +13 -1
  37. data/lib/haml_lint/linter/trailing_whitespace.rb +16 -3
  38. data/lib/haml_lint/linter/unescaped_html.rb +27 -0
  39. data/lib/haml_lint/linter/unnecessary_interpolation.rb +30 -3
  40. data/lib/haml_lint/linter/unnecessary_string_output.rb +108 -12
  41. data/lib/haml_lint/linter.rb +119 -5
  42. data/lib/haml_lint/node_transformer.rb +2 -2
  43. data/lib/haml_lint/options.rb +17 -3
  44. data/lib/haml_lint/rake_task.rb +3 -2
  45. data/lib/haml_lint/reporter/default_reporter.rb +1 -1
  46. data/lib/haml_lint/reporter/disabled_config_reporter.rb +29 -8
  47. data/lib/haml_lint/reporter/github_reporter.rb +50 -0
  48. data/lib/haml_lint/reporter/hash_reporter.rb +2 -0
  49. data/lib/haml_lint/reporter/json_reporter.rb +1 -1
  50. data/lib/haml_lint/reporter/progress_reporter.rb +2 -1
  51. data/lib/haml_lint/reporter/utils.rb +4 -2
  52. data/lib/haml_lint/reporter.rb +1 -1
  53. data/lib/haml_lint/ruby_extraction/base_chunk.rb +4 -4
  54. data/lib/haml_lint/ruby_extraction/chunk_extractor.rb +73 -32
  55. data/lib/haml_lint/ruby_extraction/coordinator.rb +9 -4
  56. data/lib/haml_lint/ruby_extraction/implicit_end_chunk.rb +1 -1
  57. data/lib/haml_lint/ruby_extraction/non_ruby_filter_chunk.rb +2 -2
  58. data/lib/haml_lint/ruby_extraction/placeholder_marker_chunk.rb +1 -1
  59. data/lib/haml_lint/ruby_extraction/ruby_filter_chunk.rb +1 -1
  60. data/lib/haml_lint/ruby_extraction/script_chunk.rb +2 -2
  61. data/lib/haml_lint/runner.rb +70 -30
  62. data/lib/haml_lint/source.rb +25 -0
  63. data/lib/haml_lint/spec/matchers/report_lint.rb +17 -6
  64. data/lib/haml_lint/spec/shared_rubocop_autocorrect_context.rb +34 -4
  65. data/lib/haml_lint/spec.rb +4 -4
  66. data/lib/haml_lint/tree/comment_node.rb +1 -1
  67. data/lib/haml_lint/tree/doctype_node.rb +1 -1
  68. data/lib/haml_lint/tree/filter_node.rb +14 -1
  69. data/lib/haml_lint/tree/haml_comment_node.rb +9 -2
  70. data/lib/haml_lint/tree/node.rb +4 -4
  71. data/lib/haml_lint/tree/root_node.rb +4 -4
  72. data/lib/haml_lint/tree/script_node.rb +1 -1
  73. data/lib/haml_lint/tree/silent_script_node.rb +1 -1
  74. data/lib/haml_lint/tree/tag_node.rb +69 -25
  75. data/lib/haml_lint/utils.rb +4 -31
  76. data/lib/haml_lint/version.rb +1 -1
  77. data/lib/haml_lint.rb +23 -23
  78. metadata +14 -10
@@ -11,32 +11,99 @@ module HamlLint
11
11
  class Linter::UnnecessaryStringOutput < Linter
12
12
  include LinterRegistry
13
13
 
14
+ supports_autocorrect(true)
15
+ autocorrect_safe(false)
16
+
14
17
  MESSAGE = '`= "..."` should be rewritten as `...`'
15
18
 
16
19
  def visit_tag(node)
17
- if tag_has_inline_script?(node) && inline_content_is_string?(node)
18
- record_lint(node, MESSAGE)
19
- end
20
+ return unless tag_has_inline_script?(node) && inline_content_is_string?(node)
21
+
22
+ plain = safe_plain_text(node.script)
23
+ corrected = plain ? correct_tag(node, plain) : false
24
+ record_lint(node, MESSAGE, corrected: corrected)
20
25
  end
21
26
 
22
27
  def visit_script(node)
23
- # Some script nodes created by the HAML parser aren't actually script
28
+ # Some script nodes created by the Haml parser aren't actually script
24
29
  # nodes declared via the `=` marker. Check for it.
25
- return if node.source_code !~ /\A\s*=/
30
+ return unless /\A\s*=/.match?(node.source_code)
31
+ return unless plain = safe_plain_text(node.script)
26
32
 
27
- if outputs_string_literal?(node)
28
- record_lint(node, MESSAGE)
29
- end
33
+ record_lint(node, MESSAGE, corrected: correct_script(node, plain))
30
34
  end
31
35
 
32
36
  private
33
37
 
34
- def outputs_string_literal?(script_node)
35
- return unless tree = parse_ruby(script_node.script)
36
- %i[str dstr].include?(tree.type) &&
37
- !starts_with_reserved_character?(tree.children.first)
38
+ # Rewrites `%tag= "..."` as `%tag ...`, dropping the `=` marker and the
39
+ # surrounding quotes.
40
+ #
41
+ # @return [Boolean] whether a correction was applied
42
+ def correct_tag(node, plain)
43
+ marker = node.inline_marker_source.rstrip
44
+ # Only unwrap plain escaped output (`= "..."`); leave `!=`, `~`, and the
45
+ # whitespace-removal markers (`<`/`>`) untouched, as they change rendering.
46
+ return false unless /\A=\s*["']/.match?(marker)
47
+
48
+ index = node.line - 1
49
+ line = autocorrected_lines[index]
50
+ return false unless line.rstrip.end_with?(marker)
51
+
52
+ prefix = line.rstrip[0...(line.rstrip.length - marker.length)]
53
+ correct_line(index, "#{prefix} #{plain}")
54
+ end
55
+
56
+ # Rewrites a standalone `= "..."` script as plain text, preserving the
57
+ # node's indentation.
58
+ #
59
+ # @return [Boolean] whether a correction was applied
60
+ def correct_script(node, plain)
61
+ index = node.line - 1
62
+ line = autocorrected_lines[index]
63
+ indentation = line[/\A\s*/]
64
+ correct_line(index, "#{indentation}#{plain}")
65
+ end
66
+
67
+ # Returns the Haml plain-text equivalent of a Ruby string literal script, or
68
+ # +nil+ when the script is not a string literal that can be safely unwrapped
69
+ # (i.e. unwrapping would change the rendered output).
70
+ #
71
+ # @return [String, nil]
72
+ def safe_plain_text(script)
73
+ return if script.include?("\n")
74
+ return unless tree = parse_ruby(script)
75
+ return unless %i[str dstr].include?(tree.type)
76
+ return unless safely_unwrappable?(tree)
77
+
78
+ plain_text_from(tree)
38
79
  rescue ::Parser::SyntaxError
39
80
  # Gracefully ignore syntax errors, as that's managed by a different linter
81
+ nil
82
+ end
83
+
84
+ # Whether unwrapping the string literal into Haml plain text would preserve
85
+ # the rendered output.
86
+ #
87
+ # @return [Boolean]
88
+ def safely_unwrappable?(tree)
89
+ !starts_with_reserved_character?(tree.children.first) &&
90
+ !contains_escape_sequence?(tree) &&
91
+ !contains_significant_whitespace?(tree)
92
+ end
93
+
94
+ # Reconstructs the Haml plain-text form of a string literal. Literal segments
95
+ # keep their text (with any `#{` escaped so Haml won't interpolate it), while
96
+ # interpolation segments are emitted verbatim in their `#{...}` form.
97
+ #
98
+ # @return [String]
99
+ def plain_text_from(tree)
100
+ string_segments(tree).map do |segment|
101
+ if segment.type == :str
102
+ segment.children.first.gsub('#{') { '\#{' }
103
+ else
104
+ segment.location.expression.source
105
+ end
106
+ end.join
40
107
  end
41
108
 
42
109
  # Returns whether a string starts with a character that would otherwise be
@@ -45,5 +112,34 @@ module HamlLint
45
112
  string = stringish.respond_to?(:children) ? stringish.children.first : stringish
46
113
  string =~ %r{\A\s*[/#-=%~]} if string.is_a?(String)
47
114
  end
115
+
116
+ # The ordered segments of a string literal, including any interpolation.
117
+ # A plain `str` node has no interpolation, so it is its own only segment.
118
+ def string_segments(tree)
119
+ tree.type == :dstr ? tree.children : [tree]
120
+ end
121
+
122
+ # Returns whether any literal portion of the string contains a backslash
123
+ # escape (e.g. `\n`, `\t`, `\u202F`). Such escapes are interpreted inside
124
+ # a Ruby string but would be emitted verbatim as Haml plain text, so the
125
+ # `= "..."` form is not equivalent to the unwrapped plain text.
126
+ def contains_escape_sequence?(tree)
127
+ string_segments(tree).any? do |segment|
128
+ segment.type == :str && segment.location.expression.source.include?('\\')
129
+ end
130
+ end
131
+
132
+ # Returns whether the string begins or ends with whitespace. Haml strips
133
+ # trailing whitespace from plain text (and leading whitespace denotes
134
+ # indentation), so unwrapping such a string would change the output.
135
+ def contains_significant_whitespace?(tree)
136
+ segments = string_segments(tree)
137
+ bounded_by_whitespace?(segments.first, /\A\s/) ||
138
+ bounded_by_whitespace?(segments.last, /\s\z/)
139
+ end
140
+
141
+ def bounded_by_whitespace?(segment, pattern)
142
+ segment.type == :str && segment.children.first.to_s.match?(pattern)
143
+ end
48
144
  end
49
145
  end
@@ -37,7 +37,7 @@ module HamlLint
37
37
  :error
38
38
  )
39
39
  rescue StandardError => e
40
- msg = "Couldn't process the file".dup
40
+ msg = +"Couldn't process the file"
41
41
  if @autocorrect
42
42
  # Those lints related to auto-correction were not saved, so don't display them
43
43
  @lints = []
@@ -56,13 +56,14 @@ module HamlLint
56
56
  end
57
57
 
58
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)
59
+ # Syntax or Haml-Lint internal errors. (For testing purposes)
60
60
  #
61
61
  # @param document [HamlLint::Document]
62
62
  def run_or_raise(document, autocorrect: nil)
63
63
  @document = document
64
64
  @lints = []
65
65
  @autocorrect = autocorrect
66
+ reset_autocorrect_state
66
67
  visit(document.tree)
67
68
  @lints
68
69
  end
@@ -85,6 +86,30 @@ module HamlLint
85
86
  self.class.supports_autocorrect?
86
87
  end
87
88
 
89
+ # Returns whether this linter's autocorrect is safe. Defaults to true (safe)
90
+ # unless the linter declares otherwise via `autocorrect_safe(false)`.
91
+ #
92
+ # @return [Boolean]
93
+ def self.autocorrect_safe?
94
+ @autocorrect_safe != false
95
+ end
96
+
97
+ # The autocorrect ordering priority for this linter. During autocorrect,
98
+ # linters with a lower priority run first and higher priority run later,
99
+ # against the same (already mutated) document. Linters with the same priority
100
+ # keep their default (alphabetical) order.
101
+ #
102
+ # Called with an argument (e.g. `autocorrect_priority(1)`) in a linter's
103
+ # top-level scope, it sets the priority; called with no argument it reads it.
104
+ # The default, when never set, is 0.
105
+ #
106
+ # @param value [Integer, nil] the new priority, or nil to read the current one
107
+ # @return [Integer]
108
+ def self.autocorrect_priority(value = nil)
109
+ @autocorrect_priority = value unless value.nil?
110
+ @autocorrect_priority || 0
111
+ end
112
+
88
113
  private
89
114
 
90
115
  attr_reader :config, :document
@@ -97,15 +122,104 @@ module HamlLint
97
122
  @supports_autocorrect = value
98
123
  end
99
124
 
125
+ # Linters can call autocorrect_safe(false) in their top-level scope to declare that their
126
+ # autocorrect is unsafe, meaning it only runs under `--auto-correct-all` (`:all`).
127
+ # The default, when not called, is safe.
128
+ #
129
+ # @params value [Boolean] The new value for autocorrect_safe
130
+ private_class_method def self.autocorrect_safe(value)
131
+ @autocorrect_safe = value
132
+ end
133
+
134
+ # Resets the per-run autocorrect bookkeeping. Linter instances are reused
135
+ # across files, so subclasses that accumulate extra autocorrect state during
136
+ # a traversal (e.g. a list of lines to merge or delete) must override this,
137
+ # calling `super`, to clear that state between files. Otherwise corrections
138
+ # from one file leak into the next.
139
+ def reset_autocorrect_state
140
+ @autocorrected_lines = nil
141
+ @autocorrect_changed = false
142
+ end
143
+
144
+ # Whether the linter is currently allowed to apply autocorrections, given the active
145
+ # autocorrect mode and this linter's declared safety:
146
+ #
147
+ # * safe linters correct under both `:safe` and `:all`;
148
+ # * unsafe linters correct only under `:all`;
149
+ # * with no mode (`nil`) nothing is corrected (detection only).
150
+ #
151
+ # @return [Boolean]
152
+ def autocorrect?
153
+ case @autocorrect
154
+ when :all
155
+ true
156
+ when :safe
157
+ self.class.autocorrect_safe?
158
+ else
159
+ false
160
+ end
161
+ end
162
+
163
+ # Applies a corrected, full-document source to the document through the single
164
+ # mutation path (`Document#change_source`), but only when the safety gate permits.
165
+ # No-ops otherwise; `change_source` itself also no-ops when the source is unchanged.
166
+ #
167
+ # @param new_source [String] the corrected Haml source
168
+ def apply_autocorrect(new_source)
169
+ return unless autocorrect?
170
+ document.change_source(new_source)
171
+ end
172
+
173
+ # Lazily-initialized working copy of the document's source lines, used to
174
+ # accumulate line-level corrections during a single tree traversal. Editing
175
+ # this copy (rather than calling `apply_autocorrect` per node) avoids
176
+ # reparsing the document mid-walk, which would invalidate the tree being
177
+ # visited.
178
+ #
179
+ # @return [Array<String>]
180
+ def autocorrected_lines
181
+ @autocorrected_lines ||= document.source_lines.dup
182
+ end
183
+
184
+ # Replaces a single source line (0-indexed) in the working copy, but only
185
+ # when autocorrect is permitted and the new text actually differs.
186
+ #
187
+ # @param index [Integer] the 0-indexed line to replace
188
+ # @param new_text [String] the corrected line
189
+ # @return [Boolean] true if a change was recorded, false otherwise
190
+ def correct_line(index, new_text)
191
+ return false unless autocorrect?
192
+ return false if autocorrected_lines[index] == new_text
193
+
194
+ autocorrected_lines[index] = new_text
195
+ @autocorrect_changed = true
196
+ end
197
+
198
+ # Flushes any corrections accumulated via `correct_line` through the single
199
+ # mutation path, once, after the whole tree has been visited. Defined on the
200
+ # base so per-node linters need no boilerplate; no-ops for linters that never
201
+ # accumulated a change (including those that apply within `visit_root`).
202
+ #
203
+ # NOTE: a linter that overrides `after_visit_root` must call `super`, or
204
+ # accumulated corrections will not be applied.
205
+ def after_visit_root(_node)
206
+ return unless @autocorrect_changed
207
+
208
+ apply_autocorrect(autocorrected_lines.join("\n"))
209
+ end
210
+
100
211
  # Record a lint for reporting back to the user.
101
212
  #
102
213
  # @param node_or_line [#line] line number or node to extract the line number from
103
214
  # @param message [String] error/warning to display to the user
104
- def record_lint(node_or_line, message, corrected: false)
215
+ # @param corrected [Boolean] whether the lint was fixed by auto-correct
216
+ # @param correctable [Boolean] whether the lint can be fixed by auto-correct;
217
+ # defaults to whether this linter supports auto-correct at all
218
+ def record_lint(node_or_line, message, corrected: false, correctable: supports_autocorrect?)
105
219
  line = node_or_line.is_a?(Integer) ? node_or_line : node_or_line.line
106
220
  @lints << HamlLint::Lint.new(self, @document.file, line, message,
107
221
  config.fetch('severity', :warning).to_sym,
108
- corrected: corrected)
222
+ corrected: corrected, correctable: correctable)
109
223
  end
110
224
 
111
225
  # Parse Ruby code into an abstract syntax tree.
@@ -231,7 +345,7 @@ module HamlLint
231
345
  # excess whitespace
232
346
  @document.source_lines[(tag_node.line - 1)...(following_node_line(tag_node) - 1)]
233
347
  .map do |line|
234
- line.strip.gsub(/\|\z/, '').rstrip
348
+ line.strip.delete_suffix('|').rstrip
235
349
  end.join(' ')
236
350
  end
237
351
  end
@@ -4,7 +4,7 @@ module HamlLint
4
4
  # Responsible for transforming {Haml::Parser::ParseNode} objects into
5
5
  # corresponding {HamlLint::Tree::Node} objects.
6
6
  #
7
- # The parse tree generated by HAML has a number of strange cases where certain
7
+ # The parse tree generated by Haml has a number of strange cases where certain
8
8
  # types of nodes are created that don't necessarily correspond to what one
9
9
  # would expect. This class is intended to isolate and handle these cases so
10
10
  # that linters don't have to deal with them.
@@ -16,7 +16,7 @@ module HamlLint
16
16
  @document = document
17
17
  end
18
18
 
19
- # Converts the given HAML parse node into its corresponding HAML-Lint parse
19
+ # Converts the given Haml parse node into its corresponding Haml-Lint parse
20
20
  # node.
21
21
  #
22
22
  # @param haml_node [Haml::Parser::ParseNode]
@@ -10,7 +10,7 @@ module HamlLint
10
10
  # @param args [Array<String>] arguments passed via the command line
11
11
  # @return [Hash] parsed options
12
12
  def parse(args)
13
- @options = {}
13
+ @options = default_options
14
14
 
15
15
  OptionParser.new do |parser|
16
16
  parser.banner = "Usage: #{APP_NAME} [options] [file1, file2, ...]"
@@ -32,7 +32,11 @@ module HamlLint
32
32
 
33
33
  private
34
34
 
35
- def add_linter_options(parser) # rubocop:disable Metrics/MethodLength
35
+ def default_options
36
+ { parallel: true }
37
+ end
38
+
39
+ def add_linter_options(parser) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
36
40
  parser.on('--auto-gen-config', 'Generate a configuration file acting as a TODO list') do
37
41
  @options[:auto_gen_config] = true
38
42
  end
@@ -56,6 +60,10 @@ module HamlLint
56
60
  @options[:parallel] = true
57
61
  end
58
62
 
63
+ parser.on('--no-parallel', 'Disable parallel linter runs') do
64
+ @options[:parallel] = false
65
+ end
66
+
59
67
  parser.on('-a', '--auto-correct', 'Auto-correct offenses (only when it’s safe)') do
60
68
  @options[:autocorrect] = :safe
61
69
  end
@@ -70,7 +78,13 @@ module HamlLint
70
78
  @options[:autocorrect] ||= :safe
71
79
  end
72
80
 
73
- parser.on('--stderr', 'Write all output to stderr') do
81
+ parser.on('-s', '--stdin FILE', 'Pipe source from STDIN, using FILE in ' \
82
+ 'offense when combined with --auto-correct and --stdin.') do |file_path|
83
+ @options[:stdin] = file_path
84
+ end
85
+
86
+ parser.on('--stderr', 'Write all output to stderr except for the autocorrected source. ' \
87
+ 'This is especially useful when combined with --auto-correct and --stdin') do
74
88
  @options[:stderr] = true
75
89
  end
76
90
  end
@@ -2,7 +2,8 @@
2
2
 
3
3
  require 'rake'
4
4
  require 'rake/tasklib'
5
- require 'haml_lint/constants'
5
+
6
+ require_relative 'constants'
6
7
 
7
8
  module HamlLint
8
9
  # Rake task interface for haml-lint command line interface.
@@ -93,7 +94,7 @@ module HamlLint
93
94
 
94
95
  task(name, [:files]) do |_task, task_args|
95
96
  # Lazy-load so task doesn't affect Rakefile load time
96
- require 'haml_lint/cli'
97
+ require_relative 'cli'
97
98
 
98
99
  run_cli(task_args)
99
100
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'haml_lint/reporter/utils'
3
+ require_relative 'utils'
4
4
 
5
5
  module HamlLint
6
6
  # Outputs lints in a simple format with the filename, line number, and lint
@@ -1,14 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'haml_lint/reporter/progress_reporter'
3
+ require_relative 'progress_reporter'
4
4
 
5
5
  module HamlLint
6
6
  # Outputs a YAML configuration file based on existing violations.
7
7
  class Reporter::DisabledConfigReporter < Reporter::ProgressReporter
8
- HEADING =
8
+ DEFAULT_EXCLUDE_LIMIT = 15
9
+
10
+ HEADING_TEMPLATE =
9
11
  ['# This configuration was generated by',
10
- '# `haml-lint --auto-gen-config`',
11
- "# on #{Time.now} using Haml-Lint version #{HamlLint::VERSION}.",
12
+ '# `%<command>s`',
13
+ '# on %<timestamp>s using Haml-Lint version %<version>s.',
12
14
  '# The point is for the user to remove these configuration records',
13
15
  '# one by one as the lints are removed from the code base.',
14
16
  '# Note that changes in the inspected code, or installation of new',
@@ -25,11 +27,12 @@ module HamlLint
25
27
  # Create the reporter that will display the report and write the config.
26
28
  #
27
29
  # @param _log [HamlLint::Logger]
28
- def initialize(log, limit: 15)
30
+ def initialize(log, limit: DEFAULT_EXCLUDE_LIMIT, options: {})
29
31
  super(log)
30
32
  @linters_with_lints = Hash.new { |hash, key| hash[key] = [] }
31
33
  @linters_lint_count = Hash.new(0)
32
34
  @exclude_limit = limit
35
+ @options = options
33
36
  end
34
37
 
35
38
  # A hash of linters with the files that have that type of lint.
@@ -81,17 +84,35 @@ module HamlLint
81
84
 
82
85
  private
83
86
 
87
+ # Reconstructs the CLI command used to generate this config.
88
+ #
89
+ # @return [String] the command string
90
+ def command
91
+ cmd = "#{HamlLint::APP_NAME} --auto-gen-config"
92
+ if @options[:auto_gen_exclude_limit]
93
+ cmd += " --auto-gen-exclude-limit #{@options[:auto_gen_exclude_limit]}"
94
+ end
95
+ cmd
96
+ end
97
+
98
+ # The heading comment for the generated config file.
99
+ #
100
+ # @return [String]
101
+ def heading
102
+ format(HEADING_TEMPLATE, command: command, timestamp: Time.now, version: HamlLint::VERSION)
103
+ end
104
+
84
105
  # The contents of the generated configuration file based on captured lint.
85
106
  #
86
107
  # @return [String] a Yaml-formatted configuration file's contents
87
108
  def config_file_contents
88
109
  output = []
89
- output << HEADING
110
+ output << heading
90
111
  output << 'linters:' if linters_with_lints.any?
91
- linters_with_lints.each do |linter, files|
112
+ linters_with_lints.sort.each do |linter, files|
92
113
  output << generate_config_for_linter(linter, files)
93
114
  end
94
- output.join("\n\n")
115
+ output.join("\n\n") + "\n"
95
116
  end
96
117
 
97
118
  # Constructs the configuration for excluding a linter in some files.
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: false
2
+
3
+ module HamlLint
4
+ # Outputs GitHub workflow commands for GitHub check annotations when run within GitHub actions.
5
+ class Reporter::GithubReporter < Reporter
6
+ # Characters to escape within a command message.
7
+ ESCAPE_MAP = { '%' => '%25', "\n" => '%0A', "\r" => '%0D' }.freeze
8
+ # Property values also escape the `:` and `,` that delimit the command.
9
+ PROPERTY_ESCAPE_MAP = ESCAPE_MAP.merge(':' => '%3A', ',' => '%2C').freeze
10
+
11
+ include Reporter::Utils
12
+
13
+ def added_lint(lint, report)
14
+ severity = lint.severity >= report.fail_level ? 'error' : 'warning'
15
+ print_workflow_command(lint, severity)
16
+ end
17
+
18
+ def display_report(report)
19
+ print_summary(report)
20
+ end
21
+
22
+ private
23
+
24
+ def print_workflow_command(lint, severity)
25
+ log.log "::#{severity} file=#{github_escape_property(lint.filename)}," \
26
+ "line=#{lint.line},title=#{github_escape_property(annotation_title(lint))}" \
27
+ "::#{github_escape(annotation_message(lint))}"
28
+ end
29
+
30
+ # Annotation title, naming the linter that produced the lint when known.
31
+ def annotation_title(lint)
32
+ lint.linter ? "haml-lint #{lint.linter.name}" : 'haml-lint'
33
+ end
34
+
35
+ # Message body, prefixed with the location and linter so it stays useful in the plain log.
36
+ def annotation_message(lint)
37
+ location = "#{lint.filename}:#{lint.line}"
38
+ location += " #{lint.linter.name}:" if lint.linter
39
+ "#{location} #{lint.message}"
40
+ end
41
+
42
+ def github_escape(string)
43
+ string.to_s.gsub(Regexp.union(ESCAPE_MAP.keys), ESCAPE_MAP)
44
+ end
45
+
46
+ def github_escape_property(string)
47
+ string.to_s.gsub(Regexp.union(PROPERTY_ESCAPE_MAP.keys), PROPERTY_ESCAPE_MAP)
48
+ end
49
+ end
50
+ end
@@ -47,6 +47,8 @@ module HamlLint
47
47
  {
48
48
  severity: offense.severity,
49
49
  message: offense.message,
50
+ corrected: offense.corrected,
51
+ correctable: offense.correctable,
50
52
  location: {
51
53
  line: offense.line,
52
54
  },
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'haml_lint/reporter/hash_reporter'
3
+ require_relative 'hash_reporter'
4
4
 
5
5
  module HamlLint
6
6
  # Outputs report as a JSON document.
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rainbow'
4
- require 'haml_lint/reporter/utils'
4
+
5
+ require_relative 'utils'
5
6
 
6
7
  module HamlLint
7
8
  # Outputs files as they are output as a simple symbol, then outputs
@@ -58,6 +58,8 @@ module HamlLint
58
58
  def print_message(lint)
59
59
  if lint.corrected
60
60
  log.success('[Corrected] ', false)
61
+ elsif lint.correctable
62
+ log.info('[Correctable] ', false)
61
63
  end
62
64
 
63
65
  if lint.linter
@@ -96,7 +98,7 @@ module HamlLint
96
98
  # Prints a summary of the number of lints found in a report.
97
99
  #
98
100
  # @param report [HamlLint::Report] the report to print
99
- # @param is_append [Boolean] if this is appending to a line. Will preffix with ", ".
101
+ # @param is_append [Boolean] if this is appending to a line. Will prefix with ", ".
100
102
  # @return [void]
101
103
  def print_summary_lints(report, is_append:)
102
104
  log.log ', ', false if is_append
@@ -114,7 +116,7 @@ module HamlLint
114
116
  # Prints a summary of the number of lints corrected in a report.
115
117
  #
116
118
  # @param report [HamlLint::Report] the report to print
117
- # @param is_append [Boolean] if this is appending to a line. Will preffix with ", ".
119
+ # @param is_append [Boolean] if this is appending to a line. Will prefix with ", ".
118
120
  # @return [void]
119
121
  def print_summary_corrected_lints(report, is_append:)
120
122
  lint_count = report.lints.count(&:corrected)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'haml_lint/reporter/hooks'
3
+ require_relative 'reporter/hooks'
4
4
 
5
5
  module HamlLint
6
6
  # Abstract lint reporter. Subclass and override {#display_report} to
@@ -2,13 +2,13 @@
2
2
 
3
3
  module HamlLint::RubyExtraction
4
4
  # This is the base class for all of the Chunks of HamlLint::RubyExtraction.
5
- # A Chunk represents a part of the HAML file that HamlLint::Linter::RuboCop
5
+ # A Chunk represents a part of the Haml file that HamlLint::Linter::RuboCop
6
6
  # is processing and will insert some Ruby code in a file passed to RuboCop.
7
7
  #
8
- # There are chunks for most HAML concepts, even if they don't represent Ruby
8
+ # There are chunks for most Haml concepts, even if they don't represent Ruby
9
9
  # code. For example, there is a chunk that represents a `%div` tag, which
10
10
  # uses a `begin` in the generated Ruby to add indentation for the children
11
- # of the %div in the Ruby file just like there is in the HAML file.
11
+ # of the %div in the Ruby file just like there is in the Haml file.
12
12
  class BaseChunk
13
13
  COMMA_CHANGES_LINES = true
14
14
 
@@ -63,7 +63,7 @@ module HamlLint::RubyExtraction
63
63
  transfer_correction_logic(coordinator, to_ruby_lines, haml_lines)
64
64
  end
65
65
 
66
- # To be overriden by subclasses.
66
+ # To be overridden by subclasses.
67
67
  #
68
68
  # Logic to transfer the corrections that turned from_ruby_lines into to_ruby_lines.
69
69
  #