theme-check 1.7.0 → 1.7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 943f8cb722f6c19c1c41e494622922e608a99b50e4ad67f2288a2ed99f406ff3
4
- data.tar.gz: a8fd83f054895fd366bba2512ff6431d8c470bb3e013ff6c6bf9f6be5366bf38
3
+ metadata.gz: 0c0071563941bd1e62d174c317667f7b1258a5154fdd0b9e65531a667aafbb53
4
+ data.tar.gz: fba3428de01a76fc042ee5c9fefe5acdcc1b446c02eafd5a2d187d503d863433
5
5
  SHA512:
6
- metadata.gz: fb37aed715f2b9f2dd0a8c17f2abf69110d583280d88508ef3f6ee439989c63fe9819025d870cfc1ed64a8deaac19586ac48ab261495e7a371575d9971754980
7
- data.tar.gz: 64727b7c9684fa094ce498c09a2a3f1dc06ce10fa99aed066af2aa327bd4de70fcc728f76ad127fd08d7b1b6e28b42b06acf96d83b26193814e7eed1b19a2008
6
+ metadata.gz: 9ebf3e7d3f24ca2d5d4f7b815199755bc997b94e870b3e60a2d24bc39f863974f7838f69548d11ccca9882335b3cf1522384b56f4bd3184d526ca968909cb192
7
+ data.tar.gz: dd9d5079b4adc9be6c13eb9cdf42e049e87988337e7881631eb0ee1163c8680930cb6efee863856221b1f8fd76fc81d9d848cd10969752f170cd032e371a42d0
data/CHANGELOG.md CHANGED
@@ -1,4 +1,11 @@
1
1
 
