theme-check 1.5.1 → 1.6.2

Sign up to get free protection for your applications and to get access to all the features.
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