liquid_lint 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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