theme-check 1.6.1 → 1.6.2
Sign up to get free protection for your applications and to get access to all the features.
- 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