slim_lint 0.35.0 → 0.36.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e9478570d94e93349a03f9f7a67789f3c0ff356bd8bd95ee7fe5628248eaaab9
4
- data.tar.gz: 23bc11a5e84b251523397d12bcf0c776d8755373fffbfa7ce183353fe8a86b73
3
+ metadata.gz: dd046afc687f2cd47b2766c90af0d4cee87282f818f60954530dc9e579a3a797
4
+ data.tar.gz: a6378966b0a438ba9cdf86f5771262f7e7eebb4a40b1b915ec95a68a7d51eb05
5
5
  SHA512:
6
- metadata.gz: f40d2b1af0278fd5f2ea727f0a7ee39322df8b7c1ddf155ece528ac7f99c8dd8b0284af8d622e05eec9ef229544fd953b1f6a51cea9e11d35c23bba1d7d7c7c3
7
- data.tar.gz: 6c96c5b859896bf1a3d04fe1ca336cfae1e7c15bbcb89f5a6143d1b952ec3508febab94dd4fa618fbbed98405bbaed941cd6a94bb3c2e2bad3a1bdc04ddb5574
6
+ metadata.gz: 13c3f8d0aacd071c62fdd1ddea37c690a48a53df0170a32575cced14ad6e828ca6a2a1bccbcc6cdd915cbf54a4b917d02099a75f51c08417395861a53b6933d4
7
+ data.tar.gz: d063b8fe3e5620ecc68a1bb6b2a38ceb0b5431971ec94c6a3cb4059be77d8b87e122c2ef7fae4cc1c4b1325bc9c5ffbb59d237b19caf22974977086410cf614d
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlimLint
4
+ # Applies the corrections attached to a set of lints to a document's
5
+ # source, producing the corrected source text.
6
+ class Corrector
7
+ # @param document [SlimLint::Document] document the lints were reported against
8
+ def initialize(document)
9
+ @document = document
10
+ end
11
+
12
+ # Applies every correctable lint's correction to the document's source
13
+ # and marks each applied lint as corrected.
14
+ #
15
+ # @param lints [Array<SlimLint::Lint>]
16
+ # @return [String] the corrected source
17
+ def correct(lints)
18
+ lines = @document.source_lines.dup
19
+
20
+ lints.select(&:correctable?).group_by(&:line).each do |line_number, line_lints|
21
+ index = line_number - 1
22
+ next unless lines[index]
23
+
24
+ line_lints.each do |lint|
25
+ lines[index] = lint.correction.call(lines[index])
26
+ lint.corrected = true
27
+ end
28
+ end
29
+
30
+ corrected_source(lines)
31
+ end
32
+
33
+ private
34
+
35
+ # @param lines [Array<String>]
36
+ # @return [String]
37
+ def corrected_source(lines)
38
+ source = lines.join("\n")
39
+ trailing_newlines = @document.source[/\n+\z/]
40
+ source += trailing_newlines if trailing_newlines
41
+ @document.source_prefix + source
42
+ end
43
+ end
44
+ end
@@ -15,6 +15,12 @@ module SlimLint
15
15
  # @return [String] original source code
16
16
  attr_reader :source
17
17
 
18
+ # @return [String] source code before optional frontmatter removal
19
+ attr_reader :original_source
20
+
21
+ # @return [String] source removed before linting, such as frontmatter
22
+ attr_reader :source_prefix
23
+
18
24
  # @return [Array<String>] original source code as an array of lines
19
25
  attr_reader :source_lines
20
26
 
@@ -36,8 +42,9 @@ module SlimLint
36
42
  # @param source [String] Slim code to parse
37
43
  # @raise [SlimLint::Exceptions::ParseError] if there was a problem parsing the document
38
44
  def process_source(source)
39
- @source = process_encoding(source)
40
- @source = strip_frontmatter(source)
45
+ @original_source = process_encoding(source)
46
+ @source = strip_frontmatter(@original_source)
47
+ @source_prefix = @original_source[0, @original_source.length - @source.length] || ''
41
48
  @source_lines = @source.split("\n")
42
49
 
43
50
  engine = SlimLint::Engine.new(file: @file)
