slim_lint_standard 0.0.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 +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
|