theme-check 1.10.3 → 1.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +29 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  4. data/.github/workflows/cla.yml +22 -0
  5. data/.github/workflows/theme-check.yml +1 -1
  6. data/.gitignore +3 -0
  7. data/CHANGELOG.md +48 -0
  8. data/CONTRIBUTING.md +82 -0
  9. data/README.md +11 -8
  10. data/Rakefile +7 -0
  11. data/TROUBLESHOOTING.md +65 -0
  12. data/config/default.yml +4 -0
  13. data/data/shopify_liquid/built_in_liquid_objects.json +60 -0
  14. data/data/shopify_liquid/documentation/filters.json +5528 -0
  15. data/data/shopify_liquid/documentation/latest.json +1 -0
  16. data/data/shopify_liquid/documentation/objects.json +19272 -0
  17. data/data/shopify_liquid/documentation/tags.json +1252 -0
  18. data/data/shopify_liquid/filters.yml +18 -0
  19. data/dev.yml +1 -1
  20. data/docs/checks/asset_preload.md +60 -0
  21. data/docs/checks/asset_size_javascript.md +2 -2
  22. data/docs/checks/missing_enable_comment.md +3 -3
  23. data/docs/checks/nested_snippet.md +8 -8
  24. data/docs/checks/translation_key_exists.md +4 -4
  25. data/docs/checks/valid_html_translation.md +1 -1
  26. data/lib/theme_check/analyzer.rb +18 -3
  27. data/lib/theme_check/check.rb +6 -1
  28. data/lib/theme_check/checks/asset_preload.rb +20 -0
  29. data/lib/theme_check/checks/deprecated_filter.rb +29 -5
  30. data/lib/theme_check/checks/missing_enable_comment.rb +4 -0
  31. data/lib/theme_check/checks/missing_required_template_files.rb +5 -1
  32. data/lib/theme_check/checks/missing_template.rb +5 -1
  33. data/lib/theme_check/checks/undefined_object.rb +4 -0
  34. data/lib/theme_check/checks/unused_assign.rb +6 -1
  35. data/lib/theme_check/checks/unused_snippet.rb +50 -2
  36. data/lib/theme_check/config.rb +2 -2
  37. data/lib/theme_check/disabled_checks.rb +11 -4
  38. data/lib/theme_check/file_system_storage.rb +2 -0
  39. data/lib/theme_check/in_memory_storage.rb +1 -1
  40. data/lib/theme_check/language_server/bridge.rb +31 -6
  41. data/lib/theme_check/language_server/completion_context.rb +52 -0
  42. data/lib/theme_check/language_server/completion_engine.rb +15 -21
  43. data/lib/theme_check/language_server/completion_provider.rb +16 -1
  44. data/lib/theme_check/language_server/completion_providers/assignments_completion_provider.rb +36 -0
  45. data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +49 -6
  46. data/lib/theme_check/language_server/completion_providers/object_attribute_completion_provider.rb +47 -0
  47. data/lib/theme_check/language_server/completion_providers/object_completion_provider.rb +10 -7
  48. data/lib/theme_check/language_server/completion_providers/render_snippet_completion_provider.rb +5 -1
  49. data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +8 -1
  50. data/lib/theme_check/language_server/diagnostics_engine.rb +80 -34
  51. data/lib/theme_check/language_server/diagnostics_manager.rb +27 -6
  52. data/lib/theme_check/language_server/execute_command_providers/run_checks_execute_command_provider.rb +7 -6
  53. data/lib/theme_check/language_server/handler.rb +93 -9
  54. data/lib/theme_check/language_server/protocol.rb +9 -0
  55. data/lib/theme_check/language_server/server.rb +42 -14
  56. data/lib/theme_check/language_server/type_helper.rb +22 -0
  57. data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder/node_handler.rb +63 -0
  58. data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder/scope.rb +57 -0
  59. data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder/scope_visitor.rb +42 -0
  60. data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder.rb +76 -0
  61. data/lib/theme_check/language_server/variable_lookup_finder/constants.rb +43 -0
  62. data/lib/theme_check/language_server/variable_lookup_finder/liquid_fixer.rb +103 -0
  63. data/lib/theme_check/language_server/variable_lookup_finder/potential_lookup.rb +10 -0
  64. data/lib/theme_check/language_server/variable_lookup_finder/tolerant_parser.rb +94 -0
  65. data/lib/theme_check/language_server/variable_lookup_finder.rb +60 -100
  66. data/lib/theme_check/language_server/variable_lookup_traverser.rb +70 -0
  67. data/lib/theme_check/language_server/versioned_in_memory_storage.rb +17 -2
  68. data/lib/theme_check/language_server.rb +12 -0
  69. data/lib/theme_check/liquid_file.rb +22 -1
  70. data/lib/theme_check/liquid_node.rb +33 -1
  71. data/lib/theme_check/liquid_visitor.rb +1 -1
  72. data/lib/theme_check/remote_asset_file.rb +13 -7
  73. data/lib/theme_check/schema_helper.rb +1 -1
  74. data/lib/theme_check/shopify_liquid/documentation/markdown_template.rb +51 -0
  75. data/lib/theme_check/shopify_liquid/documentation.rb +44 -0
  76. data/lib/theme_check/shopify_liquid/filter.rb +4 -0
  77. data/lib/theme_check/shopify_liquid/object.rb +4 -0
  78. data/lib/theme_check/shopify_liquid/source_index/base_entry.rb +60 -0
  79. data/lib/theme_check/shopify_liquid/source_index/base_state.rb +23 -0
  80. data/lib/theme_check/shopify_liquid/source_index/filter_entry.rb +18 -0
  81. data/lib/theme_check/shopify_liquid/source_index/filter_state.rb +11 -0
  82. data/lib/theme_check/shopify_liquid/source_index/object_entry.rb +14 -0
  83. data/lib/theme_check/shopify_liquid/source_index/object_state.rb +11 -0
  84. data/lib/theme_check/shopify_liquid/source_index/parameter_entry.rb +21 -0
  85. data/lib/theme_check/shopify_liquid/source_index/property_entry.rb +9 -0
  86. data/lib/theme_check/shopify_liquid/source_index/return_type_entry.rb +37 -0
  87. data/lib/theme_check/shopify_liquid/source_index/tag_entry.rb +20 -0
  88. data/lib/theme_check/shopify_liquid/source_index/tag_state.rb +11 -0
  89. data/lib/theme_check/shopify_liquid/source_index.rb +56 -0
  90. data/lib/theme_check/shopify_liquid/source_manager.rb +111 -0
  91. data/lib/theme_check/shopify_liquid/tag.rb +4 -0
  92. data/lib/theme_check/shopify_liquid.rb +17 -1
  93. data/lib/theme_check/tags.rb +2 -1
  94. data/lib/theme_check/version.rb +1 -1
  95. data/shipit.rubygems.yml +3 -0
  96. data/theme-check.gemspec +5 -3
  97. metadata +45 -6
  98. data/.github/probots.yml +0 -3
