liquid_lint 1.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 +1 -0
- data/bin/liquid-lint +7 -0
- data/config/default.yml +99 -0
- data/lib/liquid_lint/atom.rb +98 -0
- data/lib/liquid_lint/capture_map.rb +19 -0
- data/lib/liquid_lint/cli.rb +163 -0
- data/lib/liquid_lint/configuration.rb +109 -0
- data/lib/liquid_lint/configuration_loader.rb +86 -0
- data/lib/liquid_lint/constants.rb +10 -0
- data/lib/liquid_lint/document.rb +76 -0
- data/lib/liquid_lint/engine.rb +45 -0
- data/lib/liquid_lint/exceptions.rb +20 -0
- data/lib/liquid_lint/file_finder.rb +88 -0
- data/lib/liquid_lint/filters/attribute_processor.rb +31 -0
- data/lib/liquid_lint/filters/control_processor.rb +47 -0
- data/lib/liquid_lint/filters/inject_line_numbers.rb +43 -0
- data/lib/liquid_lint/filters/sexp_converter.rb +17 -0
- data/lib/liquid_lint/filters/splat_processor.rb +15 -0
- data/lib/liquid_lint/lint.rb +43 -0
- data/lib/liquid_lint/linter/comment_control_statement.rb +22 -0
- data/lib/liquid_lint/linter/consecutive_control_statements.rb +26 -0
- data/lib/liquid_lint/linter/control_statement_spacing.rb +24 -0
- data/lib/liquid_lint/linter/embedded_engines.rb +22 -0
- data/lib/liquid_lint/linter/empty_control_statement.rb +15 -0
- data/lib/liquid_lint/linter/empty_lines.rb +26 -0
- data/lib/liquid_lint/linter/file_length.rb +20 -0
- data/lib/liquid_lint/linter/line_length.rb +21 -0
- data/lib/liquid_lint/linter/redundant_div.rb +22 -0
- data/lib/liquid_lint/linter/rubocop.rb +116 -0
- data/lib/liquid_lint/linter/tab.rb +19 -0
- data/lib/liquid_lint/linter/tag_case.rb +15 -0
- data/lib/liquid_lint/linter/trailing_blank_lines.rb +21 -0
- data/lib/liquid_lint/linter/trailing_whitespace.rb +19 -0
- data/lib/liquid_lint/linter/zwsp.rb +18 -0
- data/lib/liquid_lint/linter.rb +93 -0
- data/lib/liquid_lint/linter_registry.rb +39 -0
- data/lib/liquid_lint/linter_selector.rb +79 -0
- data/lib/liquid_lint/logger.rb +103 -0
- data/lib/liquid_lint/matcher/anything.rb +11 -0
- data/lib/liquid_lint/matcher/base.rb +21 -0
- data/lib/liquid_lint/matcher/capture.rb +32 -0
- data/lib/liquid_lint/matcher/nothing.rb +13 -0
- data/lib/liquid_lint/options.rb +110 -0
- data/lib/liquid_lint/rake_task.rb +125 -0
- data/lib/liquid_lint/report.rb +25 -0
- data/lib/liquid_lint/reporter/checkstyle_reporter.rb +42 -0
- data/lib/liquid_lint/reporter/default_reporter.rb +41 -0
- data/lib/liquid_lint/reporter/emacs_reporter.rb +44 -0
- data/lib/liquid_lint/reporter/json_reporter.rb +52 -0
- data/lib/liquid_lint/reporter.rb +44 -0
- data/lib/liquid_lint/ruby_extract_engine.rb +36 -0
- data/lib/liquid_lint/ruby_extractor.rb +106 -0
- data/lib/liquid_lint/ruby_parser.rb +40 -0
- data/lib/liquid_lint/runner.rb +82 -0
- data/lib/liquid_lint/sexp.rb +106 -0
- data/lib/liquid_lint/sexp_visitor.rb +146 -0
- data/lib/liquid_lint/utils.rb +85 -0
- data/lib/liquid_lint/version.rb +6 -0
- data/lib/liquid_lint.rb +52 -0
- metadata +185 -0
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint
|
4
|
+
# Temple engine used to generate a {Sexp} parse tree for use by linters.
|
5
|
+
#
|
6
|
+
# We omit a lot of the filters that are in {Liquid::Engine} because they result
|
7
|
+
# in information potentially being removed from the parse tree (since some
|
8
|
+
# Sexp abstractions are optimized/removed or otherwise transformed). In order
|
9
|
+
# for linters to be useful, they need to operate on the original parse tree.
|
10
|
+
#
|
11
|
+
# The other key task this engine accomplishes is converting the Array-based
|
12
|
+
# S-expressions into {LiquidLint::Sexp} objects, which have a number of helper
|
13
|
+
# methods that makes working with them easier. It also annotates these
|
14
|
+
# {LiquidLint::Sexp} objects with line numbers so it's easy to cross reference
|
15
|
+
# with the original source code.
|
16
|
+
class Engine < Temple::Engine
|
17
|
+
filter :Encoding
|
18
|
+
filter :RemoveBOM
|
19
|
+
|
20
|
+
# Parse into S-expression using Liquid parser
|
21
|
+
use Liquid::Parser
|
22
|
+
|
23
|
+
# Converts Array-based S-expressions into LiquidLint::Sexp objects
|
24
|
+
use LiquidLint::Filters::SexpConverter
|
25
|
+
|
26
|
+
# Annotates Sexps with line numbers
|
27
|
+
use LiquidLint::Filters::InjectLineNumbers
|
28
|
+
|
29
|
+
# Parses the given source code into a Sexp.
|
30
|
+
#
|
31
|
+
# @param source [String] source code to parse
|
32
|
+
# @return [LiquidLint::Sexp] parsed Sexp
|
33
|
+
def parse(source)
|
34
|
+
call(source)
|
35
|
+
rescue ::Liquid::Parser::SyntaxError => e
|
36
|
+
# Convert to our own exception type to isolate from upstream changes
|
37
|
+
error = LiquidLint::Exceptions::ParseError.new(e.error,
|
38
|
+
e.file,
|
39
|
+
e.line,
|
40
|
+
e.lineno,
|
41
|
+
e.column)
|
42
|
+
raise error
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Collection of exceptions that can be raised by the application.
|
4
|
+
module LiquidLint::Exceptions
|
5
|
+
# Raised when a {Configuration} could not be loaded from a file.
|
6
|
+
class ConfigurationError < StandardError; end
|
7
|
+
|
8
|
+
# Raised when invalid/incompatible command line options are provided.
|
9
|
+
class InvalidCLIOption < StandardError; end
|
10
|
+
|
11
|
+
# Raised when an invalid file path is specified
|
12
|
+
class InvalidFilePath < StandardError; end
|
13
|
+
|
14
|
+
# Raised when the Liquid parser is unable to parse a template.
|
15
|
+
class ParseError < ::Liquid::Parser::SyntaxError; end
|
16
|
+
|
17
|
+
# Raised when attempting to execute `Runner` with options that would result in
|
18
|
+
# no linters being enabled.
|
19
|
+
class NoLintersError < StandardError; end
|
20
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'find'
|
4
|
+
|
5
|
+
module LiquidLint
|
6
|
+
# Finds Liquid files that should be linted given a specified list of paths, glob
|
7
|
+
# patterns, and configuration.
|
8
|
+
class FileFinder
|
9
|
+
# List of extensions of files to include under a directory when a directory
|
10
|
+
# is specified instead of a file.
|
11
|
+
VALID_EXTENSIONS = %w[.liquid].freeze
|
12
|
+
|
13
|
+
# Create a file finder using the specified configuration.
|
14
|
+
#
|
15
|
+
# @param config [LiquidLint::Configuration]
|
16
|
+
def initialize(config)
|
17
|
+
@config = config
|
18
|
+
end
|
19
|
+
|
20
|
+
# Return list of files to lint given the specified set of paths and glob
|
21
|
+
# patterns.
|
22
|
+
# @param patterns [Array<String>]
|
23
|
+
# @param excluded_patterns [Array<String>]
|
24
|
+
# @raise [LiquidLint::Exceptions::InvalidFilePath]
|
25
|
+
# @return [Array<String>] list of actual files
|
26
|
+
def find(patterns, excluded_patterns)
|
27
|
+
excluded_patterns = excluded_patterns.map { |pattern| normalize_path(pattern) }
|
28
|
+
|
29
|
+
extract_files_from(patterns).reject do |file|
|
30
|
+
LiquidLint::Utils.any_glob_matches?(excluded_patterns, file)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# Extract the list of matching files given the list of glob patterns, file
|
37
|
+
# paths, and directories.
|
38
|
+
#
|
39
|
+
# @param patterns [Array<String>]
|
40
|
+
# @return [Array<String>]
|
41
|
+
def extract_files_from(patterns) # rubocop:disable Metrics/MethodLength
|
42
|
+
files = []
|
43
|
+
|
44
|
+
patterns.each do |pattern|
|
45
|
+
if File.file?(pattern)
|
46
|
+
files << pattern
|
47
|
+
else
|
48
|
+
begin
|
49
|
+
::Find.find(pattern) do |file|
|
50
|
+
files << file if liquid_file?(file)
|
51
|
+
end
|
52
|
+
rescue ::Errno::ENOENT
|
53
|
+
# File didn't exist; it might be a file glob pattern
|
54
|
+
matches = ::Dir.glob(pattern)
|
55
|
+
if matches.any?
|
56
|
+
files += matches
|
57
|
+
else
|
58
|
+
# One of the paths specified does not exist; raise a more
|
59
|
+
# descriptive exception so we know which one
|
60
|
+
raise LiquidLint::Exceptions::InvalidFilePath,
|
61
|
+
"File path '#{pattern}' does not exist"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
files.uniq.sort.map { |file| normalize_path(file) }
|
68
|
+
end
|
69
|
+
|
70
|
+
# Trim "./" from the front of relative paths.
|
71
|
+
#
|
72
|
+
# @param path [String]
|
73
|
+
# @return [String]
|
74
|
+
def normalize_path(path)
|
75
|
+
path.start_with?(".#{File::SEPARATOR}") ? path[2..-1] : path
|
76
|
+
end
|
77
|
+
|
78
|
+
# Whether the given file should be treated as a Liquid file.
|
79
|
+
#
|
80
|
+
# @param file [String]
|
81
|
+
# @return [Boolean]
|
82
|
+
def liquid_file?(file)
|
83
|
+
return false unless ::FileTest.file?(file)
|
84
|
+
|
85
|
+
VALID_EXTENSIONS.include?(::File.extname(file))
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint::Filters
|
4
|
+
# A dumbed-down version of {Liquid::CodeAttributes} which doesn't introduce any
|
5
|
+
# temporary variables or other cruft.
|
6
|
+
class AttributeProcessor < Liquid::Filter
|
7
|
+
define_options :merge_attrs
|
8
|
+
|
9
|
+
# Handle attributes expression `[:html, :attrs, *attrs]`
|
10
|
+
#
|
11
|
+
# @param attrs [Array]
|
12
|
+
# @return [Array]
|
13
|
+
def on_html_attrs(*attrs)
|
14
|
+
[:multi, *attrs.map { |a| compile(a) }]
|
15
|
+
end
|
16
|
+
|
17
|
+
# Handle attribute expression `[:html, :attr, name, value]`
|
18
|
+
#
|
19
|
+
# @param name [String] name of the attribute
|
20
|
+
# @param value [Array] Sexp representing the value
|
21
|
+
def on_html_attr(name, value)
|
22
|
+
if value[0] == :liquid && value[1] == :attrvalue
|
23
|
+
code = value[3]
|
24
|
+
[:code, code]
|
25
|
+
else
|
26
|
+
@attr = name
|
27
|
+
super
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint::Filters
|
4
|
+
# A dumbed-down version of {Liquid::Controls} which doesn't introduce temporary
|
5
|
+
# variables and other cruft (which in the context of extracting Ruby code,
|
6
|
+
# results in a lot of weird cops reported by RuboCop).
|
7
|
+
class ControlProcessor < Liquid::Filter
|
8
|
+
BLOCK_RE = /\A(if|unless)\b|\bdo\s*(\|[^|]*\|)?\s*$/
|
9
|
+
|
10
|
+
# Handle control expression `[:liquid, :control, code, content]`
|
11
|
+
#
|
12
|
+
# @param code [String]
|
13
|
+
# @param content [Array]
|
14
|
+
def on_liquid_control(code, content)
|
15
|
+
[:multi,
|
16
|
+
[:code, code],
|
17
|
+
compile(content)]
|
18
|
+
end
|
19
|
+
|
20
|
+
# Handle output expression `[:liquid, :output, escape, code, content]`
|
21
|
+
#
|
22
|
+
# @param _escape [Boolean]
|
23
|
+
# @param code [String]
|
24
|
+
# @param content [Array]
|
25
|
+
# @return [Array
|
26
|
+
def on_liquid_output(_escape, code, content)
|
27
|
+
if code[BLOCK_RE]
|
28
|
+
[:multi,
|
29
|
+
[:code, code, compile(content)],
|
30
|
+
[:code, 'end']]
|
31
|
+
else
|
32
|
+
[:multi, [:dynamic, code], compile(content)]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Handle text expression `[:liquid, :text, type, content]`
|
37
|
+
#
|
38
|
+
# @param _type [Symbol]
|
39
|
+
# @param content [Array]
|
40
|
+
# @return [Array]
|
41
|
+
def on_liquid_text(_type, content)
|
42
|
+
# Ensures :newline expressions from static output are still represented in
|
43
|
+
# the final expression
|
44
|
+
compile(content)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint::Filters
|
4
|
+
# Traverses a Temple S-expression (that has already been converted to
|
5
|
+
# {LiquidLint::Sexp} instances) and annotates them with line numbers.
|
6
|
+
#
|
7
|
+
# This is a hack that allows us to access line information directly from the
|
8
|
+
# S-expressions, which makes a lot of other tasks easier.
|
9
|
+
class InjectLineNumbers < Temple::Filter
|
10
|
+
# {Sexp} representing a newline.
|
11
|
+
NEWLINE_SEXP = LiquidLint::Sexp.new([:newline])
|
12
|
+
|
13
|
+
# Annotates the given {LiquidLint::Sexp} with line number information.
|
14
|
+
#
|
15
|
+
# @param sexp [LiquidLint::Sexp]
|
16
|
+
# @return [LiquidLint::Sexp]
|
17
|
+
def call(sexp)
|
18
|
+
@line_number = 1
|
19
|
+
traverse(sexp)
|
20
|
+
sexp
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# Traverses an {Sexp}, annotating it with line numbers.
|
26
|
+
#
|
27
|
+
# @param sexp [LiquidLint::Sexp]
|
28
|
+
def traverse(sexp)
|
29
|
+
sexp.line = @line_number
|
30
|
+
|
31
|
+
case sexp
|
32
|
+
when LiquidLint::Atom
|
33
|
+
@line_number += sexp.strip.count("\n") if sexp.respond_to?(:count)
|
34
|
+
when NEWLINE_SEXP
|
35
|
+
@line_number += 1
|
36
|
+
else
|
37
|
+
sexp.each do |nested_sexp|
|
38
|
+
traverse(nested_sexp)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint::Filters
|
4
|
+
# Converts a Temple S-expression comprised of {Array}s into {LiquidLint::Sexp}s.
|
5
|
+
#
|
6
|
+
# These {LiquidLint::Sexp}s include additional helpers that makes working with
|
7
|
+
# them more pleasant.
|
8
|
+
class SexpConverter < Temple::Filter
|
9
|
+
# Converts the given {Array} to a {LiquidLint::Sexp}.
|
10
|
+
#
|
11
|
+
# @param array_sexp [Array]
|
12
|
+
# @return [LiquidLint::Sexp]
|
13
|
+
def call(array_sexp)
|
14
|
+
LiquidLint::Sexp.new(array_sexp)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint::Filters
|
4
|
+
# A dumbed-down version of {Liquid::Splat::Filter} which doesn't introduced
|
5
|
+
# temporary variables or other cruft.
|
6
|
+
class SplatProcessor < Liquid::Filter
|
7
|
+
# Handle liquid splat expressions `[:liquid, :splat, code]`
|
8
|
+
#
|
9
|
+
# @param code [String]
|
10
|
+
# @return [Array]
|
11
|
+
def on_liquid_splat(code)
|
12
|
+
[:code, code]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint
|
4
|
+
# Contains information about a problem or issue with a Liquid document.
|
5
|
+
class Lint
|
6
|
+
# @return [String] file path to which the lint applies
|
7
|
+
attr_reader :filename
|
8
|
+
|
9
|
+
# @return [String] line number of the file the lint corresponds to
|
10
|
+
attr_reader :line
|
11
|
+
|
12
|
+
# @return [LiquidLint::Linter] linter that reported the lint
|
13
|
+
attr_reader :linter
|
14
|
+
|
15
|
+
# @return [String] error/warning message to display to user
|
16
|
+
attr_reader :message
|
17
|
+
|
18
|
+
# @return [Symbol] whether this lint is a warning or an error
|
19
|
+
attr_reader :severity
|
20
|
+
|
21
|
+
# Creates a new lint.
|
22
|
+
#
|
23
|
+
# @param linter [LiquidLint::Linter]
|
24
|
+
# @param filename [String]
|
25
|
+
# @param line [Fixnum]
|
26
|
+
# @param message [String]
|
27
|
+
# @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
|
34
|
+
end
|
35
|
+
|
36
|
+
# Return whether this lint has a severity of error.
|
37
|
+
#
|
38
|
+
# @return [Boolean]
|
39
|
+
def error?
|
40
|
+
@severity == :error
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint
|
4
|
+
# Searches for control statements with only comments.
|
5
|
+
class Linter::CommentControlStatement < Linter
|
6
|
+
include LinterRegistry
|
7
|
+
|
8
|
+
on [:liquid, :control] do |sexp|
|
9
|
+
_, _, code = sexp
|
10
|
+
next unless code[/\A\s*#/]
|
11
|
+
|
12
|
+
comment = code[/\A\s*#(.*\z)/, 1]
|
13
|
+
|
14
|
+
next if comment =~ /^\s*rubocop:\w+/
|
15
|
+
next if comment =~ /^\s*Template Dependency:/
|
16
|
+
|
17
|
+
report_lint(sexp,
|
18
|
+
"Liquid code comments (`/#{comment}`) are preferred over " \
|
19
|
+
"control statement comments (`-##{comment}`)")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint
|
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
|
+
Utils.for_consecutive_items(sexp,
|
11
|
+
method(:flat_control_statement?),
|
12
|
+
config['max_consecutive'] + 1) do |group|
|
13
|
+
report_lint(group.first,
|
14
|
+
"#{group.count} consecutive control statements can be " \
|
15
|
+
'merged into a single `ruby:` filter')
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def flat_control_statement?(sexp)
|
22
|
+
sexp.match?([:liquid, :control]) &&
|
23
|
+
sexp[3] == [:multi, [:newline]]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint
|
4
|
+
# Checks for missing or superfluous spacing before and after control statements.
|
5
|
+
class Linter::ControlStatementSpacing < Linter
|
6
|
+
include LinterRegistry
|
7
|
+
|
8
|
+
MESSAGE = 'Please add a space before and after the `=`'
|
9
|
+
|
10
|
+
on [:html, :tag, anything, [],
|
11
|
+
[:liquid, :output, anything, capture(:ruby, anything)]] do |sexp|
|
12
|
+
# Fetch original Liquid code that contains an element with a control statement.
|
13
|
+
line = document.source_lines[sexp.line - 1]
|
14
|
+
|
15
|
+
# Remove any Ruby code, because our regexp below must not match inside Ruby.
|
16
|
+
ruby = captures[:ruby]
|
17
|
+
line = line.sub(ruby, 'x')
|
18
|
+
|
19
|
+
next if line =~ /[^ ] ==?<?>? [^ ]/
|
20
|
+
|
21
|
+
report_lint(sexp, MESSAGE)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint
|
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_start do |_sexp|
|
11
|
+
forbidden_engines = config['forbidden_engines']
|
12
|
+
dummy_node = Struct.new(:line)
|
13
|
+
document.source_lines.each_with_index do |line, index|
|
14
|
+
forbidden_engines.each do |forbidden_engine|
|
15
|
+
next unless line =~ /^#{forbidden_engine}.*:\s*$/
|
16
|
+
|
17
|
+
report_lint(dummy_node.new(index + 1), MESSAGE % forbidden_engine)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint
|
4
|
+
# Searches for control statements with no code.
|
5
|
+
class Linter::EmptyControlStatement < Linter
|
6
|
+
include LinterRegistry
|
7
|
+
|
8
|
+
on [:liquid, :control] do |sexp|
|
9
|
+
_, _, code = sexp
|
10
|
+
next unless code[/\A\s*\Z/]
|
11
|
+
|
12
|
+
report_lint(sexp, 'Empty control statement can be removed')
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint
|
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
|
+
dummy_node = Struct.new(:line)
|
11
|
+
|
12
|
+
was_empty = true
|
13
|
+
document.source.lines.each_with_index do |line, i|
|
14
|
+
if line.blank?
|
15
|
+
if was_empty
|
16
|
+
report_lint(dummy_node.new(i + 1),
|
17
|
+
'Extra empty line detected')
|
18
|
+
end
|
19
|
+
was_empty = true
|
20
|
+
else
|
21
|
+
was_empty = false
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint
|
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
|
+
max_length = config['max']
|
12
|
+
dummy_node = Struct.new(:line)
|
13
|
+
|
14
|
+
count = document.source_lines.size
|
15
|
+
if count > max_length
|
16
|
+
report_lint(dummy_node.new(1), format(MSG, count, max_length))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint
|
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
|
+
max_length = config['max']
|
12
|
+
dummy_node = Struct.new(:line)
|
13
|
+
|
14
|
+
document.source_lines.each_with_index do |line, index|
|
15
|
+
next if line.length <= max_length
|
16
|
+
|
17
|
+
report_lint(dummy_node.new(index + 1), format(MSG, line.length, max_length))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint
|
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
|
+
MESSAGE = '`div` is redundant when %s attribute shortcut is present'
|
10
|
+
|
11
|
+
on [:html, :tag, 'div',
|
12
|
+
[:html, :attrs,
|
13
|
+
[:html, :attr,
|
14
|
+
capture(:attr_name, anything),
|
15
|
+
[:static]]]] do |sexp|
|
16
|
+
attr = captures[:attr_name]
|
17
|
+
next unless %w[class id].include?(attr)
|
18
|
+
|
19
|
+
report_lint(sexp, MESSAGE % attr)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'liquid_lint/ruby_extractor'
|
4
|
+
require 'liquid_lint/ruby_extract_engine'
|
5
|
+
require 'rubocop'
|
6
|
+
|
7
|
+
module LiquidLint
|
8
|
+
# Runs RuboCop on Ruby code extracted from Liquid templates.
|
9
|
+
class Linter::RuboCop < Linter
|
10
|
+
include LinterRegistry
|
11
|
+
|
12
|
+
on_start do |_sexp|
|
13
|
+
processed_sexp = LiquidLint::RubyExtractEngine.new.call(document.source)
|
14
|
+
|
15
|
+
extractor = LiquidLint::RubyExtractor.new
|
16
|
+
extracted_source = extractor.extract(processed_sexp)
|
17
|
+
|
18
|
+
next if extracted_source.source.empty?
|
19
|
+
|
20
|
+
find_lints(extracted_source.source, extracted_source.source_map)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# Executes RuboCop against the given Ruby code and records the offenses as
|
26
|
+
# lints.
|
27
|
+
#
|
28
|
+
# @param ruby [String] Ruby code
|
29
|
+
# @param source_map [Hash] map of Ruby code line numbers to original line
|
30
|
+
# numbers in the template
|
31
|
+
def find_lints(ruby, source_map)
|
32
|
+
rubocop = ::RuboCop::CLI.new
|
33
|
+
|
34
|
+
filename = document.file ? "#{document.file}.rb" : 'ruby_script.rb'
|
35
|
+
|
36
|
+
with_ruby_from_stdin(ruby) do
|
37
|
+
extract_lints_from_offenses(lint_file(rubocop, filename), source_map)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Defined so we can stub the results in tests
|
42
|
+
#
|
43
|
+
# @param rubocop [RuboCop::CLI]
|
44
|
+
# @param file [String]
|
45
|
+
# @return [Array<RuboCop::Cop::Offense>]
|
46
|
+
def lint_file(rubocop, file)
|
47
|
+
rubocop.run(rubocop_flags << file)
|
48
|
+
OffenseCollector.offenses
|
49
|
+
end
|
50
|
+
|
51
|
+
# Aggregates RuboCop offenses and converts them to {LiquidLint::Lint}s
|
52
|
+
# suitable for reporting.
|
53
|
+
#
|
54
|
+
# @param offenses [Array<RuboCop::Cop::Offense>]
|
55
|
+
# @param source_map [Hash]
|
56
|
+
def extract_lints_from_offenses(offenses, source_map)
|
57
|
+
offenses.select { |offense| !config['ignored_cops'].include?(offense.cop_name) }
|
58
|
+
.each do |offense|
|
59
|
+
@lints << Lint.new(self,
|
60
|
+
document.file,
|
61
|
+
source_map[offense.line],
|
62
|
+
offense.message)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns flags that will be passed to RuboCop CLI.
|
67
|
+
#
|
68
|
+
# @return [Array<String>]
|
69
|
+
def rubocop_flags
|
70
|
+
flags = %w[--format LiquidLint::OffenseCollector]
|
71
|
+
flags += ['--config', ENV['LIQUID_LINT_RUBOCOP_CONF']] if ENV['LIQUID_LINT_RUBOCOP_CONF']
|
72
|
+
flags += ['--stdin']
|
73
|
+
flags
|
74
|
+
end
|
75
|
+
|
76
|
+
# Overrides the global stdin to allow RuboCop to read Ruby code from it.
|
77
|
+
#
|
78
|
+
# @param ruby [String] the Ruby code to write to the overridden stdin
|
79
|
+
# @param _block [Block] the block to perform with the overridden stdin
|
80
|
+
# @return [void]
|
81
|
+
def with_ruby_from_stdin(ruby, &_block)
|
82
|
+
original_stdin = $stdin
|
83
|
+
stdin = StringIO.new
|
84
|
+
stdin.write(ruby)
|
85
|
+
stdin.rewind
|
86
|
+
$stdin = stdin
|
87
|
+
yield
|
88
|
+
ensure
|
89
|
+
$stdin = original_stdin
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Collects offenses detected by RuboCop.
|
94
|
+
class OffenseCollector < ::RuboCop::Formatter::BaseFormatter
|
95
|
+
class << self
|
96
|
+
# List of offenses reported by RuboCop.
|
97
|
+
attr_accessor :offenses
|
98
|
+
end
|
99
|
+
|
100
|
+
# Executed when RuboCop begins linting.
|
101
|
+
#
|
102
|
+
# @param _target_files [Array<String>]
|
103
|
+
def started(_target_files)
|
104
|
+
self.class.offenses = []
|
105
|
+
end
|
106
|
+
|
107
|
+
# Executed when a file has been scanned by RuboCop, adding the reported
|
108
|
+
# offenses to our collection.
|
109
|
+
#
|
110
|
+
# @param _file [String]
|
111
|
+
# @param offenses [Array<RuboCop::Cop::Offense>]
|
112
|
+
def file_finished(_file, offenses)
|
113
|
+
self.class.offenses += offenses
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|