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.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +1 -0
  3. data/bin/liquid-lint +7 -0
  4. data/config/default.yml +99 -0
  5. data/lib/liquid_lint/atom.rb +98 -0
  6. data/lib/liquid_lint/capture_map.rb +19 -0
  7. data/lib/liquid_lint/cli.rb +163 -0
  8. data/lib/liquid_lint/configuration.rb +109 -0
  9. data/lib/liquid_lint/configuration_loader.rb +86 -0
  10. data/lib/liquid_lint/constants.rb +10 -0
  11. data/lib/liquid_lint/document.rb +76 -0
  12. data/lib/liquid_lint/engine.rb +45 -0
  13. data/lib/liquid_lint/exceptions.rb +20 -0
  14. data/lib/liquid_lint/file_finder.rb +88 -0
  15. data/lib/liquid_lint/filters/attribute_processor.rb +31 -0
  16. data/lib/liquid_lint/filters/control_processor.rb +47 -0
  17. data/lib/liquid_lint/filters/inject_line_numbers.rb +43 -0
  18. data/lib/liquid_lint/filters/sexp_converter.rb +17 -0
  19. data/lib/liquid_lint/filters/splat_processor.rb +15 -0
  20. data/lib/liquid_lint/lint.rb +43 -0
  21. data/lib/liquid_lint/linter/comment_control_statement.rb +22 -0
  22. data/lib/liquid_lint/linter/consecutive_control_statements.rb +26 -0
  23. data/lib/liquid_lint/linter/control_statement_spacing.rb +24 -0
  24. data/lib/liquid_lint/linter/embedded_engines.rb +22 -0
  25. data/lib/liquid_lint/linter/empty_control_statement.rb +15 -0
  26. data/lib/liquid_lint/linter/empty_lines.rb +26 -0
  27. data/lib/liquid_lint/linter/file_length.rb +20 -0
  28. data/lib/liquid_lint/linter/line_length.rb +21 -0
  29. data/lib/liquid_lint/linter/redundant_div.rb +22 -0
  30. data/lib/liquid_lint/linter/rubocop.rb +116 -0
  31. data/lib/liquid_lint/linter/tab.rb +19 -0
  32. data/lib/liquid_lint/linter/tag_case.rb +15 -0
  33. data/lib/liquid_lint/linter/trailing_blank_lines.rb +21 -0
  34. data/lib/liquid_lint/linter/trailing_whitespace.rb +19 -0
  35. data/lib/liquid_lint/linter/zwsp.rb +18 -0
  36. data/lib/liquid_lint/linter.rb +93 -0
  37. data/lib/liquid_lint/linter_registry.rb +39 -0
  38. data/lib/liquid_lint/linter_selector.rb +79 -0
  39. data/lib/liquid_lint/logger.rb +103 -0
  40. data/lib/liquid_lint/matcher/anything.rb +11 -0
  41. data/lib/liquid_lint/matcher/base.rb +21 -0
  42. data/lib/liquid_lint/matcher/capture.rb +32 -0
  43. data/lib/liquid_lint/matcher/nothing.rb +13 -0
  44. data/lib/liquid_lint/options.rb +110 -0
  45. data/lib/liquid_lint/rake_task.rb +125 -0
  46. data/lib/liquid_lint/report.rb +25 -0
  47. data/lib/liquid_lint/reporter/checkstyle_reporter.rb +42 -0
  48. data/lib/liquid_lint/reporter/default_reporter.rb +41 -0
  49. data/lib/liquid_lint/reporter/emacs_reporter.rb +44 -0
  50. data/lib/liquid_lint/reporter/json_reporter.rb +52 -0
  51. data/lib/liquid_lint/reporter.rb +44 -0
  52. data/lib/liquid_lint/ruby_extract_engine.rb +36 -0
  53. data/lib/liquid_lint/ruby_extractor.rb +106 -0
  54. data/lib/liquid_lint/ruby_parser.rb +40 -0
  55. data/lib/liquid_lint/runner.rb +82 -0
  56. data/lib/liquid_lint/sexp.rb +106 -0
  57. data/lib/liquid_lint/sexp_visitor.rb +146 -0
  58. data/lib/liquid_lint/utils.rb +85 -0
  59. data/lib/liquid_lint/version.rb +6 -0
  60. data/lib/liquid_lint.rb +52 -0
  61. 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
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'liquid_lint/cli'
5
+
6
+ logger = LiquidLint::Logger.new($stdout)
7
+ exit LiquidLint::CLI.new(logger).run(ARGV)
@@ -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