theme-check 1.9.1 → 1.10.1

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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -1
  3. data/README.md +1 -0
  4. data/RELEASING.md +4 -4
  5. data/data/shopify_liquid/filters.yml +68 -51
  6. data/lib/theme_check/analyzer.rb +31 -19
  7. data/lib/theme_check/checks/asset_size_css_stylesheet_tag.rb +1 -1
  8. data/lib/theme_check/checks/asset_url_filters.rb +2 -2
  9. data/lib/theme_check/checks/content_for_header_modification.rb +1 -1
  10. data/lib/theme_check/checks/deprecated_filter.rb +2 -2
  11. data/lib/theme_check/checks/parser_blocking_script_tag.rb +1 -1
  12. data/lib/theme_check/checks/remote_asset.rb +27 -4
  13. data/lib/theme_check/checks/translation_key_exists.rb +1 -1
  14. data/lib/theme_check/checks/undefined_object.rb +2 -0
  15. data/lib/theme_check/checks/unknown_filter.rb +1 -1
  16. data/lib/theme_check/checks.rb +1 -2
  17. data/lib/theme_check/cli.rb +5 -6
  18. data/lib/theme_check/config.rb +37 -37
  19. data/lib/theme_check/disabled_checks.rb +6 -0
  20. data/lib/theme_check/html_node.rb +31 -1
  21. data/lib/theme_check/language_server/configuration.rb +31 -12
  22. data/lib/theme_check/language_server/diagnostic.rb +4 -0
  23. data/lib/theme_check/language_server/diagnostics_engine.rb +11 -6
  24. data/lib/theme_check/language_server/diagnostics_manager.rb +29 -3
  25. data/lib/theme_check/language_server/execute_command_providers/run_checks_execute_command_provider.rb +6 -1
  26. data/lib/theme_check/language_server/handler.rb +5 -3
  27. data/lib/theme_check/liquid_node.rb +5 -0
  28. data/lib/theme_check/position.rb +10 -7
  29. data/lib/theme_check/position_helper.rb +18 -0
  30. data/lib/theme_check/theme_file.rb +3 -1
  31. data/lib/theme_check/version.rb +1 -1
  32. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 39a4bbf1f8a394045deecb8e3b4ccf5cb94f0cfac1e36de7761670a2bb817f7a
4
- data.tar.gz: 66cd6e3ae2087b468019a8257e0dd8eded88ff2f2732fa2b1902328ef837ce10
3
+ metadata.gz: 131d90a928b4130827c6e41ea99ccd852e650417e88baceb66910ff083cf3aaa
4
+ data.tar.gz: a754d8b221da85eea053ce6f7547c9cad5a3a1e5f7a41562d61922a1278abe5d
5
5
  SHA512:
6
- metadata.gz: 59144f60da497b0c067218c7408e217fc9af86dbc7b150d7c4aedd06fd07a16442a270e21f0dfba048cb3a85de423efcaea4db67eecc0b7ebc14a3293bcf23e2
7
- data.tar.gz: f27e64b460ef9d30ef11b01a4638bb32184a5b1ade3f7279043e2bb6de1749c2778d25d4e056012f6a90f4628acd05d2e35d81c697cb01c3898de081022766e4
6
+ metadata.gz: 8e2bc230eee811d2d0d8b62a09d11b3b88f2e54939f0c1aeda843d3f8d8455e34d0379aa89a9f388401d4ebb593a00abdfbb57357717fb9167bf7a73fb89943d
7
+ data.tar.gz: f901b9cd064ee47fa39a6b661524e2d4eafc52b6cda54aceabd4ff371c1d562cfb43967c666642b1ef4d1c1b72cb793ab3cf202bfaee2a5f80aedb1840335155
data/CHANGELOG.md CHANGED
@@ -1,4 +1,37 @@
1
1
 
