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.
- checksums.yaml +4 -4
- data/.github/workflows/theme-check.yml +2 -6
- data/CHANGELOG.md +50 -0
- data/CONTRIBUTING.md +1 -1
- data/README.md +39 -0
- data/RELEASING.md +34 -2
- data/bin/theme-check +29 -0
- data/bin/theme-check-language-server +29 -0
- data/config/default.yml +28 -1
- data/config/nothing.yml +11 -0
- data/config/theme_app_extension.yml +168 -0
- data/data/shopify_liquid/objects.yml +1 -0
- data/docs/checks/app_block_valid_tags.md +40 -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/deprecate_lazysizes.md +0 -3
- data/docs/checks/missing_template.md +25 -0
- data/docs/checks/pagination_size.md +44 -0
- data/docs/checks/template_length.md +1 -1
- data/docs/checks/undefined_object.md +5 -0
- data/lib/theme_check/analyzer.rb +26 -21
- data/lib/theme_check/asset_file.rb +3 -15
- data/lib/theme_check/bug.rb +3 -1
- data/lib/theme_check/check.rb +26 -4
- data/lib/theme_check/checks/app_block_valid_tags.rb +36 -0
- 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 +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/default_locale.rb +3 -1
- data/lib/theme_check/checks/deprecate_bgsizes.rb +1 -1
- data/lib/theme_check/checks/deprecate_lazysizes.rb +7 -4
- 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 +3 -3
- data/lib/theme_check/checks/space_inside_braces.rb +27 -7
- data/lib/theme_check/checks/template_length.rb +1 -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/checks.rb +11 -1
- data/lib/theme_check/cli.rb +52 -15
- data/lib/theme_check/config.rb +56 -10
- data/lib/theme_check/corrector.rb +9 -0
- data/lib/theme_check/exceptions.rb +29 -27
- 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_file.rb +11 -27
- data/lib/theme_check/json_printer.rb +26 -0
- data/lib/theme_check/language_server/constants.rb +21 -6
- data/lib/theme_check/language_server/document_link_engine.rb +3 -31
- data/lib/theme_check/language_server/document_link_provider.rb +70 -0
- data/lib/theme_check/language_server/document_link_providers/asset_document_link_provider.rb +11 -0
- data/lib/theme_check/language_server/document_link_providers/include_document_link_provider.rb +11 -0
- data/lib/theme_check/language_server/document_link_providers/render_document_link_provider.rb +11 -0
- data/lib/theme_check/language_server/document_link_providers/section_document_link_provider.rb +11 -0
- data/lib/theme_check/language_server/handler.rb +7 -4
- data/lib/theme_check/language_server/server.rb +13 -2
- data/lib/theme_check/language_server.rb +5 -0
- 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/tags.rb +26 -9
- data/lib/theme_check/template.rb +3 -32
- data/lib/theme_check/theme.rb +3 -0
- data/lib/theme_check/theme_file.rb +40 -0
- data/lib/theme_check/version.rb +1 -1
- data/lib/theme_check.rb +16 -0
- data/theme-check.gemspec +1 -1
- metadata +24 -6
- 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:
|
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
|
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,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
|
-
@
|
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
|
-
"-
|
32
|
-
"
|
33
|
-
) { |
|
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
|
-
"
|
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.
|
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.
|
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
|
-
|
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
|
-
|
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
|
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
|
@@ -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
|
-
|
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
|
@@ -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)
|