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,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rake'
|
4
|
+
require 'rake/tasklib'
|
5
|
+
require 'liquid_lint/constants'
|
6
|
+
|
7
|
+
module LiquidLint
|
8
|
+
# Rake task interface for liquid-lint command line interface.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# # Add the following to your Rakefile...
|
12
|
+
# require 'liquid_lint/rake_task'
|
13
|
+
#
|
14
|
+
# LiquidLint::RakeTask.new do |t|
|
15
|
+
# t.config = 'path/to/custom/liquid-lint.yml'
|
16
|
+
# t.files = %w[app/views/**/*.liquid custom/*.liquid]
|
17
|
+
# t.quiet = true # Don't display output from liquid-lint
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# # ...and then execute from the command line:
|
21
|
+
# rake liquid_lint
|
22
|
+
#
|
23
|
+
# You can also specify the list of files as explicit task arguments:
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
# # Add the following to your Rakefile...
|
27
|
+
# require 'liquid_lint/rake_task'
|
28
|
+
#
|
29
|
+
# LiquidLint::RakeTask.new
|
30
|
+
#
|
31
|
+
# # ...and then execute from the command line (single quotes prevent shell
|
32
|
+
# # glob expansion and allow us to have a space after commas):
|
33
|
+
# rake 'liquid_lint[app/views/**/*.liquid, other_files/**/*.liquid]'
|
34
|
+
#
|
35
|
+
class RakeTask < Rake::TaskLib
|
36
|
+
# Name of the task.
|
37
|
+
# @return [String]
|
38
|
+
attr_accessor :name
|
39
|
+
|
40
|
+
# Configuration file to use.
|
41
|
+
# @return [String]
|
42
|
+
attr_accessor :config
|
43
|
+
|
44
|
+
# List of files to lint (can contain shell globs).
|
45
|
+
#
|
46
|
+
# Note that this will be ignored if you explicitly pass a list of files as
|
47
|
+
# task arguments via the command line or a task definition.
|
48
|
+
# @return [Array<String>]
|
49
|
+
attr_accessor :files
|
50
|
+
|
51
|
+
# Whether output from liquid-lint should not be displayed to the standard out
|
52
|
+
# stream.
|
53
|
+
# @return [true,false]
|
54
|
+
attr_accessor :quiet
|
55
|
+
|
56
|
+
# Create the task so it exists in the current namespace.
|
57
|
+
#
|
58
|
+
# @param name [Symbol] task name
|
59
|
+
def initialize(name = :liquid_lint)
|
60
|
+
@name = name
|
61
|
+
@files = ['.'] # Search for everything under current directory by default
|
62
|
+
@quiet = false
|
63
|
+
|
64
|
+
yield self if block_given?
|
65
|
+
|
66
|
+
define
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
# Defines the Rake task.
|
72
|
+
def define
|
73
|
+
desc default_description unless ::Rake.application.last_description
|
74
|
+
|
75
|
+
task(name, [:files]) do |_task, task_args|
|
76
|
+
# Lazy-load so task doesn't affect Rakefile load time
|
77
|
+
require 'liquid_lint'
|
78
|
+
require 'liquid_lint/cli'
|
79
|
+
|
80
|
+
run_cli(task_args)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Executes the CLI given the specified task arguments.
|
85
|
+
#
|
86
|
+
# @param task_args [Rake::TaskArguments]
|
87
|
+
def run_cli(task_args)
|
88
|
+
cli_args = ['--config', config] if config
|
89
|
+
|
90
|
+
logger = quiet ? LiquidLint::Logger.silent : LiquidLint::Logger.new($stdout)
|
91
|
+
result = LiquidLint::CLI.new(logger).run(Array(cli_args) + files_to_lint(task_args))
|
92
|
+
|
93
|
+
fail "#{LiquidLint::APP_NAME} failed with exit code #{result}" unless result == 0
|
94
|
+
end
|
95
|
+
|
96
|
+
# Returns the list of files that should be linted given the specified task
|
97
|
+
# arguments.
|
98
|
+
#
|
99
|
+
# @param task_args [Rake::TaskArguments]
|
100
|
+
def files_to_lint(task_args)
|
101
|
+
# NOTE: we're abusing Rake's argument handling a bit here. We call the
|
102
|
+
# first argument `files` but it's actually only the first file--we pull
|
103
|
+
# the rest out of the `extras` from the task arguments. This is so we
|
104
|
+
# can specify an arbitrary list of files separated by commas on the
|
105
|
+
# command line or in a custom task definition.
|
106
|
+
explicit_files = Array(task_args[:files]) + Array(task_args.extras)
|
107
|
+
|
108
|
+
explicit_files.any? ? explicit_files : files
|
109
|
+
end
|
110
|
+
|
111
|
+
# Friendly description that shows the full command that will be executed.
|
112
|
+
#
|
113
|
+
# This allows us to change the information displayed by `rake --tasks` based
|
114
|
+
# on the options passed to the constructor which defined the task.
|
115
|
+
#
|
116
|
+
# @return [String]
|
117
|
+
def default_description
|
118
|
+
description = "Run `#{LiquidLint::APP_NAME}"
|
119
|
+
description += " --config #{config}" if config
|
120
|
+
description += " #{files.join(' ')}" if files.any?
|
121
|
+
description += ' [files...]`'
|
122
|
+
description
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint
|
4
|
+
# Contains information about all lints detected during a scan.
|
5
|
+
class Report
|
6
|
+
# List of lints that were found.
|
7
|
+
attr_accessor :lints
|
8
|
+
|
9
|
+
# List of files that were linted.
|
10
|
+
attr_reader :files
|
11
|
+
|
12
|
+
# Creates a report.
|
13
|
+
#
|
14
|
+
# @param lints [Array<LiquidLint::Lint>] lints that were found
|
15
|
+
# @param files [Array<String>] files that were linted
|
16
|
+
def initialize(lints, files)
|
17
|
+
@lints = lints.sort_by { |l| [l.filename, l.line] }
|
18
|
+
@files = files
|
19
|
+
end
|
20
|
+
|
21
|
+
def failed?
|
22
|
+
@lints.any?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rexml/document'
|
4
|
+
|
5
|
+
module LiquidLint
|
6
|
+
# Outputs report as a Checkstyle XML document.
|
7
|
+
class Reporter::CheckstyleReporter < Reporter
|
8
|
+
def display_report(report)
|
9
|
+
document = REXML::Document.new.tap do |d|
|
10
|
+
d << REXML::XMLDecl.new
|
11
|
+
end
|
12
|
+
checkstyle = REXML::Element.new('checkstyle', document)
|
13
|
+
|
14
|
+
report.lints.group_by(&:filename).map do |lint|
|
15
|
+
map_file(lint, checkstyle)
|
16
|
+
end
|
17
|
+
|
18
|
+
log.log document.to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def map_file(file, checkstyle)
|
24
|
+
REXML::Element.new('file', checkstyle).tap do |f|
|
25
|
+
path_name = file.first
|
26
|
+
path_name = relative_path(file) if defined?(relative_path)
|
27
|
+
f.attributes['name'] = path_name
|
28
|
+
|
29
|
+
file.last.map { |o| map_offense(o, f) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def map_offense(offence, parent)
|
34
|
+
REXML::Element.new('error', parent).tap do |e|
|
35
|
+
e.attributes['line'] = offence.line
|
36
|
+
e.attributes['severity'] = offence.error? ? 'error' : 'warning'
|
37
|
+
e.attributes['message'] = offence.message
|
38
|
+
e.attributes['source'] = 'liquid-lint'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint
|
4
|
+
# Outputs lints in a simple format with the filename, line number, and lint
|
5
|
+
# message.
|
6
|
+
class Reporter::DefaultReporter < Reporter
|
7
|
+
def display_report(report)
|
8
|
+
sorted_lints = report.lints.sort_by { |l| [l.filename, l.line] }
|
9
|
+
|
10
|
+
sorted_lints.each do |lint|
|
11
|
+
print_location(lint)
|
12
|
+
print_type(lint)
|
13
|
+
print_message(lint)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def print_location(lint)
|
20
|
+
log.info lint.filename, false
|
21
|
+
log.log ':', false
|
22
|
+
log.bold lint.line, false
|
23
|
+
end
|
24
|
+
|
25
|
+
def print_type(lint)
|
26
|
+
if lint.error?
|
27
|
+
log.error ' [E] ', false
|
28
|
+
else
|
29
|
+
log.warning ' [W] ', false
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def print_message(lint)
|
34
|
+
if lint.linter
|
35
|
+
log.success("#{lint.linter.name}: ", false)
|
36
|
+
end
|
37
|
+
|
38
|
+
log.log lint.message
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint
|
4
|
+
# Outputs lints in format: {filename}:{line}:{column}: {kind}: {message}.
|
5
|
+
class Reporter::EmacsReporter < Reporter
|
6
|
+
def display_report(report)
|
7
|
+
sorted_lints = report.lints.sort_by { |l| [l.filename, l.line] }
|
8
|
+
|
9
|
+
sorted_lints.each do |lint|
|
10
|
+
print_location(lint)
|
11
|
+
print_type(lint)
|
12
|
+
print_message(lint)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def print_location(lint)
|
19
|
+
log.info lint.filename, false
|
20
|
+
log.log ':', false
|
21
|
+
log.bold lint.line, false
|
22
|
+
log.log ':', false
|
23
|
+
# TODO: change 1 to column number when linter will have this info.
|
24
|
+
log.bold 1, false
|
25
|
+
log.log ':', false
|
26
|
+
end
|
27
|
+
|
28
|
+
def print_type(lint)
|
29
|
+
if lint.error?
|
30
|
+
log.error ' E: ', false
|
31
|
+
else
|
32
|
+
log.warning ' W: ', false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def print_message(lint)
|
37
|
+
if lint.linter
|
38
|
+
log.success("#{lint.linter.name}: ", false)
|
39
|
+
end
|
40
|
+
|
41
|
+
log.log lint.message
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint
|
4
|
+
# Outputs report as a JSON document.
|
5
|
+
class Reporter::JsonReporter < Reporter
|
6
|
+
def display_report(report)
|
7
|
+
lints = report.lints
|
8
|
+
grouped = lints.group_by(&:filename)
|
9
|
+
|
10
|
+
report_hash = {
|
11
|
+
metadata: metadata,
|
12
|
+
files: grouped.map { |l| map_file(l) },
|
13
|
+
summary: {
|
14
|
+
offense_count: lints.length,
|
15
|
+
target_file_count: grouped.length,
|
16
|
+
inspected_file_count: report.files.length,
|
17
|
+
},
|
18
|
+
}
|
19
|
+
|
20
|
+
log.log report_hash.to_json
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def metadata
|
26
|
+
{
|
27
|
+
liquid_lint_version: LiquidLint::VERSION,
|
28
|
+
ruby_engine: RUBY_ENGINE,
|
29
|
+
ruby_patchlevel: RUBY_PATCHLEVEL.to_s,
|
30
|
+
ruby_platform: RUBY_PLATFORM,
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def map_file(file)
|
35
|
+
{
|
36
|
+
path: file.first,
|
37
|
+
offenses: file.last.map { |o| map_offense(o) },
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def map_offense(offense)
|
42
|
+
{
|
43
|
+
severity: offense.severity,
|
44
|
+
message: offense.message,
|
45
|
+
location: {
|
46
|
+
line: offense.line,
|
47
|
+
},
|
48
|
+
linter: offense.linter&.name,
|
49
|
+
}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint
|
4
|
+
# Abstract lint reporter. Subclass and override {#display_report} to
|
5
|
+
# implement a custom lint reporter.
|
6
|
+
#
|
7
|
+
# @abstract
|
8
|
+
class Reporter
|
9
|
+
# Creates the reporter that will display the given report.
|
10
|
+
#
|
11
|
+
# @param logger [LiquidLint::Logger]
|
12
|
+
def initialize(logger)
|
13
|
+
@log = logger
|
14
|
+
end
|
15
|
+
|
16
|
+
# Implemented by subclasses to display lints from a {LiquidLint::Report}.
|
17
|
+
#
|
18
|
+
# @param report [LiquidLint::Report]
|
19
|
+
def display_report(report)
|
20
|
+
raise NotImplementedError,
|
21
|
+
"Implement `display_report` to display #{report}"
|
22
|
+
end
|
23
|
+
|
24
|
+
# Keep tracking all the descendants of this class for the list of available
|
25
|
+
# reporters.
|
26
|
+
#
|
27
|
+
# @return [Array<Class>]
|
28
|
+
def self.descendants
|
29
|
+
@descendants ||= []
|
30
|
+
end
|
31
|
+
|
32
|
+
# Executed when this class is subclassed.
|
33
|
+
#
|
34
|
+
# @param descendant [Class]
|
35
|
+
def self.inherited(descendant)
|
36
|
+
descendants << descendant
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# @return [LiquidLint::Logger] logger to send output to
|
42
|
+
attr_reader :log
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint
|
4
|
+
# Generates a {LiquidLint::Sexp} suitable for consumption by the
|
5
|
+
# {RubyExtractor}.
|
6
|
+
#
|
7
|
+
# This is mostly copied from Liquid::Engine, with some filters and generators
|
8
|
+
# omitted.
|
9
|
+
class RubyExtractEngine < Temple::Engine
|
10
|
+
filter :Encoding
|
11
|
+
filter :RemoveBOM
|
12
|
+
|
13
|
+
# Parse into S-expression using Liquid parser
|
14
|
+
use Liquid::Parser
|
15
|
+
|
16
|
+
# Perform additional processing so extracting Ruby code in {RubyExtractor}
|
17
|
+
# is easier. We don't do this for regular linters because some information
|
18
|
+
# about the original syntax tree is lost in the process, but that doesn't
|
19
|
+
# matter in this case.
|
20
|
+
use Liquid::Embedded
|
21
|
+
use Liquid::Interpolation
|
22
|
+
use LiquidLint::Filters::SplatProcessor
|
23
|
+
use Liquid::DoInserter
|
24
|
+
use Liquid::EndInserter
|
25
|
+
use LiquidLint::Filters::ControlProcessor
|
26
|
+
use LiquidLint::Filters::AttributeProcessor
|
27
|
+
filter :MultiFlattener
|
28
|
+
filter :StaticMerger
|
29
|
+
|
30
|
+
# Converts Array-based S-expressions into LiquidLint::Sexp objects, and gives
|
31
|
+
# them line numbers so we can easily map from the Ruby source to the
|
32
|
+
# original source
|
33
|
+
use LiquidLint::Filters::SexpConverter
|
34
|
+
use LiquidLint::Filters::InjectLineNumbers
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint
|
4
|
+
# Utility class for extracting Ruby script from a Liquid template that can then
|
5
|
+
# be linted with a Ruby linter (i.e. is "legal" Ruby).
|
6
|
+
#
|
7
|
+
# The goal is to turn this:
|
8
|
+
#
|
9
|
+
# - if items.any?
|
10
|
+
# table#items
|
11
|
+
# - for item in items
|
12
|
+
# tr
|
13
|
+
# td.name = item.name
|
14
|
+
# td.price = item.price
|
15
|
+
# - else
|
16
|
+
# p No items found.
|
17
|
+
#
|
18
|
+
# into (something like) this:
|
19
|
+
#
|
20
|
+
# if items.any?
|
21
|
+
# for item in items
|
22
|
+
# puts item.name
|
23
|
+
# puts item.price
|
24
|
+
# else
|
25
|
+
# puts 'No items found'
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# The translation won't be perfect, and won't make any real sense, but the
|
29
|
+
# relationship between variable declarations/uses and the flow control graph
|
30
|
+
# will remain intact.
|
31
|
+
class RubyExtractor
|
32
|
+
include SexpVisitor
|
33
|
+
extend SexpVisitor::DSL
|
34
|
+
|
35
|
+
# Stores the extracted source and a map of lines of generated source to the
|
36
|
+
# original source that created them.
|
37
|
+
#
|
38
|
+
# @attr_reader source [String] generated source code
|
39
|
+
# @attr_reader source_map [Hash] map of line numbers from generated source
|
40
|
+
# to original source line number
|
41
|
+
RubySource = Struct.new(:source, :source_map)
|
42
|
+
|
43
|
+
# Extracts Ruby code from Sexp representing a Liquid document.
|
44
|
+
#
|
45
|
+
# @param sexp [LiquidLint::Sexp]
|
46
|
+
# @return [LiquidLint::RubyExtractor::RubySource]
|
47
|
+
def extract(sexp)
|
48
|
+
trigger_pattern_callbacks(sexp)
|
49
|
+
RubySource.new(@source_lines.join("\n"), @source_map)
|
50
|
+
end
|
51
|
+
|
52
|
+
on_start do |_sexp|
|
53
|
+
@source_lines = []
|
54
|
+
@source_map = {}
|
55
|
+
@line_count = 0
|
56
|
+
@dummy_puts_count = 0
|
57
|
+
end
|
58
|
+
|
59
|
+
on [:html, :doctype] do |sexp|
|
60
|
+
append_dummy_puts(sexp)
|
61
|
+
end
|
62
|
+
|
63
|
+
on [:html, :tag] do |sexp|
|
64
|
+
append_dummy_puts(sexp)
|
65
|
+
end
|
66
|
+
|
67
|
+
on [:static] do |sexp|
|
68
|
+
append_dummy_puts(sexp)
|
69
|
+
end
|
70
|
+
|
71
|
+
on [:dynamic] do |sexp|
|
72
|
+
_, ruby = sexp
|
73
|
+
append(ruby, sexp)
|
74
|
+
end
|
75
|
+
|
76
|
+
on [:code] do |sexp|
|
77
|
+
_, ruby = sexp
|
78
|
+
append(ruby, sexp)
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
# Append code to the buffer.
|
84
|
+
#
|
85
|
+
# @param code [String]
|
86
|
+
# @param sexp [LiquidLint::Sexp]
|
87
|
+
def append(code, sexp)
|
88
|
+
return if code.empty?
|
89
|
+
|
90
|
+
original_line = sexp.line
|
91
|
+
|
92
|
+
# For code that spans multiple lines, the resulting code will span
|
93
|
+
# multiple lines, so we need to create a mapping for each line.
|
94
|
+
code.split("\n").each_with_index do |line, index|
|
95
|
+
@source_lines << line
|
96
|
+
@line_count += 1
|
97
|
+
@source_map[@line_count] = original_line + index
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def append_dummy_puts(sexp)
|
102
|
+
append("_liquid_lint_puts_#{@dummy_puts_count}", sexp)
|
103
|
+
@dummy_puts_count += 1
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rubocop'
|
4
|
+
require 'rubocop/ast/builder'
|
5
|
+
|
6
|
+
def require_parser(path)
|
7
|
+
prev = $VERBOSE
|
8
|
+
$VERBOSE = nil
|
9
|
+
require "parser/#{path}"
|
10
|
+
ensure
|
11
|
+
$VERBOSE = prev
|
12
|
+
end
|
13
|
+
|
14
|
+
module LiquidLint
|
15
|
+
# Parser for the Ruby language.
|
16
|
+
#
|
17
|
+
# This provides a convenient wrapper around the `parser` gem and the
|
18
|
+
# `astrolabe` integration to go with it. It is intended to be used for linter
|
19
|
+
# checks that require deep inspection of Ruby code.
|
20
|
+
class RubyParser
|
21
|
+
# Creates a reusable parser.
|
22
|
+
def initialize
|
23
|
+
require_parser('current')
|
24
|
+
@builder = ::RuboCop::AST::Builder.new
|
25
|
+
@parser = ::Parser::CurrentRuby.new(@builder)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Parse the given Ruby source into an abstract syntax tree.
|
29
|
+
#
|
30
|
+
# @param source [String] Ruby source code
|
31
|
+
# @return [Array] syntax tree in the form returned by Parser gem
|
32
|
+
def parse(source)
|
33
|
+
buffer = ::Parser::Source::Buffer.new('(string)')
|
34
|
+
buffer.source = source
|
35
|
+
|
36
|
+
@parser.reset
|
37
|
+
@parser.parse(buffer)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint
|
4
|
+
# Responsible for running the applicable linters against the desired files.
|
5
|
+
class Runner
|
6
|
+
# Runs the appropriate linters against the desired files given the specified
|
7
|
+
# options.
|
8
|
+
#
|
9
|
+
# @param [Hash] options
|
10
|
+
# @option options :config_file [String] path of configuration file to load
|
11
|
+
# @option options :config [LiquidLint::Configuration] configuration to use
|
12
|
+
# @option options :excluded_files [Array<String>]
|
13
|
+
# @option options :included_linters [Array<String>]
|
14
|
+
# @option options :excluded_linters [Array<String>]
|
15
|
+
# @return [LiquidLint::Report] a summary of all lints found
|
16
|
+
def run(options = {})
|
17
|
+
config = load_applicable_config(options)
|
18
|
+
linter_selector = LiquidLint::LinterSelector.new(config, options)
|
19
|
+
|
20
|
+
if options[:stdin_file_path].nil?
|
21
|
+
files = extract_applicable_files(config, options)
|
22
|
+
lints = files.map do |file|
|
23
|
+
collect_lints(File.read(file), file, linter_selector, config)
|
24
|
+
end.flatten
|
25
|
+
else
|
26
|
+
files = [options[:stdin_file_path]]
|
27
|
+
lints = collect_lints($stdin.read, options[:stdin_file_path], linter_selector, config)
|
28
|
+
end
|
29
|
+
|
30
|
+
LiquidLint::Report.new(lints, files)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# Returns the {LiquidLint::Configuration} that should be used given the
|
36
|
+
# specified options.
|
37
|
+
#
|
38
|
+
# @param options [Hash]
|
39
|
+
# @return [LiquidLint::Configuration]
|
40
|
+
def load_applicable_config(options)
|
41
|
+
if options[:config_file]
|
42
|
+
LiquidLint::ConfigurationLoader.load_file(options[:config_file])
|
43
|
+
elsif options[:config]
|
44
|
+
options[:config]
|
45
|
+
else
|
46
|
+
LiquidLint::ConfigurationLoader.load_applicable_config
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Runs all provided linters using the specified config against the given
|
51
|
+
# file.
|
52
|
+
#
|
53
|
+
# @param file [String] path to file to lint
|
54
|
+
# @param linter_selector [LiquidLint::LinterSelector]
|
55
|
+
# @param config [LiquidLint::Configuration]
|
56
|
+
def collect_lints(file_content, file_name, linter_selector, config)
|
57
|
+
begin
|
58
|
+
document = LiquidLint::Document.new(file_content, file: file_name, config: config)
|
59
|
+
rescue LiquidLint::Exceptions::ParseError => e
|
60
|
+
return [LiquidLint::Lint.new(nil, file_name, e.lineno, e.error, :error)]
|
61
|
+
end
|
62
|
+
|
63
|
+
linter_selector.linters_for_file(file_name).map do |linter|
|
64
|
+
linter.run(document)
|
65
|
+
end.flatten
|
66
|
+
end
|
67
|
+
|
68
|
+
# Returns the list of files that should be linted given the specified
|
69
|
+
# configuration and options.
|
70
|
+
#
|
71
|
+
# @param config [LiquidLint::Configuration]
|
72
|
+
# @param options [Hash]
|
73
|
+
# @return [Array<String>]
|
74
|
+
def extract_applicable_files(config, options)
|
75
|
+
included_patterns = options[:files]
|
76
|
+
excluded_patterns = config['exclude']
|
77
|
+
excluded_patterns += options.fetch(:excluded_files, [])
|
78
|
+
|
79
|
+
LiquidLint::FileFinder.new(config).find(included_patterns, excluded_patterns)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|