theme-check 1.0.0 → 1.4.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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/theme-check.yml +2 -6
  3. data/CHANGELOG.md +50 -0
  4. data/CONTRIBUTING.md +1 -1
  5. data/README.md +39 -0
  6. data/RELEASING.md +34 -2
  7. data/bin/theme-check +29 -0
  8. data/bin/theme-check-language-server +29 -0
  9. data/config/default.yml +28 -1
  10. data/config/nothing.yml +11 -0
  11. data/config/theme_app_extension.yml +168 -0
  12. data/data/shopify_liquid/objects.yml +1 -0
  13. data/docs/checks/app_block_valid_tags.md +40 -0
  14. data/docs/checks/asset_size_app_block_css.md +52 -0
  15. data/docs/checks/asset_size_app_block_javascript.md +57 -0
  16. data/docs/checks/deprecate_lazysizes.md +0 -3
  17. data/docs/checks/missing_template.md +25 -0
  18. data/docs/checks/pagination_size.md +44 -0
  19. data/docs/checks/template_length.md +1 -1
  20. data/docs/checks/undefined_object.md +5 -0
  21. data/lib/theme_check/analyzer.rb +26 -21
  22. data/lib/theme_check/asset_file.rb +3 -15
  23. data/lib/theme_check/bug.rb +3 -1
  24. data/lib/theme_check/check.rb +26 -4
  25. data/lib/theme_check/checks/app_block_valid_tags.rb +36 -0
  26. data/lib/theme_check/checks/asset_size_app_block_css.rb +44 -0
  27. data/lib/theme_check/checks/asset_size_app_block_javascript.rb +44 -0
  28. data/lib/theme_check/checks/asset_size_css.rb +3 -3
  29. data/lib/theme_check/checks/asset_size_javascript.rb +2 -2
  30. data/lib/theme_check/checks/convert_include_to_render.rb +3 -1
  31. data/lib/theme_check/checks/default_locale.rb +3 -1
  32. data/lib/theme_check/checks/deprecate_bgsizes.rb +1 -1
  33. data/lib/theme_check/checks/deprecate_lazysizes.rb +7 -4
  34. data/lib/theme_check/checks/img_lazy_loading.rb +1 -1
  35. data/lib/theme_check/checks/img_width_and_height.rb +3 -3
  36. data/lib/theme_check/checks/missing_template.rb +21 -5
  37. data/lib/theme_check/checks/pagination_size.rb +65 -0
  38. data/lib/theme_check/checks/parser_blocking_javascript.rb +1 -1
  39. data/lib/theme_check/checks/remote_asset.rb +3 -3
  40. data/lib/theme_check/checks/space_inside_braces.rb +27 -7
  41. data/lib/theme_check/checks/template_length.rb +1 -1
  42. data/lib/theme_check/checks/undefined_object.rb +1 -1
  43. data/lib/theme_check/checks/valid_html_translation.rb +1 -1
  44. data/lib/theme_check/checks.rb +11 -1
  45. data/lib/theme_check/cli.rb +52 -15
  46. data/lib/theme_check/config.rb +56 -10
  47. data/lib/theme_check/corrector.rb +9 -0
  48. data/lib/theme_check/exceptions.rb +29 -27
  49. data/lib/theme_check/file_system_storage.rb +12 -0
  50. data/lib/theme_check/html_check.rb +0 -1
  51. data/lib/theme_check/html_node.rb +37 -16
  52. data/lib/theme_check/html_visitor.rb +17 -3
  53. data/lib/theme_check/json_check.rb +2 -2
  54. data/lib/theme_check/json_file.rb +11 -27
  55. data/lib/theme_check/json_printer.rb +26 -0
  56. data/lib/theme_check/language_server/constants.rb +21 -6
  57. data/lib/theme_check/language_server/document_link_engine.rb +3 -31
  58. data/lib/theme_check/language_server/document_link_provider.rb +70 -0
  59. data/lib/theme_check/language_server/document_link_providers/asset_document_link_provider.rb +11 -0
  60. data/lib/theme_check/language_server/document_link_providers/include_document_link_provider.rb +11 -0
  61. data/lib/theme_check/language_server/document_link_providers/render_document_link_provider.rb +11 -0
  62. data/lib/theme_check/language_server/document_link_providers/section_document_link_provider.rb +11 -0
  63. data/lib/theme_check/language_server/handler.rb +7 -4
  64. data/lib/theme_check/language_server/server.rb +13 -2
  65. data/lib/theme_check/language_server.rb +5 -0
  66. data/lib/theme_check/node.rb +6 -4
  67. data/lib/theme_check/offense.rb +56 -3
  68. data/lib/theme_check/parsing_helpers.rb +4 -3
  69. data/lib/theme_check/position.rb +98 -14
  70. data/lib/theme_check/regex_helpers.rb +5 -2
  71. data/lib/theme_check/tags.rb +26 -9
  72. data/lib/theme_check/template.rb +3 -32
  73. data/lib/theme_check/theme.rb +3 -0
  74. data/lib/theme_check/theme_file.rb +40 -0
  75. data/lib/theme_check/version.rb +1 -1
  76. data/lib/theme_check.rb +16 -0
  77. data/theme-check.gemspec +1 -1
  78. metadata +24 -6
  79. data/bin/liquid-server +0 -4
