theme-check 1.0.0 → 1.4.0

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