theme-check 0.10.1 → 1.2.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/.github/workflows/theme-check.yml +2 -6
- data/CHANGELOG.md +41 -0
- data/README.md +39 -0
- data/RELEASING.md +34 -2
- data/Rakefile +1 -1
- data/config/default.yml +39 -3
- data/config/nothing.yml +11 -0
- data/config/theme_app_extension.yml +153 -0
- data/data/shopify_liquid/objects.yml +2 -0
- data/docs/checks/asset_size_app_block_css.md +52 -0
- data/docs/checks/asset_size_app_block_javascript.md +57 -0
- data/docs/checks/asset_size_css_stylesheet_tag.md +50 -0
- data/docs/checks/deprecate_bgsizes.md +66 -0
- data/docs/checks/deprecate_lazysizes.md +61 -0
- data/docs/checks/html_parsing_error.md +50 -0
- data/docs/checks/liquid_tag.md +2 -2
- data/docs/checks/template_length.md +12 -2
- data/exe/theme-check-language-server.bat +3 -0
- data/exe/theme-check.bat +3 -0
- data/lib/theme_check.rb +15 -0
- data/lib/theme_check/analyzer.rb +25 -21
- data/lib/theme_check/asset_file.rb +3 -15
- data/lib/theme_check/bug.rb +3 -1
- data/lib/theme_check/check.rb +24 -2
- data/lib/theme_check/checks/asset_size_app_block_css.rb +44 -0
- data/lib/theme_check/checks/asset_size_app_block_javascript.rb +44 -0
- data/lib/theme_check/checks/asset_size_css.rb +11 -74
- data/lib/theme_check/checks/asset_size_css_stylesheet_tag.rb +24 -0
- data/lib/theme_check/checks/asset_size_javascript.rb +10 -36
- data/lib/theme_check/checks/deprecate_bgsizes.rb +14 -0
- data/lib/theme_check/checks/deprecate_lazysizes.rb +16 -0
- data/lib/theme_check/checks/html_parsing_error.rb +12 -0
- data/lib/theme_check/checks/img_lazy_loading.rb +1 -6
- data/lib/theme_check/checks/liquid_tag.rb +2 -2
- data/lib/theme_check/checks/remote_asset.rb +2 -0
- data/lib/theme_check/checks/space_inside_braces.rb +1 -1
- data/lib/theme_check/checks/template_length.rb +18 -4
- data/lib/theme_check/cli.rb +34 -13
- data/lib/theme_check/config.rb +56 -10
- data/lib/theme_check/exceptions.rb +29 -27
- data/lib/theme_check/html_check.rb +2 -0
- data/lib/theme_check/html_visitor.rb +3 -1
- data/lib/theme_check/json_file.rb +2 -29
- data/lib/theme_check/language_server/constants.rb +8 -0
- data/lib/theme_check/language_server/document_link_engine.rb +40 -4
- data/lib/theme_check/language_server/handler.rb +1 -1
- data/lib/theme_check/language_server/server.rb +13 -2
- data/lib/theme_check/liquid_check.rb +0 -12
- data/lib/theme_check/parsing_helpers.rb +3 -1
- data/lib/theme_check/regex_helpers.rb +17 -0
- data/lib/theme_check/tags.rb +62 -8
- data/lib/theme_check/template.rb +3 -32
- data/lib/theme_check/theme_file.rb +40 -0
- data/lib/theme_check/version.rb +1 -1
- metadata +22 -3
@@ -1,12 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module ThemeCheck
|
3
|
-
# Recommends using {% liquid ... %} if
|
3
|
+
# Recommends using {% liquid ... %} if 5 or more consecutive {% ... %} are found.
|
4
4
|
class LiquidTag < LiquidCheck
|
5
5
|
severity :suggestion
|
6
6
|
category :liquid
|
7
7
|
doc docs_url(__FILE__)
|
8
8
|
|
9
|
-
def initialize(min_consecutive_statements:
|
9
|
+
def initialize(min_consecutive_statements: 5)
|
10
10
|
@first_statement = nil
|
11
11
|
@consecutive_statements = 0
|
12
12
|
@min_consecutive_statements = min_consecutive_statements
|
@@ -9,6 +9,7 @@ module ThemeCheck
|
|
9
9
|
PROTOCOL = %r{(https?:)?//}
|
10
10
|
ABSOLUTE_PATH = %r{\A/[^/]}im
|
11
11
|
RELATIVE_PATH = %r{\A(?!#{PROTOCOL})[^/\{]}oim
|
12
|
+
CDN_ROOT = "https://cdn.shopify.com/"
|
12
13
|
|
13
14
|
def on_element(node)
|
14
15
|
return unless TAGS.include?(node.name)
|
@@ -17,6 +18,7 @@ module ThemeCheck
|
|
17
18
|
return if resource_url.nil? || resource_url.empty?
|
18
19
|
|
19
20
|
# Ignore if URL is Liquid, taken care of by AssetUrlFilters check
|
21
|
+
return if resource_url.start_with?(CDN_ROOT)
|
20
22
|
return if resource_url =~ ABSOLUTE_PATH
|
21
23
|
return if resource_url =~ RELATIVE_PATH
|
22
24
|
return if url_hosted_by_shopify?(resource_url)
|
@@ -5,9 +5,11 @@ module ThemeCheck
|
|
5
5
|
category :liquid
|
6
6
|
doc docs_url(__FILE__)
|
7
7
|
|
8
|
-
def initialize(max_length:
|
8
|
+
def initialize(max_length: 500, exclude_schema: true, exclude_stylesheet: true, exclude_javascript: true)
|
9
9
|
@max_length = max_length
|
10
10
|
@exclude_schema = exclude_schema
|
11
|
+
@exclude_stylesheet = exclude_stylesheet
|
12
|
+
@exclude_javascript = exclude_javascript
|
11
13
|
end
|
12
14
|
|
13
15
|
def on_document(_node)
|
@@ -15,9 +17,15 @@ module ThemeCheck
|
|
15
17
|
end
|
16
18
|
|
17
19
|
def on_schema(node)
|
18
|
-
if @exclude_schema
|
19
|
-
|
20
|
-
|
20
|
+
exclude_node_lines(node) if @exclude_schema
|
21
|
+
end
|
22
|
+
|
23
|
+
def on_stylesheet(node)
|
24
|
+
exclude_node_lines(node) if @exclude_stylesheet
|
25
|
+
end
|
26
|
+
|
27
|
+
def on_javascript(node)
|
28
|
+
exclude_node_lines(node) if @exclude_javascript
|
21
29
|
end
|
22
30
|
|
23
31
|
def after_document(node)
|
@@ -26,5 +34,11 @@ module ThemeCheck
|
|
26
34
|
add_offense("Template has too many lines [#{lines}/#{@max_length}]", template: node.template)
|
27
35
|
end
|
28
36
|
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def exclude_node_lines(node)
|
41
|
+
@excluded_lines += node.value.nodelist.join.count("\n")
|
42
|
+
end
|
29
43
|
end
|
30
44
|
end
|
data/lib/theme_check/cli.rb
CHANGED
@@ -10,10 +10,11 @@ module ThemeCheck
|
|
10
10
|
def initialize
|
11
11
|
@path = "."
|
12
12
|
@command = :check
|
13
|
-
@
|
13
|
+
@include_categories = []
|
14
14
|
@exclude_categories = []
|
15
15
|
@auto_correct = false
|
16
16
|
@config_path = nil
|
17
|
+
@fail_level = :error
|
17
18
|
end
|
18
19
|
|
19
20
|
def option_parser(parser = OptionParser.new, help: true)
|
@@ -25,20 +26,27 @@ module ThemeCheck
|
|
25
26
|
@option_parser.separator("Basic Options:")
|
26
27
|
@option_parser.on(
|
27
28
|
"-C", "--config PATH",
|
28
|
-
"Use the config provided, overriding .theme-check.yml if present"
|
29
|
+
"Use the config provided, overriding .theme-check.yml if present",
|
30
|
+
"Use :theme_app_extension to use default checks for theme app extensions"
|
29
31
|
) { |path| @config_path = path }
|
30
32
|
@option_parser.on(
|
31
|
-
"-c", "--category CATEGORY",
|
32
|
-
"
|
33
|
-
) { |category| @
|
33
|
+
"-c", "--category CATEGORY", Check::CATEGORIES, "Only run this category of checks",
|
34
|
+
"Runs checks matching all categories when specified more than once"
|
35
|
+
) { |category| @include_categories << category.to_sym }
|
34
36
|
@option_parser.on(
|
35
|
-
"-x", "--exclude-category CATEGORY",
|
36
|
-
"
|
37
|
+
"-x", "--exclude-category CATEGORY", Check::CATEGORIES, "Exclude this category of checks",
|
38
|
+
"Excludes checks matching any category when specified more than once"
|
37
39
|
) { |category| @exclude_categories << category.to_sym }
|
38
40
|
@option_parser.on(
|
39
41
|
"-a", "--auto-correct",
|
40
42
|
"Automatically fix offenses"
|
41
43
|
) { @auto_correct = true }
|
44
|
+
@option_parser.on(
|
45
|
+
"--fail-level SEVERITY", Check::SEVERITIES,
|
46
|
+
"Minimum severity (error|suggestion|style) for exit with error code"
|
47
|
+
) do |severity|
|
48
|
+
@fail_level = severity.to_sym
|
49
|
+
end
|
42
50
|
|
43
51
|
@option_parser.separator("")
|
44
52
|
@option_parser.separator("Miscellaneous:")
|
@@ -77,6 +85,8 @@ module ThemeCheck
|
|
77
85
|
|
78
86
|
def parse(argv)
|
79
87
|
@path = option_parser.parse(argv).first || "."
|
88
|
+
rescue OptionParser::InvalidArgument => e
|
89
|
+
abort(e.message)
|
80
90
|
end
|
81
91
|
|
82
92
|
def run!
|
@@ -84,13 +94,13 @@ module ThemeCheck
|
|
84
94
|
@config = if @config_path
|
85
95
|
ThemeCheck::Config.new(
|
86
96
|
root: @path,
|
87
|
-
configuration: ThemeCheck::Config.
|
97
|
+
configuration: ThemeCheck::Config.load_config(@config_path)
|
88
98
|
)
|
89
99
|
else
|
90
100
|
ThemeCheck::Config.from_path(@path)
|
91
101
|
end
|
92
|
-
@config.
|
93
|
-
@config.exclude_categories = @exclude_categories
|
102
|
+
@config.include_categories = @include_categories unless @include_categories.empty?
|
103
|
+
@config.exclude_categories = @exclude_categories unless @exclude_categories.empty?
|
94
104
|
@config.auto_correct = @auto_correct
|
95
105
|
end
|
96
106
|
|
@@ -99,12 +109,16 @@ module ThemeCheck
|
|
99
109
|
|
100
110
|
def run
|
101
111
|
run!
|
112
|
+
exit(0)
|
102
113
|
rescue Abort => e
|
103
114
|
if e.message.empty?
|
104
115
|
exit(1)
|
105
116
|
else
|
106
117
|
abort(e.message)
|
107
118
|
end
|
119
|
+
rescue ThemeCheckError => e
|
120
|
+
STDERR.puts(e.message)
|
121
|
+
exit(2)
|
108
122
|
end
|
109
123
|
|
110
124
|
def self.parse_and_run!(argv)
|
@@ -130,11 +144,16 @@ module ThemeCheck
|
|
130
144
|
def init
|
131
145
|
dotfile_path = ThemeCheck::Config.find(@path)
|
132
146
|
if dotfile_path.nil?
|
133
|
-
|
147
|
+
config_name = if @config_path && @config_path[0] == ":"
|
148
|
+
"#{@config_path[1..]}.yml"
|
149
|
+
else
|
150
|
+
"default.yml"
|
151
|
+
end
|
152
|
+
File.write(File.join(@path, ThemeCheck::Config::DOTFILE), File.read(ThemeCheck::Config.bundled_config_path(config_name)))
|
134
153
|
|
135
154
|
puts "Writing new #{ThemeCheck::Config::DOTFILE} to #{@path}"
|
136
155
|
else
|
137
|
-
raise Abort, "#{ThemeCheck::Config::DOTFILE} already exists at #{@path}
|
156
|
+
raise Abort, "#{ThemeCheck::Config::DOTFILE} already exists at #{@path}"
|
138
157
|
end
|
139
158
|
end
|
140
159
|
|
@@ -157,7 +176,9 @@ module ThemeCheck
|
|
157
176
|
analyzer.analyze_theme
|
158
177
|
analyzer.correct_offenses
|
159
178
|
ThemeCheck::Printer.new.print(theme, analyzer.offenses, @config.auto_correct)
|
160
|
-
raise Abort, "" if analyzer.uncorrectable_offenses.any?
|
179
|
+
raise Abort, "" if analyzer.uncorrectable_offenses.any? do |offense|
|
180
|
+
offense.check.severity_value <= Check.severity_value(@fail_level)
|
181
|
+
end
|
161
182
|
end
|
162
183
|
end
|
163
184
|
end
|
data/lib/theme_check/config.rb
CHANGED
@@ -3,11 +3,11 @@
|
|
3
3
|
module ThemeCheck
|
4
4
|
class Config
|
5
5
|
DOTFILE = '.theme-check.yml'
|
6
|
-
|
6
|
+
BUNDLED_CONFIGS_DIR = "#{__dir__}/../../config"
|
7
7
|
BOOLEAN = [true, false]
|
8
8
|
|
9
9
|
attr_reader :root
|
10
|
-
attr_accessor :
|
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 ||=
|
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
|
-
|
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
|
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
|
151
|
-
|
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
|
-
|
202
|
+
merge_configurations!(value, default)
|
157
203
|
when nil
|
158
204
|
configuration[key] = default
|
159
205
|
end
|
@@ -1,32 +1,34 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require "net/http"
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
]
|
27
|
+
NET_HTTP_EXCEPTIONS = [
|
28
|
+
Net::HTTPBadResponse,
|
29
|
+
Net::HTTPHeaderSyntaxError,
|
30
|
+
Net::ProtocolError,
|
31
|
+
*TIMEOUT_EXCEPTIONS,
|
32
|
+
*CONNECTION_EXCEPTIONS,
|
33
|
+
]
|
34
|
+
end
|
@@ -13,12 +13,14 @@ module ThemeCheck
|
|
13
13
|
def visit_template(template)
|
14
14
|
doc = parse(template)
|
15
15
|
visit(HtmlNode.new(doc, template))
|
16
|
+
rescue ArgumentError => e
|
17
|
+
call_checks(:on_parse_error, e, template)
|
16
18
|
end
|
17
19
|
|
18
20
|
private
|
19
21
|
|
20
22
|
def parse(template)
|
21
|
-
Nokogiri::HTML5.fragment(template.source)
|
23
|
+
Nokogiri::HTML5.fragment(template.source, max_tree_depth: 400, max_attributes: 400)
|
22
24
|
end
|
23
25
|
|
24
26
|
def visit(node)
|
@@ -1,29 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require "json"
|
3
|
-
require "pathname"
|
4
3
|
|
5
4
|
module ThemeCheck
|
6
|
-
class JsonFile
|
5
|
+
class JsonFile < ThemeFile
|
7
6
|
def initialize(relative_path, storage)
|
8
|
-
|
9
|
-
@storage = storage
|
7
|
+
super
|
10
8
|
@loaded = false
|
11
9
|
@content = nil
|
12
10
|
@parser_error = nil
|
13
11
|
end
|
14
12
|
|
15
|
-
def path
|
16
|
-
@storage.path(@relative_path)
|
17
|
-
end
|
18
|
-
|
19
|
-
def relative_path
|
20
|
-
@relative_pathname ||= Pathname.new(@relative_path)
|
21
|
-
end
|
22
|
-
|
23
|
-
def source
|
24
|
-
@source ||= @storage.read(@relative_path)
|
25
|
-
end
|
26
|
-
|
27
13
|
def content
|
28
14
|
load!
|
29
15
|
@content
|
@@ -34,23 +20,10 @@ module ThemeCheck
|
|
34
20
|
@parser_error
|
35
21
|
end
|
36
22
|
|
37
|
-
def name
|
38
|
-
relative_path.sub_ext('').to_s
|
39
|
-
end
|
40
|
-
|
41
23
|
def json?
|
42
24
|
true
|
43
25
|
end
|
44
26
|
|
45
|
-
def liquid?
|
46
|
-
false
|
47
|
-
end
|
48
|
-
|
49
|
-
def ==(other)
|
50
|
-
other.is_a?(JsonFile) && relative_path == other.relative_path
|
51
|
-
end
|
52
|
-
alias_method :eql?, :==
|
53
|
-
|
54
27
|
private
|
55
28
|
|
56
29
|
def load!
|