@@ -5,7 +5,7 @@ module ThemeCheck
5
5
  category :liquid
6
6
  doc docs_url(__FILE__)
7
7
 
8
- def initialize(max_length: 500, exclude_schema: true, exclude_stylesheet: true, exclude_javascript: true)
8
+ def initialize(max_length: 600, exclude_schema: true, exclude_stylesheet: true, exclude_javascript: true)
9
9
  @max_length = max_length
10
10
  @exclude_schema = exclude_schema
11
11
  @exclude_stylesheet = exclude_stylesheet
@@ -55,7 +55,7 @@ module ThemeCheck
55
55
  end
56
56
  end
57
57
 
58
- def initialize(exclude_snippets: false)
58
+ def initialize(exclude_snippets: true)
59
59
  @exclude_snippets = exclude_snippets
60
60
  @files = {}
61
61
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'nokogumbo'
3
+ require 'nokogiri'
4
4
 
5
5
  module ThemeCheck
6
6
  class ValidHTMLTranslation < JsonCheck
@@ -29,8 +29,18 @@ module ThemeCheck
29
29
  def call_check_method(check, method, *args)
30
30
  return unless check.respond_to?(method) && !check.ignored?
31
31
 
32
- Timeout.timeout(CHECK_METHOD_TIMEOUT) do
32
+ # If you want to use binding.pry in unit tests, define the
33
+ # THEME_CHECK_DEBUG environment variable. e.g.
34
+ #
35
+ # $ export THEME_CHECK_DEBUG=true
36
+ # $ bundle exec rake tests:in_memory
37
+ #
38
+ if ENV['THEME_CHECK_DEBUG']
33
39
  check.send(method, *args)
40
+ else
41
+ Timeout.timeout(CHECK_METHOD_TIMEOUT) do
42
+ check.send(method, *args)
43
+ end
34
44
  end
35
45
  rescue Liquid::Error
36
46
  # Pass-through Liquid errors
@@ -5,15 +5,19 @@ module ThemeCheck
5
5
  class Cli
6
6
  class Abort < StandardError; end
7
7
 
8
+ FORMATS = [:text, :json]
9
+
8
10
  attr_accessor :path
9
11
 
10
12
  def initialize
11
13
  @path = "."
12
14
  @command = :check
13
- @only_categories = []
15
+ @include_categories = []
14
16
  @exclude_categories = []
15
17
  @auto_correct = false
16
18
  @config_path = nil
19
+ @fail_level = :error
20
+ @format = :text
17
21
  end
18
22
 
19
23
  def option_parser(parser = OptionParser.new, help: true)
