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