theme-check 1.7.2 → 1.9.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +47 -0
  4. data/README.md +10 -0
  5. data/RELEASING.md +13 -0
  6. data/config/default.yml +5 -0
  7. data/data/shopify_liquid/deprecated_filters.yml +4 -0
  8. data/data/shopify_liquid/filters.yml +3 -1
  9. data/docs/checks/TEMPLATE.md.erb +24 -19
  10. data/docs/checks/schema_json_format.md +76 -0
  11. data/docs/language_server/code-action-command-palette.png +0 -0
  12. data/docs/language_server/code-action-flow.png +0 -0
  13. data/docs/language_server/code-action-keyboard.png +0 -0
  14. data/docs/language_server/code-action-light-bulb.png +0 -0
  15. data/docs/language_server/code-action-problem.png +0 -0
  16. data/docs/language_server/code-action-quickfix.png +0 -0
  17. data/docs/language_server/how_to_correct_code_with_code_actions_and_execute_command.md +197 -0
  18. data/exe/theme-check-language-server +0 -4
  19. data/lib/theme_check/checks/asset_size_app_block_css.rb +2 -3
  20. data/lib/theme_check/checks/asset_size_app_block_javascript.rb +2 -3
  21. data/lib/theme_check/checks/asset_url_filters.rb +2 -0
  22. data/lib/theme_check/checks/default_locale.rb +1 -1
  23. data/lib/theme_check/checks/deprecated_filter.rb +81 -4
  24. data/lib/theme_check/checks/deprecated_global_app_block_type.rb +2 -3
  25. data/lib/theme_check/checks/matching_schema_translations.rb +14 -9
  26. data/lib/theme_check/checks/matching_translations.rb +1 -0
  27. data/lib/theme_check/checks/missing_required_template_files.rb +3 -3
  28. data/lib/theme_check/checks/missing_template.rb +1 -1
  29. data/lib/theme_check/checks/pagination_size.rb +2 -3
  30. data/lib/theme_check/checks/remote_asset.rb +5 -0
  31. data/lib/theme_check/checks/required_directories.rb +1 -1
  32. data/lib/theme_check/checks/required_layout_theme_object.rb +9 -4
  33. data/lib/theme_check/checks/schema_json_format.rb +29 -0
  34. data/lib/theme_check/checks/space_inside_braces.rb +132 -87
  35. data/lib/theme_check/checks/translation_key_exists.rb +33 -13
  36. data/lib/theme_check/checks/unused_assign.rb +3 -2
  37. data/lib/theme_check/checks/unused_snippet.rb +1 -1
  38. data/lib/theme_check/checks/valid_html_translation.rb +1 -1
  39. data/lib/theme_check/checks/valid_schema.rb +2 -2
  40. data/lib/theme_check/corrector.rb +34 -23
  41. data/lib/theme_check/file_system_storage.rb +4 -3
  42. data/lib/theme_check/html_node.rb +122 -6
  43. data/lib/theme_check/html_visitor.rb +1 -32
  44. data/lib/theme_check/in_memory_storage.rb +9 -0
  45. data/lib/theme_check/json_helpers.rb +14 -0
  46. data/lib/theme_check/language_server/bridge.rb +19 -5
  47. data/lib/theme_check/language_server/client_capabilities.rb +27 -0
  48. data/lib/theme_check/language_server/code_action_engine.rb +32 -0
  49. data/lib/theme_check/language_server/code_action_provider.rb +42 -0
  50. data/lib/theme_check/language_server/code_action_providers/quickfix_code_action_provider.rb +83 -0
  51. data/lib/theme_check/language_server/code_action_providers/source_fix_all_code_action_provider.rb +40 -0
  52. data/lib/theme_check/language_server/configuration.rb +69 -0
  53. data/lib/theme_check/language_server/diagnostic.rb +124 -0
  54. data/lib/theme_check/language_server/diagnostics_engine.rb +15 -60
  55. data/lib/theme_check/language_server/diagnostics_manager.rb +136 -0
  56. data/lib/theme_check/language_server/document_change_corrector.rb +267 -0
  57. data/lib/theme_check/language_server/document_link_provider.rb +6 -6
  58. data/lib/theme_check/language_server/execute_command_engine.rb +19 -0
  59. data/lib/theme_check/language_server/execute_command_provider.rb +30 -0
  60. data/lib/theme_check/language_server/execute_command_providers/correction_execute_command_provider.rb +48 -0
  61. data/lib/theme_check/language_server/execute_command_providers/run_checks_execute_command_provider.rb +22 -0
  62. data/lib/theme_check/language_server/handler.rb +83 -29
  63. data/lib/theme_check/language_server/io_messenger.rb +11 -1
  64. data/lib/theme_check/language_server/protocol.rb +4 -0
  65. data/lib/theme_check/language_server/server.rb +29 -11
  66. data/lib/theme_check/language_server/uri_helper.rb +1 -0
  67. data/lib/theme_check/language_server/versioned_in_memory_storage.rb +69 -0
  68. data/lib/theme_check/language_server.rb +23 -5
  69. data/lib/theme_check/liquid_node.rb +255 -12
  70. data/lib/theme_check/locale_diff.rb +39 -8
  71. data/lib/theme_check/node.rb +16 -0
  72. data/lib/theme_check/offense.rb +27 -23
  73. data/lib/theme_check/position.rb +4 -4
  74. data/lib/theme_check/regex_helpers.rb +1 -1
  75. data/lib/theme_check/schema_helper.rb +70 -0
  76. data/lib/theme_check/storage.rb +4 -0
  77. data/lib/theme_check/tags.rb +0 -1
  78. data/lib/theme_check/theme.rb +1 -1
  79. data/lib/theme_check/theme_file.rb +8 -1
  80. data/lib/theme_check/theme_file_rewriter.rb +28 -6
  81. data/lib/theme_check/version.rb +1 -1
  82. data/lib/theme_check.rb +11 -2
  83. metadata +26 -3
  84. 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
