theme-check 1.2.0 → 1.3.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 +4 -4
- data/CHANGELOG.md +16 -0
- data/CONTRIBUTING.md +1 -1
- data/bin/theme-check +29 -0
- data/bin/theme-check-language-server +29 -0
- data/config/default.yml +11 -0
- data/config/theme_app_extension.yml +15 -0
- data/docs/checks/app_block_valid_tags.md +40 -0
- data/docs/checks/asset_size_app_block_css.md +1 -1
- data/docs/checks/missing_template.md +25 -0
- data/docs/checks/pagination_size.md +44 -0
- data/docs/checks/undefined_object.md +5 -0
- data/lib/theme_check/check.rb +2 -2
- data/lib/theme_check/checks/app_block_valid_tags.rb +36 -0
- data/lib/theme_check/checks/asset_size_css.rb +3 -3
- data/lib/theme_check/checks/asset_size_javascript.rb +2 -2
- data/lib/theme_check/checks/convert_include_to_render.rb +3 -1
- data/lib/theme_check/checks/deprecate_bgsizes.rb +1 -1
- data/lib/theme_check/checks/deprecate_lazysizes.rb +2 -2
- data/lib/theme_check/checks/img_lazy_loading.rb +1 -1
- data/lib/theme_check/checks/img_width_and_height.rb +3 -3
- data/lib/theme_check/checks/missing_template.rb +21 -5
- data/lib/theme_check/checks/pagination_size.rb +65 -0
- data/lib/theme_check/checks/parser_blocking_javascript.rb +1 -1
- data/lib/theme_check/checks/remote_asset.rb +2 -2
- data/lib/theme_check/checks/space_inside_braces.rb +26 -6
- data/lib/theme_check/checks/undefined_object.rb +1 -1
- data/lib/theme_check/checks/valid_html_translation.rb +1 -1
- data/lib/theme_check/checks.rb +11 -1
- data/lib/theme_check/cli.rb +18 -2
- data/lib/theme_check/corrector.rb +4 -0
- data/lib/theme_check/file_system_storage.rb +12 -0
- data/lib/theme_check/html_check.rb +0 -1
- data/lib/theme_check/html_node.rb +37 -16
- data/lib/theme_check/html_visitor.rb +17 -3
- data/lib/theme_check/json_check.rb +2 -2
- data/lib/theme_check/json_printer.rb +26 -0
- data/lib/theme_check/language_server/handler.rb +6 -2
- data/lib/theme_check/node.rb +6 -4
- data/lib/theme_check/offense.rb +56 -3
- data/lib/theme_check/parsing_helpers.rb +4 -3
- data/lib/theme_check/position.rb +98 -14
- data/lib/theme_check/regex_helpers.rb +5 -2
- data/lib/theme_check/theme.rb +2 -0
- data/lib/theme_check/version.rb +1 -1
- data/lib/theme_check.rb +1 -0
- data/theme-check.gemspec +1 -1
- metadata +12 -10
- data/bin/liquid-server +0 -4
- data/exe/theme-check-language-server.bat +0 -3
- data/exe/theme-check.bat +0 -3
@@ -9,8 +9,8 @@ module ThemeCheck
|
|
9
9
|
ENDS_IN_CSS_UNIT = /(cm|mm|in|px|pt|pc|em|ex|ch|rem|vw|vh|vmin|vmax|%)$/i
|
10
10
|
|
11
11
|
def on_img(node)
|
12
|
-
width = node.attributes["width"]
|
13
|
-
height = node.attributes["height"]
|
12
|
+
width = node.attributes["width"]
|
13
|
+
height = node.attributes["height"]
|
14
14
|
|
15
15
|
record_units_in_field_offenses("width", width, node: node)
|
16
16
|
record_units_in_field_offenses("height", height, node: node)
|
@@ -35,7 +35,7 @@ module ThemeCheck
|
|
35
35
|
return unless value =~ ENDS_IN_CSS_UNIT
|
36
36
|
value_without_units = value.gsub(ENDS_IN_CSS_UNIT, '')
|
37
37
|
add_offense(
|
38
|
-
"The #{attribute} attribute does not take units. Replace with \"#{value_without_units}\"
|
38
|
+
"The #{attribute} attribute does not take units. Replace with \"#{value_without_units}\"",
|
39
39
|
node: node,
|
40
40
|
)
|
41
41
|
end
|
@@ -7,20 +7,36 @@ module ThemeCheck
|
|
7
7
|
doc docs_url(__FILE__)
|
8
8
|
single_file false
|
9
9
|
|
10
|
+
def initialize(ignore_missing: [])
|
11
|
+
@ignore_missing = ignore_missing
|
12
|
+
end
|
13
|
+
|
10
14
|
def on_include(node)
|
11
15
|
template = node.value.template_name_expr
|
12
16
|
if template.is_a?(String)
|
13
|
-
|
14
|
-
add_offense("'snippets/#{template}.liquid' is not found", node: node)
|
15
|
-
end
|
17
|
+
add_missing_offense("snippets/#{template}", node: node)
|
16
18
|
end
|
17
19
|
end
|
20
|
+
|
18
21
|
alias_method :on_render, :on_include
|
19
22
|
|
20
23
|
def on_section(node)
|
21
24
|
template = node.value.section_name
|
22
|
-
|
23
|
-
|
25
|
+
add_missing_offense("sections/#{template}", node: node)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def ignore?(path)
|
31
|
+
@ignore_missing.any? { |pattern| File.fnmatch?(pattern, path) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def add_missing_offense(name, node:)
|
35
|
+
path = "#{name}.liquid"
|
36
|
+
unless ignore?(path) || theme[name]
|
37
|
+
add_offense("'#{path}' is not found", node: node) do |corrector|
|
38
|
+
corrector.create(@theme, "#{name}.liquid", "")
|
39
|
+
end
|
24
40
|
end
|
25
41
|
end
|
26
42
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module ThemeCheck
|
3
|
+
class PaginationSize < LiquidCheck
|
4
|
+
severity :suggestion
|
5
|
+
categories :performance
|
6
|
+
doc docs_url(__FILE__)
|
7
|
+
|
8
|
+
attr_reader :min_size
|
9
|
+
attr_reader :max_size
|
10
|
+
|
11
|
+
def initialize(min_size: 1, max_size: 50)
|
12
|
+
@min_size = min_size
|
13
|
+
@max_size = max_size
|
14
|
+
end
|
15
|
+
|
16
|
+
def on_document(_node)
|
17
|
+
@paginations = {}
|
18
|
+
@schema_settings = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
def on_paginate(node)
|
22
|
+
size = node.value.page_size
|
23
|
+
unless @paginations.key?(size)
|
24
|
+
@paginations[size] = []
|
25
|
+
end
|
26
|
+
@paginations[size].push(node)
|
27
|
+
end
|
28
|
+
|
29
|
+
def on_schema(node)
|
30
|
+
schema = JSON.parse(node.value.nodelist.join)
|
31
|
+
|
32
|
+
if (settings = schema["settings"])
|
33
|
+
@schema_settings = settings
|
34
|
+
end
|
35
|
+
rescue JSON::ParserError
|
36
|
+
# Ignored, handled in ValidSchema.
|
37
|
+
end
|
38
|
+
|
39
|
+
def after_document(_node)
|
40
|
+
@paginations.each_pair do |size, nodes|
|
41
|
+
numerical_size = if size.is_a?(Numeric)
|
42
|
+
size
|
43
|
+
else
|
44
|
+
get_setting_default_value(size.lookups.last)
|
45
|
+
end
|
46
|
+
if numerical_size.nil?
|
47
|
+
nodes.each { |node| add_offense("Default pagination size should be defined in the section settings", node: node) }
|
48
|
+
elsif numerical_size > @max_size || numerical_size < @min_size || !numerical_size.is_a?(Integer)
|
49
|
+
nodes.each { |node| add_offense("Pagination size must be a positive integer between #{@min_size} and #{@max_size}", node: node) }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def get_setting_default_value(setting_id)
|
57
|
+
setting = @schema_settings.select { |s| s['id'] == setting_id }
|
58
|
+
unless setting.empty?
|
59
|
+
return setting.last['default']
|
60
|
+
end
|
61
|
+
# Setting does not exist
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -8,7 +8,7 @@ module ThemeCheck
|
|
8
8
|
|
9
9
|
def on_script(node)
|
10
10
|
return unless node.attributes["src"]
|
11
|
-
return if node.attributes["defer"] || node.attributes["async"] || node.attributes["type"]
|
11
|
+
return if node.attributes["defer"] || node.attributes["async"] || node.attributes["type"] == "module"
|
12
12
|
|
13
13
|
add_offense("Missing async or defer attribute on script tag", node: node)
|
14
14
|
end
|
@@ -14,7 +14,7 @@ module ThemeCheck
|
|
14
14
|
def on_element(node)
|
15
15
|
return unless TAGS.include?(node.name)
|
16
16
|
|
17
|
-
resource_url = node.attributes["src"]
|
17
|
+
resource_url = node.attributes["src"] || node.attributes["href"]
|
18
18
|
return if resource_url.nil? || resource_url.empty?
|
19
19
|
|
20
20
|
# Ignore if URL is Liquid, taken care of by AssetUrlFilters check
|
@@ -25,7 +25,7 @@ module ThemeCheck
|
|
25
25
|
|
26
26
|
# Ignore non-stylesheet rel tags
|
27
27
|
rel = node.attributes["rel"]
|
28
|
-
return if rel && rel
|
28
|
+
return if rel && rel != "stylesheet"
|
29
29
|
|
30
30
|
add_offense(
|
31
31
|
"Asset should be served by the Shopify CDN for better performance.",
|
@@ -14,18 +14,38 @@ module ThemeCheck
|
|
14
14
|
return unless node.markup
|
15
15
|
return if :assign == node.type_name
|
16
16
|
|
17
|
-
outside_of_strings(node.markup) do |chunk|
|
17
|
+
outside_of_strings(node.markup) do |chunk, chunk_start|
|
18
18
|
chunk.scan(/([,:|]|==|<>|<=|>=|<|>|!=) +/) do |_match|
|
19
|
-
add_offense(
|
19
|
+
add_offense(
|
20
|
+
"Too many spaces after '#{Regexp.last_match(1)}'",
|
21
|
+
node: node,
|
22
|
+
markup: Regexp.last_match(0),
|
23
|
+
node_markup_offset: chunk_start + Regexp.last_match.begin(0)
|
24
|
+
)
|
20
25
|
end
|
21
26
|
chunk.scan(/([,:|]|==|<>|<=|>=|<\b|>\b|!=)(\S|\z)/) do |_match|
|
22
|
-
add_offense(
|
27
|
+
add_offense(
|
28
|
+
"Space missing after '#{Regexp.last_match(1)}'",
|
29
|
+
node: node,
|
30
|
+
markup: Regexp.last_match(0),
|
31
|
+
node_markup_offset: chunk_start + Regexp.last_match.begin(0),
|
32
|
+
)
|
23
33
|
end
|
24
34
|
chunk.scan(/ (\||==|<>|<=|>=|<|>|!=)+/) do |_match|
|
25
|
-
add_offense(
|
35
|
+
add_offense(
|
36
|
+
"Too many spaces before '#{Regexp.last_match(1)}'",
|
37
|
+
node: node,
|
38
|
+
markup: Regexp.last_match(0),
|
39
|
+
node_markup_offset: chunk_start + Regexp.last_match.begin(0)
|
40
|
+
)
|
26
41
|
end
|
27
42
|
chunk.scan(/(\A|\S)(?<match>\||==|<>|<=|>=|<|\b>|!=)/) do |_match|
|
28
|
-
add_offense(
|
43
|
+
add_offense(
|
44
|
+
"Space missing before '#{Regexp.last_match(1)}'",
|
45
|
+
node: node,
|
46
|
+
markup: Regexp.last_match(0),
|
47
|
+
node_markup_offset: chunk_start + Regexp.last_match.begin(0)
|
48
|
+
)
|
29
49
|
end
|
30
50
|
end
|
31
51
|
end
|
@@ -57,7 +77,7 @@ module ThemeCheck
|
|
57
77
|
corrector.insert_before(node, " ")
|
58
78
|
end
|
59
79
|
end
|
60
|
-
if node.markup[-1] != " "
|
80
|
+
if node.markup[-1] != " " && node.markup[-1] != "\n"
|
61
81
|
add_offense("Space missing before '}}'", node: node) do |corrector|
|
62
82
|
corrector.insert_after(node, " ")
|
63
83
|
end
|
data/lib/theme_check/checks.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/theme_check/cli.rb
CHANGED
@@ -5,6 +5,8 @@ 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
|
@@ -15,6 +17,7 @@ module ThemeCheck
|
|
15
17
|
@auto_correct = false
|
16
18
|
@config_path = nil
|
17
19
|
@fail_level = :error
|
20
|
+
@format = :text
|
18
21
|
end
|
19
22
|
|
20
23
|
def option_parser(parser = OptionParser.new, help: true)
|
@@ -29,6 +32,10 @@ module ThemeCheck
|
|
29
32
|
"Use the config provided, overriding .theme-check.yml if present",
|
30
33
|
"Use :theme_app_extension to use default checks for theme app extensions"
|
31
34
|
) { |path| @config_path = path }
|
35
|
+
@option_parser.on(
|
36
|
+
"-o", "--output FORMAT", FORMATS,
|
37
|
+
"The output format to use. (text|json, default: text)"
|
38
|
+
) { |format| @format = format.to_sym }
|
32
39
|
@option_parser.on(
|
33
40
|
"-c", "--category CATEGORY", Check::CATEGORIES, "Only run this category of checks",
|
34
41
|
"Runs checks matching all categories when specified more than once"
|
@@ -166,7 +173,7 @@ module ThemeCheck
|
|
166
173
|
end
|
167
174
|
|
168
175
|
def check
|
169
|
-
puts "Checking #{@config.root} ..."
|
176
|
+
STDERR.puts "Checking #{@config.root} ..."
|
170
177
|
storage = ThemeCheck::FileSystemStorage.new(@config.root, ignored_patterns: @config.ignored_patterns)
|
171
178
|
theme = ThemeCheck::Theme.new(storage)
|
172
179
|
if theme.all.empty?
|
@@ -175,10 +182,19 @@ module ThemeCheck
|
|
175
182
|
analyzer = ThemeCheck::Analyzer.new(theme, @config.enabled_checks, @config.auto_correct)
|
176
183
|
analyzer.analyze_theme
|
177
184
|
analyzer.correct_offenses
|
178
|
-
|
185
|
+
output_with_format(theme, analyzer)
|
179
186
|
raise Abort, "" if analyzer.uncorrectable_offenses.any? do |offense|
|
180
187
|
offense.check.severity_value <= Check.severity_value(@fail_level)
|
181
188
|
end
|
182
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
|
198
|
+
end
|
183
199
|
end
|
184
200
|
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)
|
@@ -4,13 +4,14 @@ require "forwardable"
|
|
4
4
|
module ThemeCheck
|
5
5
|
class HtmlNode
|
6
6
|
extend Forwardable
|
7
|
-
|
7
|
+
include RegexHelpers
|
8
|
+
attr_reader :template, :parent
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
def initialize(value, template)
|
10
|
+
def initialize(value, template, placeholder_values = [], parent = nil)
|
12
11
|
@value = value
|
13
12
|
@template = template
|
13
|
+
@placeholder_values = placeholder_values
|
14
|
+
@parent = parent
|
14
15
|
end
|
15
16
|
|
16
17
|
def literal?
|
@@ -22,35 +23,55 @@ module ThemeCheck
|
|
22
23
|
end
|
23
24
|
|
24
25
|
def children
|
25
|
-
@
|
26
|
+
@children ||= @value
|
27
|
+
.children
|
28
|
+
.map { |child| HtmlNode.new(child, template, @placeholder_values, self) }
|
26
29
|
end
|
27
30
|
|
28
|
-
def
|
29
|
-
|
31
|
+
def attributes
|
32
|
+
@attributes ||= @value.attributes
|
33
|
+
.map { |k, v| [replace_placeholders(k), replace_placeholders(v.value)] }
|
34
|
+
.to_h
|
30
35
|
end
|
31
36
|
|
32
|
-
def
|
33
|
-
|
34
|
-
"document"
|
35
|
-
else
|
36
|
-
@value.name
|
37
|
-
end
|
37
|
+
def content
|
38
|
+
@content ||= replace_placeholders(@value.content)
|
38
39
|
end
|
39
40
|
|
41
|
+
# @value is not forwarded because we _need_ to replace the
|
42
|
+
# placeholders for the HtmlNode to make sense.
|
40
43
|
def value
|
41
44
|
if literal?
|
42
|
-
|
45
|
+
content
|
43
46
|
else
|
44
|
-
|
47
|
+
markup
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def name
|
52
|
+
if @value.name == "#document-fragment"
|
53
|
+
"document"
|
54
|
+
else
|
55
|
+
@value.name
|
45
56
|
end
|
46
57
|
end
|
47
58
|
|
48
59
|
def markup
|
49
|
-
@value.to_html
|
60
|
+
@markup ||= replace_placeholders(@value.to_html)
|
50
61
|
end
|
51
62
|
|
52
63
|
def line_number
|
53
64
|
@value.line
|
54
65
|
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def replace_placeholders(string)
|
70
|
+
# Replace all {%#{i}####%} with the actual content.
|
71
|
+
string.gsub(LIQUID_TAG) do |match|
|
72
|
+
key = /\d+/.match(match)[0]
|
73
|
+
@placeholder_values[key.to_i]
|
74
|
+
end
|
75
|
+
end
|
55
76
|
end
|
56
77
|
end
|
@@ -1,18 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
require "
|
2
|
+
require "nokogiri"
|
3
3
|
require "forwardable"
|
4
4
|
|
5
5
|
module ThemeCheck
|
6
6
|
class HtmlVisitor
|
7
|
+
include RegexHelpers
|
7
8
|
attr_reader :checks
|
8
9
|
|
9
10
|
def initialize(checks)
|
10
11
|
@checks = checks
|
12
|
+
@placeholder_values = []
|
11
13
|
end
|
12
14
|
|
13
15
|
def visit_template(template)
|
14
16
|
doc = parse(template)
|
15
|
-
visit(HtmlNode.new(doc, template))
|
17
|
+
visit(HtmlNode.new(doc, template, @placeholder_values))
|
16
18
|
rescue ArgumentError => e
|
17
19
|
call_checks(:on_parse_error, e, template)
|
18
20
|
end
|
@@ -20,7 +22,19 @@ module ThemeCheck
|
|
20
22
|
private
|
21
23
|
|
22
24
|
def parse(template)
|
23
|
-
|
25
|
+
parseable_source = +template.source.clone
|
26
|
+
|
27
|
+
# Replace all liquid tags with {%#{i}######%} to prevent the HTML
|
28
|
+
# parser from freaking out. We transparently replace those placeholders in
|
29
|
+
# HtmlNode.
|
30
|
+
matches(parseable_source, LIQUID_TAG_OR_VARIABLE).each do |m|
|
31
|
+
value = m[0]
|
32
|
+
@placeholder_values.push(value)
|
33
|
+
key = (@placeholder_values.size - 1).to_s
|
34
|
+
parseable_source[m.begin(0)...m.end(0)] = "{%#{key.ljust(m.end(0) - m.begin(0) - 4, '#')}%}"
|
35
|
+
end
|
36
|
+
|
37
|
+
Nokogiri::HTML5.fragment(parseable_source, max_tree_depth: 400, max_attributes: 400)
|
24
38
|
end
|
25
39
|
|
26
40
|
def visit(node)
|