theme-check 1.5.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/theme-check.yml +12 -4
  3. data/CHANGELOG.md +35 -0
  4. data/docs/api/html_check.md +7 -7
  5. data/docs/api/liquid_check.md +10 -10
  6. data/docs/checks/convert_include_to_render.md +1 -1
  7. data/docs/checks/missing_enable_comment.md +1 -1
  8. data/lib/theme_check/analyzer.rb +20 -15
  9. data/lib/theme_check/asset_file.rb +13 -2
  10. data/lib/theme_check/check.rb +3 -3
  11. data/lib/theme_check/checks/asset_size_css.rb +15 -0
  12. data/lib/theme_check/checks/asset_size_css_stylesheet_tag.rb +18 -1
  13. data/lib/theme_check/checks/convert_include_to_render.rb +2 -1
  14. data/lib/theme_check/checks/html_parsing_error.rb +2 -2
  15. data/lib/theme_check/checks/liquid_tag.rb +1 -1
  16. data/lib/theme_check/checks/matching_translations.rb +1 -1
  17. data/lib/theme_check/checks/missing_required_template_files.rb +21 -7
  18. data/lib/theme_check/checks/missing_template.rb +6 -6
  19. data/lib/theme_check/checks/nested_snippet.rb +2 -2
  20. data/lib/theme_check/checks/required_directories.rb +3 -1
  21. data/lib/theme_check/checks/required_layout_theme_object.rb +2 -2
  22. data/lib/theme_check/checks/space_inside_braces.rb +47 -24
  23. data/lib/theme_check/checks/syntax_error.rb +5 -5
  24. data/lib/theme_check/checks/template_length.rb +2 -2
  25. data/lib/theme_check/checks/translation_key_exists.rb +3 -1
  26. data/lib/theme_check/checks/undefined_object.rb +7 -7
  27. data/lib/theme_check/checks/unused_assign.rb +4 -4
  28. data/lib/theme_check/checks/unused_snippet.rb +8 -6
  29. data/lib/theme_check/checks/valid_json.rb +1 -1
  30. data/lib/theme_check/checks.rb +4 -2
  31. data/lib/theme_check/cli.rb +7 -4
  32. data/lib/theme_check/corrector.rb +25 -12
  33. data/lib/theme_check/disabled_check.rb +3 -3
  34. data/lib/theme_check/disabled_checks.rb +9 -9
  35. data/lib/theme_check/file_system_storage.rb +13 -2
  36. data/lib/theme_check/html_node.rb +40 -32
  37. data/lib/theme_check/html_visitor.rb +24 -12
  38. data/lib/theme_check/in_memory_storage.rb +9 -1
  39. data/lib/theme_check/json_check.rb +2 -2
  40. data/lib/theme_check/json_file.rb +9 -4
  41. data/lib/theme_check/language_server/diagnostics_tracker.rb +8 -8
  42. data/lib/theme_check/{template.rb → liquid_file.rb} +6 -20
  43. data/lib/theme_check/liquid_node.rb +291 -0
  44. data/lib/theme_check/{visitor.rb → liquid_visitor.rb} +4 -4
  45. data/lib/theme_check/locale_diff.rb +5 -5
  46. data/lib/theme_check/node.rb +12 -118
  47. data/lib/theme_check/offense.rb +41 -15
  48. data/lib/theme_check/position.rb +28 -17
  49. data/lib/theme_check/position_helper.rb +13 -15
  50. data/lib/theme_check/regex_helpers.rb +1 -15
  51. data/lib/theme_check/remote_asset_file.rb +4 -0
  52. data/lib/theme_check/theme.rb +1 -1
  53. data/lib/theme_check/theme_file.rb +18 -1
  54. data/lib/theme_check/theme_file_rewriter.rb +57 -0
  55. data/lib/theme_check/version.rb +1 -1
  56. data/lib/theme_check.rb +11 -9
  57. data/theme-check.gemspec +2 -1
  58. metadata +22 -6
@@ -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,143 +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
5
+ def parent
6
+ raise NotImplementedError
13
7
  end
14
8
 
15
- # The original source code of the node. Doesn't contain wrapping braces.
16
- def markup
17
- if tag?
18
- @value.raw
19
- elsif @value.instance_variable_defined?(:@markup)
20
- @value.instance_variable_get(:@markup)
21
- end
9
+ def theme_file
10
+ raise NotImplementedError
22
11
  end
23
12
 
24
- def markup=(markup)
25
- if @value.instance_variable_defined?(:@markup)
26
- @value.instance_variable_set(:@markup, markup)
27
- end
13
+ def value
14
+ raise NotImplementedError
28
15
  end
29
16
 
30
- # Array of children nodes.
31
17
  def children