- alternatives = alternatives.map { |alt| "`#{alt}`" }
15
- add_offense(
16
- "Deprecated filter `#{filter}`, consider using an alternative: #{alternatives.join(', ')}",
17
- node: node,
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 = JSON.parse(node.value.nodelist.join)
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 = JSON.parse(node.value.nodelist.join)
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["locales"], node: node)
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(locales, node:)
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)
@@ -13,6 +13,7 @@ module ThemeCheck
13
13
  def on_file(file)
14
14
  return unless file.name.start_with?("locales/")
15
15
  return unless file.content.is_a?(Hash)
16
+ return if file.name =~ /\.schema$/
16
17
  return if file.name == @theme.default_locale_json&.name
17
18
 
18
19
  @files << file
@@ -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.create(@theme, "#{file}.liquid", "")
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.create(@theme, "#{file}.liquid", "")
34
+ corrector.create_file(@theme.storage, "#{file}.liquid", "")
35
35
  else
36
- corrector.create(@theme, "#{file}.json", "")
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.create(@theme, "#{name}.liquid", "")
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 = JSON.parse(node.value.nodelist.join)
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
@@ -19,7 +19,7 @@ module ThemeCheck
19
19
 
20
20
  def add_missing_directories_offense(directory)
21
21
  add_offense("Theme is missing '#{directory}' directory") do |corrector|
22
- corrector.mkdir(@theme, directory)
22
+ corrector.mkdir(@theme.storage, directory)
23
23
  end
24
24
  end
25
25
  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 :assign == node.type_name
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
- add_offense(
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
- add_offense(
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(/( +)(\||==|<>|<=|>=|<|>|!=)+/) do |_match|
35
- add_offense(
36
- "Too many spaces before '#{Regexp.last_match(2)}'",
37
- node: node,
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)(?<match>\||==|<>|<=|>=|<|\b>|!=)/) do |_match|
43
- add_offense(
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
- unless node.inside_liquid_tag?
55
- if node.markup[-1] != " " && node.markup[-1] != "\n"
56
- add_offense(
57
- "Space missing before '#{node.end_token}'",
58
- node: node,
59
- markup: node.markup[-1],
60
- node_markup_offset: node.markup.size - 1,
61
- )
62
- elsif node.markup =~ /(\n?)( +)\z/m && Regexp.last_match(1) != "\n"
63
- add_offense(
64
- "Too many spaces before '#{node.end_token}'",
65
- node: node,
66
- markup: Regexp.last_match(2),
67
- node_markup_offset: node.markup.size - Regexp.last_match(2).size
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 after_tag(_node)
75
- @ignore = false
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 on_variable(node)
79
- return if @ignore || node.markup.empty?
80
- if node.markup[0] != " "
81
- add_offense(
82
- "Space missing after '#{node.start_token}'",
83
- node: node,
84
- markup: node.markup[0]
85
- ) do |corrector|
86
- corrector.insert_before(node, " ")
87
- end
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
- if node.markup[-1] != " " && node.markup[-1] != "\n"
90
- add_offense(
91
- "Space missing before '#{node.end_token}'",
92
- node: node,
93
- markup: node.markup[-1],
94
- node_markup_offset: node.markup.size - 1,
95
- ) do |corrector|
96
- corrector.insert_after(node, " ")
97
- end
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
- if node.markup =~ /\A( +)/m
100
- add_offense(
101
- "Too many spaces after '#{node.start_token}'",
102
- node: node,
103
- markup: Regexp.last_match(1),
104
- ) do |corrector|
105
- corrector.replace(node, " #{node.markup.lstrip}")
106
- end
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
- if node.markup =~ /(\n?)( +)\z/m && Regexp.last_match(1) != "\n"
109
- add_offense(
110
- "Too many spaces before '#{node.end_token}'",
111
- node: node,
112
- markup: Regexp.last_match(2),
113
- node_markup_offset: node.markup.size - Regexp.last_match(2).size
114
- ) do |corrector|
115
- corrector.replace(node, "#{node.markup.rstrip} ")
116
- end
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
- return unless (key_node = node.children.first)
13
- return unless key_node.value.is_a?(String)
14
-
15
- unless key_exists?(key_node.value) || ShopifyLiquid::SystemTranslations.include?(key_node.value)
16
- add_offense(
17
- "'#{key_node.value}' does not have a matching entry in '#{@theme.default_locale_json.relative_path}'",
18
- node: node,
19
- markup: key_node.value,
20
- ) do |corrector|
21
- corrector.add_default_translation_key(@theme.default_locale_json, key_node.value.split("."), "TODO")
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
- unless used.include?(name)
50
- add_offense("`#{name}` is never used", node: node)
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.remove(@theme, theme_file.relative_path.to_s)
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.end_with?('_html')
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)