2
+ v1.10.1 / 2022-02-24
3
+ ====================
4
+
5
+ * Revert "Prevent bad render tags from passing theme-check ([#551](https://github.com/shopify/theme-check/issues/551))"
6
+
7
+ v1.10.0 / 2022-02-24
8
+ ====================
9
+
10
+ ## Features
11
+
12
+ * Performance Improvements ([#556](https://github.com/shopify/theme-check/issues/556))
13
+ - Boasts ~125x faster `checkOnChange` checks.
14
+
15
+ * New language server configuration: `"themeCheck.onlySingleFileChecks"` ([#556](https://github.com/shopify/theme-check/issues/556))
16
+ - When `true`, disables whole theme checks in the editor and makes it so only the open files are checked.
17
+ - When `false` (default), behaves as before.
18
+
19
+ ## Fixes
20
+
21
+ * Do not complain about variables passed to the default filter ([#532](https://github.com/shopify/theme-check/issues/532))
22
+ * `extend:` should accept absolute and relative paths ([#555](https://github.com/shopify/theme-check/issues/555))
23
+ * Gracefully handle initialized without config. ([#552](https://github.com/shopify/theme-check/issues/552))
24
+ * Add missing metafield filters ([#550](https://github.com/shopify/theme-check/issues/550))
25
+ * Handle media.sources edge case in RemoteAsset ([#549](https://github.com/shopify/theme-check/issues/549))
26
+ * Disable HTML checks inside Liquid comments ([#548](https://github.com/shopify/theme-check/issues/548))
27
+ * Prevent bad render tags from passing theme-check ([#551](https://github.com/shopify/theme-check/issues/551))
28
+ * Handle rogue carriage returns ([#547](https://github.com/shopify/theme-check/issues/547))
29
+
30
+ v1.9.2 / 2021-12-13
31
+ ===================
32
+
33
+ * Improve HTML parsing errors
34
+
2
35
  v1.9.1 / 2021-12-09
3
36
  ===================
4
37
 
@@ -9,7 +42,7 @@ v1.9.0 / 2021-12-01
9
42
 
10
43
  ## Features
11
44
 
12
- * Add Corrections as Code Actions in Language Server (quickfix + source.fixAll) (#471)
45
+ * Add Corrections as Code Actions in Language Server (quickfix + source.fixAll) ([#471](https://github.com/shopify/theme-check/issues/471))
13
46
  * Add SchemaJsonFormat check ([#512](https://github.com/shopify/theme-check/issues/512))
14
47
  * Add checkOn{Open,Change,Save} Language Server Configurations ([#511](https://github.com/shopify/theme-check/issues/511))
15
48
  * Add support for new filters `image_tag` + `image_url`
data/README.md CHANGED
@@ -186,6 +186,7 @@ DeprecateLazysizes:
186
186
  - `themeCheck.checkOnOpen` (default: `true`) makes it so theme check runs on file open.
187
187
  - `themeCheck.checkOnChange` (default: `true`) makes it so theme check runs on file change.
188
188
  - `themeCheck.checkOnSave` (default: `true`) makes it so theme check runs on file save.
189
+ - `themeCheck.onlySingleFileChecks` (default: `false`) makes it so we only check the opened files and disable "whole theme" checks (e.g. UnusedSnippet, TranslationKeyExists)
189
190
 
190
191
  ⚠️ **Note:** Quickfixes only work on a freshly checked file. If any of those configurations are turned off, you will need to rerun theme-check in order to apply quickfixes.
191
192
 
data/RELEASING.md CHANGED
@@ -73,11 +73,11 @@
73
73
 
74
74
  1. Release `theme-check` on RubyGems by following the steps in the previous section.
75
75
 
76
- 2. Update the `theme-check` version in [`shopify-cli`](https://github.com/shopify/shopify-cli)'s `Gemfile.lock` and `shopify-cli.gemspec` files.
76
+ 2. Update the `theme-check` version in [`shopify-cli`](https://github.com/shopify/shopify-cli)'s `shopify-cli.gemspec` file.
77
77
 
78
- Such as in [this PR.](https://github.com/Shopify/shopify-cli/pull/1357/files)
78
+ 3. Run `bundle update theme-check` and get an updated `Gemfile.lock`
79
79
 
80
- 3. Create a branch + a commit on the [`shopify-cli`](https://github.com/Shopify/shopify-cli) repository.
80
+ 4. Create a branch + a commit on the [`shopify-cli`](https://github.com/Shopify/shopify-cli) repository.
81
81
 
82
82
  ```bash
83
83
  VERSION=X.X.X
@@ -87,7 +87,7 @@
87
87
  git commit -m "Bump theme-check version to $VERSION"
88
88
  ```
89
89
 
90
- 4. Create a pull-request for those changes on the [`shopify-cli`](https://github.com/Shopify/shopify-cli) repository.
90
+ 5. Create a pull-request for those changes on the [`shopify-cli`](https://github.com/Shopify/shopify-cli) repository.
91
91
 
92
92
  ```bash
93
93
  # shortcut if you have `hub` installed
@@ -1,80 +1,91 @@
1
1
  ---
2
2
  Liquid::StandardFilters:
3
- - url_encode
4
- - where
3
+ - times
4
+ - h
5
5
  - uniq
6
+ - compact
7
+ - url_encode
6
8
  - escape
9
+ - escape_once
10
+ - url_decode
11
+ - base64_encode
12
+ - base64_decode
13
+ - base64_url_safe_encode
14
+ - base64_url_safe_decode
15
+ - truncatewords
16
+ - strip_html
17
+ - strip_newlines
7
18
  - replace
8
19
  - remove
9
- - h
20
+ - sort_natural
21
+ - replace_first
22
+ - remove_first
23
+ - newline_to_br
10
24
  - upcase
11
25
  - downcase
12
26
  - capitalize
13
- - date
27
+ - divided_by
28
+ - minus
29
+ - at_most
30
+ - at_least
31
+ - size
14
32
  - last
15
33
  - split
16
- - size
17
34
  - append
18
35
  - reverse
19
- - join
20
36
  - concat
21
37
  - prepend
22
- - escape_once
23
- - url_decode
24
- - truncatewords
25
- - strip_html
38
+ - join
26
39
  - strip
27
40
  - lstrip
28
41
  - rstrip
29
- - strip_newlines
30
- - plus
31
- - sort_natural
32
- - compact
33
- - replace_first
34
- - remove_first
35
- - newline_to_br
36
- - minus
37
- - divided_by
38
- - at_least
39
- - at_most
40
42
  - sort
43
+ - where
44
+ - map
41
45
  - default
42
- - modulo
46
+ - first
43
47
  - slice
48
+ - modulo
44
49
  - abs
45
- - map
46
- - floor
50
+ - date
47
51
  - ceil
48
52
  - round
49
53
  - truncate
50
- - first
51
- - times
54
+ - plus
55
+ - floor
52
56
  FormFilter:
53
- - payment_button
54
57
  - payment_terms
55
58
  - installments_pricing
56
59
  - default_errors
60
+ - payment_button
57
61
  DateFilter:
58
62
  - date
59
63
  I18nFilter:
60
64
  - translate
65
+ - time_tag
66
+ - sentence
61
67
  - t
62
68
  - date
63
- - sentence
64
- - time_tag
69
+ - app_block_path_for
70
+ - dev_shop?
71
+ - app_extension_path_for
72
+ - global_block_type?
73
+ - app_block_path?
74
+ - app_extension_path?
75
+ - app_snippet_path?
76
+ - registration_uuid_from
77
+ - handle_from
65
78
  UrlFilter:
66
79
  - stylesheet_tag
67
80
  - script_tag
68
81
  - img_tag
69
- - image_tag
70
- - image_url
82
+ - link_to
71
83
  - shopify_asset_url
72
84
  - payment_type_img_url
73
85
  - payment_type_svg_tag
74
86
  - placeholder_svg_tag
75
- - link_to
76
- - asset_url
77
87
  - img_url
88
+ - asset_url
78
89
  - asset_img_url
79
90
  - global_asset_url
80
91
  - file_url
@@ -82,10 +93,12 @@ UrlFilter:
82
93
  - product_img_url
83
94
  - collection_img_url
84
95
  - article_img_url
96
+ - image_url
85
97
  - preload_tag
86
98
  JsonFilter:
87
99
  - json
88
100
  ColorFilter:
101
+ - color_mix
89
102
  - color_contrast
90
103
  - color_difference
91
104
  - brightness_difference
@@ -100,40 +113,39 @@ ColorFilter:
100
113
  - color_darken
101
114
  - color_saturate
102
115
  - color_desaturate
103
- - color_mix
104
116
  MoneyFilter:
105
- - money_without_currency
106
- - money_without_trailing_zeros
107
117
  - money
108
118
  - money_with_currency
119
+ - money_without_currency
120
+ - money_without_trailing_zeros
109
121
  StringFilter:
122
+ - md5
123
+ - camelcase
124
+ - format_code
125
+ - handle
126
+ - camelize
110
127
  - handleize
111
128
  - url_param_escape
112
129
  - url_escape
113
- - camelcase
114
- - camelize
115
130
  - encode_url_component
116
- - format_code
117
- - md5
118
- - handle
119
131
  CollectionFilter:
132
+ - sort_by
120
133
  - within
121
134
  - link_to_vendor
122
- - url_for_vendor
123
135
  - link_to_type
124
136
  - url_for_type
125
- - sort_by
137
+ - url_for_vendor
126
138
  TagFilter:
127
- - link_to_add_tag
128
- - link_to_remove_tag
129
139
  - link_to_tag
130
140
  - highlight_active_tag
141
+ - link_to_add_tag
142
+ - link_to_remove_tag
131
143
  CryptoFilter:
132
144
  - hmac_sha1
133
- - sha256
134
145
  - hmac_sha256
135
146
  - md5
136
147
  - sha1
148
+ - sha256
137
149
  CustomerAccountFilter:
138
150
  - customer_login_link
139
151
  - customer_logout_link
@@ -147,16 +159,16 @@ CurrencyFormFilter:
147
159
  PaginationFilter:
148
160
  - default_pagination
149
161
  WeightFilter:
162
+ - unit
150
163
  - weight
151
164
  - weight_with_unit
152
- - unit
153
165
  TextFilter:
154
- - pad_spaces
155
- - paragraphize
166
+ - pluralize
156
167
  - highlight
157
168
  - format_address
169
+ - paragraphize
158
170
  - excerpt
159
- - pluralize
171
+ - pad_spaces
160
172
  FontFilter:
161
173
  - font_face
162
174
  - font_url
@@ -164,13 +176,18 @@ FontFilter:
164
176
  DistanceFilter:
165
177
  - distance_from
166
178
  MediaFilter:
167
- - external_video_url
168
179
  - external_video_tag
169
180
  - video_tag
170
181
  - model_viewer_tag
171
182
  - media_tag
183
+ - image_tag
184
+ - external_video_url
172
185
  ThemeFilter:
173
186
  - theme_url
174
187
  - link_to_theme
188
+ - _online_store_editor_live_setting
189
+ MetafieldFilter:
190
+ - metafield_tag
191
+ - metafield_text
175
192
  DebugFilter:
176
193
  - debug
@@ -63,30 +63,36 @@ module ThemeCheck
63
63
  finish
64
64
  end
65
65
 
66
- def analyze_files(files)
66
+ def analyze_files(files, only_single_file: false)
67
67
  reset
68
68
 
69
69
  ThemeCheck.with_liquid_c_disabled do
70
- # Call all checks that run on the whole theme
71
- liquid_visitor = LiquidVisitor.new(@liquid_checks.whole_theme, @disabled_checks)
72
- html_visitor = HtmlVisitor.new(@html_checks.whole_theme)
73
- total = total_file_count + files.size
74
- @theme.liquid.each_with_index do |liquid_file, i|
75
- yield(liquid_file.relative_path.to_s, i, total) if block_given?
76
- liquid_visitor.visit_liquid_file(liquid_file)
77
- html_visitor.visit_liquid_file(liquid_file)
78
- end
70
+ total = files.size
71
+ offset = 0
72
+
73
+ unless only_single_file
74
+ # Call all checks that run on the whole theme
75
+ liquid_visitor = LiquidVisitor.new(@liquid_checks.whole_theme, @disabled_checks)
76
+ html_visitor = HtmlVisitor.new(@html_checks.whole_theme)
77
+ total += total_file_count
78
+ offset = total_file_count
79
+ @theme.liquid.each_with_index do |liquid_file, i|
80
+ yield(liquid_file.relative_path.to_s, i, total) if block_given?
81
+ liquid_visitor.visit_liquid_file(liquid_file)
82
+ html_visitor.visit_liquid_file(liquid_file)
83
+ end
79
84
 
80
- @theme.json.each_with_index do |json_file, i|
81
- yield(json_file.relative_path.to_s, liquid_file_count + i, total) if block_given?
82
- @json_checks.whole_theme.call(:on_file, json_file)
85
+ @theme.json.each_with_index do |json_file, i|
86
+ yield(json_file.relative_path.to_s, liquid_file_count + i, total) if block_given?
87
+ @json_checks.whole_theme.call(:on_file, json_file)
88
+ end
83
89
  end
84
90
 
85
91
  # Call checks that run on a single files, only on specified file
86
92
  liquid_visitor = LiquidVisitor.new(@liquid_checks.single_file, @disabled_checks)
87
93
  html_visitor = HtmlVisitor.new(@html_checks.single_file)
88
94
  files.each_with_index do |theme_file, i|
89
- yield(theme_file.relative_path.to_s, total_file_count + i, total) if block_given?
95
+ yield(theme_file.relative_path.to_s, offset + i, total) if block_given?
90
96
  if theme_file.liquid?
91
97
  liquid_visitor.visit_liquid_file(theme_file)
92
98
  html_visitor.visit_liquid_file(theme_file)
@@ -96,7 +102,7 @@ module ThemeCheck
96
102
  end
97
103
  end
98
104
 
99
- finish
105
+ finish(only_single_file)
100
106
  end
101
107
 
102
108
  def uncorrectable_offenses
@@ -138,10 +144,16 @@ module ThemeCheck
138
144
  end
139
145
  end
140
146
 
141
- def finish
142
- @liquid_checks.call(:on_end)
143
- @html_checks.call(:on_end)
144
- @json_checks.call(:on_end)
147
+ def finish(only_single_file = false)
148
+ if only_single_file
149
+ @liquid_checks.single_file.call(:on_end)
150
+ @html_checks.single_file.call(:on_end)
151
+ @json_checks.single_file.call(:on_end)
152
+ else
153
+ @liquid_checks.call(:on_end)
154
+ @html_checks.call(:on_end)
155
+ @json_checks.call(:on_end)
156
+ end
145
157
 
146
158
  @disabled_checks.remove_disabled_offenses(@liquid_checks)
147
159
  @disabled_checks.remove_disabled_offenses(@json_checks)
@@ -11,7 +11,7 @@ module ThemeCheck
11
11
  end
12
12
 
13
13
  def on_variable(node)
14
- used_filters = node.value.filters.map { |name, *_rest| name }
14
+ used_filters = node.filters.map { |name, *_rest| name }
15
15
  return unless used_filters.include?("stylesheet_tag")
16
16
  file_size = stylesheet_tag_pipeline_to_file_size(node.markup)
17
17
  return if file_size.nil?
@@ -36,12 +36,12 @@ module ThemeCheck
36
36
  end
37
37
 
38
38
  def html_resource_drop?(variable_node)
39
- variable_node.value.filters
39
+ variable_node.filters
40
40
  .any? { |(filter_name, *_filter_args)| HTML_FILTERS.include?(filter_name) }
41
41
  end
42
42
 
43
43
  def variable_hosted_by_shopify?(variable_node)
44
- variable_node.value.filters
44
+ variable_node.filters
45
45
  .any? { |(filter_name, *_filter_args)| ASSET_URL_FILTERS.include?(filter_name) }
46
46
  end
47
47
  end
@@ -14,7 +14,7 @@ module ThemeCheck
14
14
  return unless node.value.name.is_a?(Liquid::VariableLookup)
15
15
  return unless node.value.name.name == "content_for_header"
16
16
 
17
- if @in_assign || @in_capture || node.value.filters.any?
17
+ if @in_assign || @in_capture || node.filters.any?
18
18
  add_offense(
19
19
  "Do not rely on the content of `content_for_header`",
20
20
  node: node,
@@ -10,7 +10,7 @@ module ThemeCheck
10
10
  MAX_SIZE = 5760
11
11
 
12
12
  def on_variable(node)
13
- used_filters = node.value.filters.map { |name, *_rest| name }
13
+ used_filters = node.filters.map { |name, *_rest| name }
14
14
  used_filters.each do |filter|
15
15
  alternatives = ShopifyLiquid::DeprecatedFilter.alternatives(filter)
16
16
  next unless alternatives
@@ -33,7 +33,7 @@ module ThemeCheck
33
33
  end
34
34
 
35
35
  def add_img_url_offense(node)
36
- img_url_filter = node.value.filters.find { |filter| filter[0] == "img_url" }
36
+ img_url_filter = node.filters.find { |filter| filter[0] == "img_url" }
37
37
  _name, img_url_filter_size, img_url_filter_props = img_url_filter
38
38
  size_spec = img_url_filter_size&.dig(0)
39
39
  scale = img_url_filter_props&.delete("scale")
@@ -7,7 +7,7 @@ module ThemeCheck
7
7
  doc docs_url(__FILE__)
8
8
 
9
9
  def on_variable(node)
10
- used_filters = node.value.filters.map { |name, *_rest| name }
10
+ used_filters = node.filters.map { |name, *_rest| name }
11
11
  if used_filters.include?("script_tag")
12
12
  add_offense(
13
13
  "The script_tag filter is parser-blocking. Use a script tag with the async or defer " \
@@ -22,7 +22,6 @@ 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)
26
25
 
27
26
  # Ignore non-stylesheet link tags
28
27
  rel = node.attributes["rel"]
@@ -37,12 +36,36 @@ module ThemeCheck
37
36
  private
38
37
 
39
38
  def url_hosted_by_shopify?(url)
40
- url.start_with?(Liquid::VariableStart) &&
41
- AssetUrlFilters::ASSET_URL_FILTERS.any? { |filter| url.include?(filter) }
39
+ asset_url?(url) || looks_like_hosted_by_shopify?(url) || url_is_setting_variable?(url)
40
+ end
41
+
42
+ # There are some cases where it's kind of hard to tell if it's
43
+ # hosted by Shopify or not.
44
+ #
45
+ # e.g. {{ image }} is hosted on primary domain (not CDN)
46
+ #
47
+ # e.g. media.sources are on the CDN
48
+ # {% for source in media.sources %}
49
+ # {{ source.url }}
50
+ # {% endfor %}
51
+ #
52
+ # So I'll go 80/20 here and assume that people name their variable
53
+ # source in `for source in media.sources`.
54
+ def looks_like_hosted_by_shopify?(url)
55
+ liquid_variable?(url) && url =~ /source\.url/
42
56
  end
43
57
 
44
58
  def url_is_setting_variable?(url)
45
- url.start_with?(Liquid::VariableStart) && url =~ /settings\./
59
+ liquid_variable?(url) && url =~ /settings\./
60
+ end
61
+
62
+ def asset_url?(url)
63
+ liquid_variable?(url) &&
64
+ AssetUrlFilters::ASSET_URL_FILTERS.any? { |filter| url.include?(filter) }
65
+ end
66
+
67
+ def liquid_variable?(url)
68
+ url.start_with?(Liquid::VariableStart)
46
69
  end
47
70
  end
48
71
  end
@@ -16,7 +16,7 @@ module ThemeCheck
16
16
 
17
17
  def on_variable(node)
18
18
  return unless @theme.default_locale_json&.content&.is_a?(Hash)
19
- return unless node.value.filters.any? { |name, _| name == "t" || name == "translate" }
19
+ return unless node.filters.any? { |name, _| name == "t" || name == "translate" }
20
20
 
21
21
  @nodes[node.theme_file.name] << node
22
22
  end
@@ -167,6 +167,8 @@ module ThemeCheck
167
167
  node = node.parent
168
168
  node = node.parent if %i(condition variable_lookup).include?(node.type_name)
169
169
 
170
+ next if node.variable? && node.filters.any? { |(filter_name)| filter_name == "default" }
171
+
170
172
  if render_node
171
173
  add_offense("Missing argument `#{name}`", node: render_node)
172
174
  else
@@ -15,7 +15,7 @@ module ThemeCheck
15
15
  doc docs_url(__FILE__)
16
16
 
17
17
  def on_variable(node)
18
- used_filters = node.value.filters.map { |name, *_rest| name }
18
+ used_filters = node.filters.map { |name, *_rest| name }
19
19
  undefined_filters = used_filters - ShopifyLiquid::Filter.labels
20
20
 
21
21
  undefined_filters.each do |undefined_filter|
@@ -42,8 +42,7 @@ module ThemeCheck
42
42
  check.send(method, *args)
43
43
  end
44
44
  end
45
- rescue Liquid::Error
46
- # Pass-through Liquid errors
45
+ rescue Liquid::Error, ThemeCheckError
47
46
  raise
48
47
  rescue => e
49
48
  node = args.first
@@ -160,12 +160,11 @@ module ThemeCheck
160
160
  def init
161
161
  dotfile_path = ThemeCheck::Config.find(@path)
162
162
  if dotfile_path.nil?
163
- config_name = if @config_path && @config_path[0] == ":"
164
- "#{@config_path[1..]}.yml"
165
- else
166
- "default.yml"
167
- end
168
- File.write(File.join(@path, ThemeCheck::Config::DOTFILE), File.read(ThemeCheck::Config.bundled_config_path(config_name)))
163
+ config_name = @config_path || "default"
164
+ File.write(
165
+ File.join(@path, ThemeCheck::Config::DOTFILE),
166
+ File.read(ThemeCheck::Config.bundled_config_path(config_name))
167
+ )
169
168
 
170
169
  puts "Writing new #{ThemeCheck::Config::DOTFILE} to #{@path}"
171
170
  else
@@ -3,7 +3,7 @@
3
3
  module ThemeCheck
4
4
  class Config
5
5
  DOTFILE = '.theme-check.yml'
6
- BUNDLED_CONFIGS_DIR = "#{__dir__}/../../config"
6
+ BUNDLED_CONFIGS_DIR = Pathname.new("#{__dir__}/../../config").realpath
7
7
  BOOLEAN = [true, false]
8
8
 
9
9
  attr_reader :root
@@ -14,7 +14,7 @@ module ThemeCheck
14
14
 
15
15
  def from_path(path)
16
16
  if (filename = find(path))
17
- new(root: filename.dirname, configuration: load_file(filename))
17
+ new(root: filename.dirname, configuration: load_config(filename))
18
18
  else
19
19
  # No configuration file
20
20
  new(root: path)
@@ -39,49 +39,57 @@ module ThemeCheck
39
39
 
40
40
  def load_file(absolute_path)
41
41
  @last_loaded_config = absolute_path
42
- YAML.load_file(absolute_path)
42
+ # An empty file returns false, so we || {}.
43
+ YAML.load_file(absolute_path) || {}
43
44
  end
44
45
 
45
46
  def bundled_config_path(name)
46
- "#{BUNDLED_CONFIGS_DIR}/#{name}"
47
+ "#{BUNDLED_CONFIGS_DIR}/#{name.to_s.sub(/^:/, '')}.yml"
48
+ end
49
+
50
+ def bundled_config?(name)
51
+ name.is_a?(Symbol) || (name.is_a?(String) && name[0] == ":")
47
52
  end
48
53
 
49
54
  def load_bundled_config(name)
50
55
  load_file(bundled_config_path(name))
51
56
  end
52
57
 
53
- def load_config(path)
54
- if path[0] == ":"
55
- load_bundled_config("#{path[1..]}.yml")
56
- elsif path.is_a?(Symbol)
57
- load_bundled_config("#{path}.yml")
58
- else
59
- load_file(path)
58
+ def load_config(name, pwd = Pathname.pwd)
59
+ return load_bundled_config(name) if bundled_config?(name)
60
+ path = name.is_a?(Pathname) ? name : Pathname.new(name)
61
+ path = pwd.join(path) if path.relative?
62
+ return {} unless path.exist?
63
+ config = load_file(path)
64
+ extends = config["extends"] || :default
65
+ merge_configurations!(load_config(extends, path.realpath.dirname), config)
66
+ end
67
+
68
+ def merge_configurations!(config, other)
69
+ config.merge(other) do |_key, old_value, new_value|
70
+ case old_value
71
+ when Hash
72
+ merge_configurations!(old_value, new_value)
73
+ else
74
+ new_value
75
+ end
60
76
  end
61
77
  end
62
78
 
63
79
  def default
64
- @default ||= load_config(":default")
80
+ load_config(":default")
65
81
  end
66
82
  end
67
83
 
68
84
  def initialize(root: nil, configuration: nil, should_resolve_requires: true)
69
85
  @configuration = if configuration
70
- # TODO: Do we need to handle extends here? What base configuration
71
- # should we validate against once Theme App Extensions has its own
72
- # checks? :all?
73
86
  validate_configuration(configuration)
74
87
  else
75
- {}
88
+ self.class.default
76
89
  end
77
90
 
78
- # Follow extends
79
- extends = @configuration["extends"] || ":default"
80
- while extends
81
- extended_configuration = self.class.load_config(extends)
82
- extends = extended_configuration["extends"]
83
- @configuration = merge_configurations!(@configuration, extended_configuration)
84
- end
91
+ extends = @configuration["extends"] || :default
92
+ @configuration = self.class.merge_configurations!(self.class.load_config(extends), @configuration)
85
93
 
86
94
  @root = if root && @configuration.key?("root")
87
95
  Pathname.new(root).join(@configuration["root"])
@@ -177,6 +185,12 @@ module ThemeCheck
177
185
  else
178
186
  warn("bad configuration type for #{name}: expected a Hash, got #{value.inspect}")
179
187
  end
188
+ elsif key == "extends"
189
+ if value.is_a?(Symbol) || value.is_a?(String)
190
+ valid_configuration[key] = value
191
+ else
192
+ warn("bad configuration type for extends: expected a Symbol or a String, got #{value.inspect}")
193
+ end
180
194
  elsif key == "severity"
181
195
  valid_configuration[key] = value
182
196
  elsif default.nil?
@@ -193,20 +207,6 @@ module ThemeCheck
193
207
  valid_configuration
194
208
  end
195
209
 
196
- def merge_configurations!(configuration, extended_configuration)
197
- extended_configuration.each do |key, default|
198
- value = configuration[key]
199
-
200
- case value
201
- when Hash
202
- merge_configurations!(value, default)
203
- when nil
204
- configuration[key] = default
205
- end
206
- end
207
- configuration
208
- end
209
-
210
210
  def resolve_requires
211
211
  self["require"]&.each do |path|
212
212
  require(File.join(@root, path))
@@ -30,6 +30,12 @@ module ThemeCheck
30
30
  next unless disabled
31
31
  disabled.end_index = node.end_index
32
32
  end
33
+ else
34
+ # We want to disable checks inside comments
35
+ # (e.g. html checks inside {% comment %})
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
33
39
  end
34
40
  end
35
41
 
@@ -140,9 +140,39 @@ module ThemeCheck
140
140
  end
141
141
 
142
142
  def parseable_markup
143
+ return @parseable_source if @value.name == "#document-fragment"
144
+ return @value.to_str if @value.comment?
145
+ return @value.content if literal?
146
+
143
147
  start_index = from_row_column_to_index(@parseable_source, line_number - 1, 0)
144
148
  @parseable_source
145
- .match(/<\s*#{@value.name}[^>]*>/im, start_index)[0]
149
+ .match(/<\s*#{name}[^>]*>/im, start_index)[0]
150
+ rescue NoMethodError
151
+ # Don't know what's up with the following issue. Don't think
152
+ # null check is correct approach. This should give us more info.
153
+ # https://github.com/Shopify/theme-check/issues/528
154
+ ThemeCheck.bug(<<~MSG)
155
+ Can't find a parseable tag of name #{name} inside the parseable HTML.
156
+
157
+ Tag name:
158
+ #{@value.name.inspect}
159
+
160
+ File:
161
+ #{@theme_file.relative_path}
162
+
163
+ Line number:
164
+ #{line_number}
165
+
166
+ Excerpt:
167
+ ```
168
+ #{@theme_file.source.lines[line_number - 1...line_number + 5].join("")}
169
+ ```
170
+
171
+ Parseable Excerpt:
172
+ ```
173
+ #{@parseable_source.lines[line_number - 1...line_number + 5].join("")}
174
+ ```
175
+ MSG
146
176
  end
147
177
 
148
178
  def content
@@ -6,6 +6,7 @@ module ThemeCheck
6
6
  CHECK_ON_OPEN = :"themeCheck.checkOnOpen"
7
7
  CHECK_ON_SAVE = :"themeCheck.checkOnSave"
8
8
  CHECK_ON_CHANGE = :"themeCheck.checkOnChange"
9
+ ONLY_SINGLE_FILE = :"themeCheck.onlySingleFileChecks"
9
10
 
10
11
  def initialize(bridge, capabilities)
11
12
  @bridge = bridge
@@ -13,9 +14,10 @@ module ThemeCheck
13
14
  @mutex = Mutex.new
14
15
  @initialized = false
15
16
  @config = {
16
- CHECK_ON_OPEN => @capabilities.initialization_option(CHECK_ON_OPEN) || true,
17
- CHECK_ON_SAVE => @capabilities.initialization_option(CHECK_ON_SAVE) || true,
18
- CHECK_ON_CHANGE => @capabilities.initialization_option(CHECK_ON_CHANGE) || true,
17
+ CHECK_ON_OPEN => null_coalesce(@capabilities.initialization_option(CHECK_ON_OPEN), true),
18
+ CHECK_ON_SAVE => null_coalesce(@capabilities.initialization_option(CHECK_ON_SAVE), true),
19
+ CHECK_ON_CHANGE => null_coalesce(@capabilities.initialization_option(CHECK_ON_CHANGE), true),
20
+ ONLY_SINGLE_FILE => null_coalesce(@capabilities.initialization_option(ONLY_SINGLE_FILE), false),
19
21
  }
20
22
  end
21
23
 
@@ -23,17 +25,25 @@ module ThemeCheck
23
25
  @mutex.synchronize do
24
26
  return unless @capabilities.supports_workspace_configuration?
25
27
  return if initialized? && !force
26
- check_on_open, check_on_save, check_on_change = @bridge.send_request(
28
+
29
+ keys = [
30
+ CHECK_ON_OPEN,
31
+ CHECK_ON_SAVE,
32
+ CHECK_ON_CHANGE,
33
+ ONLY_SINGLE_FILE,
34
+ ]
35
+
36
+ configs = @bridge.send_request(
27
37
  "workspace/configuration",
28
- items: [
29
- { section: CHECK_ON_OPEN },
30
- { section: CHECK_ON_SAVE },
31
- { section: CHECK_ON_CHANGE },
32
- ],
38
+ items: keys.map do |key|
39
+ { section: key }
40
+ end
33
41
  )
34
- @config[CHECK_ON_OPEN] = check_on_open unless check_on_open.nil?
35
- @config[CHECK_ON_CHANGE] = check_on_change unless check_on_change.nil?
36
- @config[CHECK_ON_SAVE] = check_on_save unless check_on_save.nil?
42
+
43
+ keys.each.with_index do |key, i|
44
+ @config[key] = configs[i] unless configs[i].nil?
45
+ end
46
+
37
47
  @initialized = true
38
48
  end
39
49
  end
@@ -64,6 +74,15 @@ module ThemeCheck
64
74
  fetch # making sure we have for an initialized value
65
75
  @config[CHECK_ON_CHANGE]
66
76
  end
77
+
78
+ def only_single_file?
79
+ fetch # making sure we have for an initialized value
80
+ @config[ONLY_SINGLE_FILE]
81
+ end
82
+
83
+ def null_coalesce(value, default)
84
+ value.nil? ? default : value
85
+ end
67
86
  end
68
87
  end
69
88
  end
@@ -43,6 +43,10 @@ module ThemeCheck
43
43
  offense.single_file?
44
44
  end
45
45
 
46
+ def whole_theme?
47
+ offense.whole_theme?
48
+ end
49
+
46
50
  def correctable?
47
51
  offense.correctable?
48
52
  end
@@ -19,14 +19,14 @@ module ThemeCheck
19
19
  @diagnostics_manager.first_run?
20
20
  end
21
21
 
22
- def analyze_and_send_offenses(absolute_path, config, force: false)
22
+ def analyze_and_send_offenses(absolute_path, config, force: false, only_single_file: false)
23
23
  return unless @diagnostics_lock.try_lock
24
24
  @token += 1
25
25
  @bridge.send_create_work_done_progress_request(@token)
26
26
  theme = ThemeCheck::Theme.new(storage)
27
27
  analyzer = ThemeCheck::Analyzer.new(theme, config.enabled_checks)
28
28
 
29
- if @diagnostics_manager.first_run? || force
29
+ if (!only_single_file && @diagnostics_manager.first_run?) || force
30
30
  @bridge.send_work_done_progress_begin(@token, "Full theme check")
31
31
  @bridge.log("Checking #{storage.root}")
32
32
  offenses = nil
@@ -37,6 +37,7 @@ module ThemeCheck
37
37
  end
38
38
  end_message = "Found #{offenses.size} offenses in #{format("%0.2f", time.real)}s"
39
39
  @bridge.send_work_done_progress_end(@token, end_message)
40
+ @bridge.log(end_message)
40
41
  send_diagnostics(offenses)
41
42
  else
42
43
  # Analyze selected files
@@ -47,14 +48,14 @@ module ThemeCheck
47
48
  @bridge.send_work_done_progress_begin(@token, "Partial theme check")
48
49
  offenses = nil
49
50
  time = Benchmark.measure do
50
- offenses = analyzer.analyze_files([file]) do |path, i, total|
51
+ offenses = analyzer.analyze_files([file], only_single_file: only_single_file) do |path, i, total|
51
52
  @bridge.send_work_done_progress_report(@token, "#{i}/#{total} #{path}", (i.to_f / total * 100.0).to_i)
52
53
  end
53
54
  end
54
55
  end_message = "Found #{offenses.size} new offenses in #{format("%0.2f", time.real)}s"
55
56
  @bridge.send_work_done_progress_end(@token, end_message)
56
57
  @bridge.log(end_message)
57
- send_diagnostics(offenses, [relative_path])
58
+ send_diagnostics(offenses, [relative_path], only_single_file: only_single_file)
58
59
  end
59
60
  end
60
61
  @diagnostics_lock.unlock
@@ -62,8 +63,12 @@ module ThemeCheck
62
63
 
63
64
  private
64
65
 
65
- def send_diagnostics(offenses, analyzed_files = nil)
66
- @diagnostics_manager.build_diagnostics(offenses, analyzed_files: analyzed_files).each do |relative_path, diagnostics|
66
+ def send_diagnostics(offenses, analyzed_files = nil, only_single_file: false)
67
+ @diagnostics_manager.build_diagnostics(
68
+ offenses,
69
+ analyzed_files: analyzed_files,
70
+ only_single_file: only_single_file
71
+ ).each do |relative_path, diagnostics|
67
72
  send_diagnostic(relative_path, diagnostics)
68
73
  end
69
74
  end
@@ -27,7 +27,7 @@ module ThemeCheck
27
27
  @mutex.synchronize { @latest_diagnostics[relative_path] || [] }
28
28
  end
29
29
 
30
- def build_diagnostics(offenses, analyzed_files: nil)
30
+ def build_diagnostics(offenses, analyzed_files: nil, only_single_file: false)
31
31
  @mutex.synchronize do
32
32
  full_check = analyzed_files.nil?
33
33
  analyzed_paths = analyzed_files.map { |path| Pathname.new(path) } unless full_check
@@ -46,10 +46,23 @@ module ThemeCheck
46
46
  current_paths = paths(current_diagnostics)
47
47
 
48
48
  diagnostics_update = (current_paths + previous_paths).map do |path|
49
+ # When doing single file checks, we keep the whole theme old
50
+ # ones and accept the new single ones
51
+ if only_single_file && analyzed_paths.include?(path)
52
+ single_file_diagnostics = current_diagnostics[path] || []
53
+ whole_theme_diagnostics = whole_theme_diagnostics(path) || []
54
+ [path, single_file_diagnostics + whole_theme_diagnostics]
55
+
56
+ # If doing single file checks that are not in the
57
+ # analyzed_paths array then we just keep the old
58
+ # diagnostics
59
+ elsif only_single_file
60
+ [path, previous_diagnostics(path) || []]
61
+
49
62
  # When doing a full_check, we either send the current
50
63
  # diagnostics or an empty array to clear the diagnostics
51
64
  # for that file.
52
- if full_check
65
+ elsif full_check
53
66
  [path, current_diagnostics[path] || []]
54
67
 
55
68
  # When doing a partial check, the single file diagnostics
@@ -63,9 +76,14 @@ module ThemeCheck
63
76
  end
64
77
  end.to_h
65
78
 
66
- @latest_diagnostics = diagnostics_update.reject { |_, v| v.empty? }
79
+ @latest_diagnostics = diagnostics_update
80
+ .reject { |_, v| v.empty? }
81
+
67
82
  @first_run = false
83
+
84
+ # Only send updates for the current file when running with only_single_file
68
85
  diagnostics_update
86
+ .reject { |p, _| only_single_file && !analyzed_paths.include?(p) }
69
87
  end
70
88
  end
71
89
 
@@ -131,6 +149,14 @@ module ThemeCheck
131
149
  def single_file_diagnostics(relative_path)
132
150
  @latest_diagnostics[relative_path]&.select(&:single_file?) || []
133
151
  end
152
+
153
+ def whole_theme_diagnostics(relative_path)
154
+ @latest_diagnostics[relative_path]&.select(&:whole_theme?) || []
155
+ end
156
+
157
+ def previous_diagnostics(relative_path)
158
+ @latest_diagnostics[relative_path]
159
+ end
134
160
  end
135
161
  end
136
162
  end
@@ -14,7 +14,12 @@ module ThemeCheck
14
14
  end
15
15
 
16
16
  def execute(_args)
17
- @diagnostics_engine.analyze_and_send_offenses(@root_path, @root_config, force: true)
17
+ @diagnostics_engine.analyze_and_send_offenses(
18
+ @root_path,
19
+ @root_config,
20
+ only_single_file: false,
21
+ force: true
22
+ )
18
23
  nil
19
24
  end
20
25
  end
@@ -64,6 +64,7 @@ module ThemeCheck
64
64
  end
65
65
 
66
66
  def on_initialized(_id, _params)
67
+ return unless @configuration
67
68
  @configuration.fetch
68
69
  @configuration.register_did_change_capability
69
70
  end
@@ -85,7 +86,7 @@ module ThemeCheck
85
86
  def on_text_document_did_change(_id, params)
86
87
  relative_path = relative_path_from_text_document_uri(params)
87
88
  @storage.write(relative_path, content_changes_text(params), text_document_version(params))
88
- analyze_and_send_offenses(text_document_uri(params)) if @configuration.check_on_change?
89
+ analyze_and_send_offenses(text_document_uri(params), only_single_file: true) if @configuration.check_on_change?
89
90
  end
90
91
 
91
92
  def on_text_document_did_close(_id, params)
@@ -189,10 +190,11 @@ module ThemeCheck
189
190
  ThemeCheck::Config.from_path(root)
190
191
  end
191
192
 
192
- def analyze_and_send_offenses(absolute_path)
193
+ def analyze_and_send_offenses(absolute_path, only_single_file: nil)
193
194
  @diagnostics_engine.analyze_and_send_offenses(
194
195
  absolute_path,
195
- config_for_path(absolute_path)
196
+ config_for_path(absolute_path),
197
+ only_single_file: only_single_file.nil? ? @configuration.only_single_file? : only_single_file
196
198
  )
197
199
  end
198
200
 
@@ -184,6 +184,11 @@ module ThemeCheck
184
184
  @type_name ||= StringHelpers.underscore(StringHelpers.demodulize(@value.class.name)).to_sym
185
185
  end
186
186
 
187
+ def filters
188
+ raise TypeError, "Attempting to lookup filters of #{type_name}. Only variables have filters." unless variable?
189
+ @value.filters
190
+ end
191
+
187
192
  def source
188
193
  theme_file&.source
189
194
  end
@@ -4,6 +4,8 @@ module ThemeCheck
4
4
  class Position
5
5
  include PositionHelper
6
6
 
7
+ attr_reader :contents
8
+
7
9
  def initialize(
8
10
  needle_arg,
9
11
  contents_arg,
@@ -12,7 +14,13 @@ module ThemeCheck
12
14
  node_markup_offset: 0 # the index of markup inside the node_markup
13
15
  )
14
16
  @needle = needle_arg
15
- @contents = contents_arg
17
+
18
+ @contents = if contents_arg&.is_a?(String) && !contents_arg.empty?
19
+ contents_arg
20
+ else
21
+ ''
22
+ end
23
+
16
24
  @line_number_1_indexed = line_number_1_indexed
17
25
  @node_markup_offset = node_markup_offset
18
26
  @node_markup = node_markup
@@ -77,18 +85,13 @@ module ThemeCheck
77
85
  node_markup_start + @node_markup_offset
78
86
  end
79
87
 
80
- def contents
81
- return '' unless @contents.is_a?(String) && !@contents.empty?
82
- @contents
83
- end
84
-
85
88
  def line_number
86
89
  return 0 if @line_number_1_indexed.nil?
87
90
  bounded(0, @line_number_1_indexed - 1, content_line_count)
88
91
  end
89
92
 
90
93
  def needle
91
- if has_content_and_line_number_but_no_needle?
94
+ @cached_needle ||= if has_content_and_line_number_but_no_needle?
92
95
  entire_line_needle
93
96
  elsif contents.empty? || @needle.nil?
94
97
  ''
@@ -3,6 +3,11 @@
3
3
 
4
4
  module ThemeCheck
5
5
  module PositionHelper
6
+ # Apparently this old implementation is 2x slower (with benchmark/ips),
7
+ # so dropping with the following one... It's ugly af but
8
+ # this shit runs 100K+ times in one theme-check so it gotta go
9
+ # fast!
10
+ #
6
11
  def from_row_column_to_index(content, row, col)
7
12
  return 0 unless content.is_a?(String) && !content.empty?
8
13
  return 0 unless row.is_a?(Integer) && col.is_a?(Integer)
@@ -15,6 +20,19 @@ module ThemeCheck
15
20
  bounded(result, result + col, scanner.pre_match.size)
16
21
  end
17
22
 
23
+ # def from_row_column_to_index(content, row, col)
24
+ # return 0 unless content.is_a?(String) && !content.empty?
25
+ # return 0 unless row.is_a?(Integer) && col.is_a?(Integer)
26
+ # i = 0
27
+ # safe_row = bounded(0, row, content.count("\n"))
28
+ # charpos = -1
29
+ # charpos = content.index("\n", charpos + 1) while i < safe_row && (i += 1) && charpos
30
+ # result = charpos ? charpos + 1 : 0
31
+ # next_line = content.index("\n", result)
32
+ # upper_bound = next_line ? next_line : content.size - 1
33
+ # bounded(result, result + col, upper_bound)
34
+ # end
35
+
18
36
  def from_index_to_row_column(content, index)
19
37
  return [0, 0] unless content.is_a?(String) && !content.empty?
20
38
  return [0, 0] unless index.is_a?(Integer)
@@ -45,7 +45,9 @@ module ThemeCheck
45
45
  @source = @storage.read(@relative_path)
46
46
  end
47
47
  @eol = @source.include?("\r\n") ? "\r\n" : "\n"
48
- @source = @source.gsub("\r\n", "\n")
48
+ @source = @source
49
+ .gsub(/\r(?!\n)/, "\r\n") # fix rogue \r without followup \n with \r\n
50
+ .gsub("\r\n", "\n")
49
51
  end
50
52
 
51
53
  def json?
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module ThemeCheck
3
- VERSION = "1.9.1"
3
+ VERSION = "1.10.1"
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: theme-check
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.1
4
+ version: 1.10.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marc-André Cournoyer
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-12-09 00:00:00.000000000 Z
11
+ date: 2022-02-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: liquid