32
- @children ||= begin
33
- nodes =
34
- if comment?
35
- []
36
- elsif defined?(@value.class::ParseTreeVisitor)
37
- @value.class::ParseTreeVisitor.new(@value, {}).children
38
- elsif @value.respond_to?(:nodelist)
39
- Array(@value.nodelist)
40
- else
41
- []
42
- end
43
- # Work around a bug in Liquid::Variable::ParseTreeVisitor that doesn't return
44
- # the args in a hash as children nodes.
45
- nodes = nodes.flat_map do |node|
46
- case node
47
- when Hash
48
- node.values
49
- else
50
- node
51
- end
52
- end
53
- nodes.map { |node| Node.new(node, self, @template) }
54
- end
55
- end
56
-
57
- # Literals are hard-coded values in the template.
58
- def literal?
59
- @value.is_a?(String) || @value.is_a?(Integer)
18
+ raise NotImplementedError
60
19
  end
61
20
 
62
- # A {% tag %} node?
63
- def tag?
64
- @value.is_a?(Liquid::Tag)
65
- end
66
-
67
- # A {% comment %} block node?
68
- def comment?
69
- @value.is_a?(Liquid::Comment)
70
- end
71
-
72
- # Top level node of every template.
73
- def document?
74
- @value.is_a?(Liquid::Document)
75
- end
76
- alias_method :root?, :document?
77
-
78
- # A {% tag %}...{% endtag %} node?
79
- def block_tag?
80
- @value.is_a?(Liquid::Block)
81
- end
82
-
83
- # The body of blocks
84
- def block_body?
85
- @value.is_a?(Liquid::BlockBody)
86
- end
87
-
88
- # A block of type of node?
89
- def block?
90
- block_tag? || block_body? || document?
21
+ def markup
22
+ raise NotImplementedError
91
23
  end
92
24
 
93
- # Most nodes have a line number, but it's not guaranteed.
94
25
  def line_number
95
- @value.line_number if @value.respond_to?(:line_number)
96
- end
97
-
98
- # The `:under_score_name` of this type of node. Used to dispatch to the `on_<type_name>`
99
- # and `after_<type_name>` check methods.
100
- def type_name
101
- @type_name ||= StringHelpers.underscore(StringHelpers.demodulize(@value.class.name)).to_sym
102
- end
103
-
104
- # Is this node inside a `{% liquid ... %}` block?
105
- def inside_liquid_tag?
106
- if line_number
107
- template.excerpt(line_number).start_with?("{%")
108
- else
109
- false
110
- end
111
- end
112
-
113
- # Is this node inside a `{%- ... -%}`
114
- def whitespace_trimmed?
115
- if line_number
116
- template.excerpt(line_number).start_with?("{%-")
117
- else
118
- false
119
- end
120
- end
121
-
122
- def range
123
- start = template.full_line(line_number).index(markup)
124
- [start, start + markup.length - 1]
125
- end
126
-
127
- def position
128
- @position ||= Position.new(
129
- markup,
130
- template&.source,
131
- line_number_1_indexed: line_number
132
- )
26
+ raise NotImplementedError
133
27
  end
134
28
 
135
29
  def start_index
136
- position.start_index
30
+ raise NotImplementedError
137
31
  end
138
32
 
139
33
  def end_index
140
- position.end_index
34
+ raise NotImplementedError
141
35
  end
142
36
  end
143
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,9 +141,35 @@ 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
+ rescue => e
148
+ ThemeCheck.bug(<<~EOS)
149
+ Exception while running `Offense#correct`:
150
+ ```
151
+ #{e.class}: #{e.message}
152
+ #{e.backtrace.join("\n ")}
153
+ ```
154
+
155
+ Offense:
156
+ ```
157
+ #{JSON.pretty_generate(to_h)}
158
+ ```
159
+ Check options:
160
+ ```
161
+ #{check.options.pretty_inspect}
162
+ ```
163
+ Markup:
164
+ ```
165
+ #{markup}
166
+ ```
167
+ Node.Markup:
168
+ ```
169
+ #{node&.markup}
170
+ ```
171
+ EOS
172
+ exit(2)
147
173
  end
148
174
 
149
175
  def whole_theme?
@@ -165,7 +191,7 @@ module ThemeCheck
165
191
  alias_method :eql?, :==
166
192
 
167
193
  def to_s
168
- if template
194
+ if theme_file
169
195
  "#{message} at #{location}"
170
196
  else
171
197
  message
@@ -173,7 +199,7 @@ module ThemeCheck
173
199
  end
174
200
 
175
201
  def to_s_range
176
- if template
202
+ if theme_file
177
203
  "#{message} at #{location_range}"
178
204
  else
179
205
  message
@@ -183,7 +209,7 @@ module ThemeCheck
183
209
  def to_h
