theme-check 1.7.2 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 769a14dbfbdd0c479d57aa5dc9992a74c99fbbfb9db3d34ee384abc75dbb1028
4
- data.tar.gz: b1eea70f76270fe165543281f7ce34ca55bb9c1c7f47cc59006cb71cb13a1269
3
+ metadata.gz: e8f59cffd194662dc9f66474d70310caeec0a713a3029c92efaefbf605ee69bc
4
+ data.tar.gz: 7bc65dd34a4b387c13cb0395d3d1cec4d60095a7ba2c3e6fa8fc5fa50bea2df8
5
5
  SHA512:
6
- metadata.gz: '0197cae7eef9470beeadd192d3767a6f852ebcc89f53039cec52956ffd9b6d22bdd1b976762955b974bb8836f84f702e97aa1fdec480028e52c9f4822130ec17'
7
- data.tar.gz: 7f2da4367e81879e6109ee2b411c01f3b55440d69ec048189f1909913c22bf02f0b802109bd98af75be3bf2590bf54e1c918fddc6e9763baded0e7b3539d59fb
6
+ metadata.gz: 2ed214b8d5abb83dd3b9ede347fc52f6ed4cce4ddc42b2e0879c24e54b21c0ff30055b5ac8b3e45c3fa5da9efc5da5343c168c6cd818af863d5a73e4841b796b
7
+ data.tar.gz: 8777e953d57ae33ce11c8bd527e061567ddc179dad28c612d1c7e6f80dd8a5d2534e786b86d5f55d944695c10c08f34b3aeb26057700b50b7b6642fbf8007290
data/CHANGELOG.md CHANGED
@@ -1,4 +1,20 @@
1
1
 