@@ -25,20 +29,31 @@ module ThemeCheck
25
29
  @option_parser.separator("Basic Options:")
26
30
  @option_parser.on(
27
31
  "-C", "--config PATH",
28
- "Use the config provided, overriding .theme-check.yml if present"
32
+ "Use the config provided, overriding .theme-check.yml if present",
33
+ "Use :theme_app_extension to use default checks for theme app extensions"
29
34
  ) { |path| @config_path = path }
30
35
  @option_parser.on(
31
- "-c", "--category CATEGORY",
32
- "Only run this category of checks"
33
- ) { |category| @only_categories << category.to_sym }
36
+ "-o", "--output FORMAT", FORMATS,
37
+ "The output format to use. (text|json, default: text)"
38
+ ) { |format| @format = format.to_sym }
39
+ @option_parser.on(
40
+ "-c", "--category CATEGORY", Check::CATEGORIES, "Only run this category of checks",
41
+ "Runs checks matching all categories when specified more than once"
42
+ ) { |category| @include_categories << category.to_sym }
34
43
  @option_parser.on(
35
- "-x", "--exclude-category CATEGORY",
36
- "Exclude this category of checks"
44
+ "-x", "--exclude-category CATEGORY", Check::CATEGORIES, "Exclude this category of checks",
45
+ "Excludes checks matching any category when specified more than once"
37
46
  ) { |category| @exclude_categories << category.to_sym }
38
47
  @option_parser.on(
39
48
  "-a", "--auto-correct",
40
49
  "Automatically fix offenses"
41
50
  ) { @auto_correct = true }
51
+ @option_parser.on(
52
+ "--fail-level SEVERITY", Check::SEVERITIES,
53
+ "Minimum severity (error|suggestion|style) for exit with error code"
54
+ ) do |severity|
55
+ @fail_level = severity.to_sym
56
+ end
42
57
 
43
58
  @option_parser.separator("")
44
59
  @option_parser.separator("Miscellaneous:")
@@ -77,6 +92,8 @@ module ThemeCheck
77
92
 
78
93
  def parse(argv)
79
94
  @path = option_parser.parse(argv).first || "."
95
+ rescue OptionParser::InvalidArgument => e
96
+ abort(e.message)
80
97
  end
81
98
 
82
99
  def run!
@@ -84,13 +101,13 @@ module ThemeCheck
84
101
  @config = if @config_path
85
102
  ThemeCheck::Config.new(
86
103
  root: @path,
87
- configuration: ThemeCheck::Config.load_file(@config_path)
104
+ configuration: ThemeCheck::Config.load_config(@config_path)
88
105
  )
89
106
  else
90
107
  ThemeCheck::Config.from_path(@path)
91
108
  end
92
- @config.only_categories = @only_categories
93
- @config.exclude_categories = @exclude_categories
109
+ @config.include_categories = @include_categories unless @include_categories.empty?
110
+ @config.exclude_categories = @exclude_categories unless @exclude_categories.empty?
94
111
  @config.auto_correct = @auto_correct
95
112
  end
96
113
 
@@ -99,12 +116,16 @@ module ThemeCheck
99
116
 
100
117
  def run
101
118
  run!
119
+ exit(0)
102
120
  rescue Abort => e
103
121
  if e.message.empty?
104
122
  exit(1)
105
123
  else
106
124
  abort(e.message)
107
125
  end
126
+ rescue ThemeCheckError => e
127
+ STDERR.puts(e.message)
128
+ exit(2)
108
129
  end
109
130
 
110
131
  def self.parse_and_run!(argv)
@@ -130,11 +151,16 @@ module ThemeCheck
130
151
  def init
131
152
  dotfile_path = ThemeCheck::Config.find(@path)
132
153
  if dotfile_path.nil?
