theme-check 1.6.1 → 1.6.2
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/CHANGELOG.md +6 -0
- data/docs/api/html_check.md +7 -7
- data/docs/api/liquid_check.md +10 -10
- data/docs/checks/convert_include_to_render.md +1 -1
- data/docs/checks/missing_enable_comment.md +1 -1
- data/lib/theme_check/analyzer.rb +15 -15
- data/lib/theme_check/asset_file.rb +1 -1
- data/lib/theme_check/check.rb +2 -2
- data/lib/theme_check/checks/html_parsing_error.rb +2 -2
- data/lib/theme_check/checks/matching_translations.rb +1 -1
- data/lib/theme_check/checks/missing_template.rb +6 -6
- data/lib/theme_check/checks/nested_snippet.rb +2 -2
- data/lib/theme_check/checks/required_layout_theme_object.rb +2 -2
- data/lib/theme_check/checks/syntax_error.rb +5 -5
- data/lib/theme_check/checks/template_length.rb +2 -2
- data/lib/theme_check/checks/undefined_object.rb +7 -7
- data/lib/theme_check/checks/unused_assign.rb +4 -4
- data/lib/theme_check/checks/unused_snippet.rb +7 -7
- data/lib/theme_check/checks/valid_json.rb +1 -1
- data/lib/theme_check/checks.rb +2 -2
- data/lib/theme_check/cli.rb +1 -1
- data/lib/theme_check/corrector.rb +6 -6
- data/lib/theme_check/disabled_check.rb +3 -3
- data/lib/theme_check/disabled_checks.rb +9 -9
- data/lib/theme_check/html_node.rb +36 -28
- data/lib/theme_check/html_visitor.rb +6 -6
- data/lib/theme_check/in_memory_storage.rb +1 -1
- data/lib/theme_check/json_check.rb +2 -2
- data/lib/theme_check/language_server/diagnostics_tracker.rb +8 -8
- data/lib/theme_check/{template.rb → liquid_file.rb} +2 -2
- data/lib/theme_check/liquid_node.rb +291 -0
- data/lib/theme_check/{visitor.rb → liquid_visitor.rb} +4 -4
- data/lib/theme_check/locale_diff.rb +5 -5
- data/lib/theme_check/node.rb +12 -225
- data/lib/theme_check/offense.rb +15 -15
- data/lib/theme_check/position.rb +1 -1
- data/lib/theme_check/theme.rb +1 -1
- data/lib/theme_check/{template_rewriter.rb → theme_file_rewriter.rb} +1 -1
- data/lib/theme_check/version.rb +1 -1
- data/lib/theme_check.rb +11 -10
- data/theme-check.gemspec +1 -1
- metadata +8 -7
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module ThemeCheck
|
3
|
-
class
|
3
|
+
class LiquidVisitor
|
4
4
|
attr_reader :checks
|
5
5
|
|
6
6
|
def initialize(checks, disabled_checks)
|
@@ -8,10 +8,10 @@ module ThemeCheck
|
|
8
8
|
@disabled_checks = disabled_checks
|
9
9
|
end
|
10
10
|
|
11
|
-
def
|
12
|
-
visit(
|
11
|
+
def visit_liquid_file(liquid_file)
|
12
|
+
visit(LiquidNode.new(liquid_file.root, nil, liquid_file))
|
13
13
|
rescue Liquid::Error => exception
|
14
|
-
exception.template_name =
|
14
|
+
exception.template_name = liquid_file.name
|
15
15
|
call_checks(:on_error, exception)
|
16
16
|
end
|
17
17
|
|
@@ -14,26 +14,26 @@ module ThemeCheck
|
|
14
14
|
visit_object(@default, @other, [])
|
15
15
|
end
|
16
16
|
|
17
|
-
def add_as_offenses(check, key_prefix: [], node: nil,
|
17
|
+
def add_as_offenses(check, key_prefix: [], node: nil, theme_file: nil)
|
18
18
|
if extra_keys.any?
|
19
19
|
add_keys_offense(check, "Extra translation keys", extra_keys,
|
20
|
-
key_prefix: key_prefix, node: node,
|
20
|
+
key_prefix: key_prefix, node: node, theme_file: theme_file)
|
21
21
|
end
|
22
22
|
|
23
23
|
if missing_keys.any?
|
24
24
|
add_keys_offense(check, "Missing translation keys", missing_keys,
|
25
|
-
key_prefix: key_prefix, node: node,
|
25
|
+
key_prefix: key_prefix, node: node, theme_file: theme_file)
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
29
|
private
|
30
30
|
|
31
|
-
def add_keys_offense(check, cause, keys, key_prefix:, node: nil,
|
31
|
+
def add_keys_offense(check, cause, keys, key_prefix:, node: nil, theme_file: nil)
|
32
32
|
message = "#{cause}: #{format_keys(key_prefix, keys)}"
|
33
33
|
if node
|
34
34
|
check.add_offense(message, node: node)
|
35
35
|
else
|
36
|
-
check.add_offense(message,
|
36
|
+
check.add_offense(message, theme_file: theme_file)
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
data/lib/theme_check/node.rb
CHANGED
@@ -1,250 +1,37 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ThemeCheck
|
4
|
-
# A node from the Liquid AST, the result of parsing a template.
|
5
4
|
class Node
|
6
|
-
|
7
|
-
|
8
|
-
def initialize(value, parent, template)
|
9
|
-
raise ArgumentError, "Expected a Liquid AST Node" if value.is_a?(Node)
|
10
|
-
@value = value
|
11
|
-
@parent = parent
|
12
|
-
@template = template
|
13
|
-
@tag_markup = nil
|
14
|
-
@line_number_offset = 0
|
5
|
+
def parent
|
6
|
+
raise NotImplementedError
|
15
7
|
end
|
16
8
|
|
17
|
-
|
18
|
-
|
19
|
-
if tag?
|
20
|
-
tag_markup
|
21
|
-
elsif @value.instance_variable_defined?(:@markup)
|
22
|
-
@value.instance_variable_get(:@markup)
|
23
|
-
end
|
9
|
+
def theme_file
|
10
|
+
raise NotImplementedError
|
24
11
|
end
|
25
12
|
|
26
|
-
def
|
27
|
-
|
28
|
-
@value.instance_variable_set(:@markup, markup)
|
29
|
-
end
|
13
|
+
def value
|
14
|
+
raise NotImplementedError
|
30
15
|
end
|
31
16
|
|
32
|
-
# Array of children nodes.
|
33
17
|
def children
|
34
|
-
|
35
|
-
nodes =
|
36
|
-
if comment?
|
37
|
-
[]
|
38
|
-
elsif defined?(@value.class::ParseTreeVisitor)
|
39
|
-
@value.class::ParseTreeVisitor.new(@value, {}).children
|
40
|
-
elsif @value.respond_to?(:nodelist)
|
41
|
-
Array(@value.nodelist)
|
42
|
-
else
|
43
|
-
[]
|
44
|
-
end
|
45
|
-
# Work around a bug in Liquid::Variable::ParseTreeVisitor that doesn't return
|
46
|
-
# the args in a hash as children nodes.
|
47
|
-
nodes = nodes.flat_map do |node|
|
48
|
-
case node
|
49
|
-
when Hash
|
50
|
-
node.values
|
51
|
-
else
|
52
|
-
node
|
53
|
-
end
|
54
|
-
end
|
55
|
-
nodes.map { |node| Node.new(node, self, @template) }
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
# Literals are hard-coded values in the template.
|
60
|
-
def literal?
|
61
|
-
@value.is_a?(String) || @value.is_a?(Integer)
|
62
|
-
end
|
63
|
-
|
64
|
-
# A {% tag %} node?
|
65
|
-
def tag?
|
66
|
-
@value.is_a?(Liquid::Tag)
|
67
|
-
end
|
68
|
-
|
69
|
-
def variable?
|
70
|
-
@value.is_a?(Liquid::Variable)
|
71
|
-
end
|
72
|
-
|
73
|
-
# A {% comment %} block node?
|
74
|
-
def comment?
|
75
|
-
@value.is_a?(Liquid::Comment)
|
76
|
-
end
|
77
|
-
|
78
|
-
# Top level node of every template.
|
79
|
-
def document?
|
80
|
-
@value.is_a?(Liquid::Document)
|
81
|
-
end
|
82
|
-
alias_method :root?, :document?
|
83
|
-
|
84
|
-
# A {% tag %}...{% endtag %} node?
|
85
|
-
def block_tag?
|
86
|
-
@value.is_a?(Liquid::Block)
|
18
|
+
raise NotImplementedError
|
87
19
|
end
|
88
20
|
|
89
|
-
|
90
|
-
|
91
|
-
@value.is_a?(Liquid::BlockBody)
|
92
|
-
end
|
93
|
-
|
94
|
-
# A block of type of node?
|
95
|
-
def block?
|
96
|
-
block_tag? || block_body? || document?
|
21
|
+
def markup
|
22
|
+
raise NotImplementedError
|
97
23
|
end
|
98
24
|
|
99
|
-
# Most nodes have a line number, but it's not guaranteed.
|
100
25
|
def line_number
|
101
|
-
|
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
|
107
|
-
end
|
108
|
-
|
109
|
-
# The `:under_score_name` of this type of node. Used to dispatch to the `on_<type_name>`
|
110
|
-
# and `after_<type_name>` check methods.
|
111
|
-
def type_name
|
112
|
-
@type_name ||= StringHelpers.underscore(StringHelpers.demodulize(@value.class.name)).to_sym
|
113
|
-
end
|
114
|
-
|
115
|
-
def source
|
116
|
-
template&.source
|
117
|
-
end
|
118
|
-
|
119
|
-
WHITESPACE = /\s/
|
120
|
-
|
121
|
-
# Is this node inside a `{% liquid ... %}` block?
|
122
|
-
def inside_liquid_tag?
|
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 != "%-"
|
132
|
-
else
|
133
|
-
false
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
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] == "-"
|
143
|
-
else
|
144
|
-
false
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
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
|
157
|
-
end
|
158
|
-
|
159
|
-
def position
|
160
|
-
@position ||= Position.new(
|
161
|
-
markup,
|
162
|
-
template&.source,
|
163
|
-
line_number_1_indexed: line_number
|
164
|
-
)
|
26
|
+
raise NotImplementedError
|
165
27
|
end
|
166
28
|
|
167
29
|
def start_index
|
168
|
-
|
30
|
+
raise NotImplementedError
|
169
31
|
end
|
170
32
|
|
171
33
|
def end_index
|
172
|
-
|
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]
|
34
|
+
raise NotImplementedError
|
248
35
|
end
|
249
36
|
end
|
250
37
|
end
|
data/lib/theme_check/offense.rb
CHANGED
@@ -5,13 +5,13 @@ module ThemeCheck
|
|
5
5
|
|
6
6
|
MAX_SOURCE_EXCERPT_SIZE = 120
|
7
7
|
|
8
|
-
attr_reader :check, :message, :
|
8
|
+
attr_reader :check, :message, :theme_file, :node, :markup, :line_number, :correction
|
9
9
|
|
10
10
|
def initialize(
|
11
11
|
check:, # instance of a ThemeCheck::Check
|
12
12
|
message: nil, # error message for the offense
|
13
|
-
|
14
|
-
node: nil, # Node
|
13
|
+
theme_file: nil, # ThemeFile
|
14
|
+
node: nil, # Node
|
15
15
|
markup: nil, # string
|
16
16
|
line_number: nil, # line number of the error (1-indexed)
|
17
17
|
# node_markup_offset is the index inside node.markup to start
|
@@ -39,11 +39,11 @@ module ThemeCheck
|
|
39
39
|
end
|
40
40
|
|
41
41
|
@node = node
|
42
|
-
@
|
42
|
+
@theme_file = nil
|
43
43
|
if node
|
44
|
-
@
|
45
|
-
elsif
|
46
|
-
@
|
44
|
+
@theme_file = node.theme_file
|
45
|
+
elsif theme_file
|
46
|
+
@theme_file = theme_file
|
47
47
|
end
|
48
48
|
|
49
49
|
@markup = if markup
|
@@ -62,7 +62,7 @@ module ThemeCheck
|
|
62
62
|
|
63
63
|
@position = Position.new(
|
64
64
|
@markup,
|
65
|
-
@
|
65
|
+
@theme_file&.source,
|
66
66
|
line_number_1_indexed: @line_number,
|
67
67
|
node_markup_offset: node_markup_offset,
|
68
68
|
node_markup: node&.markup
|
@@ -72,7 +72,7 @@ module ThemeCheck
|
|
72
72
|
def source_excerpt
|
73
73
|
return unless line_number
|
74
74
|
@source_excerpt ||= begin
|
75
|
-
excerpt =
|
75
|
+
excerpt = theme_file.source_excerpt(line_number)
|
76
76
|
if excerpt.size > MAX_SOURCE_EXCERPT_SIZE
|
77
77
|
excerpt[0, MAX_SOURCE_EXCERPT_SIZE - 3] + '...'
|
78
78
|
else
|
@@ -126,12 +126,12 @@ module ThemeCheck
|
|
126
126
|
end
|
127
127
|
|
128
128
|
def location
|
129
|
-
tokens = [
|
129
|
+
tokens = [theme_file&.relative_path, line_number].compact
|
130
130
|
tokens.join(":") if tokens.any?
|
131
131
|
end
|
132
132
|
|
133
133
|
def location_range
|
134
|
-
tokens = [
|
134
|
+
tokens = [theme_file&.relative_path, start_index, end_index].compact
|
135
135
|
tokens.join(":") if tokens.any?
|
136
136
|
end
|
137
137
|
|
@@ -141,7 +141,7 @@ module ThemeCheck
|
|
141
141
|
|
142
142
|
def correct
|
143
143
|
if correctable?
|
144
|
-
corrector = Corrector.new(
|
144
|
+
corrector = Corrector.new(theme_file: theme_file)
|
145
145
|
correction.call(corrector)
|
146
146
|
end
|
147
147
|
rescue => e
|
@@ -191,7 +191,7 @@ module ThemeCheck
|
|
191
191
|
alias_method :eql?, :==
|
192
192
|
|
193
193
|
def to_s
|
194
|
-
if
|
194
|
+
if theme_file
|
195
195
|
"#{message} at #{location}"
|
196
196
|
else
|
197
197
|
message
|
@@ -199,7 +199,7 @@ module ThemeCheck
|
|
199
199
|
end
|
200
200
|
|
201
201
|
def to_s_range
|
202
|
-
if
|
202
|
+
if theme_file
|
203
203
|
"#{message} at #{location_range}"
|
204
204
|
else
|
205
205
|
message
|
@@ -209,7 +209,7 @@ module ThemeCheck
|
|
209
209
|
def to_h
|
210
210
|
{
|
211
211
|
check: check.code_name,
|
212
|
-
path:
|
212
|
+
path: theme_file&.relative_path,
|
213
213
|
severity: check.severity_value,
|
214
214
|
start_line: start_line,
|
215
215
|
start_column: start_column,
|
data/lib/theme_check/position.rb
CHANGED
data/lib/theme_check/theme.rb
CHANGED
data/lib/theme_check/version.rb
CHANGED