2
+ v1.8.0 / 2021-11-09
3
+ ===================
4
+
5
+ ## Features
6
+
7
+ **New corrections for the following checks:**
8
+
9
+ * `MissingRequiredTemplateFiles` ([#462](https://github.com/shopify/theme-check/issues/462))
10
+ * `RequiredLayoutThemeObject` ([#484](https://github.com/shopify/theme-check/issues/484))
11
+ * `UnusedAssign` ([#380](https://github.com/shopify/theme-check/issues/380))
12
+
13
+ ## Fixes
14
+
15
+ * Add support for `preload_tag` filter
16
+ * Minor Language Server improvements (close logs) ([#472](https://github.com/shopify/theme-check/issues/472))
17
+
2
18
  v1.7.2 / 2021-09-24
3
19
  ===================
4
20
 
@@ -81,6 +81,7 @@ UrlFilter:
81
81
  - product_img_url
82
82
  - collection_img_url
83
83
  - article_img_url
84
+ - preload_tag
84
85
  JsonFilter:
85
86
  - json
86
87
  ColorFilter:
@@ -1,47 +1,52 @@
1
1
  # Check Title (`<%= class_name %>`)
2
2
 
3
- A brief paragraph explaining why the check exists.
3
+ _Version THEME_CHECK_VERSION+_
4
4
 
5
- ## Check Details
5
+ A short description of what the check does.
6
6
 
7
- This check is aimed at eliminating ...
7
+ A brief paragraph explaining why the check exists (what best practice is it enforcing, and why is it important?).
8
8
 
9
- :-1: Examples of **incorrect** code for this check:
9
+ ## Examples
10
+
11
+ The following examples contain code snippets that either fail or pass this check.
12
+
13
+ ### &#x2717; Fail
10
14
 
11
15
  ```liquid
12
16
  ```
13
17
 
14
- :+1: Examples of **correct** code for this check:
18
+ ### &#x2713; Pass
15
19
 
16
20
  ```liquid
17
21
  ```
18
22
 
19
- ## Check Options
23
+ ## Options
20
24
 
21
- The default configuration for this check is the following:
25
+ The following example contains the default configuration for this check:
22
26
 
23
27
  ```yaml
24
28
  <%= class_name %>:
25
- enabled: true
26
- some_option: 10
29
+ enabled: false
30
+ severity: suggestion
31
+ other_option: 10_000
27
32
  ```
28
33
 
29
- ### `some_option`
30
-
31
- The `some_option` option (Default: `10`) determines ...
32
-
33
- ## When Not To Use It
34
+ | Parameter | Description |
35
+ | --- | --- |
36
+ | enabled | Whether the check is enabled. |
37
+ | severity | The [severity](https://shopify.developers/themes/tools/theme-check/configuration#check-severity) of the check. |
38
+ | other_option | A description of the option. |
34
39
 
35
- If you don't want to ..., then it's safe to disable this rule.
40
+ ## Disabling this check
36
41
 
37
- ## Version
42
+ [ This check is safe to disable. You might want to disable this check if ... | Disabling this check isn't recommended because ... ].
38
43
 
39
- This check has been introduced in Theme Check THEME_CHECK_VERSION.
44
+ [ This check is disabled by default when <condition>. ]
40
45
 
41
46
  ## Resources
42
47
 
43
- - [Rule Source][codesource]
44
- - [Documentation Source][docsource]
48
+ - [Rule source][codesource]
49
+ - [Documentation source][docsource]
45
50
 
46
51
  [codesource]: /<%= code_source %>
47
52
  [docsource]: /<%= doc_source %>
@@ -3,9 +3,5 @@
3
3
 
4
4
  require 'theme_check'
5
5
 
6
- if ENV["THEME_CHECK_DEBUG"] == "true"
7
- $DEBUG = true
8
- end
9
-
10
6
  status_code = ThemeCheck::LanguageServer.start
11
7
  exit! status_code
@@ -7,7 +7,6 @@ module ThemeCheck
7
7
 
8
8
  def on_schema(node)
9
9
  schema = JSON.parse(node.value.nodelist.join)
10
-
11
10
  # Get all locales used in the schema
12
11
  used_locales = Set.new([theme.default_locale])
13
12
  visit_object(schema) do |_, locales|
@@ -19,11 +18,17 @@ module ThemeCheck
19
18
  visit_object(schema) do |key, locales|
20
19
  missing = used_locales - locales
21
20
  if missing.any?
22
- add_offense("#{key} missing translations for #{missing.join(', ')}", node: node)
21
+ add_offense("#{key} missing translations for #{missing.join(', ')}", node: node) do |corrector|
22
+ key = key.split(".")
23
+ missing.each do |language|
24
+ corrector.schema_corrector(schema, key + [language], "TODO")
25
+ end
26
+ corrector.replace_block_body(node, schema)
27
+ end
23
28
  end
24
29
  end
25
30
 
26
- check_locales(schema["locales"], node: node)
31
+ check_locales(schema, node: node)
27
32
 
28
33
  rescue JSON::ParserError
29
34
  # Ignored, handled in ValidSchema.
@@ -31,14 +36,16 @@ module ThemeCheck
31
36
 
32
37
  private
33
38
 
34
- def check_locales(locales, node:)
39
+ def check_locales(schema, node:)
40
+ locales = schema["locales"]
35
41
  return unless locales.is_a?(Hash)
36
42
 
37
43
  default_locale = locales[theme.default_locale]
44
+
38
45
  if default_locale
39
46
  locales.each_pair do |name, content|
40
47
  diff = LocaleDiff.new(default_locale, content)
41
- diff.add_as_offenses(self, key_prefix: ["locales", name], node: node)
48
+ diff.add_as_offenses(self, key_prefix: ["locales", name], node: node, schema: schema)
42
49
  end
43
50
  else
44
51
  add_offense("Missing default locale in key: locales", node: node)
@@ -27,14 +27,19 @@ module ThemeCheck
27
27
  def after_document(node)
28
28
  return unless node.theme_file.name == LAYOUT_FILENAME
29
29
 
30
- add_missing_object_offense("content_for_layout") unless @content_for_layout_found
31
- add_missing_object_offense("content_for_header") unless @content_for_header_found
30
+ add_missing_object_offense("content_for_layout", "</body>") unless @content_for_layout_found
31
+ add_missing_object_offense("content_for_header", "</head>") unless @content_for_header_found
32
32
  end
33
33
 
34
34
  private
35
35
 
36
- def add_missing_object_offense(name)
37
- add_offense("#{LAYOUT_FILENAME} must include {{#{name}}}", node: @layout_theme_node)
36
+ def add_missing_object_offense(name, tag)
37
+ add_offense("#{LAYOUT_FILENAME} must include {{#{name}}}", node: @layout_theme_node) do
38
+ if @layout_theme_node.source.index(tag)
39
+ @layout_theme_node.source.insert(@layout_theme_node.source.index(tag), " {{ #{name} }}\n ")
40
+ @layout_theme_node.markup = @layout_theme_node.source
41
+ end
42
+ end
38
43
  end
39
44
  end
40
45
  end
@@ -46,8 +46,9 @@ module ThemeCheck
46
46
  @templates.each_pair do |_, info|
47
47
  used = info.collect_used_assigns(@templates)
48
48
  info.assign_nodes.each_pair do |name, node|
49
- unless used.include?(name)
50
- add_offense("`#{name}` is never used", node: node)
49
+ next if used.include?(name)
50
+ add_offense("`#{name}` is never used", node: node) do |corrector|
51
+ corrector.remove(node)
51
52
  end
52
53
  end
53
54
  end
@@ -25,7 +25,7 @@ module ThemeCheck
25
25
  def on_end
26
26
  missing_snippets.each do |theme_file|
27
27
  add_offense("This snippet is not used", theme_file: theme_file) do |corrector|
28
- corrector.remove(@theme, theme_file.relative_path.to_s)
28
+ corrector.remove_file(@theme, theme_file.relative_path.to_s)
29
29
  end
30
30
  end
31
31
  end
@@ -14,11 +14,20 @@ module ThemeCheck
14
14
  @theme_file.rewriter.insert_before(node, content)
15
15
  end
16
16
 
17
+ def remove(node)
18
+ @theme_file.rewriter.remove(node)
19
+ end
20
+
17
21
  def replace(node, content)
18
22
  @theme_file.rewriter.replace(node, content)
19
23
  node.markup = content
20
24
  end
21
25
 
26
+ def replace_block_body(node, content)
27
+ content = "\n #{JSON.pretty_generate(content, array_nl: "\n ", object_nl: "\n ")}\n" if content.is_a?(Hash)
28
+ @theme_file.rewriter.replace_body(node, content)
29
+ end
30
+
22
31
  def wrap(node, insert_before, insert_after)
23
32
  @theme_file.rewriter.wrap(node, insert_before, insert_after)
24
33
  end
@@ -28,11 +37,11 @@ module ThemeCheck
28
37
  end
29
38
 
30
39
  def create_default_locale_json(theme)
40
+ create(theme, "locales/#{theme.default_locale}.default.json", {})
31
41
  theme.default_locale_json = JsonFile.new("locales/#{theme.default_locale}.default.json", theme.storage)
32
- theme.default_locale_json.update_contents({})
33
42
  end
34
43
 
35
- def remove(theme, relative_path)
44
+ def remove_file(theme, relative_path)
36
45
  theme.storage.remove(relative_path)
37
46
  end
38
47
 
@@ -42,12 +51,40 @@ module ThemeCheck
42
51
 
43
52
  def add_default_translation_key(file, key, value)
44
53
  hash = file.content
54
+ add_key(hash, key, value)
55
+ file.update_contents(hash)
56
+ end
57
+
58
+ def remove_key(hash, key)
59
+ key.reduce(hash) do |pointer, token|
60
+ return pointer.delete(token) if token == key.last
61
+ pointer[token]
62
+ end
63
+ end
64
+
65
+ def add_key(hash, key, value)
45
66
  key.reduce(hash) do |pointer, token|
46
67
  return pointer[token] = value if token == key.last
47
68
  pointer[token] = {} unless pointer.key?(token)
48
69
  pointer[token]
49
70
  end
50
- file.update_contents(hash)
71
+ end
72
+
73
+ def schema_corrector(schema, key, value)
74
+ return unless schema.is_a?(Hash)
75
+ key.reduce(schema) do |pointer, token|
76
+ case pointer
77
+ when Array
78
+ pointer.each do |item|
79
+ schema_corrector(item, key.drop(1), value)
80
+ end
81
+
82
+ when Hash
83
+ return pointer[token] = value if token == key.last
84
+ pointer[token] = {} unless pointer.key?(token) || pointer.key?("id")
85
+ pointer[token].nil? && pointer["id"] == token ? pointer : pointer[token]
86
+ end
87
+ end
51
88
  end
52
89
  end
53
90
  end
@@ -38,14 +38,14 @@ module ThemeCheck
38
38
  def read_message
39
39
  message_body = @messenger.read_message
40
40
  message_json = JSON.parse(message_body)
41
- @messenger.log(JSON.pretty_generate(message_json)) if $DEBUG
41
+ @messenger.log(JSON.pretty_generate(message_json)) if ThemeCheck.debug?
42
42
  message_json
43
43
  end
44
44
 
45
45
  def send_message(message_hash)
46
46
  message_hash[:jsonrpc] = '2.0'
47
47
  message_body = JSON.dump(message_hash)
48
- @messenger.log(JSON.pretty_generate(message_hash)) if $DEBUG
48
+ @messenger.log(JSON.pretty_generate(message_hash)) if ThemeCheck.debug?
49
49
  @messenger.send_message(message_body)
50
50
  end
51
51
 
@@ -68,11 +68,25 @@ module ThemeCheck
68
68
  # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#responseMessage
69
69
  def send_response(id, result = nil, error = nil)
70
70
  message = { id: id }
71
- message[:result] = result if result
72
- message[:error] = error if error
71
+ if error
72
+ message[:error] = error
73
+ else
74
+ message[:result] = result
75
+ end
73
76
  send_message(message)
74
77
  end
75
78
 
79
+ # https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#responseError
80
+ def send_internal_error(id, e)
81
+ send_response(id, nil, {
82
+ code: ErrorCodes::INTERNAL_ERROR,
83
+ message: <<~EOS,
84
+ #{e.class}: #{e.message}
85
+ #{e.backtrace.join("\n ")}
86
+ EOS
87
+ })
88
+ end
89
+
76
90
  # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#notificationMessage
77
91
  def send_notification(method, params)
78
92
  message = { method: method }
@@ -47,10 +47,13 @@ module ThemeCheck
47
47
  })
48
48
  end
49
49
 
50
+ def on_shutdown(id, _params)
51
+ @bridge.send_response(id, nil)
52
+ end
53
+
50
54
  def on_exit(_id, _params)
51
55
  close!
52
56
  end
53
- alias_method :on_shutdown, :on_exit
54
57
 
55
58
  def on_text_document_did_change(_id, params)
56
59
  relative_path = relative_path_from_text_document_uri(params)
@@ -40,6 +40,8 @@ module ThemeCheck
40
40
  content += chunk
41
41
  end
42
42
  content.lstrip!
43
+ rescue IOError
44
+ raise DoneStreaming
43
45
  end
44
46
 
45
47
  def send_message(message_body)
@@ -37,5 +37,9 @@ module ThemeCheck
37
37
  FULL = 1
38
38
  INCREMENTAL = 2
39
39
  end
40
+
41
+ module ErrorCodes
42
+ INTERNAL_ERROR = -32603
43
+ end
40
44
  end
41
45
  end
@@ -37,7 +37,7 @@ module ThemeCheck
37
37
  @handlers = []
38
38
 
39
39
  # The error queue holds blocks the main thread. When filled, we exit the program.
40
- @error = SizedQueue.new(1)
40
+ @error = SizedQueue.new(number_of_threads)
41
41
 
42
42
  @should_raise_errors = should_raise_errors
43
43
  end
@@ -94,8 +94,7 @@ module ThemeCheck
94
94
 
95
95
  rescue Exception => e # rubocop:disable Lint/RescueException
96
96
  raise e if should_raise_errors
97
- @bridge.log(e)
98
- @bridge.log(e.backtrace)
97
+ @bridge.log("#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}")
99
98
  2
100
99
  end
101
100
 
@@ -110,6 +109,15 @@ module ThemeCheck
110
109
  if @handler.respond_to?(method_name)
111
110
  @handler.send(method_name, id, params)
112
111
  end
112
+
113
+ rescue DoneStreaming => e
114
+ raise e
115
+ rescue StandardError => e
116
+ is_request = id
117
+ raise e unless is_request
118
+ # Errors obtained in request handlers should be sent
119
+ # back as internal errors instead of closing the program.
120
+ @bridge.send_internal_error(id, e)
113
121
  end
114
122
 
115
123
  def handle_response(message)
@@ -135,7 +143,16 @@ module ThemeCheck
135
143
 
136
144
  # Hijack the status_code if an error occurred while cleaning up.
137
145
  # 👀 unit tests.
138
- return status_code_from_error(@error.pop) unless @error.empty?
146
+ until @error.empty?
147
+ code = status_code_from_error(@error.pop)
148
+ # Promote the status_code to ERROR if one of the threads
149
+ # resulted in an error, otherwise leave the status_code as
150
+ # is. That's because one thread could end successfully in a
151
+ # DoneStreaming error while the other failed with an
152
+ # internal error. If we had an internal error, we should
153
+ # return with a status_code that fits.
154
+ status_code = code if code > status_code
155
+ end
139
156
  status_code
140
157
  ensure
141
158
  @messenger.close_output
@@ -74,6 +74,34 @@ module ThemeCheck
74
74
  position.end_index
75
75
  end
76
76
 
77
+ def start_token_index
78
+ return position.start_index if inside_liquid_tag?
79
+ position.start_index - (start_token.length + 1)
80
+ end
81
+
82
+ def end_token_index
83
+ return position.end_index if inside_liquid_tag?
84
+ position.end_index + end_token.length
85
+ end
86
+
87
+ def render_start_tag
88
+ "#{start_token} #{@value.raw}#{end_token}"
89
+ end
90
+
91
+ def render_end_tag
92
+ "#{start_token} #{@value.block_delimiter} #{end_token}"
93
+ end
94
+
95
+ def block_body_start_index
96
+ return unless block_tag?
97
+ block_regex.begin(:body)
98
+ end
99
+
100
+ def block_body_end_index
101
+ return unless block_tag?
102
+ block_regex.end(:body)
103
+ end
104
+
77
105
  # Literals are hard-coded values in the liquid file.
78
106
  def literal?
79
107
  @value.is_a?(String) || @value.is_a?(Integer)
@@ -184,6 +212,11 @@ module ThemeCheck
184
212
 
185
213
  private
186
214
 
215
+ def block_regex
216
+ return unless block_tag?
217
+ /(?<start_token>#{render_start_tag})(?<body>.*)(?<end_token>#{render_end_tag})/m.match(source)
218
+ end
219
+
187
220
  def position
188
221
  @position ||= Position.new(
189
222
  markup,
@@ -14,24 +14,43 @@ module ThemeCheck
14
14
  visit_object(@default, @other, [])
15
15
  end
16
16
 
17
- def add_as_offenses(check, key_prefix: [], node: nil, theme_file: nil)
17
+ def add_as_offenses(check, key_prefix: [], node: nil, theme_file: nil, schema: {})
18
18
  if extra_keys.any?
19
- add_keys_offense(check, "Extra translation keys", extra_keys,
20
- key_prefix: key_prefix, node: node, theme_file: theme_file)
19
+ remove_extra_keys_offense(check, "Extra translation keys", extra_keys,
20
+ key_prefix: key_prefix, node: node, theme_file: theme_file, schema: schema)
21
21
  end
22
22
 
23
23
  if missing_keys.any?
24
- add_keys_offense(check, "Missing translation keys", missing_keys,
25
- key_prefix: key_prefix, node: node, theme_file: theme_file)
24
+ add_missing_keys_offense(check, "Missing translation keys", missing_keys,
25
+ key_prefix: key_prefix, node: node, theme_file: theme_file, schema: schema)
26
26
  end
27
27
  end
28
28
 
29
29
  private
30
30
 
31
- def add_keys_offense(check, cause, keys, key_prefix:, node: nil, theme_file: nil)
32
- message = "#{cause}: #{format_keys(key_prefix, keys)}"
31
+ def remove_extra_keys_offense(check, cause, extra_keys, key_prefix:, node: nil, theme_file: nil, schema: {})
32
+ message = "#{cause}: #{format_keys(key_prefix, extra_keys)}"
33
33
  if node
34
- check.add_offense(message, node: node)
34
+ check.add_offense(message, node: node) do |corrector|
35
+ extra_keys.each do |k|
36
+ corrector.remove_key(schema, key_prefix + k)
37
+ end
38
+ corrector.replace_block_body(node, schema)
39
+ end
40
+ else
41
+ check.add_offense(message, theme_file: theme_file)
42
+ end
43
+ end
44
+
45
+ def add_missing_keys_offense(check, cause, missing_keys, key_prefix:, node: nil, theme_file: nil, schema: {})
46
+ message = "#{cause}: #{format_keys(key_prefix, missing_keys)}"
47
+ if node
48
+ check.add_offense(message, node: node) do |corrector|
49
+ missing_keys.each do |k|
50
+ corrector.add_key(schema, key_prefix + k, "TODO")
51
+ end
52
+ corrector.replace_block_body(node, schema)
53
+ end
35
54
  else
36
55
  check.add_offense(message, theme_file: theme_file)
37
56
  end
@@ -64,6 +64,10 @@ module ThemeCheck
64
64
  strict_position.end_column
65
65
  end
66
66
 
67
+ def content_line_count
68
+ @content_line_count ||= contents.count("\n")
69
+ end
70
+
67
71
  private
68
72
 
69
73
  def compute_start_offset
@@ -78,10 +82,6 @@ module ThemeCheck
78
82
  @contents
79
83
  end
80
84
 
81
- def content_line_count
82
- @content_line_count ||= contents.count("\n")
83
- end
84
-
85
85
  def line_number
86
86
  return 0 if @line_number_1_indexed.nil?
87
87
  bounded(0, @line_number_1_indexed - 1, content_line_count)
@@ -66,7 +66,6 @@ module ThemeCheck
66
66
 
67
67
  def initialize(tag_name, markup, options)
68
68
  super
69
-
70
69
  if (matches = markup.match(SYNTAX))
71
70
  @liquid_variable_name = matches[:liquid_variable_name]
72
71
  @page_size = parse_expression(matches[:page_size])
@@ -25,6 +25,12 @@ module ThemeCheck
25
25
  )
26
26
  end
27
27
 
28
+ def remove(node)
29
+ @rewriter.remove(
30
+ range(node.start_token_index, node.end_token_index)
31
+ )
32
+ end
33
+
28
34
  def replace(node, content)
29
35
  @rewriter.replace(
30
36
  range(node.start_index, node.end_index),
@@ -32,6 +38,13 @@ module ThemeCheck
32
38
  )
33
39
  end
34
40
 
41
+ def replace_body(node, content)
42
+ @rewriter.replace(
43
+ range(node.block_body_start_index, node.block_body_end_index),
44
+ content
45
+ )
46
+ end
47
+
35
48
  def wrap(node, insert_before, insert_after)
36
49
  @rewriter.wrap(
37
50
  range(node.start_index, node.end_index),
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module ThemeCheck
3
- VERSION = "1.7.2"
3
+ VERSION = "1.8.0"
4
4
  end
data/lib/theme_check.rb CHANGED
@@ -51,6 +51,10 @@ Encoding.default_external = Encoding::UTF_8
51
51
  Encoding.default_internal = Encoding::UTF_8
52
52
 
53
53
  module ThemeCheck
54
+ def self.debug?
55
+ ENV["THEME_CHECK_DEBUG"] == "true"
56
+ end
57
+
54
58
  def self.with_liquid_c_disabled
55
59
  if defined?(Liquid::C)
56
60
  was_enabled = Liquid::C.enabled
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.7.2
4
+ version: 1.8.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-09-24 00:00:00.000000000 Z
11
+ date: 2021-11-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: liquid