@@ -18,6 +18,13 @@ module SlimLint
18
18
  # @return [Symbol] whether this lint is a warning or an error
19
19
  attr_reader :severity
20
20
 
21
+ # @return [Proc, nil] maps a line of source to its corrected version, or
22
+ # `nil` if this lint has no known automatic correction
23
+ attr_reader :correction
24
+
25
+ # @return [Boolean] whether this lint's correction has been applied
26
+ attr_accessor :corrected
27
+
21
28
  # Creates a new lint.
22
29
  #
23
30
  # @param linter [SlimLint::Linter]
@@ -25,12 +32,16 @@ module SlimLint
25
32
  # @param line [Fixnum]
26
33
  # @param message [String]
27
34
  # @param severity [Symbol]
28
- def initialize(linter, filename, line, message, severity = :warning)
29
- @linter = linter
30
- @filename = filename
31
- @line = line || 0
32
- @message = message
33
- @severity = severity
35
+ # @param correction [Proc, nil] maps a line of source to its corrected version
36
+ def initialize(linter, filename, line, message, severity = :warning, # rubocop:disable Metrics/ParameterLists
37
+ correction: nil)
38
+ @linter = linter
39
+ @filename = filename
40
+ @line = line || 0
41
+ @message = message
42
+ @severity = severity
43
+ @correction = correction
44
+ @corrected = false
34
45
  end
35
46
 
36
47
  # Return whether this lint has a severity of error.
@@ -39,5 +50,19 @@ module SlimLint
39
50
  def error?
40
51
  @severity == :error
41
52
  end
53
+
54
+ # Return whether this lint has a known automatic correction.
55
+ #
56
+ # @return [Boolean]
57
+ def correctable?
58
+ !@correction.nil?
59
+ end
60
+
61
+ # Return whether this lint's correction has been applied.
62
+ #
63
+ # @return [Boolean]
64
+ def corrected?
65
+ @corrected
66
+ end
42
67
  end
43
68
  end
@@ -4,6 +4,7 @@ module SlimLint
4
4
  # Checks for consistent quote usage in HTML attributes
5
5
  class Linter::QuoteConsistency < Linter
6
6
  include LinterRegistry
7
+ support_autocorrect
7
8
 
8
9
  MSG = 'Inconsistent quote style. %s'
9
10
 
@@ -19,15 +20,42 @@ module SlimLint
19
20
 
20
21
  if enforced_style == :single_quotes && double_quotes.any?
21
22
  report_lint(node,
22
- format(MSG, "Use single quotes for attribute values (')"))
23
+ format(MSG, "Use single quotes for attribute values (')"),
24
+ correction: ->(source_line) { correct_quotes(source_line, '"', "'") })
23
25
  elsif enforced_style == :double_quotes && single_quotes.any?
24
26
  report_lint(node,
25
- format(MSG, 'Use double quotes for attribute values (")'))
27
+ format(MSG, 'Use double quotes for attribute values (")'),
28
+ correction: ->(source_line) { correct_quotes(source_line, "'", '"') })
26
29
  end
27
30
  end
28
31
 
29
32
  private
30
33
 
