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.
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