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
@@ -7,25 +7,20 @@ module ThemeCheck
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def insert_after(node, content)
|
10
|
-
|
11
|
-
line.insert(node.range[1] + 1, content)
|
10
|
+
@template.rewriter.insert_after(node, content)
|
12
11
|
end
|
13
12
|
|
14
13
|
def insert_before(node, content)
|
15
|
-
|
16
|
-
line.insert(node.range[0], content)
|
14
|
+
@template.rewriter.insert_before(node, content)
|
17
15
|
end
|
18
16
|
|
19
17
|
def replace(node, content)
|
20
|
-
|
21
|
-
line[node.range[0]..node.range[1]] = content
|
18
|
+
@template.rewriter.replace(node, content)
|
22
19
|
node.markup = content
|
23
20
|
end
|
24
21
|
|
25
22
|
def wrap(node, insert_before, insert_after)
|
26
|
-
|
27
|
-
line.insert(node.range[0], insert_before)
|
28
|
-
line.insert(node.range[1] + 1 + insert_before.length, insert_after)
|
23
|
+
@template.rewriter.wrap(node, insert_before, insert_after)
|
29
24
|
end
|
30
25
|
|
31
26
|
def create(theme, relative_path, content)
|
@@ -34,7 +29,25 @@ module ThemeCheck
|
|
34
29
|
|
35
30
|
def create_default_locale_json(theme)
|
36
31
|
theme.default_locale_json = JsonFile.new("locales/#{theme.default_locale}.default.json", theme.storage)
|
37
|
-
theme.default_locale_json.update_contents(
|
32
|
+
theme.default_locale_json.update_contents({})
|
33
|
+
end
|
34
|
+
|
35
|
+
def remove(theme, relative_path)
|
36
|
+
theme.storage.remove(relative_path)
|
37
|
+
end
|
38
|
+
|
39
|
+
def mkdir(theme, relative_path)
|
40
|
+
theme.storage.mkdir(relative_path)
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_default_translation_key(file, key, value)
|
44
|
+
hash = file.content
|
45
|
+
key.reduce(hash) do |pointer, token|
|
46
|
+
return pointer[token] = value if token == key.last
|
47
|
+
pointer[token] = {} unless pointer.key?(token)
|
48
|
+
pointer[token]
|
49
|
+
end
|
50
|
+
file.update_contents(hash)
|
38
51
|
end
|
39
52
|
end
|
40
53
|
end
|
@@ -16,14 +16,25 @@ module ThemeCheck
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def read(relative_path)
|
19
|
-
file(relative_path).read
|
19
|
+
file(relative_path).read(mode: 'rb', encoding: 'UTF-8')
|
20
20
|
end
|
21
21
|
|
22
22
|
def write(relative_path, content)
|
23
23
|
reset_memoizers unless file_exists?(relative_path)
|
24
24
|
|
25
25
|
file(relative_path).dirname.mkpath unless file(relative_path).dirname.directory?
|
26
|
-
file(relative_path).write(content)
|
26
|
+
file(relative_path).write(content, mode: 'w+b', encoding: 'UTF-8')
|
27
|
+
end
|
28
|
+
|
29
|
+
def remove(relative_path)
|
30
|
+
file(relative_path).delete
|
31
|
+
reset_memoizers
|
32
|
+
end
|
33
|
+
|
34
|
+
def mkdir(relative_path)
|
35
|
+
reset_memoizers unless file_exists?(relative_path)
|
36
|
+
|
37
|
+
file(relative_path).mkpath unless file(relative_path).directory?
|
27
38
|
end
|
28
39
|
|
29
40
|
def files
|
@@ -67,10 +67,10 @@ module ThemeCheck
|
|
67
67
|
private
|
68
68
|
|
69
69
|
def replace_placeholders(string)
|
70
|
-
# Replace all {
|
71
|
-
string.gsub(
|
72
|
-
key =
|
73
|
-
@placeholder_values[key.to_i]
|
70
|
+
# Replace all ≬{i}####≬ with the actual content.
|
71
|
+
string.gsub(HTML_LIQUID_PLACEHOLDER) do |match|
|
72
|
+
key = /[0-9a-z]+/.match(match)[0]
|
73
|
+
@placeholder_values[key.to_i(36)]
|
74
74
|
end
|
75
75
|
end
|
76
76
|
end
|
@@ -9,12 +9,11 @@ module ThemeCheck
|
|
9
9
|
|
10
10
|
def initialize(checks)
|
11
11
|
@checks = checks
|
12
|
-
@placeholder_values = []
|
13
12
|
end
|
14
13
|
|
15
14
|
def visit_template(template)
|
16
|
-
doc = parse(template)
|
17
|
-
visit(HtmlNode.new(doc, template,
|
15
|
+
doc, placeholder_values = parse(template)
|
16
|
+
visit(HtmlNode.new(doc, template, placeholder_values))
|
18
17
|
rescue ArgumentError => e
|
19
18
|
call_checks(:on_parse_error, e, template)
|
20
19
|
end
|
@@ -22,19 +21,32 @@ module ThemeCheck
|
|
22
21
|
private
|
23
22
|
|
24
23
|
def parse(template)
|
24
|
+
placeholder_values = []
|
25
25
|
parseable_source = +template.source.clone
|
26
26
|
|
27
|
-
# Replace all liquid tags with {
|
27
|
+
# Replace all non-empty liquid tags with ≬{i}######≬ to prevent the HTML
|
28
28
|
# parser from freaking out. We transparently replace those placeholders in
|
29
29
|
# HtmlNode.
|
30
|
+
#
|
31
|
+
# We're using base36 to prevent index bleeding on 36^3 tags.
|
32
|
+
# `{{x}}` -> `≬#{i}≬` would properly be transformed for 46656 tags in a single file.
|
33
|
+
# Should be enough.
|
34
|
+
#
|
35
|
+
# The base10 alternative would have overflowed at 1000 (`{{x}}` -> `≬1000≬`) which seemed more likely.
|
36
|
+
#
|
37
|
+
# Didn't go with base64 because of the `=` character that would have messed with HTML parsing.
|
30
38
|
matches(parseable_source, LIQUID_TAG_OR_VARIABLE).each do |m|
|
31
39
|
value = m[0]
|
32
|
-
|
33
|
-
|
34
|
-
|
40
|
+
next unless value.size > 4 # skip empty tags/variables {%%} and {{}}
|
41
|
+
placeholder_values.push(value)
|
42
|
+
key = (placeholder_values.size - 1).to_s(36)
|
43
|
+
parseable_source[m.begin(0)...m.end(0)] = "≬#{key.ljust(m.end(0) - m.begin(0) - 2, '#')}≬"
|
35
44
|
end
|
36
45
|
|
37
|
-
|
46
|
+
[
|
47
|
+
Nokogiri::HTML5.fragment(parseable_source, max_tree_depth: 400, max_attributes: 400),
|
48
|
+
placeholder_values,
|
49
|
+
]
|
38
50
|
end
|
39
51
|
|
40
52
|
def visit(node)
|
@@ -20,14 +20,19 @@ module ThemeCheck
|
|
20
20
|
@parser_error
|
21
21
|
end
|
22
22
|
|
23
|
-
def update_contents(new_content =
|
23
|
+
def update_contents(new_content = {})
|
24
|
+
raise ArgumentError if new_content.is_a?(String)
|
24
25
|
@content = new_content
|
25
26
|
end
|
26
27
|
|
27
28
|
def write
|
28
|
-
|
29
|
-
|
30
|
-
|
29
|
+
pretty = JSON.pretty_generate(@content)
|
30
|
+
if source.rstrip != pretty.rstrip
|
31
|
+
# Most editors add a trailing \n at the end of files. Here we
|
32
|
+
# try to maintain the convention.
|
33
|
+
eof = source.end_with?("\n") ? "\n" : ""
|
34
|
+
@storage.write(@relative_path, pretty.gsub("\n", @eol) + eof)
|
35
|
+
@source = pretty
|
31
36
|
end
|
32
37
|
end
|
33
38
|
|
@@ -3,9 +3,13 @@ require 'json'
|
|
3
3
|
|
4
4
|
module ThemeCheck
|
5
5
|
class JsonPrinter
|
6
|
+
def initialize(out_stream = STDOUT)
|
7
|
+
@out = out_stream
|
8
|
+
end
|
9
|
+
|
6
10
|
def print(offenses)
|
7
11
|
json = offenses_by_path(offenses)
|
8
|
-
puts JSON.dump(json)
|
12
|
+
@out.puts JSON.dump(json)
|
9
13
|
end
|
10
14
|
|
11
15
|
def offenses_by_path(offenses)
|
data/lib/theme_check/node.rb
CHANGED
@@ -10,12 +10,14 @@ module ThemeCheck
|
|
10
10
|
@value = value
|
11
11
|
@parent = parent
|
12
12
|
@template = template
|
13
|
+
@tag_markup = nil
|
14
|
+
@line_number_offset = 0
|
13
15
|
end
|
14
16
|
|
15
17
|
# The original source code of the node. Doesn't contain wrapping braces.
|
16
18
|
def markup
|
17
19
|
if tag?
|
18
|
-
|
20
|
+
tag_markup
|
19
21
|
elsif @value.instance_variable_defined?(:@markup)
|
20
22
|
@value.instance_variable_get(:@markup)
|
21
23
|
end
|
@@ -64,6 +66,10 @@ module ThemeCheck
|
|
64
66
|
@value.is_a?(Liquid::Tag)
|
65
67
|
end
|
66
68
|
|
69
|
+
def variable?
|
70
|
+
@value.is_a?(Liquid::Variable)
|
71
|
+
end
|
72
|
+
|
67
73
|
# A {% comment %} block node?
|
68
74
|
def comment?
|
69
75
|
@value.is_a?(Liquid::Comment)
|
@@ -92,7 +98,12 @@ module ThemeCheck
|
|
92
98
|
|
93
99
|
# Most nodes have a line number, but it's not guaranteed.
|
94
100
|
def line_number
|
95
|
-
|
101
|
+
if tag? && @value.respond_to?(:line_number)
|
102
|
+
markup # initialize the line_number_offset
|
103
|
+
@value.line_number - @line_number_offset
|
104
|
+
elsif @value.respond_to?(:line_number)
|
105
|
+
@value.line_number
|
106
|
+
end
|
96
107
|
end
|
97
108
|
|
98
109
|
# The `:under_score_name` of this type of node. Used to dispatch to the `on_<type_name>`
|
@@ -101,27 +112,48 @@ module ThemeCheck
|
|
101
112
|
@type_name ||= StringHelpers.underscore(StringHelpers.demodulize(@value.class.name)).to_sym
|
102
113
|
end
|
103
114
|
|
115
|
+
def source
|
116
|
+
template&.source
|
117
|
+
end
|
118
|
+
|
119
|
+
WHITESPACE = /\s/
|
120
|
+
|
104
121
|
# Is this node inside a `{% liquid ... %}` block?
|
105
122
|
def inside_liquid_tag?
|
106
|
-
|
107
|
-
|
123
|
+
# What we're doing here is starting at the start of the tag and
|
124
|
+
# backtrack on all the whitespace until we land on something. If
|
125
|
+
# that something is {% or %-, then we can safely assume that
|
126
|
+
# we're inside a full tag and not a liquid tag.
|
127
|
+
@inside_liquid_tag ||= if tag? && line_number && source
|
128
|
+
i = 1
|
129
|
+
i += 1 while source[start_index - i] =~ WHITESPACE && i < start_index
|
130
|
+
first_two_backtracked_characters = source[(start_index - i - 1)..(start_index - i)]
|
131
|
+
first_two_backtracked_characters != "{%" && first_two_backtracked_characters != "%-"
|
108
132
|
else
|
109
133
|
false
|
110
134
|
end
|
111
135
|
end
|
112
136
|
|
113
|
-
# Is this node inside a
|
114
|
-
def
|
115
|
-
if line_number
|
116
|
-
|
137
|
+
# Is this node inside a tag or variable that starts by removing whitespace. i.e. {%- or {{-
|
138
|
+
def whitespace_trimmed_start?
|
139
|
+
@whitespace_trimmed_start ||= if line_number && source && !inside_liquid_tag?
|
140
|
+
i = 1
|
141
|
+
i += 1 while source[start_index - i] =~ WHITESPACE && i < start_index
|
142
|
+
source[start_index - i] == "-"
|
117
143
|
else
|
118
144
|
false
|
119
145
|
end
|
120
146
|
end
|
121
147
|
|
122
|
-
|
123
|
-
|
124
|
-
|
148
|
+
# Is this node inside a tag or variable ends starts by removing whitespace. i.e. -%} or -}}
|
149
|
+
def whitespace_trimmed_end?
|
150
|
+
@whitespace_trimmed_end ||= if line_number && source && !inside_liquid_tag?
|
151
|
+
i = 0
|
152
|
+
i += 1 while source[end_index + i] =~ WHITESPACE && i < source.size
|
153
|
+
source[end_index + i] == "-"
|
154
|
+
else
|
155
|
+
false
|
156
|
+
end
|
125
157
|
end
|
126
158
|
|
127
159
|
def position
|
@@ -139,5 +171,80 @@ module ThemeCheck
|
|
139
171
|
def end_index
|
140
172
|
position.end_index
|
141
173
|
end
|
174
|
+
|
175
|
+
def start_token
|
176
|
+
return "" if inside_liquid_tag?
|
177
|
+
output = ""
|
178
|
+
output += "{{" if variable?
|
179
|
+
output += "{%" if tag?
|
180
|
+
output += "-" if whitespace_trimmed_start?
|
181
|
+
output
|
182
|
+
end
|
183
|
+
|
184
|
+
def end_token
|
185
|
+
return "" if inside_liquid_tag?
|
186
|
+
output = ""
|
187
|
+
output += "-" if whitespace_trimmed_end?
|
188
|
+
output += "}}" if variable?
|
189
|
+
output += "%}" if tag?
|
190
|
+
output
|
191
|
+
end
|
192
|
+
|
193
|
+
private
|
194
|
+
|
195
|
+
# Here we're hacking around a glorious bug in Liquid that makes it so the
|
196
|
+
# line_number and markup of a tag is wrong if there's whitespace
|
197
|
+
# between the tag_name and the markup of the tag.
|
198
|
+
#
|
199
|
+
# {%
|
200
|
+
# render
|
201
|
+
# 'foo'
|
202
|
+
# %}
|
203
|
+
#
|
204
|
+
# Returns a raw value of "render 'foo'\n".
|
205
|
+
# The "\n " between render and 'foo' got replaced by a single space.
|
206
|
+
#
|
207
|
+
# And the line number is the one of 'foo'\n%}. Yay!
|
208
|
+
#
|
209
|
+
# This breaks any kind of position logic we have since that string
|
210
|
+
# does not exist in the template.
|
211
|
+
def tag_markup
|
212
|
+
return @value.tag_name if @value.instance_variable_get('@markup').empty?
|
213
|
+
return @tag_markup if @tag_markup
|
214
|
+
|
215
|
+
l = 1
|
216
|
+
scanner = StringScanner.new(source)
|
217
|
+
scanner.scan_until(/\n/) while l < @value.line_number && (l += 1)
|
218
|
+
start = scanner.charpos
|
219
|
+
|
220
|
+
tag_markup = @value.instance_variable_get('@markup')
|
221
|
+
|
222
|
+
# See https://github.com/Shopify/theme-check/pull/423/files#r701936559 for a detailed explanation
|
223
|
+
# of why we're doing the check below.
|
224
|
+
#
|
225
|
+
# TL;DR it's because line_numbers are not enough to accurately
|
226
|
+
# determine the position of the raw markup and because that
|
227
|
+
# markup could be present on the same line outside of a Tag. e.g.
|
228
|
+
#
|
229
|
+
# uhoh {% if uhoh %}
|
230
|
+
if (match = /#{@value.tag_name} +#{Regexp.escape(tag_markup)}/.match(source, start))
|
231
|
+
return @tag_markup = match[0]
|
232
|
+
end
|
233
|
+
|
234
|
+
# find the markup
|
235
|
+
markup_start = source.index(tag_markup, start)
|
236
|
+
markup_end = markup_start + tag_markup.size
|
237
|
+
|
238
|
+
# go back until you find the tag_name
|
239
|
+
tag_start = markup_start
|
240
|
+
tag_start -= 1 while source[tag_start - 1] =~ WHITESPACE
|
241
|
+
tag_start -= @value.tag_name.size
|
242
|
+
|
243
|
+
# keep track of the error in line_number
|
244
|
+
@line_number_offset = source[tag_start...markup_start].count("\n")
|
245
|
+
|
246
|
+
# return the real raw content
|
247
|
+
@tag_markup = source[tag_start...markup_end]
|
248
|
+
end
|
142
249
|
end
|
143
250
|
end
|
data/lib/theme_check/offense.rb
CHANGED
@@ -144,6 +144,32 @@ module ThemeCheck
|
|
144
144
|
corrector = Corrector.new(template: template)
|
145
145
|
correction.call(corrector)
|
146
146
|
end
|
147
|
+
rescue => e
|
148
|
+
ThemeCheck.bug(<<~EOS)
|
149
|
+
Exception while running `Offense#correct`:
|
150
|
+
```
|
151
|
+
#{e.class}: #{e.message}
|
152
|
+
#{e.backtrace.join("\n ")}
|
153
|
+
```
|
154
|
+
|
155
|
+
Offense:
|
156
|
+
```
|
157
|
+
#{JSON.pretty_generate(to_h)}
|
158
|
+
```
|
159
|
+
Check options:
|
160
|
+
```
|
161
|
+
#{check.options.pretty_inspect}
|
162
|
+
```
|
163
|
+
Markup:
|
164
|
+
```
|
165
|
+
#{markup}
|
166
|
+
```
|
167
|
+
Node.Markup:
|
168
|
+
```
|
169
|
+
#{node&.markup}
|
170
|
+
```
|
171
|
+
EOS
|
172
|
+
exit(2)
|
147
173
|
end
|
148
174
|
|
149
175
|
def whole_theme?
|
data/lib/theme_check/position.rb
CHANGED
@@ -16,22 +16,22 @@ module ThemeCheck
|
|
16
16
|
@line_number_1_indexed = line_number_1_indexed
|
17
17
|
@node_markup_offset = node_markup_offset
|
18
18
|
@node_markup = node_markup
|
19
|
-
@strict_position = StrictPosition.new(
|
20
|
-
needle,
|
21
|
-
contents,
|
22
|
-
start_index,
|
23
|
-
)
|
24
19
|
end
|
25
20
|
|
26
21
|
def start_line_offset
|
27
|
-
from_row_column_to_index(contents, line_number, 0)
|
22
|
+
@start_line_offset ||= from_row_column_to_index(contents, line_number, 0)
|
28
23
|
end
|
29
24
|
|
30
25
|
def start_offset
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
26
|
+
@start_offset ||= compute_start_offset
|
27
|
+
end
|
28
|
+
|
29
|
+
def strict_position
|
30
|
+
@strict_position ||= StrictPosition.new(
|
31
|
+
needle,
|
32
|
+
contents,
|
33
|
+
start_index,
|
34
|
+
)
|
35
35
|
end
|
36
36
|
|
37
37
|
# 0-indexed, inclusive
|
@@ -41,39 +41,50 @@ module ThemeCheck
|
|
41
41
|
|
42
42
|
# 0-indexed, exclusive
|
43
43
|
def end_index
|
44
|
-
|
44
|
+
strict_position.end_index
|
45
45
|
end
|
46
46
|
|
47
47
|
# 0-indexed, inclusive
|
48
48
|
def start_row
|
49
|
-
|
49
|
+
strict_position.start_row
|
50
50
|
end
|
51
51
|
|
52
52
|
# 0-indexed, inclusive
|
53
53
|
def start_column
|
54
|
-
|
54
|
+
strict_position.start_column
|
55
55
|
end
|
56
56
|
|
57
57
|
# 0-indexed, exclusive (both taken together are) therefore you
|
58
58
|
# might end up on a newline character or the next line
|
59
59
|
def end_row
|
60
|
-
|
60
|
+
strict_position.end_row
|
61
61
|
end
|
62
62
|
|
63
63
|
def end_column
|
64
|
-
|
64
|
+
strict_position.end_column
|
65
65
|
end
|
66
66
|
|
67
67
|
private
|
68
68
|
|
69
|
+
def compute_start_offset
|
70
|
+
return start_line_offset if @node_markup.nil?
|
71
|
+
node_markup_start = contents.index(@node_markup, start_line_offset)
|
72
|
+
return start_line_offset if node_markup_start.nil?
|
73
|
+
node_markup_start + @node_markup_offset
|
74
|
+
end
|
75
|
+
|
69
76
|
def contents
|
70
77
|
return '' unless @contents.is_a?(String) && !@contents.empty?
|
71
78
|
@contents
|
72
79
|
end
|
73
80
|
|
81
|
+
def content_line_count
|
82
|
+
@content_line_count ||= contents.count("\n")
|
83
|
+
end
|
84
|
+
|
74
85
|
def line_number
|
75
86
|
return 0 if @line_number_1_indexed.nil?
|
76
|
-
bounded(0, @line_number_1_indexed - 1,
|
87
|
+
bounded(0, @line_number_1_indexed - 1, content_line_count)
|
77
88
|
end
|
78
89
|
|
79
90
|
def needle
|