theme-check 1.7.2 → 1.8.0

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.
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