theme-check 0.5.0 → 0.7.3
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/.github/workflows/theme-check.yml +10 -3
- data/.rubocop.yml +6 -3
- data/CHANGELOG.md +35 -0
- data/Gemfile +5 -3
- data/LICENSE.md +2 -0
- data/README.md +3 -0
- data/RELEASING.md +10 -3
- data/Rakefile +6 -0
- data/config/default.yml +11 -1
- data/data/shopify_translation_keys.yml +850 -0
- data/docs/checks/asset_size_css.md +52 -0
- data/docs/checks/img_width_and_height.md +79 -0
- data/docs/checks/parser_blocking_javascript.md +3 -3
- data/docs/checks/remote_asset.md +82 -0
- data/exe/theme-check +1 -1
- data/lib/theme_check.rb +1 -0
- data/lib/theme_check/check.rb +1 -1
- data/lib/theme_check/checks/asset_size_css.rb +89 -0
- data/lib/theme_check/checks/asset_size_javascript.rb +2 -8
- data/lib/theme_check/checks/img_width_and_height.rb +74 -0
- data/lib/theme_check/checks/matching_translations.rb +1 -1
- data/lib/theme_check/checks/parser_blocking_javascript.rb +6 -14
- data/lib/theme_check/checks/remote_asset.rb +99 -0
- data/lib/theme_check/checks/translation_key_exists.rb +13 -1
- 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/cli.rb +106 -51
- data/lib/theme_check/config.rb +3 -0
- data/lib/theme_check/disabled_checks.rb +2 -2
- data/lib/theme_check/in_memory_storage.rb +13 -8
- data/lib/theme_check/language_server.rb +2 -0
- data/lib/theme_check/language_server/completion_engine.rb +3 -3
- data/lib/theme_check/language_server/completion_provider.rb +4 -0
- data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +6 -2
- data/lib/theme_check/language_server/completion_providers/object_completion_provider.rb +1 -1
- data/lib/theme_check/language_server/completion_providers/render_snippet_completion_provider.rb +43 -0
- data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +2 -2
- data/lib/theme_check/language_server/constants.rb +10 -0
- data/lib/theme_check/language_server/document_link_engine.rb +48 -0
- data/lib/theme_check/language_server/handler.rb +56 -17
- data/lib/theme_check/language_server/server.rb +4 -4
- data/lib/theme_check/liquid_check.rb +11 -0
- data/lib/theme_check/node.rb +1 -2
- data/lib/theme_check/offense.rb +3 -1
- data/lib/theme_check/packager.rb +1 -1
- data/lib/theme_check/releaser.rb +39 -0
- data/lib/theme_check/remote_asset_file.rb +1 -1
- data/lib/theme_check/shopify_liquid/deprecated_filter.rb +10 -8
- data/lib/theme_check/shopify_liquid/filter.rb +3 -5
- data/lib/theme_check/shopify_liquid/object.rb +2 -6
- data/lib/theme_check/shopify_liquid/tag.rb +1 -3
- data/lib/theme_check/storage.rb +3 -3
- data/lib/theme_check/string_helpers.rb +47 -0
- data/lib/theme_check/tags.rb +1 -2
- data/lib/theme_check/theme.rb +1 -1
- data/lib/theme_check/version.rb +1 -1
- data/packaging/homebrew/theme_check.base.rb +1 -1
- data/theme-check.gemspec +1 -2
- metadata +16 -18
@@ -2,15 +2,16 @@
|
|
2
2
|
module ThemeCheck
|
3
3
|
# Reports errors when trying to use parser-blocking script tags
|
4
4
|
class ParserBlockingJavaScript < LiquidCheck
|
5
|
+
include RegexHelpers
|
5
6
|
severity :error
|
6
7
|
categories :liquid, :performance
|
7
8
|
doc docs_url(__FILE__)
|
8
9
|
|
9
10
|
PARSER_BLOCKING_SCRIPT_TAG = %r{
|
10
11
|
<script # Find the start of a script tag
|
11
|
-
(?=
|
12
|
+
(?=[^>]+?src=) # Make sure src= is in the script with a lookahead
|
12
13
|
(?:(?!defer|async|type=["']module['"]).)*? # Find tags that don't have defer|async|type="module"
|
13
|
-
|
14
|
+
/?>
|
14
15
|
}xim
|
15
16
|
SCRIPT_TAG_FILTER = /\{\{[^}]+script_tag\s+\}\}/
|
16
17
|
|
@@ -33,23 +34,14 @@ module ThemeCheck
|
|
33
34
|
)
|
34
35
|
end
|
35
36
|
|
36
|
-
# The trickiness here is matching on scripts that are defined on
|
37
|
-
# multiple lines (or repeat matches). This makes the line_number
|
38
|
-
# calculation a bit weird. So instead, we traverse the string in
|
39
|
-
# a very imperative way.
|
40
37
|
def record_offenses_from_regex(regex: nil, message: nil)
|
41
|
-
|
42
|
-
while (i = @source.index(regex, i))
|
43
|
-
script = @source.match(regex, i)[0]
|
44
|
-
|
38
|
+
matches(@source, regex).each do |match|
|
45
39
|
add_offense(
|
46
40
|
message,
|
47
41
|
node: @node,
|
48
|
-
markup:
|
49
|
-
line_number: @source[0...
|
42
|
+
markup: match[0],
|
43
|
+
line_number: @source[0...match.begin(0)].count("\n") + 1
|
50
44
|
)
|
51
|
-
|
52
|
-
i += script.size
|
53
45
|
end
|
54
46
|
end
|
55
47
|
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module ThemeCheck
|
3
|
+
class RemoteAsset < LiquidCheck
|
4
|
+
include RegexHelpers
|
5
|
+
severity :suggestion
|
6
|
+
categories :liquid, :performance
|
7
|
+
doc docs_url(__FILE__)
|
8
|
+
|
9
|
+
OFFENSE_MESSAGE = "Asset should be served by the Shopify CDN for better performance."
|
10
|
+
|
11
|
+
HTML_FILTERS = [
|
12
|
+
'stylesheet_tag',
|
13
|
+
'script_tag',
|
14
|
+
'img_tag',
|
15
|
+
]
|
16
|
+
ASSET_URL_FILTERS = [
|
17
|
+
'asset_url',
|
18
|
+
'asset_img_url',
|
19
|
+
'file_img_url',
|
20
|
+
'file_url',
|
21
|
+
'global_asset_url',
|
22
|
+
'img_url',
|
23
|
+
'payment_type_img_url',
|
24
|
+
'shopify_asset_url',
|
25
|
+
]
|
26
|
+
|
27
|
+
RESOURCE_TAG = %r{<(?<tag_name>img|script|link|source)#{HTML_ATTRIBUTES}/?>}oim
|
28
|
+
RESOURCE_URL = /\s(?:src|href)=(?<resource_url>#{QUOTED_LIQUID_ATTRIBUTE})/oim
|
29
|
+
ASSET_URL_FILTER = /[\|\s]*(#{ASSET_URL_FILTERS.join('|')})/omi
|
30
|
+
PROTOCOL = %r{(https?:)?//}
|
31
|
+
ABSOLUTE_PATH = %r{\A/[^/]}im
|
32
|
+
RELATIVE_PATH = %r{\A(?!#{PROTOCOL})[^/\{]}oim
|
33
|
+
REL = /\srel=(?<rel>#{QUOTED_LIQUID_ATTRIBUTE})/oim
|
34
|
+
|
35
|
+
def on_variable(node)
|
36
|
+
record_variable_offense(node)
|
37
|
+
end
|
38
|
+
|
39
|
+
def on_document(node)
|
40
|
+
source = node.template.source
|
41
|
+
record_html_offenses(node, source)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def record_variable_offense(variable_node)
|
47
|
+
# We flag HTML tags with URLs not hosted by Shopify
|
48
|
+
return if !html_resource_drop?(variable_node) || variable_hosted_by_shopify?(variable_node)
|
49
|
+
add_offense(OFFENSE_MESSAGE, node: variable_node)
|
50
|
+
end
|
51
|
+
|
52
|
+
def html_resource_drop?(variable_node)
|
53
|
+
variable_node.value.filters
|
54
|
+
.any? { |(filter_name, *_filter_args)| HTML_FILTERS.include?(filter_name) }
|
55
|
+
end
|
56
|
+
|
57
|
+
def variable_hosted_by_shopify?(variable_node)
|
58
|
+
variable_node.value.filters
|
59
|
+
.any? { |(filter_name, *_filter_args)| ASSET_URL_FILTERS.include?(filter_name) }
|
60
|
+
end
|
61
|
+
|
62
|
+
# This part is slightly more complicated because we don't have an
|
63
|
+
# HTML AST. We have to resort to looking at the HTML with regexes
|
64
|
+
# to figure out if we have a resource (stylesheet, script, or media)
|
65
|
+
# that points to a remote domain.
|
66
|
+
def record_html_offenses(node, source)
|
67
|
+
matches(source, RESOURCE_TAG).each do |match|
|
68
|
+
tag = match[0]
|
69
|
+
|
70
|
+
# We don't flag stuff without URLs
|
71
|
+
next unless tag =~ RESOURCE_URL
|
72
|
+
resource_match = Regexp.last_match
|
73
|
+
resource_url = resource_match[:resource_url].gsub(START_OR_END_QUOTE, '')
|
74
|
+
|
75
|
+
next if non_stylesheet_link?(tag)
|
76
|
+
next if url_hosted_by_shopify?(resource_url)
|
77
|
+
next if resource_url =~ ABSOLUTE_PATH
|
78
|
+
next if resource_url =~ RELATIVE_PATH
|
79
|
+
next if resource_url.empty?
|
80
|
+
|
81
|
+
start = match.begin(0) + resource_match.begin(:resource_url)
|
82
|
+
add_offense(
|
83
|
+
OFFENSE_MESSAGE,
|
84
|
+
node: node,
|
85
|
+
markup: resource_url,
|
86
|
+
line_number: source[0...start].count("\n") + 1,
|
87
|
+
)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def non_stylesheet_link?(tag)
|
92
|
+
tag =~ REL && !(Regexp.last_match[:rel] =~ /\A['"]stylesheet['"]\Z/)
|
93
|
+
end
|
94
|
+
|
95
|
+
def url_hosted_by_shopify?(url)
|
96
|
+
url =~ /\A#{VARIABLE}\Z/oim && url =~ ASSET_URL_FILTER
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -1,5 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module ThemeCheck
|
3
|
+
module SystemTranslations
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def translations
|
7
|
+
@translations ||= YAML.load(File.read("#{__dir__}/../../../data/shopify_translation_keys.yml")).to_set
|
8
|
+
end
|
9
|
+
|
10
|
+
def include?(key)
|
11
|
+
translations.include?(key)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
3
15
|
class TranslationKeyExists < LiquidCheck
|
4
16
|
severity :error
|
5
17
|
category :translation
|
@@ -12,7 +24,7 @@ module ThemeCheck
|
|
12
24
|
return unless (key_node = node.children.first)
|
13
25
|
return unless key_node.value.is_a?(String)
|
14
26
|
|
15
|
-
unless key_exists?(key_node.value)
|
27
|
+
unless key_exists?(key_node.value) || SystemTranslations.include?(key_node.value)
|
16
28
|
add_offense(
|
17
29
|
"'#{key_node.value}' does not have a matching entry in '#{@theme.default_locale_json.relative_path}'",
|
18
30
|
node: node,
|
data/lib/theme_check/cli.rb
CHANGED
@@ -1,69 +1,104 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
require "optparse"
|
3
|
+
|
2
4
|
module ThemeCheck
|
3
5
|
class Cli
|
4
6
|
class Abort < StandardError; end
|
5
7
|
|
6
|
-
|
7
|
-
Usage: theme-check [options] /path/to/your/theme
|
8
|
+
attr_accessor :path
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
10
|
+
def initialize
|
11
|
+
@path = "."
|
12
|
+
@command = :check
|
13
|
+
@only_categories = []
|
14
|
+
@exclude_categories = []
|
15
|
+
@auto_correct = false
|
16
|
+
@config_path = nil
|
17
|
+
end
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
19
|
+
def option_parser(parser = OptionParser.new, help: true)
|
20
|
+
return @option_parser if @option_parser
|
21
|
+
@option_parser = parser
|
22
|
+
@option_parser.banner = "Usage: theme-check [options] [/path/to/your/theme]"
|
21
23
|
|
22
|
-
|
23
|
-
|
24
|
+
@option_parser.separator("")
|
25
|
+
@option_parser.separator("Basic Options:")
|
26
|
+
@option_parser.on(
|
27
|
+
"-C", "--config PATH",
|
28
|
+
"Use the config provided, overriding .theme-check.yml if present"
|
29
|
+
) { |path| @config_path = path }
|
30
|
+
@option_parser.on(
|
31
|
+
"-c", "--category CATEGORY",
|
32
|
+
"Only run this category of checks"
|
33
|
+
) { |category| @only_categories << category.to_sym }
|
34
|
+
@option_parser.on(
|
35
|
+
"-x", "--exclude-category CATEGORY",
|
36
|
+
"Exclude this category of checks"
|
37
|
+
) { |category| @exclude_categories << category.to_sym }
|
38
|
+
@option_parser.on(
|
39
|
+
"-a", "--auto-correct",
|
40
|
+
"Automatically fix offenses"
|
41
|
+
) { @auto_correct = true }
|
24
42
|
|
25
|
-
|
26
|
-
@
|
43
|
+
@option_parser.separator("")
|
44
|
+
@option_parser.separator("Miscellaneous:")
|
45
|
+
@option_parser.on(
|
46
|
+
"--init",
|
47
|
+
"Generate a .theme-check.yml file"
|
48
|
+
) { @command = :init }
|
49
|
+
@option_parser.on(
|
50
|
+
"--print",
|
51
|
+
"Output active config to STDOUT"
|
52
|
+
) { @command = :print }
|
53
|
+
@option_parser.on(
|
54
|
+
"-h", "--help",
|
55
|
+
"Show this. Hi!"
|
56
|
+
) { @command = :help } if help
|
57
|
+
@option_parser.on(
|
58
|
+
"-l", "--list",
|
59
|
+
"List enabled checks"
|
60
|
+
) { @command = :list }
|
61
|
+
@option_parser.on(
|
62
|
+
"-v", "--version",
|
63
|
+
"Print Theme Check version"
|
64
|
+
) { @command = :version }
|
65
|
+
|
66
|
+
@option_parser.separator("")
|
67
|
+
@option_parser.separator(<<~EOS)
|
68
|
+
Description:
|
69
|
+
Theme Check helps you follow Shopify Themes & Liquid best practices by analyzing the
|
70
|
+
Liquid & JSON inside your theme.
|
71
|
+
|
72
|
+
You can configure checks in the .theme-check.yml file of your theme root directory.
|
73
|
+
EOS
|
74
|
+
|
75
|
+
@option_parser
|
76
|
+
end
|
77
|
+
|
78
|
+
def parse(argv)
|
79
|
+
@path = option_parser.parse(argv).first || "."
|
80
|
+
end
|
27
81
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
case arg
|
36
|
-
when "--help", "-h"
|
37
|
-
raise Abort, USAGE
|
38
|
-
when "--version", "-v"
|
39
|
-
command = :version
|
40
|
-
when "--category", "-c"
|
41
|
-
only_categories << args.shift.to_sym
|
42
|
-
when "--exclude-category", "-x"
|
43
|
-
exclude_categories << args.shift.to_sym
|
44
|
-
when "--list", "-l"
|
45
|
-
command = :list
|
46
|
-
when "--auto-correct", "-a"
|
47
|
-
auto_correct = true
|
48
|
-
when "--init"
|
49
|
-
command = :init
|
82
|
+
def run!
|
83
|
+
unless [:version, :init, :help].include?(@command)
|
84
|
+
@config = if @config_path
|
85
|
+
ThemeCheck::Config.new(
|
86
|
+
root: @path,
|
87
|
+
configuration: ThemeCheck::Config.load_file(@config_path)
|
88
|
+
)
|
50
89
|
else
|
51
|
-
@path
|
90
|
+
ThemeCheck::Config.from_path(@path)
|
52
91
|
end
|
92
|
+
@config.only_categories = @only_categories
|
93
|
+
@config.exclude_categories = @exclude_categories
|
94
|
+
@config.auto_correct = @auto_correct
|
53
95
|
end
|
54
96
|
|
55
|
-
|
56
|
-
@config = ThemeCheck::Config.from_path(@path)
|
57
|
-
@config.only_categories = only_categories
|
58
|
-
@config.exclude_categories = exclude_categories
|
59
|
-
@config.auto_correct = auto_correct
|
60
|
-
end
|
61
|
-
|
62
|
-
send(command)
|
97
|
+
send(@command)
|
63
98
|
end
|
64
99
|
|
65
|
-
def run
|
66
|
-
run
|
100
|
+
def run
|
101
|
+
run!
|
67
102
|
rescue Abort => e
|
68
103
|
if e.message.empty?
|
69
104
|
exit(1)
|
@@ -72,6 +107,18 @@ module ThemeCheck
|
|
72
107
|
end
|
73
108
|
end
|
74
109
|
|
110
|
+
def self.parse_and_run!(argv)
|
111
|
+
cli = new
|
112
|
+
cli.parse(argv)
|
113
|
+
cli.run!
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.parse_and_run(argv)
|
117
|
+
cli = new
|
118
|
+
cli.parse(argv)
|
119
|
+
cli.run
|
120
|
+
end
|
121
|
+
|
75
122
|
def list
|
76
123
|
puts @config.enabled_checks
|
77
124
|
end
|
@@ -91,12 +138,20 @@ module ThemeCheck
|
|
91
138
|
end
|
92
139
|
end
|
93
140
|
|
141
|
+
def print
|
142
|
+
puts YAML.dump(@config.to_h)
|
143
|
+
end
|
144
|
+
|
145
|
+
def help
|
146
|
+
puts option_parser.to_s
|
147
|
+
end
|
148
|
+
|
94
149
|
def check
|
95
150
|
puts "Checking #{@config.root} ..."
|
96
151
|
storage = ThemeCheck::FileSystemStorage.new(@config.root, ignored_patterns: @config.ignored_patterns)
|
97
152
|
theme = ThemeCheck::Theme.new(storage)
|
98
153
|
if theme.all.empty?
|
99
|
-
raise Abort, "No templates found
|
154
|
+
raise Abort, "No templates found."
|
100
155
|
end
|
101
156
|
analyzer = ThemeCheck::Analyzer.new(theme, @config.enabled_checks, @config.auto_correct)
|
102
157
|
analyzer.analyze_theme
|
data/lib/theme_check/config.rb
CHANGED
@@ -10,6 +10,8 @@ module ThemeCheck
|
|
10
10
|
attr_accessor :only_categories, :exclude_categories, :auto_correct
|
11
11
|
|
12
12
|
class << self
|
13
|
+
attr_reader :last_loaded_config
|
14
|
+
|
13
15
|
def from_path(path)
|
14
16
|
if (filename = find(path))
|
15
17
|
new(root: filename.dirname, configuration: load_file(filename))
|
@@ -36,6 +38,7 @@ module ThemeCheck
|
|
36
38
|
end
|
37
39
|
|
38
40
|
def load_file(absolute_path)
|
41
|
+
@last_loaded_config = absolute_path
|
39
42
|
YAML.load_file(absolute_path)
|
40
43
|
end
|
41
44
|
|
@@ -61,11 +61,11 @@ module ThemeCheck
|
|
61
61
|
end
|
62
62
|
|
63
63
|
def start_disabling?(text)
|
64
|
-
text.strip.
|
64
|
+
text.strip.start_with?(DISABLE_START)
|
65
65
|
end
|
66
66
|
|
67
67
|
def stop_disabling?(text)
|
68
|
-
text.strip.
|
68
|
+
text.strip.start_with?(DISABLE_END)
|
69
69
|
end
|
70
70
|
|
71
71
|
# Return a list of checks from a theme-check-disable comment
|
@@ -6,24 +6,25 @@
|
|
6
6
|
# as a big hash already, leave it like that and save yourself some IO.
|
7
7
|
module ThemeCheck
|
8
8
|
class InMemoryStorage < Storage
|
9
|
-
def initialize(files = {})
|
9
|
+
def initialize(files = {}, root = "/dev/null")
|
10
10
|
@files = files
|
11
|
+
@root = Pathname.new(root)
|
11
12
|
end
|
12
13
|
|
13
|
-
def path(
|
14
|
-
|
14
|
+
def path(relative_path)
|
15
|
+
@root.join(relative_path)
|
15
16
|
end
|
16
17
|
|
17
|
-
def read(
|
18
|
-
@files[
|
18
|
+
def read(relative_path)
|
19
|
+
@files[relative_path]
|
19
20
|
end
|
20
21
|
|
21
|
-
def write(
|
22
|
-
@files[
|
22
|
+
def write(relative_path, content)
|
23
|
+
@files[relative_path] = content
|
23
24
|
end
|
24
25
|
|
25
26
|
def files
|
26
|
-
@
|
27
|
+
@files.keys
|
27
28
|
end
|
28
29
|
|
29
30
|
def directories
|
@@ -33,5 +34,9 @@ module ThemeCheck
|
|
33
34
|
.map(&:to_s)
|
34
35
|
.uniq
|
35
36
|
end
|
37
|
+
|
38
|
+
def relative_path(absolute_path)
|
39
|
+
Pathname.new(absolute_path).relative_path_from(@root).to_s
|
40
|
+
end
|
36
41
|
end
|
37
42
|
end
|