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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: cc0be0f5e41c033324636c9048e17b98c742f85856ca55880aebf2b7fff1268a
|
4
|
+
data.tar.gz: 0b3daf13f2a25aa8acad78f8c44faeaf401045f60f71edd6f8f1da9b6e5dcc55
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 21f74da0cfeed7899fe99fc8021152f46cd792293db7b2ffcad398531f681380a7517297191d3b6e801953715d5acb55fc3ec5c55fd3a6bb8588afbc14483bc9
|
7
|
+
data.tar.gz: 7029b2c2dd0cce358776e3b87c537a662ef729b492396d90aa97a415e65fe6ff203cbccacd640f2beeaee3b62ba9ec251f2a76c1d508c8b1031879a6d72263c0
|
data/LICENSE.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
|
data/bin/liquid-lint
ADDED
data/config/default.yml
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
# Default application configuration that all configurations inherit from.
|
2
|
+
#
|
3
|
+
# This is an opinionated list of which hooks are valuable to run and what their
|
4
|
+
# out of the box settings should be.
|
5
|
+
|
6
|
+
# Whether to ignore frontmatter at the beginning of Liquid documents for
|
7
|
+
# frameworks such as Jekyll/Middleman
|
8
|
+
skip_frontmatter: false
|
9
|
+
|
10
|
+
linters:
|
11
|
+
CommentControlStatement:
|
12
|
+
enabled: true
|
13
|
+
|
14
|
+
ConsecutiveControlStatements:
|
15
|
+
enabled: true
|
16
|
+
max_consecutive: 2
|
17
|
+
|
18
|
+
ControlStatementSpacing:
|
19
|
+
enabled: true
|
20
|
+
|
21
|
+
EmbeddedEngines:
|
22
|
+
enabled: false
|
23
|
+
forbidden_engines: []
|
24
|
+
|
25
|
+
EmptyControlStatement:
|
26
|
+
enabled: true
|
27
|
+
|
28
|
+
EmptyLines:
|
29
|
+
enabled: true
|
30
|
+
|
31
|
+
FileLength:
|
32
|
+
enabled: false
|
33
|
+
max: 300
|
34
|
+
|
35
|
+
LineLength:
|
36
|
+
enabled: true
|
37
|
+
max: 80
|
38
|
+
|
39
|
+
RedundantDiv:
|
40
|
+
enabled: true
|
41
|
+
|
42
|
+
RuboCop:
|
43
|
+
enabled: true
|
44
|
+
# These cops are incredibly noisy since the Ruby we extract from Liquid
|
45
|
+
# templates isn't well-formatted, so we ignore them.
|
46
|
+
# WARNING: If you define this list in your own .liquid-lint.yml file, you'll
|
47
|
+
# be overriding the list defined here.
|
48
|
+
ignored_cops:
|
49
|
+
- Layout/ArgumentAlignment
|
50
|
+
- Layout/ArrayAlignment
|
51
|
+
- Layout/BlockAlignment
|
52
|
+
- Layout/ClosingParenthesisIndentation
|
53
|
+
- Layout/EmptyLineAfterGuardClause
|
54
|
+
- Layout/EndAlignment
|
55
|
+
- Layout/FirstArgumentIndentation
|
56
|
+
- Layout/FirstArrayElementIndentation
|
57
|
+
- Layout/FirstHashElementIndentation
|
58
|
+
- Layout/FirstParameterIndentation
|
59
|
+
- Layout/HashAlignment
|
60
|
+
- Layout/IndentationConsistency
|
61
|
+
- Layout/IndentationWidth
|
62
|
+
- Layout/InitialIndentation
|
63
|
+
- Layout/LineEndStringConcatenationIndentation
|
64
|
+
- Layout/LineLength
|
65
|
+
- Layout/MultilineArrayBraceLayout
|
66
|
+
- Layout/MultilineAssignmentLayout
|
67
|
+
- Layout/MultilineHashBraceLayout
|
68
|
+
- Layout/MultilineMethodCallBraceLayout
|
69
|
+
- Layout/MultilineMethodCallIndentation
|
70
|
+
- Layout/MultilineMethodDefinitionBraceLayout
|
71
|
+
- Layout/MultilineOperationIndentation
|
72
|
+
- Layout/ParameterAlignment
|
73
|
+
- Layout/TrailingEmptyLines
|
74
|
+
- Layout/TrailingWhitespace
|
75
|
+
- Lint/Void
|
76
|
+
- Metrics/BlockLength
|
77
|
+
- Metrics/BlockNesting
|
78
|
+
- Naming/FileName
|
79
|
+
- Style/FrozenStringLiteralComment
|
80
|
+
- Style/IdenticalConditionalBranches
|
81
|
+
- Style/IfUnlessModifier
|
82
|
+
- Style/Next
|
83
|
+
- Style/WhileUntilDo
|
84
|
+
- Style/WhileUntilModifier
|
85
|
+
|
86
|
+
Tab:
|
87
|
+
enabled: true
|
88
|
+
|
89
|
+
TagCase:
|
90
|
+
enabled: true
|
91
|
+
|
92
|
+
TrailingBlankLines:
|
93
|
+
enabled: true
|
94
|
+
|
95
|
+
TrailingWhitespace:
|
96
|
+
enabled: true
|
97
|
+
|
98
|
+
Zwsp:
|
99
|
+
enabled: false
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint
|
4
|
+
# Represents an atomic, childless, literal value within an S-expression.
|
5
|
+
#
|
6
|
+
# This creates a light wrapper around literal values of S-expressions so we
|
7
|
+
# can make an {Atom} quack like a {Sexp} without being an {Sexp}.
|
8
|
+
class Atom
|
9
|
+
# Stores the line number of the code in the original document that this Atom
|
10
|
+
# came from.
|
11
|
+
attr_accessor :line
|
12
|
+
|
13
|
+
# Creates an atom from the specified value.
|
14
|
+
#
|
15
|
+
# @param value [Object]
|
16
|
+
def initialize(value)
|
17
|
+
@value = value
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns whether this atom is equivalent to another object.
|
21
|
+
#
|
22
|
+
# This defines a helper which unwraps the inner value of the atom to compare
|
23
|
+
# against a literal value, saving us having to do it ourselves everywhere
|
24
|
+
# else.
|
25
|
+
#
|
26
|
+
# @param other [Object]
|
27
|
+
# @return [Boolean]
|
28
|
+
def ==(other)
|
29
|
+
@value == (other.is_a?(Atom) ? other.instance_variable_get(:@value) : other)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns whether this atom matches the given Sexp pattern.
|
33
|
+
#
|
34
|
+
# This exists solely to make an {Atom} quack like a {Sexp}, so we don't have
|
35
|
+
# to manually check the type when doing comparisons elsewhere.
|
36
|
+
#
|
37
|
+
# @param [Array, Object]
|
38
|
+
# @return [Boolean]
|
39
|
+
def match?(pattern)
|
40
|
+
# Delegate matching logic if we're comparing against a matcher
|
41
|
+
if pattern.is_a?(LiquidLint::Matcher::Base)
|
42
|
+
return pattern.match?(@value)
|
43
|
+
end
|
44
|
+
|
45
|
+
@value == pattern
|
46
|
+
end
|
47
|
+
|
48
|
+
# Displays the string representation the value this {Atom} wraps.
|
49
|
+
#
|
50
|
+
# @return [String]
|
51
|
+
def to_s
|
52
|
+
@value.to_s
|
53
|
+
end
|
54
|
+
|
55
|
+
# Displays a string representation of this {Atom} suitable for debugging.
|
56
|
+
#
|
57
|
+
# @return [String]
|
58
|
+
def inspect
|
59
|
+
"<#Atom #{@value.inspect}>"
|
60
|
+
end
|
61
|
+
|
62
|
+
# Redirect methods to the value this {Atom} wraps.
|
63
|
+
#
|
64
|
+
# Again, this is for convenience so we don't need to manually unwrap the
|
65
|
+
# value ourselves. It's pretty magical, but results in much DRYer code.
|
66
|
+
#
|
67
|
+
# @param method_sym [Symbol] method that was called
|
68
|
+
# @param args [Array]
|
69
|
+
# @yield block that was passed to the method
|
70
|
+
def method_missing(method_sym, *args, &block)
|
71
|
+
if @value.respond_to?(method_sym)
|
72
|
+
@value.send(method_sym, *args, &block)
|
73
|
+
else
|
74
|
+
super
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# @param method_name [String,Symbol] method name
|
79
|
+
# @param args [Array]
|
80
|
+
def respond_to_missing?(method_name, *args)
|
81
|
+
@value.__send__(:respond_to_missing?, method_name, *args) || super
|
82
|
+
end
|
83
|
+
|
84
|
+
# Return whether this {Atom} or the value it wraps responds to the given
|
85
|
+
# message.
|
86
|
+
#
|
87
|
+
# @param method_sym [Symbol]
|
88
|
+
# @param include_private [Boolean]
|
89
|
+
# @return [Boolean]
|
90
|
+
def respond_to?(method_sym, include_private = false)
|
91
|
+
if super
|
92
|
+
true
|
93
|
+
else
|
94
|
+
@value.respond_to?(method_sym, include_private)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint
|
4
|
+
# Holds the list of captures, providing a convenient interface for accessing
|
5
|
+
# the values and unwrapping them on your behalf.
|
6
|
+
class CaptureMap < Hash
|
7
|
+
# Returns the captured value with the specified name.
|
8
|
+
#
|
9
|
+
# @param capture_name [Symbol]
|
10
|
+
# @return [Object]
|
11
|
+
def [](capture_name)
|
12
|
+
if key?(capture_name)
|
13
|
+
super.value
|
14
|
+
else
|
15
|
+
raise ArgumentError, "Capture #{capture_name.inspect} does not exist!"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'liquid_lint'
|
4
|
+
require 'liquid_lint/options'
|
5
|
+
|
6
|
+
module LiquidLint
|
7
|
+
# Command line application interface.
|
8
|
+
class CLI # rubocop:disable Metrics/ClassLength
|
9
|
+
# Exit codes
|
10
|
+
# @see https://man.openbsd.org/sysexits.3
|
11
|
+
EX_OK = 0
|
12
|
+
EX_USAGE = 64
|
13
|
+
EX_DATAERR = 65
|
14
|
+
EX_NOINPUT = 67
|
15
|
+
EX_SOFTWARE = 70
|
16
|
+
EX_CONFIG = 78
|
17
|
+
|
18
|
+
# Create a CLI that outputs to the specified logger.
|
19
|
+
#
|
20
|
+
# @param logger [LiquidLint::Logger]
|
21
|
+
def initialize(logger)
|
22
|
+
@log = logger
|
23
|
+
end
|
24
|
+
|
25
|
+
# Parses the given command-line arguments and executes appropriate logic
|
26
|
+
# based on those arguments.
|
27
|
+
#
|
28
|
+
# @param args [Array<String>] command line arguments
|
29
|
+
# @return [Integer] exit status code
|
30
|
+
def run(args)
|
31
|
+
options = LiquidLint::Options.new.parse(args)
|
32
|
+
act_on_options(options)
|
33
|
+
rescue StandardError => e
|
34
|
+
handle_exception(e)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
attr_reader :log
|
40
|
+
|
41
|
+
# Given the provided options, execute the appropriate command.
|
42
|
+
#
|
43
|
+
# @return [Integer] exit status code
|
44
|
+
def act_on_options(options)
|
45
|
+
log.color_enabled = options.fetch(:color, log.tty?)
|
46
|
+
|
47
|
+
if options[:help]
|
48
|
+
print_help(options)
|
49
|
+
EX_OK
|
50
|
+
elsif options[:version] || options[:verbose_version]
|
51
|
+
print_version(options)
|
52
|
+
EX_OK
|
53
|
+
elsif options[:show_linters]
|
54
|
+
print_available_linters
|
55
|
+
EX_OK
|
56
|
+
elsif options[:show_reporters]
|
57
|
+
print_available_reporters
|
58
|
+
EX_OK
|
59
|
+
else
|
60
|
+
scan_for_lints(options)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Outputs a message and returns an appropriate error code for the specified
|
65
|
+
# exception.
|
66
|
+
def handle_exception(exception)
|
67
|
+
case exception
|
68
|
+
when LiquidLint::Exceptions::ConfigurationError
|
69
|
+
log.error exception.message
|
70
|
+
EX_CONFIG
|
71
|
+
when LiquidLint::Exceptions::InvalidCLIOption
|
72
|
+
log.error exception.message
|
73
|
+
log.log "Run `#{APP_NAME}` --help for usage documentation"
|
74
|
+
EX_USAGE
|
75
|
+
when LiquidLint::Exceptions::InvalidFilePath
|
76
|
+
log.error exception.message
|
77
|
+
EX_NOINPUT
|
78
|
+
when LiquidLint::Exceptions::NoLintersError
|
79
|
+
log.error exception.message
|
80
|
+
EX_NOINPUT
|
81
|
+
else
|
82
|
+
print_unexpected_exception(exception)
|
83
|
+
EX_SOFTWARE
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Scans the files specified by the given options for lints.
|
88
|
+
#
|
89
|
+
# @return [Integer] exit status code
|
90
|
+
def scan_for_lints(options)
|
91
|
+
report = Runner.new.run(options)
|
92
|
+
print_report(report, options)
|
93
|
+
report.failed? ? EX_DATAERR : EX_OK
|
94
|
+
end
|
95
|
+
|
96
|
+
# Outputs a report of the linter run using the specified reporter.
|
97
|
+
def print_report(report, options)
|
98
|
+
reporter = options.fetch(:reporter,
|
99
|
+
LiquidLint::Reporter::DefaultReporter).new(log)
|
100
|
+
reporter.display_report(report)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Outputs a list of all currently available linters.
|
104
|
+
def print_available_linters
|
105
|
+
log.info 'Available linters:'
|
106
|
+
|
107
|
+
linter_names = LiquidLint::LinterRegistry.linters.map do |linter|
|
108
|
+
linter.name.split('::').last
|
109
|
+
end
|
110
|
+
|
111
|
+
linter_names.sort.each do |linter_name|
|
112
|
+
log.log " - #{linter_name}"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Outputs a list of currently available reporters.
|
117
|
+
def print_available_reporters
|
118
|
+
log.info 'Available reporters:'
|
119
|
+
|
120
|
+
reporter_names = LiquidLint::Reporter.descendants.map do |reporter|
|
121
|
+
reporter.name.split('::').last.sub(/Reporter$/, '').downcase
|
122
|
+
end
|
123
|
+
|
124
|
+
reporter_names.sort.each do |reporter_name|
|
125
|
+
log.log " - #{reporter_name}"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Outputs help documentation.
|
130
|
+
def print_help(options)
|
131
|
+
log.log options[:help]
|
132
|
+
end
|
133
|
+
|
134
|
+
# Outputs the application name and version.
|
135
|
+
def print_version(options)
|
136
|
+
log.log "#{LiquidLint::APP_NAME} #{LiquidLint::VERSION}"
|
137
|
+
|
138
|
+
if options[:verbose_version]
|
139
|
+
log.log "liquid #{Gem.loaded_specs['liquid'].version}"
|
140
|
+
log.log "rubocop #{Gem.loaded_specs['rubocop'].version}"
|
141
|
+
log.log RUBY_DESCRIPTION
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Outputs the backtrace of an exception with instructions on how to report
|
146
|
+
# the issue.
|
147
|
+
def print_unexpected_exception(exception) # rubocop:disable Metrics/AbcSize
|
148
|
+
log.bold_error exception.message
|
149
|
+
log.error exception.backtrace.join("\n")
|
150
|
+
log.warning 'Report this bug at ', false
|
151
|
+
log.info LiquidLint::BUG_REPORT_URL
|
152
|
+
log.newline
|
153
|
+
log.success 'To help fix this issue, please include:'
|
154
|
+
log.log '- The above stack trace'
|
155
|
+
log.log '- Liquid-Lint version: ', false
|
156
|
+
log.info LiquidLint::VERSION
|
157
|
+
log.log '- RuboCop version: ', false
|
158
|
+
log.info Gem.loaded_specs['rubocop'].version
|
159
|
+
log.log '- Ruby version: ', false
|
160
|
+
log.info RUBY_VERSION
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint
|
4
|
+
# Stores runtime configuration for the application.
|
5
|
+
#
|
6
|
+
# The purpose of this class is to validate and ensure all configurations
|
7
|
+
# satisfy some basic pre-conditions so other parts of the application don't
|
8
|
+
# have to check the configuration for errors. It should have no knowledge of
|
9
|
+
# how these configuration values are ultimately used.
|
10
|
+
class Configuration
|
11
|
+
# Internal hash storing the configuration.
|
12
|
+
attr_reader :hash
|
13
|
+
|
14
|
+
# Creates a configuration from the given options hash.
|
15
|
+
#
|
16
|
+
# @param options [Hash]
|
17
|
+
def initialize(options)
|
18
|
+
@hash = options
|
19
|
+
validate
|
20
|
+
end
|
21
|
+
|
22
|
+
# Access the configuration as if it were a hash.
|
23
|
+
#
|
24
|
+
# @param key [String]
|
25
|
+
# @return [Array,Hash,Number,String]
|
26
|
+
def [](key)
|
27
|
+
@hash[key]
|
28
|
+
end
|
29
|
+
|
30
|
+
# Compares this configuration with another.
|
31
|
+
#
|
32
|
+
# @param other [LiquidLint::Configuration]
|
33
|
+
# @return [true,false] whether the given configuration is equivalent
|
34
|
+
def ==(other)
|
35
|
+
super || @hash == other.hash
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns a non-modifiable configuration for the specified linter.
|
39
|
+
#
|
40
|
+
# @param linter [LiquidLint::Linter,Class]
|
41
|
+
def for_linter(linter)
|
42
|
+
linter_name =
|
43
|
+
case linter
|
44
|
+
when Class
|
45
|
+
linter.name.split('::').last
|
46
|
+
when LiquidLint::Linter
|
47
|
+
linter.name
|
48
|
+
end
|
49
|
+
|
50
|
+
@hash['linters'].fetch(linter_name, {}).dup.freeze
|
51
|
+
end
|
52
|
+
|
53
|
+
# Merges the given configuration with this one, returning a new
|
54
|
+
# {Configuration}. The provided configuration will either add to or replace
|
55
|
+
# any options defined in this configuration.
|
56
|
+
#
|
57
|
+
# @param config [LiquidLint::Configuration]
|
58
|
+
def merge(config)
|
59
|
+
self.class.new(smart_merge(@hash, config.hash))
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# Merge two hashes such that nested hashes are merged rather than replaced.
|
65
|
+
#
|
66
|
+
# @param parent [Hash]
|
67
|
+
# @param child [Hash]
|
68
|
+
# @return [Hash]
|
69
|
+
def smart_merge(parent, child)
|
70
|
+
parent.merge(child) do |_key, old, new|
|
71
|
+
case old
|
72
|
+
when Hash
|
73
|
+
smart_merge(old, new)
|
74
|
+
else
|
75
|
+
new
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Validates the configuration for any invalid options, normalizing it where
|
81
|
+
# possible.
|
82
|
+
def validate
|
83
|
+
ensure_exclude_option_array_exists
|
84
|
+
ensure_linter_section_exists
|
85
|
+
ensure_linter_include_exclude_arrays_exist
|
86
|
+
end
|
87
|
+
|
88
|
+
# Ensures the `exclude` global option is an array.
|
89
|
+
def ensure_exclude_option_array_exists
|
90
|
+
@hash['exclude'] = Array(@hash['exclude'])
|
91
|
+
end
|
92
|
+
|
93
|
+
# Ensures the `linters` configuration section exists.
|
94
|
+
def ensure_linter_section_exists
|
95
|
+
@hash['linters'] ||= {}
|
96
|
+
end
|
97
|
+
|
98
|
+
# Ensure `include` and `exclude` options for linters are arrays
|
99
|
+
# (since users can specify a single string glob pattern for convenience)
|
100
|
+
def ensure_linter_include_exclude_arrays_exist
|
101
|
+
@hash['linters'].each_key do |linter_name|
|
102
|
+
%w[include exclude].each do |option|
|
103
|
+
linter_config = @hash['linters'][linter_name]
|
104
|
+
linter_config[option] = Array(linter_config[option])
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
module LiquidLint
|
7
|
+
# Manages configuration file loading.
|
8
|
+
class ConfigurationLoader
|
9
|
+
DEFAULT_CONFIG_PATH = File.join(LiquidLint::HOME, 'config', 'default.yml').freeze
|
10
|
+
CONFIG_FILE_NAME = '.liquid-lint.yml'
|
11
|
+
|
12
|
+
class << self
|
13
|
+
# Load configuration file given the current working directory the
|
14
|
+
# application is running within.
|
15
|
+
def load_applicable_config
|
16
|
+
directory = File.expand_path(Dir.pwd)
|
17
|
+
config_file = possible_config_files(directory).find(&:file?)
|
18
|
+
|
19
|
+
if config_file
|
20
|
+
load_file(config_file.to_path)
|
21
|
+
else
|
22
|
+
default_configuration
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Loads the built-in default configuration.
|
27
|
+
def default_configuration
|
28
|
+
@default_configuration ||= load_from_file(DEFAULT_CONFIG_PATH)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Loads a configuration, ensuring it extends the default configuration.
|
32
|
+
#
|
33
|
+
# @param file [String]
|
34
|
+
# @return [LiquidLint::Configuration]
|
35
|
+
def load_file(file)
|
36
|
+
config = load_from_file(file)
|
37
|
+
|
38
|
+
default_configuration.merge(config)
|
39
|
+
rescue Psych::SyntaxError, Errno::ENOENT => e
|
40
|
+
raise LiquidLint::Exceptions::ConfigurationError,
|
41
|
+
"Unable to load configuration from '#{file}': #{e}",
|
42
|
+
e.backtrace
|
43
|
+
end
|
44
|
+
|
45
|
+
# Creates a configuration from the specified hash, ensuring it extends the
|
46
|
+
# default configuration.
|
47
|
+
#
|
48
|
+
# @param hash [Hash]
|
49
|
+
# @return [LiquidLint::Configuration]
|
50
|
+
def load_hash(hash)
|
51
|
+
config = LiquidLint::Configuration.new(hash)
|
52
|
+
|
53
|
+
default_configuration.merge(config)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# Parses and loads a configuration from the given file.
|
59
|
+
#
|
60
|
+
# @param file [String]
|
61
|
+
# @return [LiquidLint::Configuration]
|
62
|
+
def load_from_file(file)
|
63
|
+
hash =
|
64
|
+
if yaml = YAML.load_file(file)
|
65
|
+
yaml.to_hash
|
66
|
+
else
|
67
|
+
{}
|
68
|
+
end
|
69
|
+
|
70
|
+
LiquidLint::Configuration.new(hash)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns a list of possible configuration files given the context of the
|
74
|
+
# specified directory.
|
75
|
+
#
|
76
|
+
# @param directory [String]
|
77
|
+
# @return [Array<Pathname>]
|
78
|
+
def possible_config_files(directory)
|
79
|
+
files = Pathname.new(directory)
|
80
|
+
.enum_for(:ascend)
|
81
|
+
.map { |path| path + CONFIG_FILE_NAME }
|
82
|
+
files << Pathname.new(CONFIG_FILE_NAME)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Global application constants.
|
4
|
+
module LiquidLint
|
5
|
+
HOME = File.expand_path(File.join(File.dirname(__FILE__), '..', '..')).freeze
|
6
|
+
APP_NAME = 'liquid-lint'
|
7
|
+
|
8
|
+
REPO_URL = 'https://github.com/zeusintuivo/liquid-lint'
|
9
|
+
BUG_REPORT_URL = "#{REPO_URL}/issues"
|
10
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LiquidLint
|
4
|
+
# Represents a parsed Liquid document and its associated metadata.
|
5
|
+
class Document
|
6
|
+
# @return [LiquidLint::Configuration] Configuration used to parse template
|
7
|
+
attr_reader :config
|
8
|
+
|
9
|
+
# @return [String] Liquid template file path
|
10
|
+
attr_reader :file
|
11
|
+
|
12
|
+
# @return [LiquidLint::Sexp] Sexpression representing the parsed document
|
13
|
+
attr_reader :sexp
|
14
|
+
|
15
|
+
# @return [String] original source code
|
16
|
+
attr_reader :source
|
17
|
+
|
18
|
+
# @return [Array<String>] original source code as an array of lines
|
19
|
+
attr_reader :source_lines
|
20
|
+
|
21
|
+
# Parses the specified Liquid code into a {Document}.
|
22
|
+
#
|
23
|
+
# @param source [String] Liquid code to parse
|
24
|
+
# @param options [Hash]
|
25
|
+
# @option options :file [String] file name of document that was parsed
|
26
|
+
# @raise [Liquid::Parser::Error] if there was a problem parsing the document
|
27
|
+
def initialize(source, options)
|
28
|
+
@config = options[:config]
|
29
|
+
@file = options.fetch(:file, nil)
|
30
|
+
|
31
|
+
process_source(source)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# @param source [String] Liquid code to parse
|
37
|
+
# @raise [LiquidLint::Exceptions::ParseError] if there was a problem parsing the document
|
38
|
+
def process_source(source)
|
39
|
+
@source = process_encoding(source)
|
40
|
+
@source = strip_frontmatter(source)
|
41
|
+
@source_lines = @source.split("\n")
|
42
|
+
|
43
|
+
engine = LiquidLint::Engine.new(file: @file)
|
44
|
+
@sexp = engine.parse(@source)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Ensure the string's encoding is valid.
|
48
|
+
#
|
49
|
+
# @param source [String]
|
50
|
+
# @return [String] source encoded in a valid encoding
|
51
|
+
def process_encoding(source)
|
52
|
+
::Temple::Filters::Encoding.new.call(source)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Removes YAML frontmatter
|
56
|
+
def strip_frontmatter(source)
|
57
|
+
if config['skip_frontmatter'] &&
|
58
|
+
source =~ /
|
59
|
+
# From the start of the string
|
60
|
+
\A
|
61
|
+
# First-capture match --- followed by optional whitespace up
|
62
|
+
# to a newline then 0 or more chars followed by an optional newline.
|
63
|
+
# This matches the --- and the contents of the frontmatter
|
64
|
+
(---\s*\n.*?\n?)
|
65
|
+
# From the start of the line
|
66
|
+
^
|
67
|
+
# Second capture match --- or ... followed by optional whitespace
|
68
|
+
# and newline. This matches the closing --- for the frontmatter.
|
69
|
+
(---|\.\.\.)\s*$\n?/mx
|
70
|
+
source = ::Regexp.last_match.post_match
|
71
|
+
end
|
72
|
+
|
73
|
+
source
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|