@@ -1,4 +1,20 @@
1
1
  ---
2
+ # Here's an example workflow that's going to get ya the list of filters available
3
+ # ```bash
4
+ # spin up storefront-renderer
5
+ # spin ssh
6
+ # cd storefront-renderer
7
+ # bundle exec rake console
8
+ # ```
9
+ # ```ruby
10
+ # Pathname(ENV["HOME"]).join('filters.yml').write(YAML.dump(Liquid::StrainerFactory.global_filter_names))
11
+ # ```
12
+ # ```bash
13
+ # exit
14
+ # scp $(spin show -o fqdn):/filters.yml .
15
+ # ```
16
+ # The list of filters is now in a file named filters.yml in your $(pwd).
17
+ # Note that it'll probably need a bit of massaging before including here...
2
18
  Liquid::StandardFilters:
3
19
  - times
4
20
  - h
@@ -19,7 +35,9 @@ Liquid::StandardFilters:
19
35
  - remove
20
36
  - sort_natural
21
37
  - replace_first
38
+ - replace_last
22
39
  - remove_first
40
+ - remove_last
23
41
  - newline_to_br
24
42
  - upcase
25
43
  - downcase
data/dev.yml CHANGED
@@ -3,7 +3,7 @@ name: theme-check
3
3
  type: ruby
4
4
 
5
5
  up:
6
- - ruby: 2.6.6
6
+ - ruby: "2.7"
7
7
  - bundler
8
8
 
9
9
  commands:
