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 +4 -4
- data/CHANGELOG.md +7 -0
- data/data/shopify_liquid/tags.yml +9 -9
- data/lib/theme_check/checks/translation_key_exists.rb +1 -13
- data/lib/theme_check/exceptions.rb +1 -0
- data/lib/theme_check/file_system_storage.rb +4 -0
- data/lib/theme_check/language_server/bridge.rb +128 -0
- data/lib/theme_check/language_server/channel.rb +69 -0
- data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +3 -1
- data/lib/theme_check/language_server/diagnostics_engine.rb +125 -0
- data/lib/theme_check/language_server/handler.rb +14 -193
- data/lib/theme_check/language_server/io_messenger.rb +97 -0
- data/lib/theme_check/language_server/messenger.rb +12 -42
- data/lib/theme_check/language_server/server.rb +32 -106
- data/lib/theme_check/language_server.rb +6 -2
- data/lib/theme_check/locale_diff.rb +9 -2
- data/lib/theme_check/shopify_liquid/system_translations.rb +35 -0
- data/lib/theme_check/shopify_liquid/tag.rb +19 -1
- data/lib/theme_check/shopify_liquid.rb +1 -0
- data/lib/theme_check/version.rb +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0c0071563941bd1e62d174c317667f7b1258a5154fdd0b9e65531a667aafbb53
|
4
|
+
data.tar.gz: fba3428de01a76fc042ee5c9fefe5acdcc1b446c02eafd5a2d187d503d863433
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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,
|
@@ -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(
|
30
|
-
@
|
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
|
-
|
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 @
|
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
|
-
|
145
|
-
|
146
|
-
|
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
|
-
@
|
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
|
7
|
-
|
8
|
-
@mutex = Mutex.new
|
9
|
-
@id = 0
|
6
|
+
def send_message
|
7
|
+
raise NotImplementedError
|
10
8
|
end
|
11
9
|
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
43
|
-
|
14
|
+
def log
|
15
|
+
raise NotImplementedError
|
16
|
+
end
|
44
17
|
|
45
|
-
|
46
|
-
|
18
|
+
def close_input
|
19
|
+
raise NotImplementedError
|
47
20
|
end
|
48
21
|
|
49
|
-
|
50
|
-
|
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
|
-
|
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
|
-
|
20
|
+
# This is what does the IO
|
21
|
+
@messenger = messenger
|
23
22
|
|
24
|
-
|
25
|
-
@
|
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
|
-
#
|
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 =
|
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
|
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
|
174
|
-
|
175
|
-
|
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
|
192
|
-
|
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
|
-
@
|
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
|
-
@
|
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
|
-
|
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
|
-
@
|
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
|
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.7.
|
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-
|
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
|