133
- File.write(File.join(@path, ThemeCheck::Config::DOTFILE), File.read(ThemeCheck::Config::DEFAULT_CONFIG))
154
+ config_name = if @config_path && @config_path[0] == ":"
155
+ "#{@config_path[1..]}.yml"
156
+ else
157
+ "default.yml"
158
+ end
159
+ File.write(File.join(@path, ThemeCheck::Config::DOTFILE), File.read(ThemeCheck::Config.bundled_config_path(config_name)))
134
160
 
135
161
  puts "Writing new #{ThemeCheck::Config::DOTFILE} to #{@path}"
136
162
  else
137
- raise Abort, "#{ThemeCheck::Config::DOTFILE} already exists at #{@path}."
163
+ raise Abort, "#{ThemeCheck::Config::DOTFILE} already exists at #{@path}"
138
164
  end
139
165
  end
140
166
 
@@ -147,7 +173,7 @@ module ThemeCheck
147
173
  end
148
174
 
149
175
  def check
150
- puts "Checking #{@config.root} ..."
176
+ STDERR.puts "Checking #{@config.root} ..."
151
177
  storage = ThemeCheck::FileSystemStorage.new(@config.root, ignored_patterns: @config.ignored_patterns)
152
178
  theme = ThemeCheck::Theme.new(storage)
153
179
  if theme.all.empty?
@@ -156,8 +182,19 @@ module ThemeCheck
156
182
  analyzer = ThemeCheck::Analyzer.new(theme, @config.enabled_checks, @config.auto_correct)
157
183
  analyzer.analyze_theme
158
184
  analyzer.correct_offenses
159
- ThemeCheck::Printer.new.print(theme, analyzer.offenses, @config.auto_correct)
160
- raise Abort, "" if analyzer.uncorrectable_offenses.any?
185
+ output_with_format(theme, analyzer)
186
+ raise Abort, "" if analyzer.uncorrectable_offenses.any? do |offense|
187
+ offense.check.severity_value <= Check.severity_value(@fail_level)
188
+ end
189
+ end
190
+
191
+ def output_with_format(theme, analyzer)
192
+ case @format
193
+ when :text
194
+ ThemeCheck::Printer.new.print(theme, analyzer.offenses, @config.auto_correct)
195
+ when :json
196
+ ThemeCheck::JsonPrinter.new.print(analyzer.offenses)
197
+ end
161
198
  end
162
199
  end
163
200
  end
@@ -3,11 +3,11 @@
3
3
  module ThemeCheck
4
4
  class Config
5
5
  DOTFILE = '.theme-check.yml'
6
- DEFAULT_CONFIG = "#{__dir__}/../../config/default.yml"
6
+ BUNDLED_CONFIGS_DIR = "#{__dir__}/../../config"
7
7
  BOOLEAN = [true, false]
8
8
 
9
9
  attr_reader :root
10
- attr_accessor :only_categories, :exclude_categories, :auto_correct
10
+ attr_accessor :auto_correct
11
11
 
12
12
  class << self
13
13
  attr_reader :last_loaded_config
@@ -42,18 +42,46 @@ module ThemeCheck
42
42
  YAML.load_file(absolute_path)
43
43
  end
44
44
 
45
+ def bundled_config_path(name)
46
+ "#{BUNDLED_CONFIGS_DIR}/#{name}"
47
+ end
48
+
49
+ def load_bundled_config(name)
50
+ load_file(bundled_config_path(name))
51
+ end
52
+
53
+ def load_config(path)
54
+ if path[0] == ":"
55
+ load_bundled_config("#{path[1..]}.yml")
56
+ elsif path.is_a?(Symbol)
57
+ load_bundled_config("#{path}.yml")
58
+ else
59
+ load_file(path)
60
+ end
61
+ end
62
+
45
63
  def default
46
- @default ||= load_file(DEFAULT_CONFIG)
64
+ @default ||= load_config(":default")
47
65
  end
48
66
  end
49
67
 
50
68
  def initialize(root: nil, configuration: nil, should_resolve_requires: true)
51
69
  @configuration = if configuration
