theme-check 1.7.1 → 1.9.1
Sign up to get free protection for your applications and to get access to all the features.
- 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 +99 -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 +20 -5
- 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)
|