184
210
  {
185
211
  check: check.code_name,
186
- path: template&.relative_path,
212
+ path: theme_file&.relative_path,
187
213
  severity: check.severity_value,
188
214
  start_line: start_line,
189
215
  start_column: start_column,
@@ -16,22 +16,22 @@ module ThemeCheck
16
16
  @line_number_1_indexed = line_number_1_indexed
17
17
  @node_markup_offset = node_markup_offset
18
18
  @node_markup = node_markup
19
- @strict_position = StrictPosition.new(
20
- needle,
21
- contents,
22
- start_index,
23
- )
24
19
  end
25
20
 
26
21
  def start_line_offset
27
- from_row_column_to_index(contents, line_number, 0)
22
+ @start_line_offset ||= from_row_column_to_index(contents, line_number, 0)
28
23
  end
29
24
 
30
25
  def start_offset
31
- return start_line_offset if @node_markup.nil?
32
- node_markup_start = contents.index(@node_markup, start_line_offset)
33
- return start_line_offset if node_markup_start.nil?
34
- node_markup_start + @node_markup_offset
26
+ @start_offset ||= compute_start_offset
27
+ end
28
+
29
+ def strict_position
30
+ @strict_position ||= StrictPosition.new(
31
+ needle,
32
+ contents,
33
+ start_index,
34
+ )
35
35
  end
36
36
 
37
37
  # 0-indexed, inclusive
@@ -41,39 +41,50 @@ module ThemeCheck
41
41
 
42
42
  # 0-indexed, exclusive
43
43
  def end_index
44
- @strict_position.end_index
44
+ strict_position.end_index
45
45
  end
46
46
 
47
47
  # 0-indexed, inclusive
48
48
  def start_row
49
- @strict_position.start_row
49
+ strict_position.start_row
50
50
  end
51
51
 
52
52
  # 0-indexed, inclusive
53
53
  def start_column
54
- @strict_position.start_column
54
+ strict_position.start_column
55
55
  end
56
56
 
57
57
  # 0-indexed, exclusive (both taken together are) therefore you
58
58
  # might end up on a newline character or the next line
59
59
  def end_row
60
- @strict_position.end_row
60
+ strict_position.end_row
61
61
  end
62
62
 
63
63
  def end_column
64
- @strict_position.end_column
64
+ strict_position.end_column
65
65
  end
66
66
 
67
67
  private
68
68
 
69
+ def compute_start_offset
70
+ return start_line_offset if @node_markup.nil?
71
+ node_markup_start = contents.index(@node_markup, start_line_offset)
72
+ return start_line_offset if node_markup_start.nil?
73
+ node_markup_start + @node_markup_offset
74
+ end
75
+
69
76
  def contents
70
77
  return '' unless @contents.is_a?(String) && !@contents.empty?
71
78
  @contents
72
79
  end
73
80
 
81
+ def content_line_count
82
+ @content_line_count ||= contents.count("\n")
83
+ end
84
+
74
85
  def line_number
75
86
  return 0 if @line_number_1_indexed.nil?
76
- bounded(0, @line_number_1_indexed - 1, contents.lines.size - 1)
87
+ bounded(0, @line_number_1_indexed - 1, content_line_count)
77
88
  end
78
89
 
79
90
  def needle
@@ -93,7 +104,7 @@ module ThemeCheck
93
104
  end
94
105
 
95
106
  def can_find_needle?
96
- !!contents.index(@needle)
107
+ !!contents.index(@needle, start_offset)
97
108
  end
98
109
 
99
110
  def entire_line_needle
@@ -7,31 +7,29 @@ module ThemeCheck
7
7
  return 0 unless content.is_a?(String) && !content.empty?
8
8
  return 0 unless row.is_a?(Integer) && col.is_a?(Integer)
9
9
  i = 0
10
- result = 0
11
- safe_row = bounded(0, row, content.lines.size - 1)
12
- lines = content.lines
13
- line_size = lines[i].size
14
- while i < safe_row
15
- result += line_size
16
- i += 1
17
- line_size = lines[i].size
18
- end
19
- result += bounded(0, col, line_size - 1)
20
- result
10
+ safe_row = bounded(0, row, content.count("\n"))
11
+ scanner = StringScanner.new(content)
12
+ scanner.scan_until(/\n/) while i < safe_row && (i += 1)
13
+ result = scanner.charpos || 0
14
+ scanner.scan_until(/\n|\z/)
15
+ bounded(result, result + col, scanner.pre_match.size)
21
16
  end
22
17
 
23
18
  def from_index_to_row_column(content, index)
24
19
  return [0, 0] unless content.is_a?(String) && !content.empty?
25
20
  return [0, 0] unless index.is_a?(Integer)
26
21
  safe_index = bounded(0, index, content.size - 1)
27
- lines = content[0..safe_index].lines
28
- row = lines.size - 1
29
- col = lines.last.size - 1
22
+ content_up_to_index = content[0...safe_index]
23
+ row = content_up_to_index.count("\n")
24
+ col = 0
25
+ col += 1 while (safe_index -= 1) && safe_index >= 0 && content[safe_index] != "\n"
30
26
  [row, col]
31
27
  end
32
28
 
33
29
  def bounded(a, x, b)
34
- [a, [x, b].min].max
30
+ return a if x < a
31
+ return b if x > b
32
+ x
35
33
  end
36
34
  end
37
35
  end