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