2
+ v1.7.1 / 2021-09-24
3
+ ===================
4
+
5
+ * Handle Errno::EADDRNOTAVAIL in RemoteAsset ([#465](https://github.com/shopify/theme-check/issues/465))
6
+ * Complete end tags ([#277](https://github.com/shopify/theme-check/issues/277))
7
+ * Do not flag shopify translations as missing or extra ([#407](https://github.com/shopify/theme-check/issues/407))
8
+
2
9
  v1.7.0 / 2021-09-20
3
10
  ===================
4
11
 
@@ -2,28 +2,28 @@
2
2
  - assign
3
3
  - break
4
4
  - capture
5
- - case
6
- - comment
5
+ - case: endcase
6
+ - comment: endcomment
7
7
  - continue
8
8
  - cycle
9
9
  - decrement
10
10
  - echo
11
11
  - else
12
12
  - elsif
13
- - for
14
- - form
15
- - if
13
+ - for: endfor
14
+ - form: endform
15
+ - if: endif
16
16
  - ifchanged
17
17
  - increment
18
- - javascript
18
+ - javascript: endjavascript
19
19
  - layout
20
20
  - liquid
21
- - paginate
21
+ - paginate: endpaginate
22
22
  - raw
23
23
  - render
24
- - schema
24
+ - schema: endschema
25
25
  - section
26
- - style
26
+ - style: endstyle
27
27
  - stylesheet
28
28
  - tablerow
29
29
  - unless
@@ -1,17 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module ThemeCheck
3
- module SystemTranslations
4
- extend self
5
-
6
- def translations
7
- @translations ||= YAML.load(File.read("#{__dir__}/../../../data/shopify_translation_keys.yml")).to_set
8
- end
9
-
10
- def include?(key)
11
- translations.include?(key)
12
- end
13
- end
14
-
15
3
  class TranslationKeyExists < LiquidCheck
16
4
  severity :error
17
5
  category :translation
@@ -24,7 +12,7 @@ module ThemeCheck
24
12
  return unless (key_node = node.children.first)
25
13
  return unless key_node.value.is_a?(String)
26
14
 
27
- unless key_exists?(key_node.value) || SystemTranslations.include?(key_node.value)
15
+ unless key_exists?(key_node.value) || ShopifyLiquid::SystemTranslations.include?(key_node.value)
28
16
  add_offense(
29
17
  "'#{key_node.value}' does not have a matching entry in '#{@theme.default_locale_json.relative_path}'",
30
18
  node: node,
@@ -22,6 +22,7 @@ module ThemeCheck
22
22
  Errno::EAGAIN,
23
23
  Errno::EHOSTUNREACH,
24
24
  Errno::ENETUNREACH,
25
+ Errno::EADDRNOTAVAIL,
25
26
  ]
26
27
 
27
28
  NET_HTTP_EXCEPTIONS = [
@@ -11,6 +11,10 @@ module ThemeCheck
11
11
  @files = {}
12
12
  end
13
13
 
14
+ def relative_path(absolute_path)
15
+ Pathname.new(absolute_path).relative_path_from(@root).to_s
16
+ end
17
+
14
18
  def path(relative_path)
15
19
  @root.join(relative_path)
16
20
  end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This class exists as a bridge (or boundary) between our handlers and the outside world.
4
+ #
5
+ # It is concerned with all the Language Server Protocol constructs. i.e.
6
+ #
7
+ # - sending Hash messages as JSON
8
+ # - reading JSON messages as Hashes
9
+ # - preparing, sending and resolving requests
10
+ # - preparing and sending responses
11
+ # - preparing and sending notifications
12
+ # - preparing and sending progress notifications
13
+ #
14
+ # But it _not_ concerned by _how_ those messages are sent to the
15
+ # outside world. That's the job of the messenger.
16
+ #
17
+ # This enables us to have all the language server protocol logic
18
+ # in here living independently of how we communicate with the
19
+ # client (STDIO or websocket)
20
+ module ThemeCheck
21
+ module LanguageServer
22
+ class Bridge
23
+ attr_writer :supports_work_done_progress
24
+
25
+ def initialize(messenger)
26
+ # The messenger is responsible for IO.
27
+ # Could be STDIO or WebSockets or Mock.
28
+ @messenger = messenger
29
+
30
+ # Whether the client supports work done progress notifications
31
+ @supports_work_done_progress = false
32
+ end
33
+
34
+ def log(message)
35
+ @messenger.log(message)
36
+ end
37
+
38
+ def read_message
39
+ message_body = @messenger.read_message
40
+ message_json = JSON.parse(message_body)
41
+ @messenger.log(JSON.pretty_generate(message_json)) if $DEBUG
42
+ message_json
43
+ end
44
+
45
+ def send_message(message_hash)
46
+ message_hash[:jsonrpc] = '2.0'
47
+ message_body = JSON.dump(message_hash)
48
+ @messenger.log(JSON.pretty_generate(message_hash)) if $DEBUG
49
+ @messenger.send_message(message_body)
50
+ end
51
+
52
+ # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#requestMessage
53
+ def send_request(method, params = nil)
54
+ channel = Channel.create
55
+ message = { id: channel.id }
56
+ message[:method] = method
57
+ message[:params] = params if params
58
+ send_message(message)
59
+ channel.pop
60
+ ensure
61
+ channel.close
62
+ end
63
+
64
+ def receive_response(id, result)
65
+ Channel.by_id(id) << result
66
+ end
67
+
68
+ # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#responseMessage
69
+ def send_response(id, result = nil, error = nil)
70
+ message = { id: id }
71
+ message[:result] = result if result
72
+ message[:error] = error if error
73
+ send_message(message)
74
+ end
75
+
76
+ # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#notificationMessage
77
+ def send_notification(method, params)
78
+ message = { method: method }
79
+ message[:params] = params
80
+ send_message(message)
81
+ end
82
+
83
+ # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#progress
84
+ def send_progress(token, value)
85
+ send_notification("$/progress", token: token, value: value)
86
+ end
87
+
88
+ def supports_work_done_progress?
89
+ @supports_work_done_progress
90
+ end
91
+
92
+ def send_create_work_done_progress_request(token)
93
+ return unless supports_work_done_progress?
94
+ send_request("window/workDoneProgress/create", {
95
+ token: token,
96
+ })
97
+ end
98
+
99
+ def send_work_done_progress_begin(token, title)
100
+ return unless supports_work_done_progress?
101
+ send_progress(token, {
102
+ kind: 'begin',
103
+ title: title,
104
+ cancellable: false,
105
+ percentage: 0,
106
+ })
107
+ end
108
+
109
+ def send_work_done_progress_report(token, message, percentage)
110
+ return unless supports_work_done_progress?
111
+ send_progress(token, {
112
+ kind: 'report',
113
+ message: message,
114
+ cancellable: false,
115
+ percentage: percentage,
116
+ })
117
+ end
118
+
119
+ def send_work_done_progress_end(token, message)
120
+ return unless supports_work_done_progress?
121
+ send_progress(token, {
122
+ kind: 'end',
123
+ message: message,
124
+ })
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ # How you'd use this class:
6
+ #
7
+ # In thread #1:
8
+ # def foo
9
+ # chan = Channel.create
10
+ # send_request(chan.id, ...)
11
+ # result = chan.pop
12
+ # do_stuff_with_result(result)
13
+ # ensure
14
+ # chan.close
15
+ # end
16
+ #
17
+ # In thread #2:
18
+ # Channel.by_id(id) << result
19
+ class Channel
20
+ MUTEX = Mutex.new
21
+ CHANNELS = {}
22
+
23
+ class << self
24
+ def create
25
+ id = new_id
26
+ CHANNELS[id] = new(id)
27
+ CHANNELS[id]
28
+ end
29
+
30
+ def by_id(id)
31
+ CHANNELS[id]
32
+ end
33
+
34
+ def close(id)
35
+ CHANNELS.delete(id)
36
+ end
37
+
38
+ private
39
+
40
+ def new_id
41
+ MUTEX.synchronize do
42
+ @id ||= 0
43
+ @id += 1
44
+ end
45
+ end
46
+ end
47
+
48
+ attr_reader :id
49
+
50
+ def initialize(id)
51
+ @id = id
52
+ @response = SizedQueue.new(1)
53
+ end
54
+
55
+ def pop
56
+ @response.pop
57
+ end
58
+
59
+ def <<(value)
60
+ @response << value
61
+ end
62
+
63
+ def close
64
+ @response.close
65
+ Channel.close(id)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -6,7 +6,9 @@ module ThemeCheck
6
6
  def completions(content, cursor)
7
7
  return [] unless can_complete?(content, cursor)
8
8
  partial = first_word(content) || ''
9
- ShopifyLiquid::Tag.labels
9
+ labels = ShopifyLiquid::Tag.labels
10
+ labels += ShopifyLiquid::Tag.end_labels
11
+ labels
10
12
  .select { |w| w.start_with?(partial) }
11
13
  .map { |tag| tag_to_completion(tag) }
12
14
  end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class DiagnosticsEngine
6
+ include URIHelper
7
+
8
+ def initialize(bridge)
9
+ @diagnostics_lock = Mutex.new
10
+ @diagnostics_tracker = DiagnosticsTracker.new
11
+ @bridge = bridge
12
+ @token = 0
13
+ end
14
+
15
+ def first_run?
16
+ @diagnostics_tracker.first_run?
17
+ end
18
+
19
+ def analyze_and_send_offenses(absolute_path, config)
20
+ return unless @diagnostics_lock.try_lock
21
+ @token += 1
22
+ @bridge.send_create_work_done_progress_request(@token)
23
+ storage = ThemeCheck::FileSystemStorage.new(
24
+ config.root,
25
+ ignored_patterns: config.ignored_patterns
26
+ )
27
+ theme = ThemeCheck::Theme.new(storage)
28
+ analyzer = ThemeCheck::Analyzer.new(theme, config.enabled_checks)
29
+
30
+ if @diagnostics_tracker.first_run?
31
+ @bridge.send_work_done_progress_begin(@token, "Full theme check")
32
+ @bridge.log("Checking #{config.root}")
33
+ offenses = nil
34
+ time = Benchmark.measure do
35
+ offenses = analyzer.analyze_theme do |path, i, total|
36
+ @bridge.send_work_done_progress_report(@token, "#{i}/#{total} #{path}", (i.to_f / total * 100.0).to_i)
37
+ end
38
+ end
39
+ end_message = "Found #{offenses.size} offenses in #{format("%0.2f", time.real)}s"
40
+ @bridge.send_work_done_progress_end(@token, end_message)
41
+ send_diagnostics(offenses)
42
+ else
43
+ # Analyze selected files
44
+ relative_path = Pathname.new(storage.relative_path(absolute_path))
45
+ file = theme[relative_path]
46
+ # Skip if not a theme file
47
+ if file
48
+ @bridge.send_work_done_progress_begin(@token, "Partial theme check")
49
+ offenses = nil
50
+ time = Benchmark.measure do
51
+ offenses = analyzer.analyze_files([file]) do |path, i, total|
52
+ @bridge.send_work_done_progress_report(@token, "#{i}/#{total} #{path}", (i.to_f / total * 100.0).to_i)
53
+ end
54
+ end
55
+ end_message = "Found #{offenses.size} new offenses in #{format("%0.2f", time.real)}s"
56
+ @bridge.send_work_done_progress_end(@token, end_message)
57
+ @bridge.log(end_message)
58
+ send_diagnostics(offenses, [absolute_path])
59
+ end
60
+ end
61
+ @diagnostics_lock.unlock
62
+ end
63
+
64
+ private
65
+
66
+ def send_diagnostics(offenses, analyzed_files = nil)
67
+ @diagnostics_tracker.build_diagnostics(offenses, analyzed_files: analyzed_files) do |path, diagnostic_offenses|
68
+ send_diagnostic(path, diagnostic_offenses)
69
+ end
70
+ end
71
+
72
+ def send_diagnostic(path, offenses)
73
+ # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#notificationMessage
74
+ @bridge.send_notification('textDocument/publishDiagnostics', {
75
+ uri: file_uri(path),
76
+ diagnostics: offenses.map { |offense| offense_to_diagnostic(offense) },
77
+ })
78
+ end
79
+
80
+ def offense_to_diagnostic(offense)
81
+ diagnostic = {
82
+ code: offense.code_name,
83
+ message: offense.message,
84
+ range: range(offense),
85
+ severity: severity(offense),
86
+ source: "theme-check",
87
+ }
88
+ diagnostic["codeDescription"] = code_description(offense) unless offense.doc.nil?
89
+ diagnostic
90
+ end
91
+
92
+ def code_description(offense)
93
+ {
94
+ href: offense.doc,
95
+ }
96
+ end
97
+
98
+ def severity(offense)
99
+ case offense.severity
100
+ when :error
101
+ 1
102
+ when :suggestion
103
+ 2
104
+ when :style
105
+ 3
106
+ else
107
+ 4
108
+ end
109
+ end
110
+
111
+ def range(offense)
112
+ {
113
+ start: {
114
+ line: offense.start_line,
115
+ character: offense.start_column,
116
+ },
117
+ end: {
118
+ line: offense.end_line,
119
+ character: offense.end_column,
120
+ },
121
+ }
122
+ end
123
+ end
124
+ end
125
+ end
@@ -26,28 +26,22 @@ module ThemeCheck
26
26
  },
27
27
  }
28
28
 
29
- def initialize(server)
30
- @server = server
31
- @diagnostics_tracker = DiagnosticsTracker.new
32
- @diagnostics_lock = Mutex.new
33
- @supports_progress = false
34
- end
35
-
36
- def supports_progress_notifications?
37
- @supports_progress
29
+ def initialize(bridge)
30
+ @bridge = bridge
38
31
  end
39
32
 
40
33
  def on_initialize(id, params)
41
34
  @root_path = root_path_from_params(params)
42
- @supports_progress = params.dig('capabilities', 'window', 'workDoneProgress')
43
35
 
44
36
  # Tell the client we don't support anything if there's no rootPath
45
- return send_response(id, { capabilities: {} }) if @root_path.nil?
37
+ return @bridge.send_response(id, { capabilities: {} }) if @root_path.nil?
38
+
39
+ @bridge.supports_work_done_progress = params.dig('capabilities', 'window', 'workDoneProgress') || false
46
40
  @storage = in_memory_storage(@root_path)
47
41
  @completion_engine = CompletionEngine.new(@storage)
48
42
  @document_link_engine = DocumentLinkEngine.new(@storage)
49
- # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#responseMessage
50
- send_response(id, {
43
+ @diagnostics_engine = DiagnosticsEngine.new(@bridge)
44
+ @bridge.send_response(id, {
51
45
  capabilities: CAPABILITIES,
52
46
  serverInfo: SERVER_INFO,
53
47
  })
@@ -71,7 +65,7 @@ module ThemeCheck
71
65
  def on_text_document_did_open(_id, params)
72
66
  relative_path = relative_path_from_text_document_uri(params)
73
67
  @storage.write(relative_path, text_document_text(params))
74
- analyze_and_send_offenses(text_document_uri(params)) if @diagnostics_tracker.first_run?
68
+ analyze_and_send_offenses(text_document_uri(params)) if @diagnostics_engine.first_run?
75
69
  end
76
70
 
77
71
  def on_text_document_did_save(_id, params)
@@ -80,14 +74,14 @@ module ThemeCheck
80
74
 
81
75
  def on_text_document_document_link(id, params)
82
76
  relative_path = relative_path_from_text_document_uri(params)
83
- send_response(id, document_links(relative_path))
77
+ @bridge.send_response(id, document_links(relative_path))
84
78
  end
85
79
 
86
80
  def on_text_document_completion(id, params)
87
81
  relative_path = relative_path_from_text_document_uri(params)
88
82
  line = params.dig('position', 'line')
89
83
  col = params.dig('position', 'character')
90
- send_response(id, completions(relative_path, line, col))
84
+ @bridge.send_response(id, completions(relative_path, line, col))
91
85
  end
92
86
 
93
87
  private
@@ -141,50 +135,10 @@ module ThemeCheck
141
135
  end
142
136
 
143
137
  def analyze_and_send_offenses(absolute_path)
144
- return unless @diagnostics_lock.try_lock
145
- token = send_create_work_done_progress_request
146
- config = config_for_path(absolute_path)
147
- storage = ThemeCheck::FileSystemStorage.new(
148
- config.root,
149
- ignored_patterns: config.ignored_patterns
138
+ @diagnostics_engine.analyze_and_send_offenses(
139
+ absolute_path,
140
+ config_for_path(absolute_path)
150
141
  )
151
- theme = ThemeCheck::Theme.new(storage)
152
- analyzer = ThemeCheck::Analyzer.new(theme, config.enabled_checks)
153
-
154
- if @diagnostics_tracker.first_run?
155
- send_work_done_progress_begin(token, "Full theme check")
156
- log("Checking #{config.root}")
157
- offenses = nil
158
- time = Benchmark.measure do
159
- offenses = analyzer.analyze_theme do |path, i, total|
160
- send_work_done_progress_report(token, "#{i}/#{total} #{path}", (i.to_f / total * 100.0).to_i)
161
- end
162
- end
163
- end_message = "Found #{offenses.size} offenses in #{format("%0.2f", time.real)}s"
164
- log(end_message)
165
- send_work_done_progress_end(token, end_message)
166
- send_diagnostics(offenses)
167
- else
168
- # Analyze selected files
169
- relative_path = Pathname.new(@storage.relative_path(absolute_path))
170
- file = theme[relative_path]
171
- # Skip if not a theme file
172
- if file
173
- log("Checking #{relative_path}")
174
- send_work_done_progress_begin(token, "Partial theme check")
175
- offenses = nil
176
- time = Benchmark.measure do
177
- offenses = analyzer.analyze_files([file]) do |path, i, total|
178
- send_work_done_progress_report(token, "#{i}/#{total} #{path}", (i.to_f / total * 100.0).to_i)
179
- end
180
- end
181
- end_message = "Found #{offenses.size} new offenses in #{format("%0.2f", time.real)}s"
182
- send_work_done_progress_end(token, end_message)
183
- log(end_message)
184
- send_diagnostics(offenses, [absolute_path])
185
- end
186
- end
187
- @diagnostics_lock.unlock
188
142
  end
189
143
 
190
144
  def completions(relative_path, line, col)
@@ -195,141 +149,8 @@ module ThemeCheck
195
149
  @document_link_engine.document_links(relative_path)
196
150
  end
197
151
 
198
- def send_diagnostics(offenses, analyzed_files = nil)
199
- @diagnostics_tracker.build_diagnostics(offenses, analyzed_files: analyzed_files) do |path, diagnostic_offenses|
200
- send_diagnostic(path, diagnostic_offenses)
201
- end
202
- end
203
-
204
- def send_diagnostic(path, offenses)
205
- # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#notificationMessage
206
- send_notification('textDocument/publishDiagnostics', {
207
- uri: file_uri(path),
208
- diagnostics: offenses.map { |offense| offense_to_diagnostic(offense) },
209
- })
210
- end
211
-
212
- def offense_to_diagnostic(offense)
213
- diagnostic = {
214
- code: offense.code_name,
215
- message: offense.message,
216
- range: range(offense),
217
- severity: severity(offense),
218
- source: "theme-check",
219
- }
220
- diagnostic["codeDescription"] = code_description(offense) unless offense.doc.nil?
221
- diagnostic
222
- end
223
-
224
- def code_description(offense)
225
- {
226
- href: offense.doc,
227
- }
228
- end
229
-
230
- def severity(offense)
231
- case offense.severity
232
- when :error
233
- 1
234
- when :suggestion
235
- 2
236
- when :style
237
- 3
238
- else
239
- 4
240
- end
241
- end
242
-
243
- def range(offense)
244
- {
245
- start: {
246
- line: offense.start_line,
247
- character: offense.start_column,
248
- },
249
- end: {
250
- line: offense.end_line,
251
- character: offense.end_column,
252
- },
253
- }
254
- end
255
-
256
- def send_create_work_done_progress_request
257
- return unless supports_progress_notifications?
258
- token = nil
259
- @server.request do |id|
260
- token = id # we'll reuse the RQID as token
261
- send_message({
262
- id: id,
263
- method: "window/workDoneProgress/create",
264
- params: {
265
- token: id,
266
- },
267
- })
268
- end
269
- token
270
- end
271
-
272
- def send_work_done_progress_begin(token, title)
273
- return unless supports_progress_notifications?
274
- send_progress(token, {
275
- kind: 'begin',
276
- title: title,
277
- cancellable: false,
278
- percentage: 0,
279
- })
280
- end
281
-
282
- def send_work_done_progress_report(token, message, percentage)
283
- return unless supports_progress_notifications?
284
- send_progress(token, {
285
- kind: 'report',
286
- message: message,
287
- cancellable: false,
288
- percentage: percentage,
289
- })
290
- end
291
-
292
- def send_work_done_progress_end(token, message)
293
- return unless supports_progress_notifications?
294
- send_progress(token, {
295
- kind: 'end',
296
- message: message,
297
- })
298
- end
299
-
300
- def send_progress(token, value)
301
- send_notification("$/progress", token: token, value: value)
302
- end
303
-
304
- def send_message(message)
305
- message[:jsonrpc] = '2.0'
306
- @server.send_message(message)
307
- end
308
-
309
- def send_response(id, result = nil, error = nil)
310
- message = { id: id }
311
- message[:result] = result if result
312
- message[:error] = error if error
313
- send_message(message)
314
- end
315
-
316
- def send_request(method, params = nil)
317
- @server.request do |id|
318
- message = { id: id }
319
- message[:method] = method
320
- message[:params] = params if params
321
- send_message(message)
322
- end
323
- end
324
-
325
- def send_notification(method, params)
326
- message = { method: method }
327
- message[:params] = params
328
- send_message(message)
329
- end
330
-
331
152
  def log(message)
332
- @server.log(message)
153
+ @bridge.log(message)
333
154
  end
334
155
 
335
156
  def close!
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class IOMessenger < Messenger
6
+ def initialize(
7
+ in_stream: STDIN,
8
+ out_stream: STDOUT,
9
+ err_stream: STDERR
10
+ )
11
+ validate!([in_stream, out_stream, err_stream])
12
+
13
+ @in = in_stream
14
+ @out = out_stream
15
+ @err = err_stream
16
+
17
+ # Because programming is fun,
18
+ #
19
+ # Ruby on Windows turns \n into \r\n. Which means that \r\n
20
+ # gets turned into \r\r\n. Which means that the protocol
21
+ # breaks on windows unless we turn STDOUT into binary mode.
22
+ #
23
+ # Hours wasted: 9.
24
+ @out.binmode
25
+
26
+ @out.sync = true # do not buffer
27
+ @err.sync = true # do not buffer
28
+ end
29
+
30
+ def read_message
31
+ length = initial_line.match(/Content-Length: (\d+)/)[1].to_i
32
+ content = ''
33
+ length_to_read = 2 + length # 2 is the empty line length (\r\n)
34
+ while content.length < length_to_read
35
+ chunk = @in.read(length_to_read - content.length)
36
+ raise DoneStreaming if chunk.nil?
37
+ content += chunk
38
+ end
39
+ content.lstrip!
40
+ end
41
+
42
+ def send_message(message_body)
43
+ @out.write("Content-Length: #{message_body.bytesize}\r\n")
44
+ @out.write("\r\n")
45
+ @out.write(message_body)
46
+ @out.flush
47
+ end
48
+
49
+ def log(message)
50
+ @err.puts(message)
51
+ @err.flush
52
+ end
53
+
54
+ def close_input
55
+ @in.close unless @in.closed?
56
+ end
57
+
58
+ def close_output
59
+ @err.close
60
+ @out.close
61
+ end
62
+
63
+ private
64
+
65
+ def initial_line
66
+ # Scanning for lines that fit the protocol.
67
+ while true
68
+ initial_line = @in.gets
69
+ # gets returning nil means the stream was closed.
70
+ raise DoneStreaming if initial_line.nil?
71
+
72
+ if initial_line.match(/Content-Length: (\d+)/)
73
+ break
74
+ end
75
+ end
76
+ initial_line
77
+ end
78
+
79
+ def supported_io_classes
80
+ [IO, StringIO]
81
+ end
82
+
83
+ def validate!(streams = [])
84
+ streams.each do |stream|
85
+ unless supported_io_classes.find { |klass| stream.is_a?(klass) }
86
+ raise IncompatibleStream, incompatible_stream_message
87
+ end
88
+ end
89
+ end
90
+
91
+ def incompatible_stream_message
92
+ 'if provided, in_stream, out_stream, and err_stream must be a kind of '\
93
+ "one of the following: #{supported_io_classes.join(', ')}"
94
+ end
95
+ end
96
+ end
97
+ end
@@ -3,54 +3,24 @@
3
3
  module ThemeCheck
4
4
  module LanguageServer
5
5
  class Messenger
6
- def initialize
7
- @responses = {}
8
- @mutex = Mutex.new
9
- @id = 0
6
+ def send_message
7
+ raise NotImplementedError
10
8
  end
11
9
 
12
- # Here's how you'd use this:
13
- #
14
- # def some_method_that_communicates_both_ways
15
- #
16
- # # this will block until the JSON rpc loop has an answer
17
- # token = @server.request do |id|
18
- # send_create_work_done_progress_request(id, ...)
19
- # end
20
- #
21
- # send_create_work_done_begin_notification(token, "...")
22
- #
23
- # do_stuff do |file, i, total|
24
- # send_create_work_done_progress_notification(token, "...")
25
- # end
26
- #
27
- # send_create_work_done_end_notification(token, "...")
28
- #
29
- # end
30
- def request(&block)
31
- id = @mutex.synchronize { @id += 1 }
32
- @responses[id] = SizedQueue.new(1)
33
-
34
- # Execute the block in the parent thread with an ID
35
- # So that we're able to relinquish control in the right
36
- # place when we have a response.
37
- block.call(id)
38
-
39
- # this call is blocking until we get a response from somewhere
40
- result = @responses[id].pop
10
+ def read_message
11
+ raise NotImplementedError
12
+ end
41
13
 
42
- # cleanup when done
43
- @responses.delete(id)
14
+ def log
15
+ raise NotImplementedError
16
+ end
44
17
 
45
- # return the response
46
- result
18
+ def close_input
19
+ raise NotImplementedError
47
20
  end
48
21
 
49
- # In the JSONRPC loop, when we find the response to the
50
- # request, we unblock the thread that made the request with the
51
- # response.
52
- def respond(id, value)
53
- @responses[id] << value
22
+ def close_output
23
+ raise NotImplementedError
54
24
  end
55
25
  end
56
26
  end
@@ -13,30 +13,18 @@ module ThemeCheck
13
13
  attr_reader :should_raise_errors
14
14
 
15
15
  def initialize(
16
- in_stream: STDIN,
17
- out_stream: STDOUT,
18
- err_stream: STDERR,
16
+ messenger:,
19
17
  should_raise_errors: false,
20
18
  number_of_threads: 2
21
19
  )
22
- validate!([in_stream, out_stream, err_stream])
20
+ # This is what does the IO
21
+ @messenger = messenger
23
22
 
24
- @handler = Handler.new(self)
25
- @in = in_stream
26
- @out = out_stream
27
- @err = err_stream
23
+ # This is what you use to communicate with the language client
24
+ @bridge = Bridge.new(@messenger)
28
25
 
29
- # Because programming is fun,
30
- #
31
- # Ruby on Windows turns \n into \r\n. Which means that \r\n
32
- # gets turned into \r\r\n. Which means that the protocol
33
- # breaks on windows unless we turn STDOUT into binary mode.
34
- #
35
- # Hours wasted: 9.
36
- @out.binmode
37
-
38
- @out.sync = true # do not buffer
39
- @err.sync = true # do not buffer
26
+ # The handler handles messages from the language client
27
+ @handler = Handler.new(@bridge)
40
28
 
41
29
  # The queue holds the JSON RPC messages
42
30
  @queue = Queue.new
@@ -48,10 +36,6 @@ module ThemeCheck
48
36
  @number_of_threads = number_of_threads
49
37
  @handlers = []
50
38
 
51
- # The messenger permits requests to be made from the handler
52
- # to the language client and for those messages to be resolved in place.
53
- @messenger = Messenger.new
54
-
55
39
  # The error queue holds blocks the main thread. When filled, we exit the program.
56
40
  @error = SizedQueue.new(1)
57
41
 
@@ -61,19 +45,23 @@ module ThemeCheck
61
45
  def listen
62
46
  start_handler_threads
63
47
  start_json_rpc_thread
64
- status_code_from_error(@error.pop)
48
+ status_code = status_code_from_error(@error.pop)
49
+ cleanup(status_code)
65
50
  rescue SignalException
66
51
  0
67
- ensure
68
- cleanup
69
52
  end
70
53
 
71
54
  def start_json_rpc_thread
72
55
  @json_rpc_thread = Thread.new do
73
56
  loop do
74
- message = read_json_rpc_message
57
+ message = @bridge.read_message
75
58
  if message['method'] == 'initialize'
76
59
  handle_message(message)
60
+ elsif message.key?('result')
61
+ # Responses are handled on the main thread to prevent
62
+ # a potential deadlock caused by all handlers waiting
63
+ # for a responses.
64
+ handle_response(message)
77
65
  else
78
66
  @queue << message
79
67
  end
@@ -106,103 +94,37 @@ module ThemeCheck
106
94
 
107
95
  rescue Exception => e # rubocop:disable Lint/RescueException
108
96
  raise e if should_raise_errors
109
- log(e)
110
- log(e.backtrace)
97
+ @bridge.log(e)
98
+ @bridge.log(e.backtrace)
111
99
  2
112
100
  end
113
101
 
114
- def request(&block)
115
- @messenger.request(&block)
116
- end
117
-
118
- def send_message(message)
119
- message_body = JSON.dump(message)
120
- log(JSON.pretty_generate(message)) if $DEBUG
121
-
122
- @out.write("Content-Length: #{message_body.bytesize}\r\n")
123
- @out.write("\r\n")
124
- @out.write(message_body)
125
- @out.flush
126
- end
127
-
128
- def log(message)
129
- @err.puts(message)
130
- @err.flush
131
- end
132
-
133
102
  private
134
103
 
135
- def supported_io_classes
136
- [IO, StringIO]
137
- end
138
-
139
- def validate!(streams = [])
140
- streams.each do |stream|
141
- unless supported_io_classes.find { |klass| stream.is_a?(klass) }
142
- raise IncompatibleStream, incompatible_stream_message
143
- end
144
- end
145
- end
146
-
147
- def incompatible_stream_message
148
- 'if provided, in_stream, out_stream, and err_stream must be a kind of '\
149
- "one of the following: #{supported_io_classes.join(', ')}"
150
- end
151
-
152
- def read_json_rpc_message
153
- message_body = read_new_content
154
- message_json = JSON.parse(message_body)
155
- log(JSON.pretty_generate(message_json)) if $DEBUG
156
- message_json
157
- end
158
-
159
104
  def handle_message(message)
160
105
  id = message['id']
161
106
  method_name = message['method']
162
107
  method_name &&= "on_#{to_snake_case(method_name)}"
163
108
  params = message['params']
164
- result = message['result']
165
109
 
166
- if message.key?('result')
167
- @messenger.respond(id, result)
168
- elsif @handler.respond_to?(method_name)
110
+ if @handler.respond_to?(method_name)
169
111
  @handler.send(method_name, id, params)
170
112
  end
171
113
  end
172
114
 
173
- def to_snake_case(method_name)
174
- StringHelpers.underscore(method_name.gsub(/[^\w]/, '_'))
175
- end
176
-
177
- def initial_line
178
- # Scanning for lines that fit the protocol.
179
- while true
180
- initial_line = @in.gets
181
- # gets returning nil means the stream was closed.
182
- raise DoneStreaming if initial_line.nil?
183
-
184
- if initial_line.match(/Content-Length: (\d+)/)
185
- break
186
- end
187
- end
188
- initial_line
115
+ def handle_response(message)
116
+ id = message['id']
117
+ result = message['result']
118
+ @bridge.receive_response(id, result)
189
119
  end
190
120
 
191
- def read_new_content
192
- length = initial_line.match(/Content-Length: (\d+)/)[1].to_i
193
- content = ''
194
- while content.length < length + 2
195
- # Why + 2? Because \r\n
196
- content += @in.read(length + 2)
197
- raise DoneStreaming if @in.closed?
198
- end
199
-
200
- content
121
+ def to_snake_case(method_name)
122
+ StringHelpers.underscore(method_name.gsub(/[^\w]/, '_'))
201
123
  end
202
124
 
203
- def cleanup
125
+ def cleanup(status_code)
204
126
  # Stop listenting to RPC calls
205
- @in.close unless @in.closed?
127
+ @messenger.close_input
206
128
  # Wait for rpc loop to close
207
129
  @json_rpc_thread&.join if @json_rpc_thread&.alive?
208
130
  # Close the queue
@@ -210,9 +132,13 @@ module ThemeCheck
210
132
  # Give 10 seconds for the handlers to wrap up what they were
211
133
  # doing/emptying the queue. 👀 unit tests.
212
134
  @handlers.each { |thread| thread.join(10) if thread.alive? }
135
+
136
+ # Hijack the status_code if an error occurred while cleaning up.
137
+ # 👀 unit tests.
138
+ return status_code_from_error(@error.pop) unless @error.empty?
139
+ status_code
213
140
  ensure
214
- @err.close
215
- @out.close
141
+ @messenger.close_output
216
142
  end
217
143
  end
218
144
  end
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
  require_relative "language_server/protocol"
3
- require_relative "language_server/messenger"
4
3
  require_relative "language_server/constants"
4
+ require_relative "language_server/channel"
5
+ require_relative "language_server/messenger"
6
+ require_relative "language_server/io_messenger"
7
+ require_relative "language_server/bridge"
5
8
  require_relative "language_server/uri_helper"
6
9
  require_relative "language_server/handler"
7
10
  require_relative "language_server/server"
@@ -13,6 +16,7 @@ require_relative "language_server/completion_engine"
13
16
  require_relative "language_server/document_link_provider"
14
17
  require_relative "language_server/document_link_engine"
15
18
  require_relative "language_server/diagnostics_tracker"
19
+ require_relative "language_server/diagnostics_engine"
16
20
 
17
21
  Dir[__dir__ + "/language_server/completion_providers/*.rb"].each do |file|
18
22
  require file
@@ -25,7 +29,7 @@ end
25
29
  module ThemeCheck
26
30
  module LanguageServer
27
31
  def self.start
28
- Server.new.listen
32
+ Server.new(messenger: IOMessenger.new).listen
29
33
  end
30
34
  end
31
35
  end
@@ -46,10 +46,12 @@ module ThemeCheck
46
46
  other = {} unless other.is_a?(Hash)
47
47
  return if pluralization?(default) && pluralization?(other)
48
48
 
49
- @extra_keys += (other.keys - default.keys).map { |key| path + [key] }
49
+ shopify_translations = system_translations(path)
50
+
51
+ @extra_keys += (other.keys - default.keys - shopify_translations.keys).map { |key| path + [key] }
50
52
 
51
53
  default.each do |key, default_value|
52
- translated_value = other[key]
54
+ translated_value = other[key] || shopify_translations[key]
53
55
  new_path = path + [key]
54
56
 
55
57
  if translated_value.nil?
@@ -65,5 +67,10 @@ module ThemeCheck
65
67
  PLURALIZATION_KEYS.include?(key) && !value.is_a?(Hash)
66
68
  end
67
69
  end
70
+
71
+ def system_translations(path)
72
+ return ShopifyLiquid::SystemTranslations.translations_hash if path.empty?
73
+ ShopifyLiquid::SystemTranslations.translations_hash.dig(*path) || {}
74
+ end
68
75
  end
69
76
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+ module ThemeCheck
3
+ module ShopifyLiquid
4
+ module SystemTranslations
5
+ extend self
6
+
7
+ def translations
8
+ @translations ||= YAML.load(File.read("#{__dir__}/../../../data/shopify_translation_keys.yml")).to_set
9
+ end
10
+
11
+ def translations_hash
12
+ @translations_hash ||= translations.reduce({}) do |acc, k|
13
+ dig_set(acc, k.split('.'), "")
14
+ end
15
+ end
16
+
17
+ def include?(key)
18
+ translations.include?(key)
19
+ end
20
+
21
+ private
22
+
23
+ def dig_set(obj, keys, value)
24
+ key = keys.first
25
+ if keys.length == 1
26
+ obj[key] = value
27
+ else
28
+ obj[key] = {} unless obj[key]
29
+ dig_set(obj[key], keys.slice(1..-1), value)
30
+ end
31
+ obj
32
+ end
33
+ end
34
+ end
35
+ end
@@ -7,10 +7,17 @@ module ThemeCheck
7
7
  extend self
8
8
 
9
9
  def labels
10
- @tags ||= YAML.load(File.read("#{__dir__}/../../../data/shopify_liquid/tags.yml"))
10
+ @labels ||= tags_file_contents
11
+ .map { |x| to_label(x) }
11
12
  .to_set
12
13
  end
13
14
 
15
+ def end_labels
16
+ @end_labels ||= tags_file_contents
17
+ .select { |x| x.is_a?(Hash) }
18
+ .map { |x| x.values[0] }
19
+ end
20
+
14
21
  def tag_regex(tag)
15
22
  return unless labels.include?(tag)
16
23
  @tag_regexes ||= {}
@@ -22,6 +29,17 @@ module ThemeCheck
22
29
  @tag_liquid_regexes ||= {}
23
30
  @tag_liquid_regexes[tag] ||= /^\s*#{tag}/m
24
31
  end
32
+
33
+ private
34
+
35
+ def to_label(label)
36
+ return label if label.is_a?(String)
37
+ label.keys[0]
38
+ end
39
+
40
+ def tags_file_contents
41
+ @tags_file_contents ||= YAML.load(File.read("#{__dir__}/../../../data/shopify_liquid/tags.yml"))
42
+ end
25
43
  end
26
44
  end
27
45
  end
@@ -3,3 +3,4 @@ require_relative 'shopify_liquid/deprecated_filter'
3
3
  require_relative 'shopify_liquid/filter'
4
4
  require_relative 'shopify_liquid/object'
5
5
  require_relative 'shopify_liquid/tag'
6
+ require_relative 'shopify_liquid/system_translations'
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module ThemeCheck
3
- VERSION = "1.7.0"
3
+ VERSION = "1.7.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.7.0
4
+ version: 1.7.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-09-20 00:00:00.000000000 Z
11
+ date: 2021-09-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: liquid
@@ -201,6 +201,8 @@ files:
201
201
  - lib/theme_check/json_helpers.rb
202
202
  - lib/theme_check/json_printer.rb
203
203
  - lib/theme_check/language_server.rb
204
+ - lib/theme_check/language_server/bridge.rb
205
+ - lib/theme_check/language_server/channel.rb
204
206
  - lib/theme_check/language_server/completion_engine.rb
205
207
  - lib/theme_check/language_server/completion_helper.rb
206
208
  - lib/theme_check/language_server/completion_provider.rb
@@ -209,6 +211,7 @@ files:
209
211
  - lib/theme_check/language_server/completion_providers/render_snippet_completion_provider.rb
210
212
  - lib/theme_check/language_server/completion_providers/tag_completion_provider.rb
211
213
  - lib/theme_check/language_server/constants.rb
214
+ - lib/theme_check/language_server/diagnostics_engine.rb
212
215
  - lib/theme_check/language_server/diagnostics_tracker.rb
213
216
  - lib/theme_check/language_server/document_link_engine.rb
214
217
  - lib/theme_check/language_server/document_link_provider.rb
@@ -217,6 +220,7 @@ files:
217
220
  - lib/theme_check/language_server/document_link_providers/render_document_link_provider.rb
218
221
  - lib/theme_check/language_server/document_link_providers/section_document_link_provider.rb
219
222
  - lib/theme_check/language_server/handler.rb
223
+ - lib/theme_check/language_server/io_messenger.rb
220
224
  - lib/theme_check/language_server/messenger.rb
221
225
  - lib/theme_check/language_server/protocol.rb
222
226
  - lib/theme_check/language_server/server.rb
@@ -242,6 +246,7 @@ files:
242
246
  - lib/theme_check/shopify_liquid/deprecated_filter.rb
243
247
  - lib/theme_check/shopify_liquid/filter.rb
244
248
  - lib/theme_check/shopify_liquid/object.rb
249
+ - lib/theme_check/shopify_liquid/system_translations.rb
245
250
  - lib/theme_check/shopify_liquid/tag.rb
246
251
  - lib/theme_check/storage.rb
247
252
  - lib/theme_check/string_helpers.rb