theme-check 0.3.1 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +13 -3
- data/CHANGELOG.md +61 -0
- data/CONTRIBUTING.md +5 -2
- data/Gemfile +5 -3
- data/README.md +11 -4
- data/RELEASING.md +2 -2
- data/config/default.yml +14 -0
- data/data/shopify_liquid/tags.yml +27 -0
- data/data/shopify_translation_keys.yml +850 -0
- data/docs/checks/CHECK_DOCS_TEMPLATE.md +47 -0
- data/docs/checks/asset_size_css.md +52 -0
- data/docs/checks/asset_size_javascript.md +79 -0
- data/docs/checks/convert_include_to_render.md +48 -0
- data/docs/checks/default_locale.md +46 -0
- data/docs/checks/deprecated_filter.md +46 -0
- data/docs/checks/img_width_and_height.md +79 -0
- data/docs/checks/liquid_tag.md +65 -0
- data/docs/checks/matching_schema_translations.md +93 -0
- data/docs/checks/matching_translations.md +72 -0
- data/docs/checks/missing_enable_comment.md +50 -0
- data/docs/checks/missing_required_template_files.md +26 -0
- data/docs/checks/missing_template.md +40 -0
- data/docs/checks/nested_snippet.md +69 -0
- data/docs/checks/parser_blocking_javascript.md +97 -0
- data/docs/checks/required_directories.md +25 -0
- data/docs/checks/required_layout_theme_object.md +28 -0
- data/docs/checks/space_inside_braces.md +63 -0
- data/docs/checks/syntax_error.md +49 -0
- data/docs/checks/template_length.md +50 -0
- data/docs/checks/translation_key_exists.md +63 -0
- data/docs/checks/undefined_object.md +53 -0
- data/docs/checks/unknown_filter.md +45 -0
- data/docs/checks/unused_assign.md +47 -0
- data/docs/checks/unused_snippet.md +32 -0
- data/docs/checks/valid_html_translation.md +53 -0
- data/docs/checks/valid_json.md +60 -0
- data/docs/checks/valid_schema.md +50 -0
- data/lib/theme_check.rb +3 -0
- data/lib/theme_check/asset_file.rb +34 -0
- data/lib/theme_check/check.rb +19 -9
- data/lib/theme_check/checks/asset_size_css.rb +89 -0
- data/lib/theme_check/checks/asset_size_javascript.rb +68 -0
- data/lib/theme_check/checks/convert_include_to_render.rb +1 -1
- data/lib/theme_check/checks/default_locale.rb +1 -0
- data/lib/theme_check/checks/deprecated_filter.rb +1 -1
- data/lib/theme_check/checks/img_width_and_height.rb +74 -0
- data/lib/theme_check/checks/liquid_tag.rb +3 -3
- data/lib/theme_check/checks/matching_schema_translations.rb +1 -0
- data/lib/theme_check/checks/matching_translations.rb +1 -0
- data/lib/theme_check/checks/missing_enable_comment.rb +1 -0
- data/lib/theme_check/checks/missing_required_template_files.rb +1 -2
- data/lib/theme_check/checks/missing_template.rb +1 -0
- data/lib/theme_check/checks/nested_snippet.rb +1 -0
- data/lib/theme_check/checks/parser_blocking_javascript.rb +7 -14
- data/lib/theme_check/checks/required_directories.rb +1 -1
- data/lib/theme_check/checks/required_layout_theme_object.rb +1 -1
- data/lib/theme_check/checks/space_inside_braces.rb +1 -0
- data/lib/theme_check/checks/syntax_error.rb +1 -0
- data/lib/theme_check/checks/template_length.rb +1 -0
- data/lib/theme_check/checks/translation_key_exists.rb +17 -1
- data/lib/theme_check/checks/undefined_object.rb +29 -10
- data/lib/theme_check/checks/unknown_filter.rb +1 -0
- data/lib/theme_check/checks/unused_assign.rb +5 -3
- data/lib/theme_check/checks/unused_snippet.rb +1 -0
- data/lib/theme_check/checks/valid_html_translation.rb +1 -0
- data/lib/theme_check/checks/valid_json.rb +1 -0
- data/lib/theme_check/checks/valid_schema.rb +1 -0
- data/lib/theme_check/cli.rb +39 -12
- data/lib/theme_check/config.rb +5 -2
- data/lib/theme_check/in_memory_storage.rb +11 -3
- data/lib/theme_check/language_server.rb +12 -0
- data/lib/theme_check/language_server/completion_engine.rb +38 -0
- data/lib/theme_check/language_server/completion_helper.rb +25 -0
- data/lib/theme_check/language_server/completion_provider.rb +28 -0
- data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +51 -0
- data/lib/theme_check/language_server/completion_providers/object_completion_provider.rb +31 -0
- data/lib/theme_check/language_server/completion_providers/render_snippet_completion_provider.rb +43 -0
- data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +31 -0
- data/lib/theme_check/language_server/constants.rb +10 -0
- data/lib/theme_check/language_server/document_link_engine.rb +47 -0
- data/lib/theme_check/language_server/handler.rb +93 -6
- data/lib/theme_check/language_server/position_helper.rb +27 -0
- data/lib/theme_check/language_server/protocol.rb +41 -0
- data/lib/theme_check/language_server/server.rb +8 -2
- data/lib/theme_check/language_server/tokens.rb +55 -0
- data/lib/theme_check/liquid_check.rb +11 -0
- data/lib/theme_check/offense.rb +51 -14
- data/lib/theme_check/regex_helpers.rb +15 -0
- data/lib/theme_check/remote_asset_file.rb +44 -0
- data/lib/theme_check/shopify_liquid.rb +1 -0
- data/lib/theme_check/shopify_liquid/deprecated_filter.rb +4 -0
- data/lib/theme_check/shopify_liquid/tag.rb +16 -0
- data/lib/theme_check/theme.rb +7 -1
- data/lib/theme_check/version.rb +1 -1
- metadata +52 -2
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module ThemeCheck
|
3
|
+
# Reports errors when trying to use parser-blocking script tags
|
4
|
+
class ImgWidthAndHeight < LiquidCheck
|
5
|
+
include RegexHelpers
|
6
|
+
severity :error
|
7
|
+
categories :liquid, :performance
|
8
|
+
doc docs_url(__FILE__)
|
9
|
+
|
10
|
+
# Not implemented with lookbehinds and lookaheads because performance was shit!
|
11
|
+
IMG_TAG = /<img#{HTML_ATTRIBUTES}>/oxim
|
12
|
+
SRC_ATTRIBUTE = /\s(src)=(#{QUOTED_LIQUID_ATTRIBUTE})/oxim
|
13
|
+
WIDTH_ATTRIBUTE = /\s(width)=(#{QUOTED_LIQUID_ATTRIBUTE})/oxim
|
14
|
+
HEIGHT_ATTRIBUTE = /\s(height)=(#{QUOTED_LIQUID_ATTRIBUTE})/oxim
|
15
|
+
|
16
|
+
FIELDS = [WIDTH_ATTRIBUTE, HEIGHT_ATTRIBUTE]
|
17
|
+
ENDS_IN_CSS_UNIT = /(cm|mm|in|px|pt|pc|em|ex|ch|rem|vw|vh|vmin|vmax|%)$/i
|
18
|
+
|
19
|
+
def on_document(node)
|
20
|
+
@source = node.template.source
|
21
|
+
@node = node
|
22
|
+
record_offenses
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def record_offenses
|
28
|
+
matches(@source, IMG_TAG).each do |img_match|
|
29
|
+
next unless img_match[0] =~ SRC_ATTRIBUTE
|
30
|
+
record_missing_field_offenses(img_match)
|
31
|
+
record_units_in_field_offenses(img_match)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def record_missing_field_offenses(img_match)
|
36
|
+
width = WIDTH_ATTRIBUTE.match(img_match[0])
|
37
|
+
height = HEIGHT_ATTRIBUTE.match(img_match[0])
|
38
|
+
return if width.present? && height.present?
|
39
|
+
missing_width = width.nil?
|
40
|
+
missing_height = height.nil?
|
41
|
+
error_message = if missing_width && missing_height
|
42
|
+
"Missing width and height attributes"
|
43
|
+
elsif missing_width
|
44
|
+
"Missing width attribute"
|
45
|
+
elsif missing_height
|
46
|
+
"Missing height attribute"
|
47
|
+
end
|
48
|
+
|
49
|
+
add_offense(
|
50
|
+
error_message,
|
51
|
+
node: @node,
|
52
|
+
markup: img_match[0],
|
53
|
+
line_number: @source[0...img_match.begin(0)].count("\n") + 1
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
def record_units_in_field_offenses(img_match)
|
58
|
+
FIELDS.each do |field|
|
59
|
+
field_match = field.match(img_match[0])
|
60
|
+
next if field_match.nil?
|
61
|
+
value = field_match[2].gsub(START_OR_END_QUOTE, '')
|
62
|
+
next unless value =~ ENDS_IN_CSS_UNIT
|
63
|
+
value_without_units = value.gsub(ENDS_IN_CSS_UNIT, '')
|
64
|
+
start = img_match.begin(0) + field_match.begin(2)
|
65
|
+
add_offense(
|
66
|
+
"The #{field_match[1]} attribute does not take units. Replace with \"#{value_without_units}\".",
|
67
|
+
node: @node,
|
68
|
+
markup: value,
|
69
|
+
line_number: @source[0...start].count("\n") + 1
|
70
|
+
)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -1,12 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module ThemeCheck
|
3
|
-
# Recommends using {% liquid ... %} if
|
3
|
+
# Recommends using {% liquid ... %} if 4 or more consecutive {% ... %} are found.
|
4
4
|
class LiquidTag < LiquidCheck
|
5
5
|
severity :suggestion
|
6
6
|
category :liquid
|
7
|
-
doc
|
7
|
+
doc docs_url(__FILE__)
|
8
8
|
|
9
|
-
def initialize(min_consecutive_statements:
|
9
|
+
def initialize(min_consecutive_statements: 4)
|
10
10
|
@first_statement = nil
|
11
11
|
@consecutive_statements = 0
|
12
12
|
@min_consecutive_statements = min_consecutive_statements
|
@@ -3,11 +3,10 @@
|
|
3
3
|
module ThemeCheck
|
4
4
|
# Reports missing shopify required theme files
|
5
5
|
# required templates: https://shopify.dev/tutorials/review-theme-store-requirements-files
|
6
|
-
|
7
6
|
class MissingRequiredTemplateFiles < LiquidCheck
|
8
7
|
severity :error
|
9
8
|
category :liquid
|
10
|
-
doc
|
9
|
+
doc docs_url(__FILE__)
|
11
10
|
|
12
11
|
REQUIRED_LIQUID_FILES = %w(layout/theme)
|
13
12
|
REQUIRED_TEMPLATE_FILES = %w(
|
@@ -2,12 +2,14 @@
|
|
2
2
|
module ThemeCheck
|
3
3
|
# Reports errors when trying to use parser-blocking script tags
|
4
4
|
class ParserBlockingJavaScript < LiquidCheck
|
5
|
+
include RegexHelpers
|
5
6
|
severity :error
|
6
|
-
|
7
|
+
categories :liquid, :performance
|
8
|
+
doc docs_url(__FILE__)
|
7
9
|
|
8
10
|
PARSER_BLOCKING_SCRIPT_TAG = %r{
|
9
11
|
<script # Find the start of a script tag
|
10
|
-
(?=
|
12
|
+
(?=[^>]+?src=) # Make sure src= is in the script with a lookahead
|
11
13
|
(?:(?!defer|async|type=["']module['"]).)*? # Find tags that don't have defer|async|type="module"
|
12
14
|
>
|
13
15
|
}xim
|
@@ -32,23 +34,14 @@ module ThemeCheck
|
|
32
34
|
)
|
33
35
|
end
|
34
36
|
|
35
|
-
# The trickiness here is matching on scripts that are defined on
|
36
|
-
# multiple lines (or repeat matches). This makes the line_number
|
37
|
-
# calculation a bit weird. So instead, we traverse the string in
|
38
|
-
# a very imperative way.
|
39
37
|
def record_offenses_from_regex(regex: nil, message: nil)
|
40
|
-
|
41
|
-
while (i = @source.index(regex, i))
|
42
|
-
script = @source.match(regex, i)[0]
|
43
|
-
|
38
|
+
matches(@source, regex).each do |match|
|
44
39
|
add_offense(
|
45
40
|
message,
|
46
41
|
node: @node,
|
47
|
-
markup:
|
48
|
-
line_number: @source[0...
|
42
|
+
markup: match[0],
|
43
|
+
line_number: @source[0...match.begin(0)].count("\n") + 1
|
49
44
|
)
|
50
|
-
|
51
|
-
i += script.size
|
52
45
|
end
|
53
46
|
end
|
54
47
|
end
|
@@ -5,7 +5,7 @@ module ThemeCheck
|
|
5
5
|
class RequiredDirectories < LiquidCheck
|
6
6
|
severity :error
|
7
7
|
category :liquid
|
8
|
-
doc
|
8
|
+
doc docs_url(__FILE__)
|
9
9
|
|
10
10
|
REQUIRED_DIRECTORIES = %w(assets config layout locales sections snippets templates)
|
11
11
|
|
@@ -1,8 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module ThemeCheck
|
3
|
+
module SystemTranslations
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def translations
|
7
|
+
@translations ||= begin
|
8
|
+
# loaded as a Set because the include? lookup will be much faster.
|
9
|
+
YAML.load(File.read("#{__dir__}/../../../data/shopify_translation_keys.yml")).to_set
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def include?(key)
|
14
|
+
translations.include?(key)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
3
18
|
class TranslationKeyExists < LiquidCheck
|
4
19
|
severity :error
|
5
20
|
category :translation
|
21
|
+
doc docs_url(__FILE__)
|
6
22
|
|
7
23
|
def on_variable(node)
|
8
24
|
return unless @theme.default_locale_json&.content&.is_a?(Hash)
|
@@ -11,7 +27,7 @@ module ThemeCheck
|
|
11
27
|
return unless (key_node = node.children.first)
|
12
28
|
return unless key_node.value.is_a?(String)
|
13
29
|
|
14
|
-
unless key_exists?(key_node.value)
|
30
|
+
unless key_exists?(key_node.value) || SystemTranslations.include?(key_node.value)
|
15
31
|
add_offense(
|
16
32
|
"'#{key_node.value}' does not have a matching entry in '#{@theme.default_locale_json.relative_path}'",
|
17
33
|
node: node,
|
@@ -2,7 +2,7 @@
|
|
2
2
|
module ThemeCheck
|
3
3
|
class UndefinedObject < LiquidCheck
|
4
4
|
category :liquid
|
5
|
-
doc
|
5
|
+
doc docs_url(__FILE__)
|
6
6
|
severity :error
|
7
7
|
|
8
8
|
class TemplateInfo
|
@@ -21,7 +21,13 @@ module ThemeCheck
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def add_variable_lookup(name:, node:)
|
24
|
-
|
24
|
+
parent = node
|
25
|
+
line_number = nil
|
26
|
+
loop do
|
27
|
+
line_number = parent.line_number
|
28
|
+
parent = parent.parent
|
29
|
+
break unless line_number.nil? && parent
|
30
|
+
end
|
25
31
|
key = [name, line_number]
|
26
32
|
@all_variable_lookups[key] = node
|
27
33
|
end
|
@@ -49,23 +55,28 @@ module ThemeCheck
|
|
49
55
|
end
|
50
56
|
end
|
51
57
|
|
52
|
-
def initialize
|
58
|
+
def initialize(exclude_snippets: false)
|
59
|
+
@exclude_snippets = exclude_snippets
|
53
60
|
@files = {}
|
54
61
|
end
|
55
62
|
|
56
63
|
def on_document(node)
|
64
|
+
return if ignore?(node)
|
57
65
|
@files[node.template.name] = TemplateInfo.new
|
58
66
|
end
|
59
67
|
|
60
68
|
def on_assign(node)
|
69
|
+
return if ignore?(node)
|
61
70
|
@files[node.template.name].all_assigns[node.value.to] = node
|
62
71
|
end
|
63
72
|
|
64
73
|
def on_capture(node)
|
74
|
+
return if ignore?(node)
|
65
75
|
@files[node.template.name].all_captures[node.value.instance_variable_get('@to')] = node
|
66
76
|
end
|
67
77
|
|
68
78
|
def on_for(node)
|
79
|
+
return if ignore?(node)
|
69
80
|
@files[node.template.name].all_forloops[node.value.variable_name] = node
|
70
81
|
end
|
71
82
|
|
@@ -75,6 +86,7 @@ module ThemeCheck
|
|
75
86
|
end
|
76
87
|
|
77
88
|
def on_render(node)
|
89
|
+
return if ignore?(node)
|
78
90
|
return unless node.value.template_name_expr.is_a?(String)
|
79
91
|
|
80
92
|
snippet_name = "snippets/#{node.value.template_name_expr}"
|
@@ -85,6 +97,7 @@ module ThemeCheck
|
|
85
97
|
end
|
86
98
|
|
87
99
|
def on_variable_lookup(node)
|
100
|
+
return if ignore?(node)
|
88
101
|
@files[node.template.name].add_variable_lookup(
|
89
102
|
name: node.value.name,
|
90
103
|
node: node,
|
@@ -114,15 +127,20 @@ module ThemeCheck
|
|
114
127
|
end
|
115
128
|
end
|
116
129
|
|
130
|
+
private
|
131
|
+
|
132
|
+
def ignore?(node)
|
133
|
+
@exclude_snippets && node.template.snippet?
|
134
|
+
end
|
135
|
+
|
117
136
|
def each_template
|
118
137
|
@files.each do |(name, info)|
|
119
138
|
next if name.starts_with?('snippets/')
|
120
139
|
yield [name, info]
|
121
140
|
end
|
122
141
|
end
|
123
|
-
private :each_template
|
124
142
|
|
125
|
-
def check_object(info, all_global_objects, render_node = nil)
|
143
|
+
def check_object(info, all_global_objects, render_node = nil, visited_snippets = Set.new)
|
126
144
|
check_undefined(info, all_global_objects, render_node)
|
127
145
|
|
128
146
|
info.each_snippet do |(snippet_name, node)|
|
@@ -131,16 +149,18 @@ module ThemeCheck
|
|
131
149
|
|
132
150
|
snippet_variables = node.value.attributes.keys +
|
133
151
|
Array[node.value.instance_variable_get("@alias_name")]
|
134
|
-
|
152
|
+
unless visited_snippets.include?(snippet_name)
|
153
|
+
visited_snippets << snippet_name
|
154
|
+
check_object(snippet_info, all_global_objects + snippet_variables, node, visited_snippets)
|
155
|
+
end
|
135
156
|
end
|
136
157
|
end
|
137
|
-
private :check_object
|
138
158
|
|
139
159
|
def check_undefined(info, all_global_objects, render_node)
|
140
160
|
all_variables = info.all_variables
|
141
161
|
|
142
162
|
info.each_variable_lookup(!!render_node) do |(key, node)|
|
143
|
-
name,
|
163
|
+
name, line_number = key
|
144
164
|
next if all_variables.include?(name)
|
145
165
|
next if all_global_objects.include?(name)
|
146
166
|
|
@@ -150,10 +170,9 @@ module ThemeCheck
|
|
150
170
|
if render_node
|
151
171
|
add_offense("Missing argument `#{name}`", node: render_node)
|
152
172
|
else
|
153
|
-
add_offense("Undefined object `#{name}`", node: node)
|
173
|
+
add_offense("Undefined object `#{name}`", node: node, line_number: line_number)
|
154
174
|
end
|
155
175
|
end
|
156
176
|
end
|
157
|
-
private :check_undefined
|
158
177
|
end
|
159
178
|
end
|
@@ -4,14 +4,16 @@ module ThemeCheck
|
|
4
4
|
class UnusedAssign < LiquidCheck
|
5
5
|
severity :suggestion
|
6
6
|
category :liquid
|
7
|
+
doc docs_url(__FILE__)
|
7
8
|
|
8
9
|
class TemplateInfo < Struct.new(:used_assigns, :assign_nodes, :includes)
|
9
|
-
def collect_used_assigns(templates)
|
10
|
+
def collect_used_assigns(templates, visited = Set.new)
|
10
11
|
collected = used_assigns
|
11
12
|
# Check recursively inside included snippets for use
|
12
13
|
includes.each do |name|
|
13
|
-
if templates[name]
|
14
|
-
|
14
|
+
if templates[name] && !visited.include?(name)
|
15
|
+
visited << name
|
16
|
+
collected += templates[name].collect_used_assigns(templates, visited)
|
15
17
|
end
|
16
18
|
end
|
17
19
|
collected
|