theme-check 1.7.0 → 1.7.1

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