theme-check 1.5.0 → 1.6.1
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 +12 -4
- data/CHANGELOG.md +34 -0
- data/CONTRIBUTING.md +58 -0
- data/Gemfile +3 -0
- data/docs/flamegraph.svg +18488 -0
- data/lib/theme_check/analyzer.rb +5 -0
- data/lib/theme_check/asset_file.rb +13 -2
- data/lib/theme_check/check.rb +1 -1
- data/lib/theme_check/checks/asset_size_css.rb +15 -0
- data/lib/theme_check/checks/asset_size_css_stylesheet_tag.rb +18 -1
- data/lib/theme_check/checks/convert_include_to_render.rb +2 -1
- data/lib/theme_check/checks/liquid_tag.rb +1 -1
- data/lib/theme_check/checks/missing_required_template_files.rb +21 -7
- data/lib/theme_check/checks/pagination_size.rb +30 -10
- data/lib/theme_check/checks/required_directories.rb +3 -1
- data/lib/theme_check/checks/space_inside_braces.rb +47 -24
- data/lib/theme_check/checks/translation_key_exists.rb +3 -1
- data/lib/theme_check/checks/unused_snippet.rb +3 -1
- data/lib/theme_check/checks.rb +2 -0
- data/lib/theme_check/cli.rb +32 -6
- data/lib/theme_check/corrector.rb +23 -10
- data/lib/theme_check/file_system_storage.rb +13 -2
- data/lib/theme_check/html_node.rb +4 -4
- data/lib/theme_check/html_visitor.rb +20 -8
- data/lib/theme_check/in_memory_storage.rb +8 -0
- data/lib/theme_check/json_file.rb +9 -4
- data/lib/theme_check/json_printer.rb +5 -1
- data/lib/theme_check/node.rb +118 -11
- data/lib/theme_check/offense.rb +26 -0
- data/lib/theme_check/position.rb +27 -16
- data/lib/theme_check/position_helper.rb +13 -15
- data/lib/theme_check/printer.rb +9 -5
- data/lib/theme_check/regex_helpers.rb +1 -15
- data/lib/theme_check/remote_asset_file.rb +4 -0
- data/lib/theme_check/template.rb +5 -19
- data/lib/theme_check/template_rewriter.rb +57 -0
- data/lib/theme_check/theme_file.rb +18 -1
- data/lib/theme_check/version.rb +1 -1
- data/lib/theme_check.rb +1 -0
- data/theme-check.gemspec +1 -0
- metadata +18 -2
data/lib/theme_check/analyzer.rb
CHANGED
@@ -9,10 +9,21 @@ module ThemeCheck
|
|
9
9
|
@content = nil
|
10
10
|
end
|
11
11
|
|
12
|
-
|
12
|
+
def rewriter
|
13
|
+
@rewriter ||= TemplateRewriter.new(@relative_path, source)
|
14
|
+
end
|
15
|
+
|
16
|
+
def write
|
17
|
+
content = rewriter.to_s
|
18
|
+
if source != content
|
19
|
+
@storage.write(@relative_path, content.gsub("\n", @eol))
|
20
|
+
@source = content
|
21
|
+
@rewriter = nil
|
22
|
+
end
|
23
|
+
end
|
13
24
|
|
14
25
|
def gzipped_size
|
15
|
-
@gzipped_size ||= Zlib.gzip(
|
26
|
+
@gzipped_size ||= Zlib.gzip(source).bytesize
|
16
27
|
end
|
17
28
|
|
18
29
|
def name
|
data/lib/theme_check/check.rb
CHANGED
@@ -22,5 +22,20 @@ module ThemeCheck
|
|
22
22
|
node: node
|
23
23
|
)
|
24
24
|
end
|
25
|
+
|
26
|
+
def href_to_file_size(href)
|
27
|
+
# asset_url (+ optional stylesheet_tag) variables
|
28
|
+
if href =~ /^#{LIQUID_VARIABLE}$/o && href =~ /asset_url/ && href =~ Liquid::QuotedString
|
29
|
+
asset_id = Regexp.last_match(0).gsub(START_OR_END_QUOTE, "")
|
30
|
+
asset = @theme.assets.find { |a| a.name.end_with?("/" + asset_id) }
|
31
|
+
return if asset.nil?
|
32
|
+
asset.gzipped_size
|
33
|
+
|
34
|
+
# remote URLs
|
35
|
+
elsif href =~ %r{^(https?:)?//}
|
36
|
+
asset = RemoteAssetFile.from_src(href)
|
37
|
+
asset.gzipped_size
|
38
|
+
end
|
39
|
+
end
|
25
40
|
end
|
26
41
|
end
|
@@ -13,12 +13,29 @@ module ThemeCheck
|
|
13
13
|
def on_variable(node)
|
14
14
|
used_filters = node.value.filters.map { |name, *_rest| name }
|
15
15
|
return unless used_filters.include?("stylesheet_tag")
|
16
|
-
file_size =
|
16
|
+
file_size = stylesheet_tag_pipeline_to_file_size(node.markup)
|
17
|
+
return if file_size.nil?
|
17
18
|
return if file_size <= @threshold_in_bytes
|
18
19
|
add_offense(
|
19
20
|
"CSS on every page load exceeding compressed size threshold (#{@threshold_in_bytes} Bytes).",
|
20
21
|
node: node
|
21
22
|
)
|
22
23
|
end
|
24
|
+
|
25
|
+
def stylesheet_tag_pipeline_to_file_size(href)
|
26
|
+
# asset_url
|
27
|
+
if href =~ /asset_url/ && href =~ Liquid::QuotedString
|
28
|
+
asset_id = Regexp.last_match(0).gsub(START_OR_END_QUOTE, "")
|
29
|
+
asset = @theme.assets.find { |a| a.name.end_with?("/" + asset_id) }
|
30
|
+
return if asset.nil?
|
31
|
+
asset.gzipped_size
|
32
|
+
|
33
|
+
# remote URLs
|
34
|
+
elsif href =~ %r{(https?:)?//[^'"]+}
|
35
|
+
url = Regexp.last_match(0)
|
36
|
+
asset = RemoteAssetFile.from_src(url)
|
37
|
+
asset.gzipped_size
|
38
|
+
end
|
39
|
+
end
|
23
40
|
end
|
24
41
|
end
|
@@ -8,7 +8,8 @@ module ThemeCheck
|
|
8
8
|
|
9
9
|
def on_include(node)
|
10
10
|
add_offense("`include` is deprecated - convert it to `render`", node: node) do |corrector|
|
11
|
-
|
11
|
+
# We need to fix #445 and pass the variables from the context or don't replace at all.
|
12
|
+
# corrector.replace(node, "render \'#{node.value.template_name_expr}\' ")
|
12
13
|
end
|
13
14
|
end
|
14
15
|
end
|
@@ -9,19 +9,33 @@ module ThemeCheck
|
|
9
9
|
doc docs_url(__FILE__)
|
10
10
|
|
11
11
|
REQUIRED_LIQUID_FILES = %w(layout/theme)
|
12
|
-
|
13
|
-
|
12
|
+
|
13
|
+
REQUIRED_LIQUID_TEMPLATE_FILES = %w(
|
14
14
|
gift_card customers/account customers/activate_account customers/addresses
|
15
|
-
customers/login customers/order customers/register customers/reset_password
|
16
|
-
)
|
17
|
-
|
15
|
+
customers/login customers/order customers/register customers/reset_password
|
16
|
+
).map { |file| "templates/#{file}" }
|
17
|
+
|
18
|
+
REQUIRED_JSON_TEMPLATE_FILES = %w(
|
19
|
+
index product collection cart blog article page list-collections search 404
|
20
|
+
password
|
21
|
+
).map { |file| "templates/#{file}" }
|
22
|
+
|
23
|
+
REQUIRED_TEMPLATE_FILES = (REQUIRED_LIQUID_TEMPLATE_FILES + REQUIRED_JSON_TEMPLATE_FILES)
|
18
24
|
|
19
25
|
def on_end
|
20
26
|
(REQUIRED_LIQUID_FILES - theme.liquid.map(&:name)).each do |file|
|
21
|
-
add_offense("'#{file}.liquid' is missing")
|
27
|
+
add_offense("'#{file}.liquid' is missing") do |corrector|
|
28
|
+
corrector.create(@theme, "#{file}.liquid", "")
|
29
|
+
end
|
22
30
|
end
|
23
31
|
(REQUIRED_TEMPLATE_FILES - (theme.liquid + theme.json).map(&:name)).each do |file|
|
24
|
-
add_offense("'#{file}.liquid' or '#{file}.json' is missing")
|
32
|
+
add_offense("'#{file}.liquid' or '#{file}.json' is missing") do |corrector|
|
33
|
+
if REQUIRED_LIQUID_TEMPLATE_FILES.include?(file)
|
34
|
+
corrector.create(@theme, "#{file}.liquid", "")
|
35
|
+
else
|
36
|
+
corrector.create(@theme, "#{file}.json", "")
|
37
|
+
end
|
38
|
+
end
|
25
39
|
end
|
26
40
|
end
|
27
41
|
end
|
@@ -36,16 +36,32 @@ module ThemeCheck
|
|
36
36
|
# Ignored, handled in ValidSchema.
|
37
37
|
end
|
38
38
|
|
39
|
+
##
|
40
|
+
# Section settings look like:
|
41
|
+
# #<Liquid::VariableLookup:0x00007fd699c50c48 @name="section", @lookups=["settings", "products_per_page"], @command_flags=0>
|
42
|
+
def size_is_a_section_setting?(size)
|
43
|
+
size.is_a?(Liquid::VariableLookup) &&
|
44
|
+
size.name == 'section' &&
|
45
|
+
size.lookups.first == 'settings'
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# We'll work with either an explicit value, or the default value of the section setting.
|
50
|
+
def get_value(size)
|
51
|
+
return size if size.is_a?(Numeric)
|
52
|
+
return get_setting_default_value(size) if size_is_a_section_setting?(size)
|
53
|
+
end
|
54
|
+
|
39
55
|
def after_document(_node)
|
40
56
|
@paginations.each_pair do |size, nodes|
|
41
|
-
|
42
|
-
|
43
|
-
else
|
44
|
-
get_setting_default_value(size.lookups.last)
|
45
|
-
end
|
46
|
-
if numerical_size.nil?
|
57
|
+
# Validate presence of default section setting.
|
58
|
+
if size_is_a_section_setting?(size) && !get_setting_default_value(size)
|
47
59
|
nodes.each { |node| add_offense("Default pagination size should be defined in the section settings", node: node) }
|
48
|
-
|
60
|
+
end
|
61
|
+
|
62
|
+
# Validate if size is within range.
|
63
|
+
next unless (numerical_size = get_value(size))
|
64
|
+
if numerical_size > @max_size || numerical_size < @min_size || !numerical_size.is_a?(Integer)
|
49
65
|
nodes.each { |node| add_offense("Pagination size must be a positive integer between #{@min_size} and #{@max_size}", node: node) }
|
50
66
|
end
|
51
67
|
end
|
@@ -53,11 +69,15 @@ module ThemeCheck
|
|
53
69
|
|
54
70
|
private
|
55
71
|
|
56
|
-
def get_setting_default_value(
|
57
|
-
setting = @schema_settings.
|
72
|
+
def get_setting_default_value(variable_lookup)
|
73
|
+
setting = @schema_settings.select { |s| s['id'] == variable_lookup.lookups.last }
|
74
|
+
|
75
|
+
# Setting does not exist.
|
58
76
|
return nil if setting.empty?
|
59
|
-
|
77
|
+
|
78
|
+
default_value = setting.last['default'].to_i
|
60
79
|
return nil if default_value == 0
|
80
|
+
|
61
81
|
default_value
|
62
82
|
end
|
63
83
|
end
|
@@ -18,7 +18,9 @@ module ThemeCheck
|
|
18
18
|
private
|
19
19
|
|
20
20
|
def add_missing_directories_offense(directory)
|
21
|
-
add_offense("Theme is missing '#{directory}' directory")
|
21
|
+
add_offense("Theme is missing '#{directory}' directory") do |corrector|
|
22
|
+
corrector.mkdir(@theme, directory)
|
23
|
+
end
|
22
24
|
end
|
23
25
|
end
|
24
26
|
end
|
@@ -15,52 +15,57 @@ module ThemeCheck
|
|
15
15
|
return if :assign == node.type_name
|
16
16
|
|
17
17
|
outside_of_strings(node.markup) do |chunk, chunk_start|
|
18
|
-
chunk.scan(/([,:|]|==|<>|<=|>=|<|>|!=)
|
18
|
+
chunk.scan(/([,:|]|==|<>|<=|>=|<|>|!=)( +)/) do |_match|
|
19
19
|
add_offense(
|
20
20
|
"Too many spaces after '#{Regexp.last_match(1)}'",
|
21
21
|
node: node,
|
22
|
-
markup: Regexp.last_match(
|
23
|
-
node_markup_offset: chunk_start + Regexp.last_match.begin(
|
22
|
+
markup: Regexp.last_match(2),
|
23
|
+
node_markup_offset: chunk_start + Regexp.last_match.begin(2)
|
24
24
|
)
|
25
25
|
end
|
26
26
|
chunk.scan(/([,:|]|==|<>|<=|>=|<\b|>\b|!=)(\S|\z)/) do |_match|
|
27
27
|
add_offense(
|
28
28
|
"Space missing after '#{Regexp.last_match(1)}'",
|
29
29
|
node: node,
|
30
|
-
markup: Regexp.last_match(
|
30
|
+
markup: Regexp.last_match(1),
|
31
31
|
node_markup_offset: chunk_start + Regexp.last_match.begin(0),
|
32
32
|
)
|
33
33
|
end
|
34
|
-
chunk.scan(/ (\||==|<>|<=|>=|<|>|!=)+/) do |_match|
|
34
|
+
chunk.scan(/( +)(\||==|<>|<=|>=|<|>|!=)+/) do |_match|
|
35
35
|
add_offense(
|
36
|
-
"Too many spaces before '#{Regexp.last_match(
|
36
|
+
"Too many spaces before '#{Regexp.last_match(2)}'",
|
37
37
|
node: node,
|
38
|
-
markup: Regexp.last_match(
|
39
|
-
node_markup_offset: chunk_start + Regexp.last_match.begin(
|
38
|
+
markup: Regexp.last_match(1),
|
39
|
+
node_markup_offset: chunk_start + Regexp.last_match.begin(1)
|
40
40
|
)
|
41
41
|
end
|
42
42
|
chunk.scan(/(\A|\S)(?<match>\||==|<>|<=|>=|<|\b>|!=)/) do |_match|
|
43
43
|
add_offense(
|
44
44
|
"Space missing before '#{Regexp.last_match(1)}'",
|
45
45
|
node: node,
|
46
|
-
markup: Regexp.last_match(
|
47
|
-
node_markup_offset: chunk_start + Regexp.last_match.begin(
|
46
|
+
markup: Regexp.last_match(:match),
|
47
|
+
node_markup_offset: chunk_start + Regexp.last_match.begin(:match)
|
48
48
|
)
|
49
49
|
end
|
50
50
|
end
|
51
51
|
end
|
52
52
|
|
53
53
|
def on_tag(node)
|
54
|
-
|
55
|
-
markup = if node.whitespace_trimmed?
|
56
|
-
"-%}"
|
57
|
-
else
|
58
|
-
"%}"
|
59
|
-
end
|
54
|
+
unless node.inside_liquid_tag?
|
60
55
|
if node.markup[-1] != " " && node.markup[-1] != "\n"
|
61
|
-
add_offense(
|
56
|
+
add_offense(
|
57
|
+
"Space missing before '#{node.end_token}'",
|
58
|
+
node: node,
|
59
|
+
markup: node.markup[-1],
|
60
|
+
node_markup_offset: node.markup.size - 1,
|
61
|
+
)
|
62
62
|
elsif node.markup =~ /(\n?)( +)\z/m && Regexp.last_match(1) != "\n"
|
63
|
-
add_offense(
|
63
|
+
add_offense(
|
64
|
+
"Too many spaces before '#{node.end_token}'",
|
65
|
+
node: node,
|
66
|
+
markup: Regexp.last_match(2),
|
67
|
+
node_markup_offset: node.markup.size - Regexp.last_match(2).size
|
68
|
+
)
|
64
69
|
end
|
65
70
|
end
|
66
71
|
@ignore = true
|
@@ -73,22 +78,40 @@ module ThemeCheck
|
|
73
78
|
def on_variable(node)
|
74
79
|
return if @ignore || node.markup.empty?
|
75
80
|
if node.markup[0] != " "
|
76
|
-
add_offense(
|
81
|
+
add_offense(
|
82
|
+
"Space missing after '#{node.start_token}'",
|
83
|
+
node: node,
|
84
|
+
markup: node.markup[0]
|
85
|
+
) do |corrector|
|
77
86
|
corrector.insert_before(node, " ")
|
78
87
|
end
|
79
88
|
end
|
80
89
|
if node.markup[-1] != " " && node.markup[-1] != "\n"
|
81
|
-
add_offense(
|
90
|
+
add_offense(
|
91
|
+
"Space missing before '#{node.end_token}'",
|
92
|
+
node: node,
|
93
|
+
markup: node.markup[-1],
|
94
|
+
node_markup_offset: node.markup.size - 1,
|
95
|
+
) do |corrector|
|
82
96
|
corrector.insert_after(node, " ")
|
83
97
|
end
|
84
98
|
end
|
85
|
-
if node.markup
|
86
|
-
add_offense(
|
99
|
+
if node.markup =~ /\A( +)/m
|
100
|
+
add_offense(
|
101
|
+
"Too many spaces after '#{node.start_token}'",
|
102
|
+
node: node,
|
103
|
+
markup: Regexp.last_match(1),
|
104
|
+
) do |corrector|
|
87
105
|
corrector.replace(node, " #{node.markup.lstrip}")
|
88
106
|
end
|
89
107
|
end
|
90
|
-
if node.markup
|
91
|
-
add_offense(
|
108
|
+
if node.markup =~ /(\n?)( +)\z/m && Regexp.last_match(1) != "\n"
|
109
|
+
add_offense(
|
110
|
+
"Too many spaces before '#{node.end_token}'",
|
111
|
+
node: node,
|
112
|
+
markup: Regexp.last_match(2),
|
113
|
+
node_markup_offset: node.markup.size - Regexp.last_match(2).size
|
114
|
+
) do |corrector|
|
92
115
|
corrector.replace(node, "#{node.markup.rstrip} ")
|
93
116
|
end
|
94
117
|
end
|
@@ -29,7 +29,9 @@ module ThemeCheck
|
|
29
29
|
"'#{key_node.value}' does not have a matching entry in '#{@theme.default_locale_json.relative_path}'",
|
30
30
|
node: node,
|
31
31
|
markup: key_node.value,
|
32
|
-
)
|
32
|
+
) do |corrector|
|
33
|
+
corrector.add_default_translation_key(@theme.default_locale_json, key_node.value.split("."), "TODO")
|
34
|
+
end
|
33
35
|
end
|
34
36
|
end
|
35
37
|
|
@@ -24,7 +24,9 @@ module ThemeCheck
|
|
24
24
|
|
25
25
|
def on_end
|
26
26
|
missing_snippets.each do |template|
|
27
|
-
add_offense("This template is not used", template: template)
|
27
|
+
add_offense("This template is not used", template: template) do |corrector|
|
28
|
+
corrector.remove(@theme, template.relative_path.to_s)
|
29
|
+
end
|
28
30
|
end
|
29
31
|
end
|
30
32
|
|
data/lib/theme_check/checks.rb
CHANGED
@@ -50,6 +50,7 @@ module ThemeCheck
|
|
50
50
|
template = node.respond_to?(:template) ? node.template.relative_path : "?"
|
51
51
|
markup = node.respond_to?(:markup) ? node.markup : ""
|
52
52
|
node_class = node.respond_to?(:value) ? node.value.class : "?"
|
53
|
+
line_number = node.respond_to?(:line_number) ? node.line_number : "?"
|
53
54
|
|
54
55
|
ThemeCheck.bug(<<~EOS)
|
55
56
|
Exception while running `#{check.code_name}##{method}`:
|
@@ -64,6 +65,7 @@ module ThemeCheck
|
|
64
65
|
```
|
65
66
|
#{markup}
|
66
67
|
```
|
68
|
+
Line number: #{line_number}
|
67
69
|
Check options: `#{check.options.pretty_inspect}`
|
68
70
|
EOS
|
69
71
|
end
|
data/lib/theme_check/cli.rb
CHANGED
@@ -49,7 +49,7 @@ module ThemeCheck
|
|
49
49
|
"Automatically fix offenses"
|
50
50
|
) { @auto_correct = true }
|
51
51
|
@option_parser.on(
|
52
|
-
"--fail-level SEVERITY", Check::SEVERITIES,
|
52
|
+
"--fail-level SEVERITY", [:crash] + Check::SEVERITIES,
|
53
53
|
"Minimum severity (error|suggestion|style) for exit with error code"
|
54
54
|
) do |severity|
|
55
55
|
@fail_level = severity.to_sym
|
@@ -78,6 +78,15 @@ module ThemeCheck
|
|
78
78
|
"Print Theme Check version"
|
79
79
|
) { @command = :version }
|
80
80
|
|
81
|
+
if ENV["THEME_CHECK_DEBUG"]
|
82
|
+
@option_parser.separator("")
|
83
|
+
@option_parser.separator("Debugging:")
|
84
|
+
@option_parser.on(
|
85
|
+
"--profile",
|
86
|
+
"Output a profile to STDOUT compatible with FlameGraph."
|
87
|
+
) { @command = :profile }
|
88
|
+
end
|
89
|
+
|
81
90
|
@option_parser.separator("")
|
82
91
|
@option_parser.separator(<<~EOS)
|
83
92
|
Description:
|
@@ -172,7 +181,7 @@ module ThemeCheck
|
|
172
181
|
puts option_parser.to_s
|
173
182
|
end
|
174
183
|
|
175
|
-
def check
|
184
|
+
def check(out_stream = STDOUT)
|
176
185
|
STDERR.puts "Checking #{@config.root} ..."
|
177
186
|
storage = ThemeCheck::FileSystemStorage.new(@config.root, ignored_patterns: @config.ignored_patterns)
|
178
187
|
theme = ThemeCheck::Theme.new(storage)
|
@@ -182,18 +191,35 @@ module ThemeCheck
|
|
182
191
|
analyzer = ThemeCheck::Analyzer.new(theme, @config.enabled_checks, @config.auto_correct)
|
183
192
|
analyzer.analyze_theme
|
184
193
|
analyzer.correct_offenses
|
185
|
-
|
194
|
+
print_with_format(theme, analyzer, out_stream)
|
195
|
+
# corrections are committed after printing so that the
|
196
|
+
# source_excerpts are still pointing to the uncorrected source.
|
197
|
+
analyzer.write_corrections
|
186
198
|
raise Abort, "" if analyzer.uncorrectable_offenses.any? do |offense|
|
187
199
|
offense.check.severity_value <= Check.severity_value(@fail_level)
|
188
200
|
end
|
189
201
|
end
|
190
202
|
|
191
|
-
def
|
203
|
+
def profile
|
204
|
+
require 'ruby-prof-flamegraph'
|
205
|
+
|
206
|
+
result = RubyProf.profile do
|
207
|
+
check(STDERR)
|
208
|
+
end
|
209
|
+
|
210
|
+
# Print a graph profile to text
|
211
|
+
printer = RubyProf::FlameGraphPrinter.new(result)
|
212
|
+
printer.print(STDOUT, {})
|
213
|
+
rescue LoadError
|
214
|
+
STDERR.puts "Profiling is only available in development"
|
215
|
+
end
|
216
|
+
|
217
|
+
def print_with_format(theme, analyzer, out_stream)
|
192
218
|
case @format
|
193
219
|
when :text
|
194
|
-
ThemeCheck::Printer.new.print(theme, analyzer.offenses, @config.auto_correct)
|
220
|
+
ThemeCheck::Printer.new(out_stream).print(theme, analyzer.offenses, @config.auto_correct)
|
195
221
|
when :json
|
196
|
-
ThemeCheck::JsonPrinter.new.print(analyzer.offenses)
|
222
|
+
ThemeCheck::JsonPrinter.new(out_stream).print(analyzer.offenses)
|
197
223
|
end
|
198
224
|
end
|
199
225
|
end
|