34
+ def correct_quotes(source_line, from, to)
35
+ state = { quote: nil, corrected: [] }
36
+ source_line.scan(/\\.|['"]|[^\\'"]+/).each do |token|
37
+ correct_quote_token(token, state, from, to)
38
+ end
39
+ state[:corrected].join
40
+ end
41
+
42
+ def correct_quote_token(token, state, from, to)
43
+ return state[:corrected] << token if token.length > 1 || token.start_with?('\\')
44
+
45
+ if state[:quote]
46
+ close_quote_token(token, state, from, to)
47
+ else
48
+ state[:quote] = token
49
+ state[:corrected] << (token == from ? to : token)
50
+ end
51
+ end
52
+
53
+ def close_quote_token(token, state, from, to)
54
+ replacement = token == state[:quote] && token == from ? to : token
55
+ state[:corrected] << replacement
56
+ state[:quote] = nil if token == state[:quote]
57
+ end
58
+
31
59
  def enforced_style
32
60
  config['enforced_style']&.to_sym || :single_quotes
33
61
  end
@@ -5,6 +5,7 @@ module SlimLint
5
5
  # already implies a div.
6
6
  class Linter::RedundantDiv < Linter
7
7
  include LinterRegistry
8
+ support_autocorrect
8
9
 
9
10
  MESSAGE = '`div` is redundant when %s attribute shortcut is present'
10
11
 
@@ -16,7 +17,8 @@ module SlimLint
16
17
  attr = captures[:attr_name]
17
18
  next unless %w[class id].include?(attr)
18
19
 
19
- report_lint(sexp, MESSAGE % attr)
20
+ report_lint(sexp, MESSAGE % attr,
21
+ correction: ->(line) { line.sub(/\bdiv(?=[.#])/, '') })
20
22
  end
21
23
  end
22
24
  end
@@ -4,12 +4,16 @@ module SlimLint
4
4
  # Searches for tags with uppercase characters.
5
5
  class Linter::TagCase < Linter
6
6
  include LinterRegistry
7
+ support_autocorrect
7
8
 
8
9
  on [:html, :tag] do |sexp|
9
10
  _, _, name = sexp
10
11
  next unless name[/[A-Z]/]
11
12
 
12
- report_lint(sexp, "Tag `#{name}` should be written as `#{name.downcase}`")
13
+ report_lint(sexp, "Tag `#{name}` should be written as `#{name.downcase}`",
14
+ correction: ->(line) {
15
+ line.sub(/\b#{Regexp.escape(name.to_s)}\b/, name.downcase)
16
+ })
13
17
  end
14
18
  end
15
19
  end
@@ -4,6 +4,7 @@ module SlimLint
4
4
  # Checks for trailing whitespace.
5
5
  class Linter::TrailingWhitespace < Linter
6
6
  include LinterRegistry
7
+ support_autocorrect
7
8
 
8
9
  on_start do |_sexp|
9
10
  dummy_node = Struct.new(:line)
@@ -12,7 +13,8 @@ module SlimLint
12
13
  next unless line =~ /\s+$/
13
14
 
14
15
  report_lint(dummy_node.new(index + 1),
15
- 'Line contains trailing whitespace')
16
+ 'Line contains trailing whitespace',
17
+ correction: lambda(&:rstrip))
16
18
  end
17
19
  end
18
20
  end
@@ -15,6 +15,21 @@ module SlimLint
15
15
  # lints for the subject instead of the linter itself.
16
16
  attr_reader :lints
17
17
 
18
+ class << self
19
+ # Declares that this linter can automatically correct (some of) the
20
+ # offenses it reports.
21
+ def support_autocorrect
22
+ @supports_autocorrect = true
23
+ end
24
+
25
+ # Returns whether this linter declared autocorrect support.
26
+ #
27
+ # @return [Boolean]
28
+ def supports_autocorrect?
29
+ @supports_autocorrect || false
30
+ end
31
+ end
32
+
18
33
  # Initializes a linter with the specified configuration.
19
34
  #
20
35
  # @param config [Hash] configuration for this linter
@@ -49,10 +64,13 @@ module SlimLint
49
64
  #
50
65
  # @param node [#line] node to extract the line number from
51
66
  # @param message [String] error/warning to display to the user
52
- def report_lint(node, message)
67
+ # @param correction [Proc, nil] maps the offending line of source to its
68
+ # corrected version, for linters that declared {support_autocorrect}
69
+ def report_lint(node, message, correction: nil)
53
70
  return if disabled_for_line?(node.line)
54
71
 
55
- @lints << SlimLint::Lint.new(self, @document.file, node.line, message)
72
+ @lints << SlimLint::Lint.new(self, @document.file, node.line, message,
73
+ correction: correction)
56
74
  end
57
75
 
58
76
  # Parse Ruby code into an abstract syntax tree.
@@ -48,6 +48,11 @@ module SlimLint
48
48
  'Specify which reporter you want to use to generate the output') do |reporter|
49
49
  @options[:reporter] = load_reporter_class(reporter.capitalize)
50
50
  end
51
+
52
+ parser.on('-a', '--autocorrect',
53
+ 'Automatically correct offenses that support it') do
54
+ @options[:autocorrect] = true
55
+ end
51
56
  end
52
57
 
53
58
  # Returns the class of the specified Reporter.
@@ -19,7 +19,7 @@ module SlimLint
19
19
  end
20
20
 
21
21
  def failed?
22
- @lints.any?
22
+ @lints.any? { |lint| !lint.corrected? }
23
23
  end
24
24
  end
25
25
  end
@@ -35,7 +35,8 @@ module SlimLint
35
35
  log.success("#{lint.linter.name}: ", false)
36
36
  end
37
37
 
38
- log.log lint.message
38
+ log.log lint.message, false
39
+ lint.corrected? ? log.success(' [Corrected]') : log.newline
39
40
  end
40
41
  end
41
42
  end
@@ -20,7 +20,8 @@ module SlimLint
20
20
  if options[:stdin_file_path].nil?
21
21
  files = extract_applicable_files(config, options)
22
22
  lints = files.map do |file|
23
- collect_lints(File.read(file), file, linter_selector, config)
23
+ collect_lints(File.read(file), file, linter_selector, config,
24
+ autocorrect: options[:autocorrect])
24
25
  end.flatten
25
26
  else
26
27
  files = [options[:stdin_file_path]]
@@ -53,16 +54,34 @@ module SlimLint
53
54
  # @param file [String] path to file to lint
54
55
  # @param linter_selector [SlimLint::LinterSelector]
55
56
  # @param config [SlimLint::Configuration]
56
- def collect_lints(file_content, file_name, linter_selector, config)
57
+ # @param autocorrect [Boolean] whether to write corrections back to the file
58
+ def collect_lints(file_content, file_name, linter_selector, config, autocorrect: false)
57
59
  begin
58
60
  document = SlimLint::Document.new(file_content, file: file_name, config: config)
59
61
  rescue SlimLint::Exceptions::ParseError => e
60
62
  return [SlimLint::Lint.new(nil, file_name, e.lineno, e.error, :error)]
61
63
  end
62
64
 
63
- linter_selector.linters_for_file(file_name).map do |linter|
65
+ lints = linter_selector.linters_for_file(file_name).map do |linter|
64
66
  linter.run(document)
65
67
  end.flatten
68
+
69
+ correct_lints(document, file_name, lints) if autocorrect
70
+
71
+ lints
72
+ end
73
+
74
+ # Applies any available corrections to the given file's lints and writes
75
+ # the result back to disk if anything changed.
76
+ #
77
+ # @param document [SlimLint::Document]
78
+ # @param file_name [String] path to file to correct
79
+ # @param lints [Array<SlimLint::Lint>]
80
+ def correct_lints(document, file_name, lints)
81
+ corrected_source = SlimLint::Corrector.new(document).correct(lints)
82
+ return if corrected_source == document.original_source
83
+
84
+ File.write(file_name, corrected_source)
66
85
  end
67
86
 
68
87
  # Returns the list of files that should be linted given the specified
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Defines the gem version.
4
4
  module SlimLint
5
- VERSION = '0.35.0'
5
+ VERSION = '0.36.0'
6
6
  end
data/lib/slim_lint.rb CHANGED
@@ -30,6 +30,7 @@ require_relative 'slim_lint/sexp_visitor'
30
30
  require_relative 'slim_lint/lint'
31
31
  require_relative 'slim_lint/ruby_parser'
32
32
  require_relative 'slim_lint/linter'
33
+ require_relative 'slim_lint/corrector'
33
34
  require_relative 'slim_lint/reporter'
34
35
  require_relative 'slim_lint/report'
35
36
  require_relative 'slim_lint/linter_selector'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: slim_lint
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.35.0
4
+ version: 0.36.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shane da Silva
@@ -109,6 +109,7 @@ files:
109
109
  - lib/slim_lint/configuration.rb
110
110
  - lib/slim_lint/configuration_loader.rb
111
111
  - lib/slim_lint/constants.rb
112
+ - lib/slim_lint/corrector.rb
112
113
  - lib/slim_lint/document.rb
113
114
  - lib/slim_lint/engine.rb
114
115
  - lib/slim_lint/exceptions.rb