liquid_lint 1.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 +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
|