theme-check 1.9.2 → 1.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -1
  3. data/README.md +1 -0
  4. data/data/shopify_liquid/filters.yml +68 -51
  5. data/lib/theme_check/analyzer.rb +31 -19
  6. data/lib/theme_check/checks/asset_size_css_stylesheet_tag.rb +1 -1
  7. data/lib/theme_check/checks/asset_url_filters.rb +2 -2
  8. data/lib/theme_check/checks/content_for_header_modification.rb +1 -1
  9. data/lib/theme_check/checks/deprecated_filter.rb +2 -2
  10. data/lib/theme_check/checks/parser_blocking_script_tag.rb +1 -1
  11. data/lib/theme_check/checks/remote_asset.rb +27 -4
  12. data/lib/theme_check/checks/translation_key_exists.rb +1 -1
  13. data/lib/theme_check/checks/undefined_object.rb +2 -0
  14. data/lib/theme_check/checks/unknown_filter.rb +1 -1
  15. data/lib/theme_check/checks.rb +1 -2
  16. data/lib/theme_check/cli.rb +5 -6
  17. data/lib/theme_check/config.rb +37 -37
  18. data/lib/theme_check/disabled_checks.rb +6 -0
  19. data/lib/theme_check/html_node.rb +8 -1
  20. data/lib/theme_check/language_server/configuration.rb +31 -12
  21. data/lib/theme_check/language_server/diagnostic.rb +4 -0
  22. data/lib/theme_check/language_server/diagnostics_engine.rb +11 -6
  23. data/lib/theme_check/language_server/diagnostics_manager.rb +29 -3
  24. data/lib/theme_check/language_server/execute_command_providers/run_checks_execute_command_provider.rb +6 -1
  25. data/lib/theme_check/language_server/handler.rb +5 -3
  26. data/lib/theme_check/liquid_node.rb +5 -0
  27. data/lib/theme_check/position.rb +10 -7
  28. data/lib/theme_check/position_helper.rb +18 -0
  29. data/lib/theme_check/tags.rb +2 -2
  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: c667ad2be8d10ffe3ded0b949e5ceb4372350462addcfa0f4ecc30b575b3a1cf
4
- data.tar.gz: 0ce4f520a0992413b151d2a2d607d81848ed151e6bb0d71ebd154978b44ffd28
3
+ metadata.gz: 542ffd23d66103c0ad13f740b9838c28ed1ef010faed69a9066a6301ed0e9692
4
+ data.tar.gz: fdaa5345072521c1467ca5dc8c17d3460e5e299fbceed14b242bb27dc2001ca3
5
5
  SHA512:
6
- metadata.gz: 4a3b48d0bc8e896385bdf838e6e6e722e7a960f20af11987a51771c8203da2c39fc76ac97c9621da3c807efe2eca13b7599466f12c086785b3c3812cdd5351dc
7
- data.tar.gz: 39daf33306825516b5316356c1ea8a2523cbe7c0b1406fa988d46d50d94356815d817b9a24f6e2c373a3041f6ac0856e186d160104590948c27291333a1c2a03
6
+ metadata.gz: 4c0897c7d37ada167a220691d892879884fa3ef82539fd9b649e4b8f9ab10d813907bb77420a471ff3545c192a7794378fcf19acdb16a9a866715c49d2a5f03a
7
+ data.tar.gz: 5543d2dafa855b025a28a3dc83742896d90ad8eba4c3bcb4b2bbea41fbd9c89b1f3a2695354bc35c62af4e8ca460d68ac6d5b3f4fa47032b218fe31eb8b70015
data/CHANGELOG.md CHANGED
@@ -1,4 +1,27 @@
1
1
 