70
+ # TODO: Do we need to handle extends here? What base configuration
71
+ # should we validate against once Theme App Extensions has its own
72
+ # checks? :all?
52
73
  validate_configuration(configuration)
53
74
  else
54
75
  {}
55
76
  end
56
- merge_with_default_configuration!(@configuration)
77
+
78
+ # Follow extends
79
+ extends = @configuration["extends"] || ":default"
80
+ while extends
81
+ extended_configuration = self.class.load_config(extends)
82
+ extends = extended_configuration["extends"]
83
+ @configuration = merge_configurations!(@configuration, extended_configuration)
84
+ end
57
85
 
58
86
  @root = if root && @configuration.key?("root")
59
87
  Pathname.new(root).join(@configuration["root"])
@@ -61,8 +89,6 @@ module ThemeCheck
61
89
  Pathname.new(root)
62
90
  end
63
91
 
64
- @only_categories = []
65
- @exclude_categories = []
66
92
  @auto_correct = false
67
93
 
68
94
  resolve_requires if @root && should_resolve_requires
@@ -87,16 +113,18 @@ module ThemeCheck
87
113
  check_class = ThemeCheck.const_get(check_name)
88
114
 
89
115
  next if check_class.categories.any? { |category| exclude_categories.include?(category) }
90
- next if only_categories.any? && check_class.categories.none? { |category| only_categories.include?(category) }
116
+ next if include_categories.any? && !include_categories.all? { |category| check_class.categories.include?(category) }
91
117
 
92
118
  options_for_check = options.transform_keys(&:to_sym)
93
119
  options_for_check.delete(:enabled)
120
+ severity = options_for_check.delete(:severity)
94
121
  ignored_patterns = options_for_check.delete(:ignore) || []
95
122
  check = if options_for_check.empty?
96
123
  check_class.new
97
124
  else
98
125
  check_class.new(**options_for_check)
99
126
  end
127
+ check.severity = severity.to_sym if severity
100
128
  check.ignored_patterns = ignored_patterns
101
129
  check.options = options_for_check
102
130
  check
@@ -107,6 +135,22 @@ module ThemeCheck
107
135
  self["ignore"] || []
108
136
  end
109
137
 
138
+ def include_categories
139
+ self["include_categories"] || []
140
+ end
141
+
142
+ def include_categories=(categories)
143
+ @configuration["include_categories"] = categories
144
+ end
145
+
146
+ def exclude_categories
147
+ self["exclude_categories"] || []
148
+ end
149
+
150
+ def exclude_categories=(categories)
151
+ @configuration["exclude_categories"] = categories
152
+ end
153
+
110
154
  private
111
155
 
112
156
  def check_name?(name)
@@ -133,6 +177,8 @@ module ThemeCheck
133
177
  else
134
178
  warn("bad configuration type for #{name}: expected a Hash, got #{value.inspect}")
135
179
  end
180
+ elsif key == "severity"
181
+ valid_configuration[key] = value
136
182
  elsif default.nil?
137
183
  warn("unknown configuration: #{name}")
138
184
  elsif BOOLEAN.include?(default) && !BOOLEAN.include?(value)
@@ -147,13 +193,13 @@ module ThemeCheck
147
193
  valid_configuration
148
194
  end
149
195
 
150
- def merge_with_default_configuration!(configuration, default_configuration = self.class.default)
151
- default_configuration.each do |key, default|
196
+ def merge_configurations!(configuration, extended_configuration)
197
+ extended_configuration.each do |key, default|
152
198
  value = configuration[key]
153
199
 
154
200
  case value
155
201
  when Hash
156
- merge_with_default_configuration!(value, default)
202
+ merge_configurations!(value, default)
157
203
  when nil
158
204
  configuration[key] = default
159
205
  end
@@ -27,5 +27,14 @@ module ThemeCheck
27
27
  line.insert(node.range[0], insert_before)
28
28
  line.insert(node.range[1] + 1 + insert_before.length, insert_after)
29
29
  end
