theme-check 1.7.2 → 1.9.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/.gitignore +1 -0
- data/CHANGELOG.md +47 -0
- data/README.md +10 -0
- data/RELEASING.md +13 -0
- data/config/default.yml +5 -0
- data/data/shopify_liquid/deprecated_filters.yml +4 -0
- data/data/shopify_liquid/filters.yml +3 -1
- data/docs/checks/TEMPLATE.md.erb +24 -19
- data/docs/checks/schema_json_format.md +76 -0
- data/docs/language_server/code-action-command-palette.png +0 -0
- data/docs/language_server/code-action-flow.png +0 -0
- data/docs/language_server/code-action-keyboard.png +0 -0
- data/docs/language_server/code-action-light-bulb.png +0 -0
- data/docs/language_server/code-action-problem.png +0 -0
- data/docs/language_server/code-action-quickfix.png +0 -0
- data/docs/language_server/how_to_correct_code_with_code_actions_and_execute_command.md +197 -0
- data/exe/theme-check-language-server +0 -4
- data/lib/theme_check/checks/asset_size_app_block_css.rb +2 -3
- data/lib/theme_check/checks/asset_size_app_block_javascript.rb +2 -3
- data/lib/theme_check/checks/asset_url_filters.rb +2 -0
- data/lib/theme_check/checks/default_locale.rb +1 -1
- data/lib/theme_check/checks/deprecated_filter.rb +81 -4
- data/lib/theme_check/checks/deprecated_global_app_block_type.rb +2 -3
- data/lib/theme_check/checks/matching_schema_translations.rb +14 -9
- data/lib/theme_check/checks/matching_translations.rb +1 -0
- data/lib/theme_check/checks/missing_required_template_files.rb +3 -3
- data/lib/theme_check/checks/missing_template.rb +1 -1
- data/lib/theme_check/checks/pagination_size.rb +2 -3
- data/lib/theme_check/checks/remote_asset.rb +5 -0
- data/lib/theme_check/checks/required_directories.rb +1 -1
- data/lib/theme_check/checks/required_layout_theme_object.rb +9 -4
- data/lib/theme_check/checks/schema_json_format.rb +29 -0
- data/lib/theme_check/checks/space_inside_braces.rb +132 -87
- data/lib/theme_check/checks/translation_key_exists.rb +33 -13
- data/lib/theme_check/checks/unused_assign.rb +3 -2
- data/lib/theme_check/checks/unused_snippet.rb +1 -1
- data/lib/theme_check/checks/valid_html_translation.rb +1 -1
- data/lib/theme_check/checks/valid_schema.rb +2 -2
- data/lib/theme_check/corrector.rb +34 -23
- data/lib/theme_check/file_system_storage.rb +4 -3
- data/lib/theme_check/html_node.rb +122 -6
- data/lib/theme_check/html_visitor.rb +1 -32
- data/lib/theme_check/in_memory_storage.rb +9 -0
- data/lib/theme_check/json_helpers.rb +14 -0
- data/lib/theme_check/language_server/bridge.rb +19 -5
- data/lib/theme_check/language_server/client_capabilities.rb +27 -0
- data/lib/theme_check/language_server/code_action_engine.rb +32 -0
- data/lib/theme_check/language_server/code_action_provider.rb +42 -0
- data/lib/theme_check/language_server/code_action_providers/quickfix_code_action_provider.rb +83 -0
- data/lib/theme_check/language_server/code_action_providers/source_fix_all_code_action_provider.rb +40 -0
- data/lib/theme_check/language_server/configuration.rb +69 -0
- data/lib/theme_check/language_server/diagnostic.rb +124 -0
- data/lib/theme_check/language_server/diagnostics_engine.rb +15 -60
- data/lib/theme_check/language_server/diagnostics_manager.rb +136 -0
- data/lib/theme_check/language_server/document_change_corrector.rb +267 -0
- data/lib/theme_check/language_server/document_link_provider.rb +6 -6
- data/lib/theme_check/language_server/execute_command_engine.rb +19 -0
- data/lib/theme_check/language_server/execute_command_provider.rb +30 -0
- data/lib/theme_check/language_server/execute_command_providers/correction_execute_command_provider.rb +48 -0
- data/lib/theme_check/language_server/execute_command_providers/run_checks_execute_command_provider.rb +22 -0
- data/lib/theme_check/language_server/handler.rb +83 -29
- data/lib/theme_check/language_server/io_messenger.rb +11 -1
- data/lib/theme_check/language_server/protocol.rb +4 -0
- data/lib/theme_check/language_server/server.rb +29 -11
- data/lib/theme_check/language_server/uri_helper.rb +1 -0
- data/lib/theme_check/language_server/versioned_in_memory_storage.rb +69 -0
- data/lib/theme_check/language_server.rb +23 -5
- data/lib/theme_check/liquid_node.rb +255 -12
- data/lib/theme_check/locale_diff.rb +39 -8
- data/lib/theme_check/node.rb +16 -0
- data/lib/theme_check/offense.rb +27 -23
- data/lib/theme_check/position.rb +4 -4
- data/lib/theme_check/regex_helpers.rb +1 -1
- data/lib/theme_check/schema_helper.rb +70 -0
- data/lib/theme_check/storage.rb +4 -0
- data/lib/theme_check/tags.rb +0 -1
- data/lib/theme_check/theme.rb +1 -1
- data/lib/theme_check/theme_file.rb +8 -1
- data/lib/theme_check/theme_file_rewriter.rb +28 -6
- data/lib/theme_check/version.rb +1 -1
- data/lib/theme_check.rb +11 -2
- metadata +26 -3
- data/lib/theme_check/language_server/diagnostics_tracker.rb +0 -66
|
@@ -5,18 +5,95 @@ module ThemeCheck
|
|
|
5
5
|
category :liquid
|
|
6
6
|
severity :suggestion
|
|
7
7
|
|
|
8
|
+
# The image_url filter does not accept width or height values
|
|
9
|
+
# greater than this numbr.
|
|
10
|
+
MAX_SIZE = 5760
|
|
11
|
+
|
|
8
12
|
def on_variable(node)
|
|
9
13
|
used_filters = node.value.filters.map { |name, *_rest| name }
|
|
10
14
|
used_filters.each do |filter|
|
|
11
15
|
alternatives = ShopifyLiquid::DeprecatedFilter.alternatives(filter)
|
|
12
16
|
next unless alternatives
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
case filter
|
|
19
|
+
when 'img_url'
|
|
20
|
+
add_img_url_offense(node)
|
|
21
|
+
else
|
|
22
|
+
add_default_offense(node, filter, alternatives)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def add_default_offense(node, filter, alternatives)
|
|
28
|
+
alternatives = alternatives.map { |alt| "`#{alt}`" }
|
|
29
|
+
add_offense(
|
|
30
|
+
"Deprecated filter `#{filter}`, consider using an alternative: #{alternatives.join(', ')}",
|
|
31
|
+
node: node,
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def add_img_url_offense(node)
|
|
36
|
+
img_url_filter = node.value.filters.find { |filter| filter[0] == "img_url" }
|
|
37
|
+
_name, img_url_filter_size, img_url_filter_props = img_url_filter
|
|
38
|
+
size_spec = img_url_filter_size&.dig(0)
|
|
39
|
+
scale = img_url_filter_props&.delete("scale")
|
|
40
|
+
|
|
41
|
+
# Can't correct those.
|
|
42
|
+
return add_default_offense(node, 'img_url', ['image_url']) unless
|
|
43
|
+
(size_spec.nil? || size_spec.is_a?(String)) &&
|
|
44
|
+
(scale.nil? || scale.is_a?(Numeric)) &&
|
|
45
|
+
size_spec != 'small'
|
|
46
|
+
|
|
47
|
+
node_source = node.markup
|
|
48
|
+
node_start_index = node.start_index
|
|
49
|
+
match = node_source.match(/img_url[^|]*/)
|
|
50
|
+
img_url_character_range =
|
|
51
|
+
(node_start_index + match.begin(0))...(node_start_index + match.end(0))
|
|
52
|
+
|
|
53
|
+
scale = (scale || 1).to_i
|
|
54
|
+
width, height = (size_spec&.split('x') || [100, 100])
|
|
55
|
+
.map { |v| v.to_i * scale }
|
|
56
|
+
|
|
57
|
+
image_url_filter_params = [
|
|
58
|
+
width && width > 0 ? "width: #{[width, MAX_SIZE].min}" : nil,
|
|
59
|
+
height && height > 0 ? "height: #{[height, MAX_SIZE].min}" : nil,
|
|
60
|
+
]
|
|
61
|
+
image_url_filter_params += (img_url_filter_props || {})
|
|
62
|
+
.map do |k, v|
|
|
63
|
+
case v
|
|
64
|
+
when Liquid::VariableLookup
|
|
65
|
+
"#{k}: #{v.name}"
|
|
66
|
+
when String
|
|
67
|
+
"#{k}: '#{v}'"
|
|
68
|
+
else
|
|
69
|
+
"#{k}: #{v}"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
image_url_filter_params = image_url_filter_params
|
|
73
|
+
.reject(&:nil?)
|
|
74
|
+
.join(", ")
|
|
75
|
+
|
|
76
|
+
trailing_whitespace = match[0].match(/\s*\Z/)[0]
|
|
77
|
+
|
|
78
|
+
image_url_filter = "image_url"
|
|
79
|
+
image_url_filter += ": " + image_url_filter_params unless image_url_filter_params.empty?
|
|
80
|
+
image_url_filter += trailing_whitespace
|
|
81
|
+
|
|
82
|
+
add_offense(
|
|
83
|
+
"Deprecated filter `img_url`, consider using `image_url`",
|
|
84
|
+
node: node,
|
|
85
|
+
markup: match[0]
|
|
86
|
+
) do |corrector|
|
|
87
|
+
corrector.replace(
|
|
88
|
+
node,
|
|
89
|
+
image_url_filter,
|
|
90
|
+
img_url_character_range,
|
|
18
91
|
)
|
|
19
92
|
end
|
|
93
|
+
|
|
94
|
+
# If anything goes wrong, fail gracefully by returning the default offense.
|
|
95
|
+
rescue
|
|
96
|
+
add_default_offense(node, 'img_url', ['image_url'])
|
|
20
97
|
end
|
|
21
98
|
end
|
|
22
99
|
end
|
|
@@ -9,7 +9,8 @@ module ThemeCheck
|
|
|
9
9
|
VALID_GLOBAL_APP_BLOCK_TYPE = "@app"
|
|
10
10
|
|
|
11
11
|
def on_schema(node)
|
|
12
|
-
schema =
|
|
12
|
+
schema = node.inner_json
|
|
13
|
+
return if schema.nil?
|
|
13
14
|
|
|
14
15
|
if block_types_from(schema).include?(INVALID_GLOBAL_APP_BLOCK_TYPE)
|
|
15
16
|
add_offense(
|
|
@@ -17,8 +18,6 @@ module ThemeCheck
|
|
|
17
18
|
node: node
|
|
18
19
|
)
|
|
19
20
|
end
|
|
20
|
-
rescue JSON::ParserError
|
|
21
|
-
# Ignored, handled in ValidSchema.
|
|
22
21
|
end
|
|
23
22
|
|
|
24
23
|
def on_case(node)
|
|
@@ -6,8 +6,8 @@ module ThemeCheck
|
|
|
6
6
|
doc docs_url(__FILE__)
|
|
7
7
|
|
|
8
8
|
def on_schema(node)
|
|
9
|
-
schema =
|
|
10
|
-
|
|
9
|
+
schema = node.inner_json
|
|
10
|
+
return if schema.nil?
|
|
11
11
|
# Get all locales used in the schema
|
|
12
12
|
used_locales = Set.new([theme.default_locale])
|
|
13
13
|
visit_object(schema) do |_, locales|
|
|
@@ -19,26 +19,31 @@ module ThemeCheck
|
|
|
19
19
|
visit_object(schema) do |key, locales|
|
|
20
20
|
missing = used_locales - locales
|
|
21
21
|
if missing.any?
|
|
22
|
-
add_offense("#{key} missing translations for #{missing.join(', ')}", node: node)
|
|
22
|
+
add_offense("#{key} missing translations for #{missing.join(', ')}", node: node) do |corrector|
|
|
23
|
+
key = key.split(".")
|
|
24
|
+
missing.each do |language|
|
|
25
|
+
SchemaHelper.schema_corrector(schema, key + [language], "TODO")
|
|
26
|
+
end
|
|
27
|
+
corrector.replace_inner_json(node, schema)
|
|
28
|
+
end
|
|
23
29
|
end
|
|
24
30
|
end
|
|
25
31
|
|
|
26
|
-
check_locales(schema
|
|
27
|
-
|
|
28
|
-
rescue JSON::ParserError
|
|
29
|
-
# Ignored, handled in ValidSchema.
|
|
32
|
+
check_locales(schema, node: node)
|
|
30
33
|
end
|
|
31
34
|
|
|
32
35
|
private
|
|
33
36
|
|
|
34
|
-
def check_locales(
|
|
37
|
+
def check_locales(schema, node:)
|
|
38
|
+
locales = schema["locales"]
|
|
35
39
|
return unless locales.is_a?(Hash)
|
|
36
40
|
|
|
37
41
|
default_locale = locales[theme.default_locale]
|
|
42
|
+
|
|
38
43
|
if default_locale
|
|
39
44
|
locales.each_pair do |name, content|
|
|
40
45
|
diff = LocaleDiff.new(default_locale, content)
|
|
41
|
-
diff.add_as_offenses(self, key_prefix: ["locales", name], node: node)
|
|
46
|
+
diff.add_as_offenses(self, key_prefix: ["locales", name], node: node, schema: schema)
|
|
42
47
|
end
|
|
43
48
|
else
|
|
44
49
|
add_offense("Missing default locale in key: locales", node: node)
|
|
@@ -25,15 +25,15 @@ module ThemeCheck
|
|
|
25
25
|
def on_end
|
|
26
26
|
(REQUIRED_LIQUID_FILES - theme.liquid.map(&:name)).each do |file|
|
|
27
27
|
add_offense("'#{file}.liquid' is missing") do |corrector|
|
|
28
|
-
corrector.
|
|
28
|
+
corrector.create_file(@theme.storage, "#{file}.liquid", "")
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
(REQUIRED_TEMPLATE_FILES - (theme.liquid + theme.json).map(&:name)).each do |file|
|
|
32
32
|
add_offense("'#{file}.liquid' or '#{file}.json' is missing") do |corrector|
|
|
33
33
|
if REQUIRED_LIQUID_TEMPLATE_FILES.include?(file)
|
|
34
|
-
corrector.
|
|
34
|
+
corrector.create_file(@theme.storage, "#{file}.liquid", "")
|
|
35
35
|
else
|
|
36
|
-
corrector.
|
|
36
|
+
corrector.create_file(@theme.storage, "#{file}.json", "")
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
end
|
|
@@ -35,7 +35,7 @@ module ThemeCheck
|
|
|
35
35
|
path = "#{name}.liquid"
|
|
36
36
|
unless ignore?(path) || theme[name]
|
|
37
37
|
add_offense("'#{path}' is not found", node: node) do |corrector|
|
|
38
|
-
corrector.
|
|
38
|
+
corrector.create_file(@theme.storage, "#{name}.liquid", "")
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
end
|
|
@@ -27,13 +27,12 @@ module ThemeCheck
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def on_schema(node)
|
|
30
|
-
schema =
|
|
30
|
+
schema = node.inner_json
|
|
31
|
+
return if schema.nil?
|
|
31
32
|
|
|
32
33
|
if (settings = schema["settings"])
|
|
33
34
|
@schema_settings = settings
|
|
34
35
|
end
|
|
35
|
-
rescue JSON::ParserError
|
|
36
|
-
# Ignored, handled in ValidSchema.
|
|
37
36
|
end
|
|
38
37
|
|
|
39
38
|
##
|
|
@@ -22,6 +22,7 @@ module ThemeCheck
|
|
|
22
22
|
return if resource_url =~ ABSOLUTE_PATH
|
|
23
23
|
return if resource_url =~ RELATIVE_PATH
|
|
24
24
|
return if url_hosted_by_shopify?(resource_url)
|
|
25
|
+
return if url_is_setting_variable?(resource_url)
|
|
25
26
|
|
|
26
27
|
# Ignore non-stylesheet link tags
|
|
27
28
|
rel = node.attributes["rel"]
|
|
@@ -39,5 +40,9 @@ module ThemeCheck
|
|
|
39
40
|
url.start_with?(Liquid::VariableStart) &&
|
|
40
41
|
AssetUrlFilters::ASSET_URL_FILTERS.any? { |filter| url.include?(filter) }
|
|
41
42
|
end
|
|
43
|
+
|
|
44
|
+
def url_is_setting_variable?(url)
|
|
45
|
+
url.start_with?(Liquid::VariableStart) && url =~ /settings\./
|
|
46
|
+
end
|
|
42
47
|
end
|
|
43
48
|
end
|
|
@@ -27,14 +27,19 @@ module ThemeCheck
|
|
|
27
27
|
def after_document(node)
|
|
28
28
|
return unless node.theme_file.name == LAYOUT_FILENAME
|
|
29
29
|
|
|
30
|
-
add_missing_object_offense("content_for_layout") unless @content_for_layout_found
|
|
31
|
-
add_missing_object_offense("content_for_header") unless @content_for_header_found
|
|
30
|
+
add_missing_object_offense("content_for_layout", "</body>") unless @content_for_layout_found
|
|
31
|
+
add_missing_object_offense("content_for_header", "</head>") unless @content_for_header_found
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
private
|
|
35
35
|
|
|
36
|
-
def add_missing_object_offense(name)
|
|
37
|
-
add_offense("#{LAYOUT_FILENAME} must include {{#{name}}}", node: @layout_theme_node)
|
|
36
|
+
def add_missing_object_offense(name, tag)
|
|
37
|
+
add_offense("#{LAYOUT_FILENAME} must include {{#{name}}}", node: @layout_theme_node) do
|
|
38
|
+
if @layout_theme_node.source.index(tag)
|
|
39
|
+
@layout_theme_node.source.insert(@layout_theme_node.source.index(tag), " {{ #{name} }}\n ")
|
|
40
|
+
@layout_theme_node.markup = @layout_theme_node.source
|
|
41
|
+
end
|
|
42
|
+
end
|
|
38
43
|
end
|
|
39
44
|
end
|
|
40
45
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module ThemeCheck
|
|
3
|
+
class SchemaJsonFormat < LiquidCheck
|
|
4
|
+
severity :style
|
|
5
|
+
category :liquid
|
|
6
|
+
doc docs_url(__FILE__)
|
|
7
|
+
|
|
8
|
+
def initialize(start_level: 0, indent: ' ')
|
|
9
|
+
@pretty_json_opts = {
|
|
10
|
+
indent: indent,
|
|
11
|
+
start_level: start_level,
|
|
12
|
+
}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def on_schema(node)
|
|
16
|
+
schema = node.inner_json
|
|
17
|
+
return if schema.nil?
|
|
18
|
+
pretty_schema = pretty_json(schema, **@pretty_json_opts)
|
|
19
|
+
if pretty_schema != node.inner_markup
|
|
20
|
+
add_offense(
|
|
21
|
+
"JSON formatting could be improved",
|
|
22
|
+
node: node,
|
|
23
|
+
) do |corrector|
|
|
24
|
+
corrector.replace_inner_json(node, schema, **@pretty_json_opts)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -6,115 +6,160 @@ module ThemeCheck
|
|
|
6
6
|
category :liquid
|
|
7
7
|
doc docs_url(__FILE__)
|
|
8
8
|
|
|
9
|
-
def initialize
|
|
10
|
-
@ignore = false
|
|
11
|
-
end
|
|
12
|
-
|
|
13
9
|
def on_node(node)
|
|
14
10
|
return unless node.markup
|
|
15
|
-
return if
|
|
11
|
+
return if node.literal?
|
|
12
|
+
return if node.assigned_or_echoed_variable?
|
|
16
13
|
|
|
17
14
|
outside_of_strings(node.markup) do |chunk, chunk_start|
|
|
18
|
-
chunk.scan(/([,:|]|==|<>|<=|>=|<|>|!=)( +)/) do |_match|
|
|
19
|
-
|
|
20
|
-
"Too many spaces after '#{Regexp.last_match(1)}'",
|
|
21
|
-
node: node,
|
|
22
|
-
markup: Regexp.last_match(2),
|
|
23
|
-
node_markup_offset: chunk_start + Regexp.last_match.begin(2)
|
|
24
|
-
)
|
|
15
|
+
chunk.scan(/(?<token>[,:|]|==|<>|<=|>=|<|>|!=)(?<offense> +)/) do |_match|
|
|
16
|
+
add_too_many_spaces_after_offense(Regexp.last_match, node, chunk_start)
|
|
25
17
|
end
|
|
26
|
-
chunk.scan(/([,:|]|==|<>|<=|>=|<\b|>\b|!=)(\S|\z)/) do |_match|
|
|
27
|
-
|
|
28
|
-
"Space missing after '#{Regexp.last_match(1)}'",
|
|
29
|
-
node: node,
|
|
30
|
-
markup: Regexp.last_match(1),
|
|
31
|
-
node_markup_offset: chunk_start + Regexp.last_match.begin(0),
|
|
32
|
-
)
|
|
18
|
+
chunk.scan(/(?<offense>(?<token>[,:|]|==|<>|<=|>=|<\b|>\b|!=)(\S|\z))/) do |_match|
|
|
19
|
+
add_space_missing_after_offense(Regexp.last_match, node, chunk_start)
|
|
33
20
|
end
|
|
34
|
-
chunk.scan(/(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
markup: Regexp.last_match(1),
|
|
39
|
-
node_markup_offset: chunk_start + Regexp.last_match.begin(1)
|
|
40
|
-
)
|
|
21
|
+
chunk.scan(/(?<offense>\s{2,})(?<token>\||==|<>|<=|>=|<|>|!=)+/) do |_match|
|
|
22
|
+
unless Regexp.last_match(:offense).include?("\n")
|
|
23
|
+
add_too_many_spaces_before_offense(Regexp.last_match, node, chunk_start)
|
|
24
|
+
end
|
|
41
25
|
end
|
|
42
|
-
chunk.scan(/(\A|\S)(?<
|
|
43
|
-
|
|
44
|
-
"Space missing before '#{Regexp.last_match(1)}'",
|
|
45
|
-
node: node,
|
|
46
|
-
markup: Regexp.last_match(:match),
|
|
47
|
-
node_markup_offset: chunk_start + Regexp.last_match.begin(:match)
|
|
48
|
-
)
|
|
26
|
+
chunk.scan(/(\A|\S)(?<offense>(?<token>\||==|<>|<=|>=|<|\b>|!=))/) do |_match|
|
|
27
|
+
add_space_missing_before_offense(Regexp.last_match, node, chunk_start)
|
|
49
28
|
end
|
|
50
29
|
end
|
|
51
30
|
end
|
|
52
31
|
|
|
32
|
+
BlockMarkup = Struct.new(:markup, :node_markup_offset)
|
|
33
|
+
|
|
53
34
|
def on_tag(node)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
35
|
+
return if node.inside_liquid_tag?
|
|
36
|
+
|
|
37
|
+
# Both the start and end tags
|
|
38
|
+
blocks = [
|
|
39
|
+
BlockMarkup.new(node.block_start_markup, node.block_start_start_index - node.start_index),
|
|
40
|
+
BlockMarkup.new(node.block_end_markup, node.block_end_start_index - node.start_index),
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
blocks.each do |block|
|
|
44
|
+
# Looking at spaces after the start token
|
|
45
|
+
if block.markup =~ /^(?<token>{%-?)(?<offense>[^ \n\t-])/
|
|
46
|
+
add_space_missing_after_offense(Regexp.last_match, node, block.node_markup_offset)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
if block.markup =~ /^(?<token>{%-?)(?<offense> {2,})\S/
|
|
50
|
+
add_too_many_spaces_after_offense(Regexp.last_match, node, block.node_markup_offset)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Looking at spaces before the end token
|
|
54
|
+
if block.markup =~ /(?<offense>[^ \n\t-])(?<token>-?%})$/
|
|
55
|
+
add_space_missing_before_offense(Regexp.last_match, node, block.node_markup_offset)
|
|
69
56
|
end
|
|
57
|
+
|
|
58
|
+
if block.markup =~ /\S(?<offense> {2,})(?<token>-?%})$/
|
|
59
|
+
add_too_many_spaces_before_offense(Regexp.last_match, node, block.node_markup_offset)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
next
|
|
70
63
|
end
|
|
71
|
-
@ignore = true
|
|
72
64
|
end
|
|
73
65
|
|
|
74
|
-
def
|
|
75
|
-
|
|
66
|
+
def on_variable(node)
|
|
67
|
+
return if node.markup.empty?
|
|
68
|
+
return if node.assigned_or_echoed_variable?
|
|
69
|
+
|
|
70
|
+
block_start_offset = node.block_start_start_index - node.start_index
|
|
71
|
+
|
|
72
|
+
# Looking at spaces after the start token
|
|
73
|
+
if node.block_start_markup =~ /^(?<token>{{-?)(?<offense>[^ \n\t-])/
|
|
74
|
+
add_space_missing_after_offense(Regexp.last_match, node, block_start_offset)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
if node.block_start_markup =~ /^(?<token>{{-?)(?<offense> {2,})\S/
|
|
78
|
+
add_too_many_spaces_after_offense(Regexp.last_match, node, block_start_offset)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Looking at spaces before the end token
|
|
82
|
+
if node.block_start_markup =~ /(?<offense>[^ \n\t-])(?<token>-?}})$/
|
|
83
|
+
add_space_missing_before_offense(Regexp.last_match, node, block_start_offset)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
if node.block_start_markup =~ /\S(?<offense> {2,})(?<token>-?}})$/
|
|
87
|
+
add_too_many_spaces_before_offense(Regexp.last_match, node, block_start_offset)
|
|
88
|
+
end
|
|
76
89
|
end
|
|
77
90
|
|
|
78
|
-
def
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
def add_space_missing_after_offense(match, node, source_offset)
|
|
92
|
+
add_offense_for_match(
|
|
93
|
+
"Space missing after '#{match[:token]}'",
|
|
94
|
+
match,
|
|
95
|
+
node,
|
|
96
|
+
source_offset
|
|
97
|
+
) do |corrector|
|
|
98
|
+
corrector.insert_after(
|
|
99
|
+
node,
|
|
100
|
+
' ',
|
|
101
|
+
(node.start_index + source_offset + match.begin(:token))...
|
|
102
|
+
(node.start_index + source_offset + match.end(:token))
|
|
103
|
+
)
|
|
88
104
|
end
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def add_too_many_spaces_after_offense(match, node, source_offset)
|
|
108
|
+
add_offense_for_match(
|
|
109
|
+
"Too many spaces after '#{match[:token]}'",
|
|
110
|
+
match,
|
|
111
|
+
node,
|
|
112
|
+
source_offset
|
|
113
|
+
) do |corrector|
|
|
114
|
+
corrector.replace(
|
|
115
|
+
node,
|
|
116
|
+
' ',
|
|
117
|
+
(node.start_index + source_offset + match.begin(:offense))...
|
|
118
|
+
(node.start_index + source_offset + match.end(:offense))
|
|
119
|
+
)
|
|
98
120
|
end
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def add_space_missing_before_offense(match, node, source_offset)
|
|
124
|
+
add_offense_for_match(
|
|
125
|
+
"Space missing before '#{match[:token]}'",
|
|
126
|
+
match,
|
|
127
|
+
node,
|
|
128
|
+
source_offset
|
|
129
|
+
) do |corrector|
|
|
130
|
+
corrector.insert_before(
|
|
131
|
+
node,
|
|
132
|
+
' ',
|
|
133
|
+
(node.start_index + source_offset + match.begin(:token))...
|
|
134
|
+
(node.start_index + source_offset + match.end(:token))
|
|
135
|
+
)
|
|
107
136
|
end
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def add_too_many_spaces_before_offense(match, node, source_offset)
|
|
140
|
+
add_offense_for_match(
|
|
141
|
+
"Too many spaces before '#{match[:token]}'",
|
|
142
|
+
match,
|
|
143
|
+
node,
|
|
144
|
+
source_offset
|
|
145
|
+
) do |corrector|
|
|
146
|
+
corrector.replace(
|
|
147
|
+
node,
|
|
148
|
+
' ',
|
|
149
|
+
(node.start_index + source_offset + match.begin(:offense))...
|
|
150
|
+
(node.start_index + source_offset + match.end(:offense))
|
|
151
|
+
)
|
|
117
152
|
end
|
|
118
153
|
end
|
|
154
|
+
|
|
155
|
+
def add_offense_for_match(message, match, node, source_offset, &block)
|
|
156
|
+
add_offense(
|
|
157
|
+
message,
|
|
158
|
+
node: node,
|
|
159
|
+
markup: match[:offense],
|
|
160
|
+
node_markup_offset: source_offset + match.begin(:offense),
|
|
161
|
+
&block
|
|
162
|
+
)
|
|
163
|
+
end
|
|
119
164
|
end
|
|
120
165
|
end
|
|
@@ -5,28 +5,48 @@ module ThemeCheck
|
|
|
5
5
|
category :translation
|
|
6
6
|
doc docs_url(__FILE__)
|
|
7
7
|
|
|
8
|
+
def initialize
|
|
9
|
+
@schema_locales = {}
|
|
10
|
+
@nodes = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def on_document(node)
|
|
14
|
+
@nodes[node.theme_file.name] = []
|
|
15
|
+
end
|
|
16
|
+
|
|
8
17
|
def on_variable(node)
|
|
9
18
|
return unless @theme.default_locale_json&.content&.is_a?(Hash)
|
|
10
|
-
|
|
11
19
|
return unless node.value.filters.any? { |name, _| name == "t" || name == "translate" }
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
|
|
21
|
+
@nodes[node.theme_file.name] << node
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def on_schema(node)
|
|
25
|
+
if (schema_locales = node.inner_json&.dig("locales", @theme.default_locale))
|
|
26
|
+
@schema_locales = schema_locales
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def on_end
|
|
31
|
+
@nodes.each_pair do |_file_name, file_nodes|
|
|
32
|
+
file_nodes.each do |node|
|
|
33
|
+
next unless (key_node = node.children.first)
|
|
34
|
+
next unless key_node.value.is_a?(String)
|
|
35
|
+
next if key_exists?(key_node.value, @theme.default_locale_json.content) || key_exists?(key_node.value, @schema_locales) || ShopifyLiquid::SystemTranslations.include?(key_node.value)
|
|
36
|
+
add_offense(
|
|
37
|
+
@schema_locales.empty? ? "'#{key_node.value}' does not have a matching entry in '#{@theme.default_locale_json.relative_path}'" : "'#{key_node.value}' does not have a matching entry in '#{@theme.default_locale_json.relative_path}' or '#{node.theme_file.relative_path}'",
|
|
38
|
+
node: node,
|
|
39
|
+
markup: key_node.value
|
|
40
|
+
) do |corrector|
|
|
41
|
+
corrector.add_translation(@theme.default_locale_json, key_node.value.split("."), "TODO")
|
|
42
|
+
end
|
|
22
43
|
end
|
|
23
44
|
end
|
|
24
45
|
end
|
|
25
46
|
|
|
26
47
|
private
|
|
27
48
|
|
|
28
|
-
def key_exists?(key)
|
|
29
|
-
pointer = @theme.default_locale_json.content
|
|
49
|
+
def key_exists?(key, pointer)
|
|
30
50
|
key.split(".").each do |token|
|
|
31
51
|
return false unless pointer.key?(token)
|
|
32
52
|
pointer = pointer[token]
|
|
@@ -46,8 +46,9 @@ module ThemeCheck
|
|
|
46
46
|
@templates.each_pair do |_, info|
|
|
47
47
|
used = info.collect_used_assigns(@templates)
|
|
48
48
|
info.assign_nodes.each_pair do |name, node|
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
next if used.include?(name)
|
|
50
|
+
add_offense("`#{name}` is never used", node: node) do |corrector|
|
|
51
|
+
corrector.remove(node)
|
|
51
52
|
end
|
|
52
53
|
end
|
|
53
54
|
end
|
|
@@ -25,7 +25,7 @@ module ThemeCheck
|
|
|
25
25
|
def on_end
|
|
26
26
|
missing_snippets.each do |theme_file|
|
|
27
27
|
add_offense("This snippet is not used", theme_file: theme_file) do |corrector|
|
|
28
|
-
corrector.
|
|
28
|
+
corrector.remove_file(@theme.storage, theme_file.relative_path.to_s)
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
end
|
|
@@ -19,7 +19,7 @@ module ThemeCheck
|
|
|
19
19
|
|
|
20
20
|
def html_key?(keys)
|
|
21
21
|
pluralized_key = keys[-2] if keys.length > 1
|
|
22
|
-
keys[-1].end_with?('_html') || pluralized_key
|
|
22
|
+
keys[-1].end_with?('_html') || pluralized_key&.end_with?('_html')
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def parse_and_add_offense(key, value)
|