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,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
|