30
+
31
+ def create(theme, relative_path, content)
32
+ theme.storage.write(relative_path, content)
33
+ end
34
+
35
+ def create_default_locale_json(theme)
36
+ theme.default_locale_json = JsonFile.new("locales/#{theme.default_locale}.default.json", theme.storage)
37
+ theme.default_locale_json.update_contents('{}')
38
+ end
30
39
  end
31
40
  end
@@ -1,32 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
  require "net/http"
3
3
 
4
- TIMEOUT_EXCEPTIONS = [
5
- Net::ReadTimeout,
6
- Net::OpenTimeout,
7
- Net::WriteTimeout,
8
- Errno::ETIMEDOUT,
9
- Timeout::Error,
10
- ]
4
+ module ThemeCheck
5
+ TIMEOUT_EXCEPTIONS = [
6
+ Net::ReadTimeout,
7
+ Net::OpenTimeout,
8
+ Net::WriteTimeout,
9
+ Errno::ETIMEDOUT,
10
+ Timeout::Error,
11
+ ]
11
12
 
12
- CONNECTION_EXCEPTIONS = [
13
- IOError,
14
- EOFError,
15
- SocketError,
16
- Errno::EINVAL,
17
- Errno::ECONNRESET,
18
- Errno::ECONNABORTED,
19
- Errno::EPIPE,
20
- Errno::ECONNREFUSED,
21
- Errno::EAGAIN,
22
- Errno::EHOSTUNREACH,
23
- Errno::ENETUNREACH,
24
- ]
13
+ CONNECTION_EXCEPTIONS = [
14
+ IOError,
15
+ EOFError,
16
+ SocketError,
17
+ Errno::EINVAL,
18
+ Errno::ECONNRESET,
19
+ Errno::ECONNABORTED,
20
+ Errno::EPIPE,
21
+ Errno::ECONNREFUSED,
22
+ Errno::EAGAIN,
23
+ Errno::EHOSTUNREACH,
24
+ Errno::ENETUNREACH,
25
+ ]
25
26
 
26
- NET_HTTP_EXCEPTIONS = [
27
- Net::HTTPBadResponse,
28
- Net::HTTPHeaderSyntaxError,
29
- Net::ProtocolError,
30
- *TIMEOUT_EXCEPTIONS,
31
- *CONNECTION_EXCEPTIONS,
32
- ]
27
+ NET_HTTP_EXCEPTIONS = [
28
+ Net::HTTPBadResponse,
29
+ Net::HTTPHeaderSyntaxError,
30
+ Net::ProtocolError,
31
+ *TIMEOUT_EXCEPTIONS,
32
+ *CONNECTION_EXCEPTIONS,
33
+ ]
34
+ end
@@ -20,6 +20,9 @@ module ThemeCheck
20
20
  end
21
21
 
22
22
  def write(relative_path, content)
23
+ reset_memoizers unless file_exists?(relative_path)
24
+
25
+ file(relative_path).dirname.mkpath unless file(relative_path).dirname.directory?
23
26
  file(relative_path).write(content)
24
27
  end
25
28
 
@@ -36,6 +39,15 @@ module ThemeCheck
36
39
 
37
40
  private
38
41
 
42
+ def file_exists?(relative_path)
43
+ !!@files[relative_path]
44
+ end
45
+
46
+ def reset_memoizers
47
+ @file_array = nil
48
+ @directories = nil
49
+ end
50
+
39
51
  def glob(pattern)
40
52
  @root.glob(pattern).reject do |path|
41
53
  relative_path = path.relative_path_from(@root)
@@ -3,7 +3,6 @@
3
3
  module ThemeCheck
4
4
  class HtmlCheck < Check
5
5
  extend ChecksTracking
6
- VARIABLE = /#{Liquid::VariableStart}.*?#{Liquid::VariableEnd}/om
7
6
  START_OR_END_QUOTE = /(^['"])|(['"]$)/
8
7
  end
9
8
  end