@@ -0,0 +1,60 @@
1
+ # Prevent Manual Preloading of Assets (`AssetPreload`)
2
+
3
+ _Version 1.11.0+_
4
+
5
+ Preloading can be a useful way of making sure that critical assets are downloaded by the browser as soon as possible for better rendering performance.
6
+
7
+ Liquid provides multiple filters to [preload key resources][preload_key_resources] so they can be converted into `Link` headers automatically. This enables them to be discovered even faster, especially when combined with Early Hints that Shopify supports.
8
+
9
+ ## Examples
10
+
11
+ The following examples contain code snippets that either fail or pass this check.
12
+
13
+ ### ✗ Fail
14
+
15
+ ```liquid
16
+ <link href="{{ 'script.js' | asset_url }}" rel="preload" as="script">
17
+ <link href="{{ 'style.css' | asset_url }}" rel="preload" as="style">
18
+ <link href="{{ 'image.png' | asset_url }}" rel="preload" as="image">
19
+ ```
20
+
21
+ ### &#x2713; Pass
22
+
23
+ ```liquid
24
+ {{ 'script.js' | asset_url | preload_tag: as: 'script' }}
25
+ {{ 'style.css' | asset_url | stylesheet_tag: preload: true }}
26
+ {{
27
+ product.featured_image
28
+ | image_url: width: 600
29
+ | image_tag: preload: true
30
+ }}
31
+ ```
32
+
33
+ ## Options
34
+
35
+ The following example contains the default configuration for this check:
36
+
37
+ ```yaml
38
+ AssetPreload:
39
+ enabled: true
40
+ severity: suggestion
41
+ ```
42
+
43
+ | Parameter | Description |
44
+ | --- | --- |
45
+ | enabled | Whether the check is enabled. |
46
+ | severity | The [severity](https://shopify.dev/themes/tools/theme-check/configuration#check-severity) of the check. |
47
+
48
+ ## Disabling this check
49
+
50
+ It's safe to disable this rule. You may want to do it when trying to preload assets from external domain and it is not possible
51
+ to move them to Shopify because they change frequently or are dynamically generated.
52
+
53
+ ## Resources
54
+
55
+ - [Rule source][codesource]
56
+ - [Documentation source][docsource]
57
+
58
+ [codesource]: /lib/theme_check/checks/asset_preload.rb
59
+ [docsource]: /docs/checks/asset_preload.md
60
+ [preload_key_resources]: https://shopify.dev/themes/best-practices/performance#use-resource-hints-to-preload-key-resources
@@ -57,9 +57,9 @@ This includes theme and remote scripts.
57
57
  When you can't do anything about it, it is preferable to disable this rule using the comment syntax:
58
58
 
59
59
  ```
60
- {% comment %}theme-check-disable AssetSizeJavaScript{% endcomment %}
60
+ {% # theme-check-disable AssetSizeJavaScript %}
61
61
  <script src="https://code.jquery.com/jquery-3.6.0.min.js" defer></script>
62
- {% comment %}theme-check-enable AssetSizeJavaScript{% endcomment %}
62
+ {% # theme-check-enable AssetSizeJavaScript %}
63
63
  ```
64
64
 
65
65
  This makes disabling the rule an explicit affair and shows that the code is smelly.
@@ -12,7 +12,7 @@ This check aims at eliminating missing `theme-check-enable` comments.
12
12
  <!doctype html>
13
13
  <html>
14
14
  <head>
15
- {% comment %}theme-check-disable ParserBlockingJavaScript{% endcomment %}
15
+ {% # theme-check-disable ParserBlockingJavaScript %}
16
16
  <script src="https://cdnjs.com/jquery.min.js"></script>
17
17
  </head>
18
18
  <body>
@@ -27,9 +27,9 @@ This check aims at eliminating missing `theme-check-enable` comments.
27
27
  <!doctype html>
28
28
  <html>
29
29
  <head>
30
- {% comment %}theme-check-disable ParserBlockingJavaScript{% endcomment %}
30
+ {% # theme-check-disable ParserBlockingJavaScript %}
31
31
  <script src="https://cdnjs.com/jquery.min.js"></script>
32
- {% comment %}theme-check-enable ParserBlockingJavaScript{% endcomment %}
32
+ {% # theme-check-enable ParserBlockingJavaScript %}
33
33
  </head>
34
34
  <body>
35
35
  <!-- ... -->
@@ -9,32 +9,32 @@ This check is aimed at eliminating excessive nesting of snippets.
9
9
  :-1: Examples of **incorrect** code for this check:
10
10
 
11
11
  ```liquid
12
- {% comment %}templates/index.liquid{% endcomment %}
12
+ {% # templates/index.liquid %}
13
13
  {% render 'one' %}
14
14
 
15
- {% comment %}snippets/one.liquid{% endcomment %}
15
+ {% # snippets/one.liquid %}
16
16
  {% render 'two' %}
17
17
 
18
- {% comment %}snippets/two.liquid{% endcomment %}
18
+ {% # snippets/two.liquid %}
19
19
  {% render 'three' %}
20
20
 
21
- {% comment %}snippets/three.liquid{% endcomment %}
21
+ {% # snippets/three.liquid %}
22
22
  {% render 'four' %}
23
23
 
24
- {% comment %}snippets/four.liquid{% endcomment %}
24
+ {% # snippets/four.liquid %}
25
25
  ok
26
26
  ```
27
27
 
28
28
  :+1: Examples of **correct** code for this check:
29
29
 
30
30
  ```liquid
31
- {% comment %}templates/index.liquid{% endcomment %}
31
+ {% # templates/index.liquid %}
32
32
  {% render 'one' %}
33
33
 
34
- {% comment %}snippets/one.liquid{% endcomment %}
34
+ {% # snippets/one.liquid %}
35
35
  {% render 'two' %}
36
36
 
37
- {% comment %}snippets/two.liquid{% endcomment %}
37
+ {% # snippets/two.liquid %}
38
38
  ok
39
39
  ```
40
40
 
@@ -9,7 +9,7 @@ This check is aimed at eliminating the use of translations that do not exist.
9
9
  :-1: Examples of **incorrect** code for this check:
10
10
 
11
11
  ```liquid
12
- {% comment %}locales/en.default.json{% endcomment %}
12
+ {% # locales/en.default.json %}
13
13
  {
14
14
  "greetings": "Hello, world!",
15
15
  "general": {
@@ -17,14 +17,14 @@ This check is aimed at eliminating the use of translations that do not exist.
17
17
  }
18
18
  }
19
19
 
20
- {% comment %}templates/index.liquid{% endcomment %}
20
+ {% # templates/index.liquid %}
21
21
  {{ "notfound" | t }}
22
22
  ```
23
23
 
24
24
  :+1: Examples of **correct** code for this check:
25
25
 
26
26
  ```liquid
27
- {% comment %}locales/en.default.json{% endcomment %}
27
+ {% # locales/en.default.json %}
28
28
  {
29
29
  "greetings": "Hello, world!",
30
30
  "general": {
@@ -32,7 +32,7 @@ This check is aimed at eliminating the use of translations that do not exist.
32
32
  }
33
33
  }
34
34
 
35
- {% comment %}templates/index.liquid{% endcomment %}
35
+ {% # templates/index.liquid %}
36
36
  {{ "greetings" | t }}
37
37
  {{ "general.close" | t }}
38
38
  ```
@@ -18,7 +18,7 @@ This check is aimed at eliminating invalid HTML in translations.
18
18
  :+1: Examples of **correct** code for this check:
19
19
 
20
20
  ```liquid
21
- {% comment %}locales/en.default.json{% endcomment %}
21
+ {% # locales/en.default.json %}
22
22
  {
23
23
  "hello_html": "<h1>Hello, world</h1>",
24
24
  "image_html": "<img src='spongebob.png'>",
@@ -41,6 +41,7 @@ module ThemeCheck
41
41
  json_file_count + liquid_file_count
42
42
  end
43
43
 
44
+ # Returns all offenses for all files in theme
44
45
  def analyze_theme
45
46
  reset
46
47
 
@@ -60,9 +61,23 @@ module ThemeCheck
60
61
  @json_checks.call(:on_file, json_file)
61
62
  end
62
63
 
63
- finish
64
+ finish(false)
65
+
66
+ offenses
64
67
  end
65
68
 
69
+ # When only_single_file is false:
70
+ # Runs single file checks for each file in `files`
71
+ # Runs whole theme checks
72
+ # Returns single file checks offenses for file in `files` + whole theme checks
73
+ # When only_single_file is true:
74
+ # Runs single file checks for each file in `files`
75
+ # Does not run whole theme checks
76
+ # Returns single file checks offenses for file in `files`
77
+ # When files is empty and only_single_file is false:
78
+ # Only returns whole theme checks
79
+ # When files is empty and only_single_file is true:
80
+ # Returns empty array
66
81
  def analyze_files(files, only_single_file: false)
67
82
  reset
68
83
 
@@ -103,6 +118,8 @@ module ThemeCheck
103
118
  end
104
119
 
105
120
  finish(only_single_file)
121
+
122
+ offenses
106
123
  end
107
124
 
108
125
  def uncorrectable_offenses
@@ -158,8 +175,6 @@ module ThemeCheck
158
175
  @disabled_checks.remove_disabled_offenses(@liquid_checks)
159
176
  @disabled_checks.remove_disabled_offenses(@json_checks)
160
177
  @disabled_checks.remove_disabled_offenses(@html_checks)
161
-
162
- offenses
163
178
  end
164
179
  end
165
180
  end
@@ -6,7 +6,8 @@ module ThemeCheck
6
6
  include JsonHelpers
7
7
 
8
8
  attr_accessor :theme
9
- attr_accessor :options, :ignored_patterns
9
+ attr_accessor :options
10
+ attr_writer :ignored_patterns
10
11
  attr_writer :offenses
11
12
 
12
13
  # The order matters.
@@ -130,6 +131,10 @@ module ThemeCheck
130
131
  defined?(@ignored) && @ignored
131
132
  end
132
133
 
134
+ def ignored_patterns
135
+ @ignored_patterns ||= []
136
+ end
137
+
133
138
  def can_disable?
134
139
  self.class.can_disable
135
140
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ module ThemeCheck
3
+ class AssetPreload < HtmlCheck
4
+ severity :suggestion
5
+ categories :html, :performance
6
+ doc docs_url(__FILE__)
7
+
8
+ def on_link(node)
9
+ return if node.attributes["rel"]&.downcase != "preload"
10
+ case node.attributes["as"]&.downcase
11
+ when "style"
12
+ add_offense("For better performance, prefer using the preload argument of the stylesheet_tag filter", node: node)
13
+ when "image"
14
+ add_offense("For better performance, prefer using the preload argument of the image_tag filter", node: node)
15
+ else
16
+ add_offense("For better performance, prefer using the preload_tag filter", node: node)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module ThemeCheck
3
4
  class DeprecatedFilter < LiquidCheck
4
5
  doc docs_url(__FILE__)
@@ -8,6 +9,19 @@ module ThemeCheck
8
9
  # The image_url filter does not accept width or height values
9
10
  # greater than this numbr.
10
11
  MAX_SIZE = 5760
12
+ SIZE_REGEX = /^\d*x\d*$/
13
+ NAMED_SIZES = {
14
+ "pico" => 16,
15
+ "icon" => 32,
16
+ "thumb" => 50,
17
+ "small" => 100,
18
+ "compact" => 160,
19
+ "medium" => 240,
20
+ "large" => 480,
21
+ "grande" => 600,
22
+ "original" => 1024,
23
+ "master" => nil,
24
+ }
11
25
 
12
26
  def on_variable(node)
13
27
  used_filters = node.filters.map { |name, *_rest| name }
@@ -40,19 +54,29 @@ module ThemeCheck
40
54
 
41
55
  # Can't correct those.
42
56
  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'
57
+ (size_spec.nil? || size_spec.is_a?(String)) &&
58
+ (scale.nil? || scale.is_a?(Numeric))
59
+
60
+ return add_default_offense(node, 'img_url', ['image_url']) if
61
+ size_spec.is_a?(String) &&
62
+ size_spec !~ SIZE_REGEX &&
63
+ !NAMED_SIZES.key?(size_spec)
46
64
 
47
65
  node_source = node.markup
66
+
48
67
  node_start_index = node.start_index
49
68
  match = node_source.match(/img_url[^|]*/)
50
69
  img_url_character_range =
51
70
  (node_start_index + match.begin(0))...(node_start_index + match.end(0))
52
71
 
53
72
  scale = (scale || 1).to_i
54
- width, height = (size_spec&.split('x') || [100, 100])
55
- .map { |v| v.to_i * scale }
73
+ width, height = if size_spec.nil?
74
+ [100, 100]
75
+ elsif NAMED_SIZES.key?(size_spec)
76
+ [NAMED_SIZES[size_spec], NAMED_SIZES[size_spec]]
77
+ else
78
+ size_spec.split('x')
79
+ end.map { |v| v.to_i * scale }
56
80
 
57
81
  image_url_filter_params = [
58
82
  width && width > 0 ? "width: #{[width, MAX_SIZE].min}" : nil,
@@ -16,6 +16,10 @@ module ThemeCheck
16
16
  @disabled_checks.update(node)
17
17
  end
18
18
 
19
+ def on_inline_comment(node)
20
+ @disabled_checks.update(node)
21
+ end
22
+
19
23
  def after_document(node)
20
24
  checks_missing_end_index = @disabled_checks.checks_missing_end_index
21
25
  return if checks_missing_end_index.empty?
@@ -33,7 +33,11 @@ module ThemeCheck
33
33
  if REQUIRED_LIQUID_TEMPLATE_FILES.include?(file)
34
34
  corrector.create_file(@theme.storage, "#{file}.liquid", "")
35
35
  else
36
- corrector.create_file(@theme.storage, "#{file}.json", "")
36
+ corrector.create_file(@theme.storage, "#{file}.json", JSON.pretty_generate({
37
+ name: "TODO",
38
+ sections: {},
39
+ order: [],
40
+ }))
37
41
  end
38
42
  end
39
43
  end
@@ -28,7 +28,11 @@ module ThemeCheck
28
28
  private
29
29
 
30
30
  def ignore?(path)
31
- @ignore_missing.any? { |pattern| File.fnmatch?(pattern, path) }
31
+ all_ignored_patterns.any? { |pattern| File.fnmatch?(pattern, path) }
32
+ end
33
+
34
+ def all_ignored_patterns
35
+ @all_ignored_patterns ||= @ignore_missing + ignored_patterns
32
36
  end
33
37
 
34
38
  def add_missing_offense(name, node:)
@@ -120,6 +120,10 @@ module ThemeCheck
120
120
  # NOTE: `email` is exceptionally exposed as a theme object in
121
121
  # the customers' reset password template
122
122
  check_object(info, all_global_objects + ['email'])
123
+ elsif 'templates/robots.txt' == name
124
+ # NOTE: `robots` is the only object exposed object in
125
+ # the robots.txt template
126
+ check_object(info, ['robots'])
123
127
  elsif 'layout/checkout' == name
124
128
  # NOTE: Shopify Plus has exceptionally exposed objects in
125
129
  # the checkout template
@@ -39,7 +39,12 @@ module ThemeCheck
39
39
  end
40
40
 
41
41
  def on_variable_lookup(node)
42
- @templates[node.theme_file.name].used_assigns << node.value.name
42
+ @templates[node.theme_file.name].used_assigns << case node.value.name
43
+ when Liquid::VariableLookup
44
+ node.value.name.name
45
+ else
46
+ node.value.name
47
+ end
43
48
  end
44
49
 
45
50
  def on_end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "set"
3
4
 
4
5
  module ThemeCheck
@@ -11,16 +12,22 @@ module ThemeCheck
11
12
  @used_snippets = Set.new
12
13
  end
13
14
 
14
- def on_include(node)
15
+ def on_render(node)
15
16
  if node.value.template_name_expr.is_a?(String)
16
17
  @used_snippets << "snippets/#{node.value.template_name_expr}"
18
+
19
+ elsif might_have_a_block_as_variable_lookup?(node)
20
+ # We ignore this case, because that's a "proper" use case for
21
+ # the render tag with OS 2.0
22
+ # {% render block %} shouldn't turn off the UnusedSnippet check
23
+
17
24
  else
18
25
  # Can't reliably track unused snippets if an expression is used, ignore this check
19
26
  @used_snippets.clear
20
27
  ignore!
21
28
  end
22
29
  end
23
- alias_method :on_render, :on_include
30
+ alias_method :on_include, :on_render
24
31
 
25
32
  def on_end
26
33
  missing_snippets.each do |theme_file|
@@ -33,5 +40,46 @@ module ThemeCheck
33
40
  def missing_snippets
34
41
  theme.snippets.reject { |t| @used_snippets.include?(t.name) }
35
42
  end
43
+
44
+ private
45
+
46
+ # This function returns true when the render node passed might have a
47
+ # variable lookup that refers to a block as template_name_expr.
48
+ #
49
+ # e.g.
50
+ #
51
+ # {% for block in col %}
52
+ # {% render block %}
53
+ # {% endfor %}
54
+ #
55
+ # In this case, the `block` variable_lookup in the render tag might be
56
+ # a Block because col might be an array of blocks.
57
+ #
58
+ # @param node [Node]
59
+ def might_have_a_block_as_variable_lookup?(node)
60
+ return false unless node.type_name == :render
61
+
62
+ return false unless node.value.template_name_expr.is_a?(Liquid::VariableLookup)
63
+
64
+ name = node.value.template_name_expr.name
65
+ return false unless name.is_a?(String)
66
+
67
+ # We're going through all the parents of the nodes until we find
68
+ # a For node with variable_name === to the template_name_expr's name
69
+ find_parent(node.parent) do |parent_node|
70
+ next false unless parent_node.type_name == :for
71
+
72
+ parent_node.value.variable_name == name
73
+ end
74
+ end
75
+
76
+ # @param node [Node]
77
+ def find_parent(node, &pred)
78
+ return nil unless node
79
+
80
+ return node if yield node
81
+
82
+ find_parent(node.parent, &pred)
83
+ end
36
84
  end
37
85
  end
@@ -126,14 +126,14 @@ module ThemeCheck
126
126
  options_for_check = options.transform_keys(&:to_sym)
127
127
  options_for_check.delete(:enabled)
128
128
  severity = options_for_check.delete(:severity)
129
- ignored_patterns = options_for_check.delete(:ignore) || []
129
+ check_ignored_patterns = options_for_check.delete(:ignore) || []
130
130
  check = if options_for_check.empty?
131
131
  check_class.new
132
132
  else
133
133
  check_class.new(**options_for_check)
134
134
  end
135
135
  check.severity = severity.to_sym if severity
136
- check.ignored_patterns = ignored_patterns
136
+ check.ignored_patterns = check_ignored_patterns + ignored_patterns
137
137
  check.options = options_for_check
138
138
  check
139
139
  end.compact
@@ -34,14 +34,16 @@ module ThemeCheck
34
34
  # We want to disable checks inside comments
35
35
  # (e.g. html checks inside {% comment %})
36
36
  disabled = @disabled_checks[[node.theme_file, :all]]
37
- disabled.start_index = node.inner_markup_start_index
38
- disabled.end_index = node.inner_markup_end_index
37
+ unless disabled.first_line
38
+ disabled.start_index = node.inner_markup_start_index
39
+ disabled.end_index = node.inner_markup_end_index
40
+ end
39
41
  end
40
42
  end
41
43
 
42
44
  def disabled?(check, theme_file, check_name, index)
43
45
  return true if check.ignored_patterns&.any? do |pattern|
44
- theme_file.relative_path.fnmatch?(pattern)
46
+ theme_file&.relative_path&.fnmatch?(pattern)
45
47
  end
46
48
 
47
49
  @disabled_checks[[theme_file, :all]]&.disabled?(index) ||
@@ -65,7 +67,12 @@ module ThemeCheck
65
67
  private
66
68
 
67
69
  def comment_text(node)
68
- node.value.nodelist.join
70
+ case node.type_name
71
+ when :comment
72
+ node.value.nodelist.join
73
+ when :inline_comment
74
+ node.markup.sub(/\s*#+\s*/, '')
75
+ end
69
76
  end
70
77
 
71
78
  def start_disabling?(text)
@@ -21,6 +21,8 @@ module ThemeCheck
21
21
 
22
22
  def read(relative_path)
23
23
  file(relative_path).read(mode: 'rb', encoding: 'UTF-8')
24
+ rescue Errno::ENOENT
25
+ nil
24
26
  end
25
27
 
26
28
  def write(relative_path, content)
@@ -9,7 +9,7 @@ module ThemeCheck
9
9
  attr_reader :root
10
10
 
11
11
  def initialize(files = {}, root = "/dev/null")
12
- @files = files
12
+ @files = files # Hash<String, String>
13
13
  @root = Pathname.new(root)
14
14
  end
15
15