2
+ v1.10.0 / 2022-02-24
3
+ ====================
4
+
5
+ ## Features
6
+
7
+ * Performance Improvements ([#556](https://github.com/shopify/theme-check/issues/556))
8
+ - Boasts ~125x faster `checkOnChange` checks.
9
+
10
+ * New language server configuration: `"themeCheck.onlySingleFileChecks"` ([#556](https://github.com/shopify/theme-check/issues/556))
11
+ - When `true`, disables whole theme checks in the editor and makes it so only the open files are checked.
12
+ - When `false` (default), behaves as before.
13
+
14
+ ## Fixes
15
+
16
+ * Do not complain about variables passed to the default filter ([#532](https://github.com/shopify/theme-check/issues/532))
17
+ * `extend:` should accept absolute and relative paths ([#555](https://github.com/shopify/theme-check/issues/555))
18
+ * Gracefully handle initialized without config. ([#552](https://github.com/shopify/theme-check/issues/552))
19
+ * Add missing metafield filters ([#550](https://github.com/shopify/theme-check/issues/550))
20
+ * Handle media.sources edge case in RemoteAsset ([#549](https://github.com/shopify/theme-check/issues/549))
21
+ * Disable HTML checks inside Liquid comments ([#548](https://github.com/shopify/theme-check/issues/548))
22
+ * Prevent bad render tags from passing theme-check ([#551](https://github.com/shopify/theme-check/issues/551))
23
+ * Handle rogue carriage returns ([#547](https://github.com/shopify/theme-check/issues/547))
24
+
2
25
  v1.9.2 / 2021-12-13
3
26
  ===================
4
27
 
@@ -14,7 +37,7 @@ v1.9.0 / 2021-12-01
14
37
 
15
38
  ## Features
16
39
 
17
- * Add Corrections as Code Actions in Language Server (quickfix + source.fixAll) (#471)
40
+ * Add Corrections as Code Actions in Language Server (quickfix + source.fixAll) ([#471](https://github.com/shopify/theme-check/issues/471))
18
41
  * Add SchemaJsonFormat check ([#512](https://github.com/shopify/theme-check/issues/512))
19
42
  * Add checkOn{Open,Change,Save} Language Server Configurations ([#511](https://github.com/shopify/theme-check/issues/511))
20
43
  * 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
 
@@ -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
 
@@ -141,6 +141,8 @@ module ThemeCheck
141
141
 
142
142
  def parseable_markup
143
143
  return @parseable_source if @value.name == "#document-fragment"
144
+ return @value.to_str if @value.comment?
145
+ return @value.content if literal?
144
146
 
145
147
  start_index = from_row_column_to_index(@parseable_source, line_number - 1, 0)
146
148
  @parseable_source
@@ -163,7 +165,12 @@ module ThemeCheck
163
165
 
164
166
  Excerpt:
165
167
  ```
166
- #{@parseable_source.lines[line_number - 1...line_number + 5]}
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("")}
167
174
  ```
168
175
  MSG
169
176
  end
@@ -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)
@@ -125,7 +125,7 @@ module ThemeCheck
125
125
  end
126
126
 
127
127
  class Render < Liquid::Tag
128
- SYNTAX = /((?:#{Liquid::QuotedString}|#{Liquid::VariableSegment})+)(\s+(with|#{Liquid::Render::FOR})\s+(#{Liquid::QuotedFragment}+))?(\s+(?:as)\s+(#{Liquid::VariableSegment}+))?/o
128
+ SYNTAX = /((?:#{Liquid::QuotedString}|\A#{Liquid::VariableSegment})+)(\s+(with|#{Liquid::Render::FOR})\s+(#{Liquid::QuotedFragment}+))?(\s+(?:as)\s+(#{Liquid::VariableSegment}+))?/o
129
129
 
130
130
  disable_tags "include"
131
131
 
@@ -134,7 +134,7 @@ module ThemeCheck
134
134
  def initialize(tag_name, markup, options)
135
135
  super
136
136
 
137
- raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX
137
+ raise Liquid::SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX
138
138
 
139
139
  template_name = Regexp.last_match(1)
140
140
  with_or_for = Regexp.last_match(3)
@@ -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.2"
3
+ VERSION = "1.10.0"
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.2
4
+ version: 1.10.0
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-13 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