theme-check 1.6.0 → 1.7.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -0
- data/data/shopify_liquid/tags.yml +9 -9
- 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 +41 -17
- 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/translation_key_exists.rb +1 -13
- 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 +4 -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/exceptions.rb +1 -0
- data/lib/theme_check/file_system_storage.rb +4 -0
- 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/bridge.rb +128 -0
- data/lib/theme_check/language_server/channel.rb +69 -0
- data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +3 -1
- data/lib/theme_check/language_server/diagnostics_engine.rb +125 -0
- data/lib/theme_check/language_server/diagnostics_tracker.rb +8 -8
- data/lib/theme_check/language_server/handler.rb +20 -117
- data/lib/theme_check/language_server/io_messenger.rb +97 -0
- data/lib/theme_check/language_server/messenger.rb +27 -0
- data/lib/theme_check/language_server/server.rb +95 -104
- data/lib/theme_check/language_server.rb +6 -1
- 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 +14 -7
- 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/shopify_liquid/system_translations.rb +35 -0
- data/lib/theme_check/shopify_liquid/tag.rb +19 -1
- data/lib/theme_check/shopify_liquid.rb +1 -0
- 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 +14 -7
@@ -0,0 +1,291 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
# A node from the Liquid AST, the result of parsing a liquid file.
|
5
|
+
class LiquidNode < Node
|
6
|
+
attr_reader :value, :parent, :theme_file
|
7
|
+
|
8
|
+
def initialize(value, parent, theme_file)
|
9
|
+
raise ArgumentError, "Expected a Liquid AST Node" if value.is_a?(LiquidNode)
|
10
|
+
@value = value
|
11
|
+
@parent = parent
|
12
|
+
@theme_file = theme_file
|
13
|
+
@tag_markup = nil
|
14
|
+
@line_number_offset = 0
|
15
|
+
end
|
16
|
+
|
17
|
+
# Array of children nodes.
|
18
|
+
def children
|
19
|
+
@children ||= begin
|
20
|
+
nodes =
|
21
|
+
if comment?
|
22
|
+
[]
|
23
|
+
elsif defined?(@value.class::ParseTreeVisitor)
|
24
|
+
@value.class::ParseTreeVisitor.new(@value, {}).children
|
25
|
+
elsif @value.respond_to?(:nodelist)
|
26
|
+
Array(@value.nodelist)
|
27
|
+
else
|
28
|
+
[]
|
29
|
+
end
|
30
|
+
# Work around a bug in Liquid::Variable::ParseTreeVisitor that doesn't return
|
31
|
+
# the args in a hash as children nodes.
|
32
|
+
nodes = nodes.flat_map do |node|
|
33
|
+
case node
|
34
|
+
when Hash
|
35
|
+
node.values
|
36
|
+
else
|
37
|
+
node
|
38
|
+
end
|
39
|
+
end
|
40
|
+
nodes.map { |node| LiquidNode.new(node, self, @theme_file) }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# The original source code of the node. Doesn't contain wrapping braces.
|
45
|
+
def markup
|
46
|
+
if tag?
|
47
|
+
tag_markup
|
48
|
+
elsif @value.instance_variable_defined?(:@markup)
|
49
|
+
@value.instance_variable_get(:@markup)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def markup=(markup)
|
54
|
+
if @value.instance_variable_defined?(:@markup)
|
55
|
+
@value.instance_variable_set(:@markup, markup)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Most nodes have a line number, but it's not guaranteed.
|
60
|
+
def line_number
|
61
|
+
if tag? && @value.respond_to?(:line_number)
|
62
|
+
markup # initialize the line_number_offset
|
63
|
+
@value.line_number - @line_number_offset
|
64
|
+
elsif @value.respond_to?(:line_number)
|
65
|
+
@value.line_number
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def start_index
|
70
|
+
position.start_index
|
71
|
+
end
|
72
|
+
|
73
|
+
def end_index
|
74
|
+
position.end_index
|
75
|
+
end
|
76
|
+
|
77
|
+
# Literals are hard-coded values in the liquid file.
|
78
|
+
def literal?
|
79
|
+
@value.is_a?(String) || @value.is_a?(Integer)
|
80
|
+
end
|
81
|
+
|
82
|
+
# A {% tag %} node?
|
83
|
+
def tag?
|
84
|
+
@value.is_a?(Liquid::Tag)
|
85
|
+
end
|
86
|
+
|
87
|
+
def variable?
|
88
|
+
@value.is_a?(Liquid::Variable)
|
89
|
+
end
|
90
|
+
|
91
|
+
# A {% comment %} block node?
|
92
|
+
def comment?
|
93
|
+
@value.is_a?(Liquid::Comment)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Top level node of every liquid_file.
|
97
|
+
def document?
|
98
|
+
@value.is_a?(Liquid::Document)
|
99
|
+
end
|
100
|
+
alias_method :root?, :document?
|
101
|
+
|
102
|
+
# A {% tag %}...{% endtag %} node?
|
103
|
+
def block_tag?
|
104
|
+
@value.is_a?(Liquid::Block)
|
105
|
+
end
|
106
|
+
|
107
|
+
# The body of blocks
|
108
|
+
def block_body?
|
109
|
+
@value.is_a?(Liquid::BlockBody)
|
110
|
+
end
|
111
|
+
|
112
|
+
# A block of type of node?
|
113
|
+
def block?
|
114
|
+
block_tag? || block_body? || document?
|
115
|
+
end
|
116
|
+
|
117
|
+
# The `:under_score_name` of this type of node. Used to dispatch to the `on_<type_name>`
|
118
|
+
# and `after_<type_name>` check methods.
|
119
|
+
def type_name
|
120
|
+
@type_name ||= StringHelpers.underscore(StringHelpers.demodulize(@value.class.name)).to_sym
|
121
|
+
end
|
122
|
+
|
123
|
+
def source
|
124
|
+
theme_file&.source
|
125
|
+
end
|
126
|
+
|
127
|
+
WHITESPACE = /\s/
|
128
|
+
|
129
|
+
# Is this node inside a `{% liquid ... %}` block?
|
130
|
+
def inside_liquid_tag?
|
131
|
+
# What we're doing here is starting at the start of the tag and
|
132
|
+
# backtrack on all the whitespace until we land on something. If
|
133
|
+
# that something is {% or %-, then we can safely assume that
|
134
|
+
# we're inside a full tag and not a liquid tag.
|
135
|
+
@inside_liquid_tag ||= if tag? && start_index && source
|
136
|
+
i = 1
|
137
|
+
i += 1 while source[start_index - i] =~ WHITESPACE && i < start_index
|
138
|
+
first_two_backtracked_characters = source[(start_index - i - 1)..(start_index - i)]
|
139
|
+
first_two_backtracked_characters != "{%" && first_two_backtracked_characters != "%-"
|
140
|
+
else
|
141
|
+
false
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Is this node inside a tag or variable that starts by removing whitespace. i.e. {%- or {{-
|
146
|
+
def whitespace_trimmed_start?
|
147
|
+
@whitespace_trimmed_start ||= if start_index && source && !inside_liquid_tag?
|
148
|
+
i = 1
|
149
|
+
i += 1 while source[start_index - i] =~ WHITESPACE && i < start_index
|
150
|
+
source[start_index - i] == "-"
|
151
|
+
else
|
152
|
+
false
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Is this node inside a tag or variable ends starts by removing whitespace. i.e. -%} or -}}
|
157
|
+
def whitespace_trimmed_end?
|
158
|
+
@whitespace_trimmed_end ||= if end_index && source && !inside_liquid_tag?
|
159
|
+
i = 0
|
160
|
+
i += 1 while source[end_index + i] =~ WHITESPACE && i < source.size
|
161
|
+
source[end_index + i] == "-"
|
162
|
+
else
|
163
|
+
false
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def start_token
|
168
|
+
return "" if inside_liquid_tag?
|
169
|
+
output = ""
|
170
|
+
output += "{{" if variable?
|
171
|
+
output += "{%" if tag?
|
172
|
+
output += "-" if whitespace_trimmed_start?
|
173
|
+
output
|
174
|
+
end
|
175
|
+
|
176
|
+
def end_token
|
177
|
+
return "" if inside_liquid_tag?
|
178
|
+
output = ""
|
179
|
+
output += "-" if whitespace_trimmed_end?
|
180
|
+
output += "}}" if variable?
|
181
|
+
output += "%}" if tag?
|
182
|
+
output
|
183
|
+
end
|
184
|
+
|
185
|
+
private
|
186
|
+
|
187
|
+
def position
|
188
|
+
@position ||= Position.new(
|
189
|
+
markup,
|
190
|
+
theme_file&.source,
|
191
|
+
line_number_1_indexed: line_number
|
192
|
+
)
|
193
|
+
end
|
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 theme_file.
|
211
|
+
def tag_markup
|
212
|
+
return @tag_markup if @tag_markup
|
213
|
+
|
214
|
+
l = 1
|
215
|
+
scanner = StringScanner.new(source)
|
216
|
+
scanner.scan_until(/\n/) while l < @value.line_number && (l += 1)
|
217
|
+
start = scanner.charpos
|
218
|
+
|
219
|
+
tag_name = @value.tag_name
|
220
|
+
tag_markup = @value.instance_variable_get('@markup')
|
221
|
+
|
222
|
+
# This is tricky, if the tag_markup is empty, then the tag could
|
223
|
+
# either start on a previous line, or the tag could start on the
|
224
|
+
# same line.
|
225
|
+
#
|
226
|
+
# Consider this:
|
227
|
+
# 1 {%
|
228
|
+
# 2 comment
|
229
|
+
# 3 %}{% endcomment %}{%comment%}
|
230
|
+
#
|
231
|
+
# Both comments would markup == "" AND line_number == 3
|
232
|
+
#
|
233
|
+
# There's no way to determine which one is the correct one, but
|
234
|
+
# we'll try our best to at least give you one.
|
235
|
+
#
|
236
|
+
# To screw with you even more, the name of the tag could be
|
237
|
+
# outside of a tag on the same line :) But I won't do anything
|
238
|
+
# about that (yet?).
|
239
|
+
#
|
240
|
+
# {% comment
|
241
|
+
# %}comment{% endcomment %}
|
242
|
+
if tag_markup.empty?
|
243
|
+
eol = source.index("\n", start) || source.size
|
244
|
+
|
245
|
+
# OK here I'm trying one of two things. Either tag_start is on
|
246
|
+
# the same line OR tag_start is on a previous line. The line
|
247
|
+
# number would be at the end of the whitespace after tag_name.
|
248
|
+
unless (tag_start = source.index(tag_name, start)) && tag_start < eol
|
249
|
+
tag_start = start
|
250
|
+
tag_start -= 1 while source[tag_start - 1] =~ WHITESPACE
|
251
|
+
tag_start -= @value.tag_name.size
|
252
|
+
|
253
|
+
# keep track of the error in line_number
|
254
|
+
@line_number_offset = source[tag_start...start].count("\n")
|
255
|
+
end
|
256
|
+
tag_end = tag_start + tag_name.size
|
257
|
+
tag_end += 1 while source[tag_end] =~ WHITESPACE
|
258
|
+
|
259
|
+
# return the real raw content
|
260
|
+
@tag_markup = source[tag_start...tag_end]
|
261
|
+
return @tag_markup
|
262
|
+
|
263
|
+
# See https://github.com/Shopify/theme-check/pull/423/files#r701936559 for a detailed explanation
|
264
|
+
# of why we're doing the check below.
|
265
|
+
#
|
266
|
+
# TL;DR it's because line_numbers are not enough to accurately
|
267
|
+
# determine the position of the raw markup and because that
|
268
|
+
# markup could be present on the same line outside of a Tag. e.g.
|
269
|
+
#
|
270
|
+
# uhoh {% if uhoh %}
|
271
|
+
elsif (match = /#{tag_name} +#{Regexp.escape(tag_markup)}/.match(source, start))
|
272
|
+
return @tag_markup = match[0]
|
273
|
+
end
|
274
|
+
|
275
|
+
# find the markup
|
276
|
+
markup_start = source.index(tag_markup, start)
|
277
|
+
markup_end = markup_start + tag_markup.size
|
278
|
+
|
279
|
+
# go back until you find the tag_name
|
280
|
+
tag_start = markup_start
|
281
|
+
tag_start -= 1 while source[tag_start - 1] =~ WHITESPACE
|
282
|
+
tag_start -= tag_name.size
|
283
|
+
|
284
|
+
# keep track of the error in line_number
|
285
|
+
@line_number_offset = source[tag_start...markup_start].count("\n")
|
286
|
+
|
287
|
+
# return the real raw content
|
288
|
+
@tag_markup = source[tag_start...markup_end]
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
@@ -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
|
|
@@ -46,10 +46,12 @@ module ThemeCheck
|
|
46
46
|
other = {} unless other.is_a?(Hash)
|
47
47
|
return if pluralization?(default) && pluralization?(other)
|
48
48
|
|
49
|
-
|
49
|
+
shopify_translations = system_translations(path)
|
50
|
+
|
51
|
+
@extra_keys += (other.keys - default.keys - shopify_translations.keys).map { |key| path + [key] }
|
50
52
|
|
51
53
|
default.each do |key, default_value|
|
52
|
-
translated_value = other[key]
|
54
|
+
translated_value = other[key] || shopify_translations[key]
|
53
55
|
new_path = path + [key]
|
54
56
|
|
55
57
|
if translated_value.nil?
|
@@ -65,5 +67,10 @@ module ThemeCheck
|
|
65
67
|
PLURALIZATION_KEYS.include?(key) && !value.is_a?(Hash)
|
66
68
|
end
|
67
69
|
end
|
70
|
+
|
71
|
+
def system_translations(path)
|
72
|
+
return ShopifyLiquid::SystemTranslations.translations_hash if path.empty?
|
73
|
+
ShopifyLiquid::SystemTranslations.translations_hash.dig(*path) || {}
|
74
|
+
end
|
68
75
|
end
|
69
76
|
end
|
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.raw 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
|