theme-check 1.6.0 → 1.7.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/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
|