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