slim_lint_standard 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.md +21 -0
- data/bin/slim-lint-standard +7 -0
- data/config/default.yml +109 -0
- data/lib/slim_lint/atom.rb +129 -0
- data/lib/slim_lint/capture_map.rb +19 -0
- data/lib/slim_lint/cli.rb +167 -0
- data/lib/slim_lint/configuration.rb +111 -0
- data/lib/slim_lint/configuration_loader.rb +86 -0
- data/lib/slim_lint/constants.rb +10 -0
- data/lib/slim_lint/document.rb +78 -0
- data/lib/slim_lint/engine.rb +41 -0
- data/lib/slim_lint/exceptions.rb +20 -0
- data/lib/slim_lint/file_finder.rb +88 -0
- data/lib/slim_lint/filter.rb +126 -0
- data/lib/slim_lint/filters/attribute_processor.rb +46 -0
- data/lib/slim_lint/filters/auto_indenter.rb +39 -0
- data/lib/slim_lint/filters/control_processor.rb +46 -0
- data/lib/slim_lint/filters/do_inserter.rb +39 -0
- data/lib/slim_lint/filters/end_inserter.rb +74 -0
- data/lib/slim_lint/filters/interpolation.rb +73 -0
- data/lib/slim_lint/filters/multi_flattener.rb +32 -0
- data/lib/slim_lint/filters/splat_processor.rb +20 -0
- data/lib/slim_lint/filters/static_merger.rb +47 -0
- data/lib/slim_lint/lint.rb +70 -0
- data/lib/slim_lint/linter/avoid_multiline_expressions.rb +41 -0
- data/lib/slim_lint/linter/comment_control_statement.rb +26 -0
- data/lib/slim_lint/linter/consecutive_control_statements.rb +26 -0
- data/lib/slim_lint/linter/control_statement_spacing.rb +32 -0
- data/lib/slim_lint/linter/dynamic_output_spacing.rb +77 -0
- data/lib/slim_lint/linter/embedded_engines.rb +18 -0
- data/lib/slim_lint/linter/empty_control_statement.rb +15 -0
- data/lib/slim_lint/linter/empty_lines.rb +24 -0
- data/lib/slim_lint/linter/file_length.rb +18 -0
- data/lib/slim_lint/linter/line_length.rb +18 -0
- data/lib/slim_lint/linter/redundant_div.rb +21 -0
- data/lib/slim_lint/linter/rubocop.rb +131 -0
- data/lib/slim_lint/linter/standard.rb +69 -0
- data/lib/slim_lint/linter/tab.rb +20 -0
- data/lib/slim_lint/linter/tag_case.rb +15 -0
- data/lib/slim_lint/linter/trailing_blank_lines.rb +19 -0
- data/lib/slim_lint/linter/trailing_whitespace.rb +17 -0
- data/lib/slim_lint/linter.rb +93 -0
- data/lib/slim_lint/linter_registry.rb +37 -0
- data/lib/slim_lint/linter_selector.rb +87 -0
- data/lib/slim_lint/logger.rb +103 -0
- data/lib/slim_lint/matcher/anything.rb +11 -0
- data/lib/slim_lint/matcher/base.rb +21 -0
- data/lib/slim_lint/matcher/capture.rb +32 -0
- data/lib/slim_lint/matcher/nothing.rb +13 -0
- data/lib/slim_lint/options.rb +110 -0
- data/lib/slim_lint/parser.rb +584 -0
- data/lib/slim_lint/rake_task.rb +125 -0
- data/lib/slim_lint/report.rb +25 -0
- data/lib/slim_lint/reporter/checkstyle_reporter.rb +42 -0
- data/lib/slim_lint/reporter/default_reporter.rb +40 -0
- data/lib/slim_lint/reporter/emacs_reporter.rb +40 -0
- data/lib/slim_lint/reporter/json_reporter.rb +50 -0
- data/lib/slim_lint/reporter.rb +44 -0
- data/lib/slim_lint/ruby_extract_engine.rb +30 -0
- data/lib/slim_lint/ruby_extractor.rb +175 -0
- data/lib/slim_lint/ruby_parser.rb +32 -0
- data/lib/slim_lint/runner.rb +82 -0
- data/lib/slim_lint/sexp.rb +134 -0
- data/lib/slim_lint/sexp_visitor.rb +150 -0
- data/lib/slim_lint/source_location.rb +45 -0
- data/lib/slim_lint/utils.rb +84 -0
- data/lib/slim_lint/version.rb +6 -0
- data/lib/slim_lint.rb +55 -0
- metadata +218 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
module SlimLint
|
2
|
+
module Filters
|
3
|
+
# Flattens nested multi expressions while respecting source locatoins.
|
4
|
+
#
|
5
|
+
# @api public
|
6
|
+
class MultiFlattener < Filter
|
7
|
+
def on_slim_embedded(*args)
|
8
|
+
@self
|
9
|
+
end
|
10
|
+
|
11
|
+
def on_multi(*exps)
|
12
|
+
# If the multi contains a single element, just return the element
|
13
|
+
return compile(exps.first) if exps.size == 1
|
14
|
+
|
15
|
+
result = @self
|
16
|
+
result.clear
|
17
|
+
result.concat(@key)
|
18
|
+
|
19
|
+
exps.each do |exp|
|
20
|
+
exp = compile(exp)
|
21
|
+
if exp.first == :multi
|
22
|
+
result.concat(exp[1..])
|
23
|
+
else
|
24
|
+
result << exp
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
result
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
module Filters
|
5
|
+
# A dumbed-down version of {Slim::Splat::Filter} which doesn't introduced
|
6
|
+
# temporary variables or other cruft.
|
7
|
+
class SplatProcessor < Filter
|
8
|
+
# Handle slim splat expressions `[:slim, :splat, code]`
|
9
|
+
#
|
10
|
+
# @param code [String]
|
11
|
+
# @return [Array]
|
12
|
+
def on_slim_splat(code)
|
13
|
+
return code if code[0] == :multi
|
14
|
+
@self.delete_at(1)
|
15
|
+
@self.first.value = :code
|
16
|
+
@self
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module SlimLint
|
2
|
+
module Filters
|
3
|
+
# Merges several statics into a single static while respecting source
|
4
|
+
# location data. Example:
|
5
|
+
#
|
6
|
+
# [:multi,
|
7
|
+
# [:static, "Hello "],
|
8
|
+
# [:static, "World!"]]
|
9
|
+
#
|
10
|
+
# Compiles to:
|
11
|
+
#
|
12
|
+
# [:static, "Hello World!"]
|
13
|
+
#
|
14
|
+
# @api public
|
15
|
+
class StaticMerger < Filter
|
16
|
+
def on_slim_embedded(*exps)
|
17
|
+
@self
|
18
|
+
end
|
19
|
+
|
20
|
+
def on_multi(*exps)
|
21
|
+
result = @self
|
22
|
+
result.clear
|
23
|
+
result.concat(@key)
|
24
|
+
|
25
|
+
static = nil
|
26
|
+
exps.each do |exp|
|
27
|
+
if exp.first == :static
|
28
|
+
if static
|
29
|
+
static.finish = exp.finish if later_pos?(static.finish, exp.finish)
|
30
|
+
static.last.finish = exp.finish if later_pos?(static.last.finish, exp.finish)
|
31
|
+
static.last.value << exp.last.value
|
32
|
+
else
|
33
|
+
static = exp
|
34
|
+
static[1] = exp.last.dup
|
35
|
+
result << static
|
36
|
+
end
|
37
|
+
else
|
38
|
+
result << compile(exp)
|
39
|
+
static = nil unless exp.first == :newline
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
result.size == 2 ? result[1] : result
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Contains information about a problem or issue with a Slim document.
|
5
|
+
class Lint
|
6
|
+
# @return [String] file path to which the lint applies
|
7
|
+
attr_reader :filename
|
8
|
+
|
9
|
+
# @return [SourceLocation] location in the file the lint corresponds to
|
10
|
+
attr_reader :location
|
11
|
+
|
12
|
+
# @return [SlimLint::Linter] linter that reported the lint
|
13
|
+
attr_reader :linter
|
14
|
+
|
15
|
+
# @return [String] sublinter that reported the lint
|
16
|
+
attr_reader :sublinter
|
17
|
+
|
18
|
+
# @return [String] error/warning message to display to user
|
19
|
+
attr_reader :message
|
20
|
+
|
21
|
+
# @return [Symbol] whether this lint is a warning or an error
|
22
|
+
attr_reader :severity
|
23
|
+
|
24
|
+
# Creates a new lint.
|
25
|
+
#
|
26
|
+
# @param linter [SlimLint::Linter]
|
27
|
+
# @param filename [String]
|
28
|
+
# @param location [SourceLocation]
|
29
|
+
# @param message [String]
|
30
|
+
# @param severity [Symbol]
|
31
|
+
def initialize(linter, filename, location, message, severity = :warning)
|
32
|
+
@linter, @sublinter = Array(linter)
|
33
|
+
@filename = filename
|
34
|
+
@location = location
|
35
|
+
@message = message
|
36
|
+
@severity = severity
|
37
|
+
end
|
38
|
+
|
39
|
+
def line
|
40
|
+
location.line
|
41
|
+
end
|
42
|
+
|
43
|
+
def column
|
44
|
+
location.column
|
45
|
+
end
|
46
|
+
|
47
|
+
def last_line
|
48
|
+
location.last_line
|
49
|
+
end
|
50
|
+
|
51
|
+
def last_column
|
52
|
+
location.last_column
|
53
|
+
end
|
54
|
+
|
55
|
+
def cop
|
56
|
+
@sublinter || @linter.name if @linter
|
57
|
+
end
|
58
|
+
|
59
|
+
def name
|
60
|
+
[@linter.name, @sublinter].compact.join("/") if @linter
|
61
|
+
end
|
62
|
+
|
63
|
+
# Return whether this lint has a severity of error.
|
64
|
+
#
|
65
|
+
# @return [Boolean]
|
66
|
+
def error?
|
67
|
+
@severity == :error
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Searches for multi-line control statements, dynamic output statements,
|
5
|
+
# attribute values, and splats.
|
6
|
+
class Linter::AvoidMultilineExpressions < Linter
|
7
|
+
include LinterRegistry
|
8
|
+
|
9
|
+
on [:slim, :control] do |sexp|
|
10
|
+
_, _, code = sexp
|
11
|
+
next unless code.size > 2
|
12
|
+
|
13
|
+
msg = "Avoid control statements that span multiple lines."
|
14
|
+
report_lint(sexp, msg)
|
15
|
+
end
|
16
|
+
|
17
|
+
on [:slim, :output] do |sexp|
|
18
|
+
_, _, _, code = sexp
|
19
|
+
next unless code.size > 2
|
20
|
+
|
21
|
+
msg = "Avoid dynamic output statements that span multiple lines."
|
22
|
+
report_lint(sexp, msg)
|
23
|
+
end
|
24
|
+
|
25
|
+
on [:slim, :attrvalue] do |sexp|
|
26
|
+
_, _, _, code = sexp
|
27
|
+
next unless code.size > 2
|
28
|
+
|
29
|
+
msg = "Avoid attribute values that span multiple lines."
|
30
|
+
report_lint(sexp, msg)
|
31
|
+
end
|
32
|
+
|
33
|
+
on [:slim, :splat] do |sexp|
|
34
|
+
_, _, code = sexp
|
35
|
+
next unless code.size > 2
|
36
|
+
|
37
|
+
msg = "Avoid attribute values that span multiple lines."
|
38
|
+
report_lint(sexp, msg)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Searches for control statements with only comments.
|
5
|
+
class Linter::CommentControlStatement < Linter
|
6
|
+
include LinterRegistry
|
7
|
+
|
8
|
+
RUBOCOP_CONTROL_COMMENT_RE = /^\s*(rubocop|standard):\w+/
|
9
|
+
TEMPLATE_DEPENDENCY_CONTROL_COMMENT_RE = /^\s*Template Dependency:/
|
10
|
+
|
11
|
+
on [:slim, :control] do |sexp|
|
12
|
+
_, _, code = sexp
|
13
|
+
next unless code.last[1][/\A\s*#/]
|
14
|
+
|
15
|
+
comment = code.last[1][/\A\s*#(.*\z)/, 1]
|
16
|
+
|
17
|
+
next if RUBOCOP_CONTROL_COMMENT_RE.match?(comment)
|
18
|
+
next if TEMPLATE_DEPENDENCY_CONTROL_COMMENT_RE.match?(comment)
|
19
|
+
|
20
|
+
msg =
|
21
|
+
"Slim code comments (`/#{comment}`) are preferred over " \
|
22
|
+
"control statement comments (`-##{comment}`)"
|
23
|
+
report_lint(sexp, msg)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Searches for more than an allowed number of consecutive control code
|
5
|
+
# statements that could be condensed into a :ruby filter.
|
6
|
+
class Linter::ConsecutiveControlStatements < Linter
|
7
|
+
include LinterRegistry
|
8
|
+
|
9
|
+
on [:multi] do |sexp|
|
10
|
+
max = config["max_consecutive"] + 1
|
11
|
+
Utils.for_consecutive_items(sexp, method(:flat_control_statement?), max) do |group|
|
12
|
+
report_lint(
|
13
|
+
group.first,
|
14
|
+
"#{group.count} consecutive control statements can be " \
|
15
|
+
"merged into a single `ruby:` filter"
|
16
|
+
)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def flat_control_statement?(sexp)
|
23
|
+
sexp.match?([:slim, :control]) && sexp[3] == [:multi]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Checks for missing or superfluous spacing before and after control statements.
|
5
|
+
class Linter::ControlStatementSpacing < Linter
|
6
|
+
include LinterRegistry
|
7
|
+
|
8
|
+
on [:slim, :control] do |sexp|
|
9
|
+
expr = sexp.last[0]
|
10
|
+
expr_line, expr_col = sexp.start
|
11
|
+
line = document.source_lines[expr_line - 1][(expr_col - 1)..]
|
12
|
+
after_pattern, after_action = after_config
|
13
|
+
|
14
|
+
unless line.match?(after_pattern)
|
15
|
+
report_lint(expr, "Please #{after_action} the dash")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def after_config
|
20
|
+
@after_config ||= case config["space_after"]
|
21
|
+
when "never", false, nil
|
22
|
+
[/^ *-#?[^# ]/, "remove spaces after"]
|
23
|
+
when "always", "single", true
|
24
|
+
[/^ *-#? [^ ]/, "use one space after"]
|
25
|
+
when "ignore", "any"
|
26
|
+
[//, ""]
|
27
|
+
else
|
28
|
+
raise ArgumentError, "Unknown value for `space_after`; please use 'never' or 'always'"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Checks for missing or superfluous spacing before and after dynamic tag output indicators.
|
5
|
+
class Linter::DynamicOutputSpacing < Linter
|
6
|
+
include LinterRegistry
|
7
|
+
|
8
|
+
PATTERN = "==?['<>]*"
|
9
|
+
|
10
|
+
on [:html, :tag, anything, [], capture(:expr, [:slim, :output, anything, anything])] do |sexp|
|
11
|
+
# Fetch original Slim code that contains an element with a control statement.
|
12
|
+
expr_line, expr_col = captures[:expr].start
|
13
|
+
line = document.source_lines[expr_line - 1][(expr_col - 1)..]
|
14
|
+
|
15
|
+
before_pattern, _ = before_config
|
16
|
+
after_pattern, _ = after_config
|
17
|
+
|
18
|
+
report(captures[:expr], line.match?(before_pattern), line.match?(after_pattern))
|
19
|
+
|
20
|
+
# Visit any children of the HTML tag, but don't _revisit_ this Slim output.
|
21
|
+
traverse_children(captures[:expr].last)
|
22
|
+
:stop
|
23
|
+
end
|
24
|
+
|
25
|
+
on [:slim, :output] do |sexp|
|
26
|
+
expr_line, expr_col = sexp.start
|
27
|
+
line = document.source_lines[expr_line - 1][(expr_col - 1)..]
|
28
|
+
after_pattern, _ = after_config
|
29
|
+
|
30
|
+
report(sexp, true, line.match?(after_pattern))
|
31
|
+
end
|
32
|
+
|
33
|
+
def report(expr, *results)
|
34
|
+
_, before_action = before_config
|
35
|
+
_, after_action = after_config
|
36
|
+
|
37
|
+
case results
|
38
|
+
when [false, true]
|
39
|
+
report_lint(expr, "Please #{before_action} the equals sign")
|
40
|
+
when [true, false]
|
41
|
+
report_lint(expr, "Please #{after_action} the equals sign")
|
42
|
+
when [false, false]
|
43
|
+
if before_action[0] == after_action[0]
|
44
|
+
report_lint(expr, "Please #{before_action} and after the equals sign")
|
45
|
+
else
|
46
|
+
report_lint(expr, "Please #{before_action} and #{after_action} the equals sign")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def before_config
|
52
|
+
@before_config ||= case config["space_before"]
|
53
|
+
when "never", false, nil
|
54
|
+
[/^#{PATTERN}/, "remove spaces before"]
|
55
|
+
when "always", "single", true
|
56
|
+
[/^ #{PATTERN}/, "use one space before"]
|
57
|
+
when "ignore", "any"
|
58
|
+
[//, ""]
|
59
|
+
else
|
60
|
+
raise ArgumentError, "Unknown value for `space_before`; please use 'never', 'always', or 'ignore'"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def after_config
|
65
|
+
@after_config ||= case config["space_after"]
|
66
|
+
when "never", false, nil
|
67
|
+
[/^ *#{PATTERN}[^ ]/, "remove spaces after"]
|
68
|
+
when "always", "single", true
|
69
|
+
[/^ *#{PATTERN} [^ ]/, "use one space after"]
|
70
|
+
when "ignore", "any"
|
71
|
+
[//, ""]
|
72
|
+
else
|
73
|
+
raise ArgumentError, "Unknown value for `space_after`; please use 'never', 'always', or 'ignore'"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Checks for forbidden embedded engines.
|
5
|
+
class Linter::EmbeddedEngines < Linter
|
6
|
+
include LinterRegistry
|
7
|
+
|
8
|
+
MESSAGE = "Forbidden embedded engine `%s` found"
|
9
|
+
|
10
|
+
on [:slim, :embedded] do |sexp|
|
11
|
+
_, _, engine, _ = sexp
|
12
|
+
|
13
|
+
forbidden_engines = config["forbidden_engines"]
|
14
|
+
next unless forbidden_engines.include?(engine)
|
15
|
+
report_lint(sexp, MESSAGE % engine)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Searches for control statements with no code.
|
5
|
+
class Linter::EmptyControlStatement < Linter
|
6
|
+
include LinterRegistry
|
7
|
+
|
8
|
+
on [:slim, :control] do |sexp|
|
9
|
+
_, _, code = sexp
|
10
|
+
next unless code.last[1][/\A\s*\Z/]
|
11
|
+
|
12
|
+
report_lint(sexp, "Empty control statement can be removed")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# This linter checks for two or more consecutive blank lines
|
5
|
+
# and for the first blank line in file.
|
6
|
+
class Linter::EmptyLines < Linter
|
7
|
+
include LinterRegistry
|
8
|
+
|
9
|
+
on_start do |_sexp|
|
10
|
+
was_empty = true
|
11
|
+
document.source.lines.each.with_index(1) do |line, i|
|
12
|
+
if line.blank?
|
13
|
+
if was_empty
|
14
|
+
sexp = Sexp.new(start: [i, 0], finish: [i, 0])
|
15
|
+
report_lint(sexp, "Extra empty line detected")
|
16
|
+
end
|
17
|
+
was_empty = true
|
18
|
+
else
|
19
|
+
was_empty = false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Checks for file longer than a maximum number of lines.
|
5
|
+
class Linter::FileLength < Linter
|
6
|
+
include LinterRegistry
|
7
|
+
|
8
|
+
MSG = "File is too long. [%d/%d]"
|
9
|
+
|
10
|
+
on_start do |_sexp|
|
11
|
+
count = document.source_lines.size
|
12
|
+
if count > config["max"]
|
13
|
+
sexp = Sexp.new(start: [1, 0], finish: [1, 0])
|
14
|
+
report_lint(sexp, format(MSG, count, config["max"]))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Checks for lines longer than a maximum number of columns.
|
5
|
+
class Linter::LineLength < Linter
|
6
|
+
include LinterRegistry
|
7
|
+
|
8
|
+
MSG = "Line is too long. [%d/%d]"
|
9
|
+
|
10
|
+
on_start do |_sexp|
|
11
|
+
document.source_lines.each.with_index(1) do |line, i|
|
12
|
+
next if line.length <= config["max"]
|
13
|
+
sexp = Sexp.new(start: [i, 0], finish: [i, 0])
|
14
|
+
report_lint(sexp, format(MSG, line.length, config["max"]))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Checks for unnecessary uses of the `div` tag where a class name or ID
|
5
|
+
# already implies a div.
|
6
|
+
class Linter::RedundantDiv < Linter
|
7
|
+
include LinterRegistry
|
8
|
+
|
9
|
+
SHORTCUT_ATTRS = %w[id class]
|
10
|
+
MESSAGE = "`div` is redundant when %s attribute shortcut is present"
|
11
|
+
|
12
|
+
on [:html, :tag, "div", capture(:attrs, [:html, :attrs]), anything] do |sexp|
|
13
|
+
_, _, name, value = captures[:attrs][2]
|
14
|
+
next unless name
|
15
|
+
next unless value[0] == :static
|
16
|
+
next unless SHORTCUT_ATTRS.include?(name.value)
|
17
|
+
|
18
|
+
report_lint(sexp[2], MESSAGE % name)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "slim_lint/ruby_extractor"
|
4
|
+
require "slim_lint/ruby_extract_engine"
|
5
|
+
require "rubocop"
|
6
|
+
|
7
|
+
module SlimLint
|
8
|
+
class Linter
|
9
|
+
# Runs RuboCop on Ruby code extracted from Slim templates.
|
10
|
+
class RuboCop < Linter
|
11
|
+
include LinterRegistry
|
12
|
+
|
13
|
+
on_start do |_sexp|
|
14
|
+
processed_sexp = SlimLint::RubyExtractEngine.new.call(document.source)
|
15
|
+
|
16
|
+
extractor = SlimLint::RubyExtractor.new
|
17
|
+
extracted_source = extractor.extract(processed_sexp)
|
18
|
+
|
19
|
+
next if extracted_source.source.empty?
|
20
|
+
|
21
|
+
find_lints(extracted_source.source, extracted_source.source_map)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# Executes RuboCop against the given Ruby code and records the offenses as
|
27
|
+
# lints.
|
28
|
+
#
|
29
|
+
# @param ruby [String] Ruby code
|
30
|
+
# @param source_map [Hash] map of Ruby code line numbers to original line
|
31
|
+
# numbers in the template
|
32
|
+
def find_lints(ruby, source_map)
|
33
|
+
rubocop = ::RuboCop::CLI.new
|
34
|
+
|
35
|
+
filename = document.file ? "#{document.file}.rb" : "ruby_script.rb"
|
36
|
+
|
37
|
+
with_ruby_from_stdin(ruby) do
|
38
|
+
extract_lints_from_offenses(lint_file(rubocop, filename), source_map)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Defined so we can stub the results in tests
|
43
|
+
#
|
44
|
+
# @param rubocop [RuboCop::CLI]
|
45
|
+
# @param file [String]
|
46
|
+
# @return [Array<RuboCop::Cop::Offense>]
|
47
|
+
def lint_file(rubocop, file)
|
48
|
+
rubocop.run(rubocop_flags << file)
|
49
|
+
OffenseCollector.offenses
|
50
|
+
end
|
51
|
+
|
52
|
+
# Aggregates RuboCop offenses and converts them to {SlimLint::Lint}s
|
53
|
+
# suitable for reporting.
|
54
|
+
#
|
55
|
+
# @param offenses [Array<RuboCop::Cop::Offense>]
|
56
|
+
# @param source_map [Hash]
|
57
|
+
def extract_lints_from_offenses(offenses, source_map)
|
58
|
+
offenses.reject! { |offense| config["ignored_cops"].include?(offense.cop_name) }
|
59
|
+
offenses.each do |offense|
|
60
|
+
@lints << Lint.new(
|
61
|
+
[self, offense.cop_name],
|
62
|
+
document.file,
|
63
|
+
location_for_line(source_map, offense),
|
64
|
+
offense.message.gsub(/ at \d+, \d+/, "")
|
65
|
+
)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns flags that will be passed to RuboCop CLI.
|
70
|
+
#
|
71
|
+
# @return [Array<String>]
|
72
|
+
def rubocop_flags
|
73
|
+
flags = %w[--format SlimLint::Linter::RuboCop::OffenseCollector]
|
74
|
+
flags += ["--no-display-cop-names"]
|
75
|
+
flags += ["--config", ENV["SLIM_LINT_RUBOCOP_CONF"]] if ENV["SLIM_LINT_RUBOCOP_CONF"]
|
76
|
+
flags += ["--stdin"]
|
77
|
+
flags
|
78
|
+
end
|
79
|
+
|
80
|
+
# Overrides the global stdin to allow RuboCop to read Ruby code from it.
|
81
|
+
#
|
82
|
+
# @param ruby [String] the Ruby code to write to the overridden stdin
|
83
|
+
# @param _block [Block] the block to perform with the overridden stdin
|
84
|
+
# @return [void]
|
85
|
+
def with_ruby_from_stdin(ruby, &_block)
|
86
|
+
original_stdin = $stdin
|
87
|
+
stdin = StringIO.new
|
88
|
+
stdin.puts(ruby.chomp)
|
89
|
+
stdin.rewind
|
90
|
+
$stdin = stdin
|
91
|
+
yield
|
92
|
+
ensure
|
93
|
+
$stdin = original_stdin
|
94
|
+
end
|
95
|
+
|
96
|
+
def location_for_line(source_map, offense)
|
97
|
+
if source_map.key?(offense.line)
|
98
|
+
start = source_map[offense.line].adjust(column: offense.column)
|
99
|
+
finish = source_map[offense.last_line].adjust(column: offense.last_column)
|
100
|
+
SourceLocation.merge(start, finish, length: offense.column_length)
|
101
|
+
else
|
102
|
+
SourceLocation.new(start_line: document.source_lines.size, start_column: 0)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Collects offenses detected by RuboCop.
|
107
|
+
class OffenseCollector < ::RuboCop::Formatter::BaseFormatter
|
108
|
+
class << self
|
109
|
+
# List of offenses reported by RuboCop.
|
110
|
+
attr_accessor :offenses
|
111
|
+
end
|
112
|
+
|
113
|
+
# Executed when RuboCop begins linting.
|
114
|
+
#
|
115
|
+
# @param _target_files [Array<String>]
|
116
|
+
def started(_target_files)
|
117
|
+
self.class.offenses = []
|
118
|
+
end
|
119
|
+
|
120
|
+
# Executed when a file has been scanned by RuboCop, adding the reported
|
121
|
+
# offenses to our collection.
|
122
|
+
#
|
123
|
+
# @param _file [String]
|
124
|
+
# @param offenses [Array<RuboCop::Cop::Offense>]
|
125
|
+
def file_finished(_file, offenses)
|
126
|
+
self.class.offenses += offenses
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|