theme-check 1.6.2 → 1.7.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 +4 -4
- data/CHANGELOG.md +9 -0
- data/lib/theme_check/analyzer.rb +29 -5
- data/lib/theme_check/language_server/handler.rb +88 -6
- data/lib/theme_check/language_server/messenger.rb +57 -0
- data/lib/theme_check/language_server/server.rb +105 -40
- data/lib/theme_check/language_server.rb +1 -0
- data/lib/theme_check/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 943f8cb722f6c19c1c41e494622922e608a99b50e4ad67f2288a2ed99f406ff3
|
4
|
+
data.tar.gz: a8fd83f054895fd366bba2512ff6431d8c470bb3e013ff6c6bf9f6be5366bf38
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fb37aed715f2b9f2dd0a8c17f2abf69110d583280d88508ef3f6ee439989c63fe9819025d870cfc1ed64a8deaac19586ac48ab261495e7a371575d9971754980
|
7
|
+
data.tar.gz: 64727b7c9684fa094ce498c09a2a3f1dc06ce10fa99aed066af2aa327bd4de70fcc728f76ad127fd08d7b1b6e28b42b06acf96d83b26193814e7eed1b19a2008
|
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,13 @@
|
|
1
1
|
|
2
|
+
v1.7.0 / 2021-09-20
|
3
|
+
===================
|
4
|
+
|
5
|
+
### Features
|
6
|
+
|
7
|
+
* Handle LSP messages concurrently in the Language Server ([#459](https://github.com/shopify/theme-check/issues/459))
|
8
|
+
* Adds progress reporting while checking (:eyes: VS Code status bar)
|
9
|
+
* Makes completions work while checking (more noticeable on Windows since ruby is 3x slower on Windows)
|
10
|
+
|
2
11
|
v1.6.2 / 2021-09-16
|
3
12
|
===================
|
4
13
|
|
data/lib/theme_check/analyzer.rb
CHANGED
@@ -29,19 +29,36 @@ module ThemeCheck
|
|
29
29
|
@html_checks.flat_map(&:offenses)
|
30
30
|
end
|
31
31
|
|
32
|
+
def json_file_count
|
33
|
+
@json_file_count ||= @theme.json.size
|
34
|
+
end
|
35
|
+
|
36
|
+
def liquid_file_count
|
37
|
+
@liquid_file_count ||= @theme.liquid.size
|
38
|
+
end
|
39
|
+
|
40
|
+
def total_file_count
|
41
|
+
json_file_count + liquid_file_count
|
42
|
+
end
|
43
|
+
|
32
44
|
def analyze_theme
|
33
45
|
reset
|
34
46
|
|
35
47
|
liquid_visitor = LiquidVisitor.new(@liquid_checks, @disabled_checks)
|
36
48
|
html_visitor = HtmlVisitor.new(@html_checks)
|
49
|
+
|
37
50
|
ThemeCheck.with_liquid_c_disabled do
|
38
|
-
@theme.liquid.
|
51
|
+
@theme.liquid.each_with_index do |liquid_file, i|
|
52
|
+
yield(liquid_file.relative_path.to_s, i, total_file_count) if block_given?
|
39
53
|
liquid_visitor.visit_liquid_file(liquid_file)
|
40
54
|
html_visitor.visit_liquid_file(liquid_file)
|
41
55
|
end
|
42
56
|
end
|
43
57
|
|
44
|
-
@theme.json.
|
58
|
+
@theme.json.each_with_index do |json_file, i|
|
59
|
+
yield(json_file.relative_path.to_s, liquid_file_count + i, total_file_count) if block_given?
|
60
|
+
@json_checks.call(:on_file, json_file)
|
61
|
+
end
|
45
62
|
|
46
63
|
finish
|
47
64
|
end
|
@@ -53,16 +70,23 @@ module ThemeCheck
|
|
53
70
|
# Call all checks that run on the whole theme
|
54
71
|
liquid_visitor = LiquidVisitor.new(@liquid_checks.whole_theme, @disabled_checks)
|
55
72
|
html_visitor = HtmlVisitor.new(@html_checks.whole_theme)
|
56
|
-
|
73
|
+
total = total_file_count + files.size
|
74
|
+
@theme.liquid.each_with_index do |liquid_file, i|
|
75
|
+
yield(liquid_file.relative_path.to_s, i, total) if block_given?
|
57
76
|
liquid_visitor.visit_liquid_file(liquid_file)
|
58
77
|
html_visitor.visit_liquid_file(liquid_file)
|
59
78
|
end
|
60
|
-
|
79
|
+
|
80
|
+
@theme.json.each_with_index do |json_file, i|
|
81
|
+
yield(json_file.relative_path.to_s, liquid_file_count + i, total) if block_given?
|
82
|
+
@json_checks.whole_theme.call(:on_file, json_file)
|
83
|
+
end
|
61
84
|
|
62
85
|
# Call checks that run on a single files, only on specified file
|
63
86
|
liquid_visitor = LiquidVisitor.new(@liquid_checks.single_file, @disabled_checks)
|
64
87
|
html_visitor = HtmlVisitor.new(@html_checks.single_file)
|
65
|
-
files.
|
88
|
+
files.each_with_index do |theme_file, i|
|
89
|
+
yield(theme_file.relative_path.to_s, total_file_count + i, total) if block_given?
|
66
90
|
if theme_file.liquid?
|
67
91
|
liquid_visitor.visit_liquid_file(theme_file)
|
68
92
|
html_visitor.visit_liquid_file(theme_file)
|
@@ -7,6 +7,11 @@ module ThemeCheck
|
|
7
7
|
class Handler
|
8
8
|
include URIHelper
|
9
9
|
|
10
|
+
SERVER_INFO = {
|
11
|
+
name: $PROGRAM_NAME,
|
12
|
+
version: ThemeCheck::VERSION,
|
13
|
+
}
|
14
|
+
|
10
15
|
CAPABILITIES = {
|
11
16
|
completionProvider: {
|
12
17
|
triggerCharacters: ['.', '{{ ', '{% '],
|
@@ -24,10 +29,17 @@ module ThemeCheck
|
|
24
29
|
def initialize(server)
|
25
30
|
@server = server
|
26
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
|
27
38
|
end
|
28
39
|
|
29
40
|
def on_initialize(id, params)
|
30
41
|
@root_path = root_path_from_params(params)
|
42
|
+
@supports_progress = params.dig('capabilities', 'window', 'workDoneProgress')
|
31
43
|
|
32
44
|
# Tell the client we don't support anything if there's no rootPath
|
33
45
|
return send_response(id, { capabilities: {} }) if @root_path.nil?
|
@@ -37,6 +49,7 @@ module ThemeCheck
|
|
37
49
|
# https://microsoft.github.io/language-server-protocol/specifications/specification-current/#responseMessage
|
38
50
|
send_response(id, {
|
39
51
|
capabilities: CAPABILITIES,
|
52
|
+
serverInfo: SERVER_INFO,
|
40
53
|
})
|
41
54
|
end
|
42
55
|
|
@@ -128,6 +141,8 @@ module ThemeCheck
|
|
128
141
|
end
|
129
142
|
|
130
143
|
def analyze_and_send_offenses(absolute_path)
|
144
|
+
return unless @diagnostics_lock.try_lock
|
145
|
+
token = send_create_work_done_progress_request
|
131
146
|
config = config_for_path(absolute_path)
|
132
147
|
storage = ThemeCheck::FileSystemStorage.new(
|
133
148
|
config.root,
|
@@ -137,13 +152,17 @@ module ThemeCheck
|
|
137
152
|
analyzer = ThemeCheck::Analyzer.new(theme, config.enabled_checks)
|
138
153
|
|
139
154
|
if @diagnostics_tracker.first_run?
|
140
|
-
|
155
|
+
send_work_done_progress_begin(token, "Full theme check")
|
141
156
|
log("Checking #{config.root}")
|
142
157
|
offenses = nil
|
143
158
|
time = Benchmark.measure do
|
144
|
-
offenses = analyzer.analyze_theme
|
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
|
145
162
|
end
|
146
|
-
|
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)
|
147
166
|
send_diagnostics(offenses)
|
148
167
|
else
|
149
168
|
# Analyze selected files
|
@@ -152,14 +171,20 @@ module ThemeCheck
|
|
152
171
|
# Skip if not a theme file
|
153
172
|
if file
|
154
173
|
log("Checking #{relative_path}")
|
174
|
+
send_work_done_progress_begin(token, "Partial theme check")
|
155
175
|
offenses = nil
|
156
176
|
time = Benchmark.measure do
|
157
|
-
offenses = analyzer.analyze_files([file])
|
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
|
158
180
|
end
|
159
|
-
|
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)
|
160
184
|
send_diagnostics(offenses, [absolute_path])
|
161
185
|
end
|
162
186
|
end
|
187
|
+
@diagnostics_lock.unlock
|
163
188
|
end
|
164
189
|
|
165
190
|
def completions(relative_path, line, col)
|
@@ -228,9 +253,57 @@ module ThemeCheck
|
|
228
253
|
}
|
229
254
|
end
|
230
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
|
+
|
231
304
|
def send_message(message)
|
232
305
|
message[:jsonrpc] = '2.0'
|
233
|
-
@server.
|
306
|
+
@server.send_message(message)
|
234
307
|
end
|
235
308
|
|
236
309
|
def send_response(id, result = nil, error = nil)
|
@@ -240,6 +313,15 @@ module ThemeCheck
|
|
240
313
|
send_message(message)
|
241
314
|
end
|
242
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
|
+
|
243
325
|
def send_notification(method, params)
|
244
326
|
message = { method: method }
|
245
327
|
message[:params] = params
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class Messenger
|
6
|
+
def initialize
|
7
|
+
@responses = {}
|
8
|
+
@mutex = Mutex.new
|
9
|
+
@id = 0
|
10
|
+
end
|
11
|
+
|
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
|
41
|
+
|
42
|
+
# cleanup when done
|
43
|
+
@responses.delete(id)
|
44
|
+
|
45
|
+
# return the response
|
46
|
+
result
|
47
|
+
end
|
48
|
+
|
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
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -16,7 +16,8 @@ module ThemeCheck
|
|
16
16
|
in_stream: STDIN,
|
17
17
|
out_stream: STDOUT,
|
18
18
|
err_stream: STDERR,
|
19
|
-
should_raise_errors: false
|
19
|
+
should_raise_errors: false,
|
20
|
+
number_of_threads: 2
|
20
21
|
)
|
21
22
|
validate!([in_stream, out_stream, err_stream])
|
22
23
|
|
@@ -37,33 +38,90 @@ module ThemeCheck
|
|
37
38
|
@out.sync = true # do not buffer
|
38
39
|
@err.sync = true # do not buffer
|
39
40
|
|
41
|
+
# The queue holds the JSON RPC messages
|
42
|
+
@queue = Queue.new
|
43
|
+
|
44
|
+
# The JSON RPC thread pushes messages onto the queue
|
45
|
+
@json_rpc_thread = nil
|
46
|
+
|
47
|
+
# The handler threads read messages from the queue
|
48
|
+
@number_of_threads = number_of_threads
|
49
|
+
@handlers = []
|
50
|
+
|
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
|
+
# The error queue holds blocks the main thread. When filled, we exit the program.
|
56
|
+
@error = SizedQueue.new(1)
|
57
|
+
|
40
58
|
@should_raise_errors = should_raise_errors
|
41
59
|
end
|
42
60
|
|
43
61
|
def listen
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
62
|
+
start_handler_threads
|
63
|
+
start_json_rpc_thread
|
64
|
+
status_code_from_error(@error.pop)
|
65
|
+
rescue SignalException
|
66
|
+
0
|
67
|
+
ensure
|
68
|
+
cleanup
|
69
|
+
end
|
70
|
+
|
71
|
+
def start_json_rpc_thread
|
72
|
+
@json_rpc_thread = Thread.new do
|
73
|
+
loop do
|
74
|
+
message = read_json_rpc_message
|
75
|
+
if message['method'] == 'initialize'
|
76
|
+
handle_message(message)
|
77
|
+
else
|
78
|
+
@queue << message
|
79
|
+
end
|
80
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
81
|
+
break @error << e
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def start_handler_threads
|
87
|
+
@number_of_threads.times do
|
88
|
+
@handlers << Thread.new do
|
89
|
+
loop do
|
90
|
+
message = @queue.pop
|
91
|
+
break if @queue.closed? && @queue.empty?
|
92
|
+
handle_message(message)
|
93
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
94
|
+
break @error << e
|
95
|
+
end
|
96
|
+
end
|
57
97
|
end
|
58
98
|
end
|
59
99
|
|
60
|
-
def
|
61
|
-
|
62
|
-
|
100
|
+
def status_code_from_error(e)
|
101
|
+
raise e
|
102
|
+
|
103
|
+
# support ctrl+c and stuff
|
104
|
+
rescue SignalException, DoneStreaming
|
105
|
+
0
|
106
|
+
|
107
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
108
|
+
raise e if should_raise_errors
|
109
|
+
log(e)
|
110
|
+
log(e.backtrace)
|
111
|
+
2
|
112
|
+
end
|
113
|
+
|
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
|
63
121
|
|
64
|
-
@out.write("Content-Length: #{
|
122
|
+
@out.write("Content-Length: #{message_body.bytesize}\r\n")
|
65
123
|
@out.write("\r\n")
|
66
|
-
@out.write(
|
124
|
+
@out.write(message_body)
|
67
125
|
@out.flush
|
68
126
|
end
|
69
127
|
|
@@ -91,17 +149,23 @@ module ThemeCheck
|
|
91
149
|
"one of the following: #{supported_io_classes.join(', ')}"
|
92
150
|
end
|
93
151
|
|
94
|
-
def
|
95
|
-
|
96
|
-
|
97
|
-
log(JSON.pretty_generate(
|
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
|
98
158
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
method_name
|
159
|
+
def handle_message(message)
|
160
|
+
id = message['id']
|
161
|
+
method_name = message['method']
|
162
|
+
method_name &&= "on_#{to_snake_case(method_name)}"
|
163
|
+
params = message['params']
|
164
|
+
result = message['result']
|
103
165
|
|
104
|
-
if
|
166
|
+
if message.key?('result')
|
167
|
+
@messenger.respond(id, result)
|
168
|
+
elsif @handler.respond_to?(method_name)
|
105
169
|
@handler.send(method_name, id, params)
|
106
170
|
end
|
107
171
|
end
|
@@ -128,26 +192,27 @@ module ThemeCheck
|
|
128
192
|
length = initial_line.match(/Content-Length: (\d+)/)[1].to_i
|
129
193
|
content = ''
|
130
194
|
while content.length < length + 2
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
rescue => e
|
135
|
-
log(e)
|
136
|
-
log(e.backtrace)
|
137
|
-
# We have almost certainly been disconnected from the server
|
138
|
-
cleanup
|
139
|
-
raise DoneStreaming
|
140
|
-
end
|
195
|
+
# Why + 2? Because \r\n
|
196
|
+
content += @in.read(length + 2)
|
197
|
+
raise DoneStreaming if @in.closed?
|
141
198
|
end
|
142
199
|
|
143
200
|
content
|
144
201
|
end
|
145
202
|
|
146
203
|
def cleanup
|
204
|
+
# Stop listenting to RPC calls
|
205
|
+
@in.close unless @in.closed?
|
206
|
+
# Wait for rpc loop to close
|
207
|
+
@json_rpc_thread&.join if @json_rpc_thread&.alive?
|
208
|
+
# Close the queue
|
209
|
+
@queue.close unless @queue.closed?
|
210
|
+
# Give 10 seconds for the handlers to wrap up what they were
|
211
|
+
# doing/emptying the queue. 👀 unit tests.
|
212
|
+
@handlers.each { |thread| thread.join(10) if thread.alive? }
|
213
|
+
ensure
|
147
214
|
@err.close
|
148
215
|
@out.close
|
149
|
-
rescue
|
150
|
-
# I did my best
|
151
216
|
end
|
152
217
|
end
|
153
218
|
end
|
data/lib/theme_check/version.rb
CHANGED
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.
|
4
|
+
version: 1.7.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-
|
11
|
+
date: 2021-09-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: liquid
|
@@ -217,6 +217,7 @@ files:
|
|
217
217
|
- lib/theme_check/language_server/document_link_providers/render_document_link_provider.rb
|
218
218
|
- lib/theme_check/language_server/document_link_providers/section_document_link_provider.rb
|
219
219
|
- lib/theme_check/language_server/handler.rb
|
220
|
+
- lib/theme_check/language_server/messenger.rb
|
220
221
|
- lib/theme_check/language_server/protocol.rb
|
221
222
|
- lib/theme_check/language_server/server.rb
|
222
223
|
- lib/theme_check/language_server/tokens.rb
|