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.
- checksums.yaml +4 -4
- data/.github/workflows/theme-check.yml +12 -4
- data/CHANGELOG.md +35 -0
- 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 +20 -15
- data/lib/theme_check/asset_file.rb +13 -2
- data/lib/theme_check/check.rb +3 -3
- data/lib/theme_check/checks/asset_size_css.rb +15 -0
- data/lib/theme_check/checks/asset_size_css_stylesheet_tag.rb +18 -1
- data/lib/theme_check/checks/convert_include_to_render.rb +2 -1
- data/lib/theme_check/checks/html_parsing_error.rb +2 -2
- data/lib/theme_check/checks/liquid_tag.rb +1 -1
- data/lib/theme_check/checks/matching_translations.rb +1 -1
- data/lib/theme_check/checks/missing_required_template_files.rb +21 -7
- 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_directories.rb +3 -1
- data/lib/theme_check/checks/required_layout_theme_object.rb +2 -2
- data/lib/theme_check/checks/space_inside_braces.rb +47 -24
- 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 +3 -1
- 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 +8 -6
- data/lib/theme_check/checks/valid_json.rb +1 -1
- data/lib/theme_check/checks.rb +4 -2
- data/lib/theme_check/cli.rb +7 -4
- data/lib/theme_check/corrector.rb +25 -12
- data/lib/theme_check/disabled_check.rb +3 -3
- data/lib/theme_check/disabled_checks.rb +9 -9
- data/lib/theme_check/file_system_storage.rb +13 -2
- data/lib/theme_check/html_node.rb +40 -32
- data/lib/theme_check/html_visitor.rb +24 -12
- data/lib/theme_check/in_memory_storage.rb +9 -1
- data/lib/theme_check/json_check.rb +2 -2
- data/lib/theme_check/json_file.rb +9 -4
- data/lib/theme_check/language_server/diagnostics_tracker.rb +8 -8
- data/lib/theme_check/{template.rb → liquid_file.rb} +6 -20
- 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 +5 -5
- data/lib/theme_check/node.rb +12 -118
- data/lib/theme_check/offense.rb +41 -15
- data/lib/theme_check/position.rb +28 -17
- data/lib/theme_check/position_helper.rb +13 -15
- data/lib/theme_check/regex_helpers.rb +1 -15
- data/lib/theme_check/remote_asset_file.rb +4 -0
- data/lib/theme_check/theme.rb +1 -1
- data/lib/theme_check/theme_file.rb +18 -1
- data/lib/theme_check/theme_file_rewriter.rb +57 -0
- data/lib/theme_check/version.rb +1 -1
- data/lib/theme_check.rb +11 -9
- data/theme-check.gemspec +2 -1
- metadata +22 -6
@@ -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
|
|
data/lib/theme_check/node.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
16
|
-
|
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
|
25
|
-
|
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
|
-
|
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
|
-
|
63
|
-
|
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
|
-
|
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
|
-
|
30
|
+
raise NotImplementedError
|
137
31
|
end
|
138
32
|
|
139
33
|
def end_index
|
140
|
-
|
34
|
+
raise NotImplementedError
|
141
35
|
end
|
142
36
|
end
|
143
37
|
end
|
data/lib/theme_check/offense.rb
CHANGED
@@ -5,13 +5,13 @@ module ThemeCheck
|
|
5
5
|
|
6
6
|
MAX_SOURCE_EXCERPT_SIZE = 120
|
7
7
|
|
8
|
-
attr_reader :check, :message, :
|
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
|
-
|
14
|
-
node: nil, # Node
|
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
|
-
@
|
42
|
+
@theme_file = nil
|
43
43
|
if node
|
44
|
-
@
|
45
|
-
elsif
|
46
|
-
@
|
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
|
-
@
|
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 =
|
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 = [
|
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 = [
|
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(
|
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
|
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
|
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:
|
212
|
+
path: theme_file&.relative_path,
|
187
213
|
severity: check.severity_value,
|
188
214
|
start_line: start_line,
|
189
215
|
start_column: start_column,
|
data/lib/theme_check/position.rb
CHANGED
@@ -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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
44
|
+
strict_position.end_index
|
45
45
|
end
|
46
46
|
|
47
47
|
# 0-indexed, inclusive
|
48
48
|
def start_row
|
49
|
-
|
49
|
+
strict_position.start_row
|
50
50
|
end
|
51
51
|
|
52
52
|
# 0-indexed, inclusive
|
53
53
|
def start_column
|
54
|
-
|
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
|
-
|
60
|
+
strict_position.end_row
|
61
61
|
end
|
62
62
|
|
63
63
|
def end_column
|
64
|
-
|
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,
|
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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
28
|
-
row =
|
29
|
-
col =
|
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
|
-
|
30
|
+
return a if x < a
|
31
|
+
return b if x > b
|
32
|
+
x
|
35
33
|
end
|
36
34
|
end
|
37
35
|
end
|