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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/docs/api/html_check.md +7 -7
  4. data/docs/api/liquid_check.md +10 -10
  5. data/docs/checks/convert_include_to_render.md +1 -1
  6. data/docs/checks/missing_enable_comment.md +1 -1
  7. data/lib/theme_check/analyzer.rb +15 -15
  8. data/lib/theme_check/asset_file.rb +1 -1
  9. data/lib/theme_check/check.rb +2 -2
  10. data/lib/theme_check/checks/html_parsing_error.rb +2 -2
  11. data/lib/theme_check/checks/matching_translations.rb +1 -1
  12. data/lib/theme_check/checks/missing_template.rb +6 -6
  13. data/lib/theme_check/checks/nested_snippet.rb +2 -2
  14. data/lib/theme_check/checks/required_layout_theme_object.rb +2 -2
  15. data/lib/theme_check/checks/syntax_error.rb +5 -5
  16. data/lib/theme_check/checks/template_length.rb +2 -2
  17. data/lib/theme_check/checks/undefined_object.rb +7 -7
  18. data/lib/theme_check/checks/unused_assign.rb +4 -4
  19. data/lib/theme_check/checks/unused_snippet.rb +7 -7
  20. data/lib/theme_check/checks/valid_json.rb +1 -1
  21. data/lib/theme_check/checks.rb +2 -2
  22. data/lib/theme_check/cli.rb +1 -1
  23. data/lib/theme_check/corrector.rb +6 -6
  24. data/lib/theme_check/disabled_check.rb +3 -3
  25. data/lib/theme_check/disabled_checks.rb +9 -9
  26. data/lib/theme_check/html_node.rb +36 -28
  27. data/lib/theme_check/html_visitor.rb +6 -6
  28. data/lib/theme_check/in_memory_storage.rb +1 -1
  29. data/lib/theme_check/json_check.rb +2 -2
  30. data/lib/theme_check/language_server/diagnostics_tracker.rb +8 -8
  31. data/lib/theme_check/{template.rb → liquid_file.rb} +2 -2
  32. data/lib/theme_check/liquid_node.rb +291 -0
  33. data/lib/theme_check/{visitor.rb → liquid_visitor.rb} +4 -4
  34. data/lib/theme_check/locale_diff.rb +5 -5
  35. data/lib/theme_check/node.rb +12 -225
  36. data/lib/theme_check/offense.rb +15 -15
  37. data/lib/theme_check/position.rb +1 -1
  38. data/lib/theme_check/theme.rb +1 -1
  39. data/lib/theme_check/{template_rewriter.rb → theme_file_rewriter.rb} +1 -1
  40. data/lib/theme_check/version.rb +1 -1
  41. data/lib/theme_check.rb +11 -10
  42. data/theme-check.gemspec +1 -1
  43. metadata +8 -7
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  module ThemeCheck
3
- class Visitor
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 visit_template(template)
12
- visit(Node.new(template.root, nil, template))
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 = 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, template: 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, template: template)
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, template: template)
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, template: 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, template: template)
36
+ check.add_offense(message, theme_file: theme_file)
37
37
  end
38
38
  end
39
39
 
@@ -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
- attr_reader :value, :parent, :template
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
- # The original source code of the node. Doesn't contain wrapping braces.
18
- def markup
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 markup=(markup)
27
- if @value.instance_variable_defined?(:@markup)
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
- @children ||= begin
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
- # The body of blocks
90
- def block_body?
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
- 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
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
- position.start_index
30
+ raise NotImplementedError
169
31
  end
170
32
 
171
33
  def end_index
172
- position.end_index
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
@@ -5,13 +5,13 @@ module ThemeCheck
5
5
 
6
6
  MAX_SOURCE_EXCERPT_SIZE = 120
7
7
 
8
- attr_reader :check, :message, :template, :node, :markup, :line_number, :correction
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
- template: nil, # Template
14
- node: nil, # Node or HtmlNode
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
- @template = nil
42
+ @theme_file = nil
43
43
  if node
44
- @template = node.template
45
- elsif template
46
- @template = template
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
- @template&.source,
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 = template.source_excerpt(line_number)
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 = [template&.relative_path, line_number].compact
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 = [template&.relative_path, start_index, end_index].compact
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(template: template)
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 template
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 template
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: template&.relative_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,
@@ -104,7 +104,7 @@ module ThemeCheck
104
104
  end
105
105
 
106
106
  def can_find_needle?
107
- !!contents.index(@needle)
107
+ !!contents.index(@needle, start_offset)
108
108
  end
109
109
 
110
110
  def entire_line_needle
@@ -23,7 +23,7 @@ module ThemeCheck
23
23
  def liquid
24
24
  @liquid ||= @storage.files
25
25
  .select { |path| LIQUID_REGEX.match?(path) }
26
- .map { |path| Template.new(path, @storage) }
26
+ .map { |path| LiquidFile.new(path, @storage) }
27
27
  end
28
28
 
29
29
  def json
@@ -3,7 +3,7 @@
3
3
  require 'parser'
4
4
 
5
5
  module ThemeCheck
6
- class TemplateRewriter
6
+ class ThemeFileRewriter
7
7
  def initialize(name, source)
8
8
  @buffer = Parser::Source::Buffer.new(name, source: source)
9
9
  @rewriter = Parser::Source::TreeRewriter.new(
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module ThemeCheck
3
- VERSION = "1.6.1"
3
+ VERSION = "1.6.2"
4
4
  end