theme-check 1.7.0 → 1.9.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/.gitignore +1 -0
- data/CHANGELOG.md +49 -0
- data/README.md +10 -0
- data/RELEASING.md +13 -0
- data/config/default.yml +5 -0
- data/data/shopify_liquid/deprecated_filters.yml +4 -0
- data/data/shopify_liquid/filters.yml +3 -1
- data/data/shopify_liquid/tags.yml +9 -9
- data/docs/checks/TEMPLATE.md.erb +24 -19
- data/docs/checks/schema_json_format.md +76 -0
- data/docs/language_server/code-action-command-palette.png +0 -0
- data/docs/language_server/code-action-flow.png +0 -0
- data/docs/language_server/code-action-keyboard.png +0 -0
- data/docs/language_server/code-action-light-bulb.png +0 -0
- data/docs/language_server/code-action-problem.png +0 -0
- data/docs/language_server/code-action-quickfix.png +0 -0
- data/docs/language_server/how_to_correct_code_with_code_actions_and_execute_command.md +197 -0
- data/exe/theme-check-language-server +0 -4
- data/lib/theme_check/checks/asset_size_app_block_css.rb +2 -3
- data/lib/theme_check/checks/asset_size_app_block_javascript.rb +2 -3
- data/lib/theme_check/checks/asset_url_filters.rb +2 -0
- data/lib/theme_check/checks/default_locale.rb +1 -1
- data/lib/theme_check/checks/deprecated_filter.rb +79 -4
- data/lib/theme_check/checks/deprecated_global_app_block_type.rb +2 -3
- data/lib/theme_check/checks/matching_schema_translations.rb +14 -9
- data/lib/theme_check/checks/matching_translations.rb +1 -0
- data/lib/theme_check/checks/missing_required_template_files.rb +3 -3
- data/lib/theme_check/checks/missing_template.rb +1 -1
- data/lib/theme_check/checks/pagination_size.rb +2 -3
- data/lib/theme_check/checks/remote_asset.rb +5 -0
- data/lib/theme_check/checks/required_directories.rb +1 -1
- data/lib/theme_check/checks/required_layout_theme_object.rb +9 -4
- data/lib/theme_check/checks/schema_json_format.rb +29 -0
- data/lib/theme_check/checks/space_inside_braces.rb +132 -87
- data/lib/theme_check/checks/translation_key_exists.rb +33 -25
- data/lib/theme_check/checks/unused_assign.rb +3 -2
- data/lib/theme_check/checks/unused_snippet.rb +1 -1
- data/lib/theme_check/checks/valid_html_translation.rb +1 -1
- data/lib/theme_check/checks/valid_schema.rb +2 -2
- data/lib/theme_check/corrector.rb +34 -23
- data/lib/theme_check/exceptions.rb +1 -0
- data/lib/theme_check/file_system_storage.rb +8 -3
- data/lib/theme_check/html_node.rb +99 -6
- data/lib/theme_check/html_visitor.rb +1 -32
- data/lib/theme_check/in_memory_storage.rb +9 -0
- data/lib/theme_check/json_helpers.rb +14 -0
- data/lib/theme_check/language_server/bridge.rb +142 -0
- data/lib/theme_check/language_server/channel.rb +69 -0
- data/lib/theme_check/language_server/client_capabilities.rb +27 -0
- data/lib/theme_check/language_server/code_action_engine.rb +32 -0
- data/lib/theme_check/language_server/code_action_provider.rb +42 -0
- data/lib/theme_check/language_server/code_action_providers/quickfix_code_action_provider.rb +83 -0
- data/lib/theme_check/language_server/code_action_providers/source_fix_all_code_action_provider.rb +40 -0
- data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +3 -1
- data/lib/theme_check/language_server/configuration.rb +69 -0
- data/lib/theme_check/language_server/diagnostic.rb +124 -0
- data/lib/theme_check/language_server/diagnostics_engine.rb +80 -0
- data/lib/theme_check/language_server/diagnostics_manager.rb +136 -0
- data/lib/theme_check/language_server/document_change_corrector.rb +267 -0
- data/lib/theme_check/language_server/document_link_provider.rb +6 -6
- data/lib/theme_check/language_server/execute_command_engine.rb +19 -0
- data/lib/theme_check/language_server/execute_command_provider.rb +30 -0
- data/lib/theme_check/language_server/execute_command_providers/correction_execute_command_provider.rb +48 -0
- data/lib/theme_check/language_server/execute_command_providers/run_checks_execute_command_provider.rb +22 -0
- data/lib/theme_check/language_server/handler.rb +92 -217
- data/lib/theme_check/language_server/io_messenger.rb +112 -0
- data/lib/theme_check/language_server/messenger.rb +12 -42
- data/lib/theme_check/language_server/protocol.rb +4 -0
- data/lib/theme_check/language_server/server.rb +54 -110
- data/lib/theme_check/language_server/uri_helper.rb +1 -0
- data/lib/theme_check/language_server/versioned_in_memory_storage.rb +69 -0
- data/lib/theme_check/language_server.rb +28 -6
- data/lib/theme_check/liquid_node.rb +255 -12
- data/lib/theme_check/locale_diff.rb +48 -10
- data/lib/theme_check/node.rb +16 -0
- data/lib/theme_check/offense.rb +27 -23
- data/lib/theme_check/position.rb +4 -4
- data/lib/theme_check/regex_helpers.rb +1 -1
- data/lib/theme_check/schema_helper.rb +70 -0
- 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/storage.rb +4 -0
- data/lib/theme_check/tags.rb +0 -1
- data/lib/theme_check/theme.rb +1 -1
- data/lib/theme_check/theme_file.rb +8 -1
- data/lib/theme_check/theme_file_rewriter.rb +28 -6
- data/lib/theme_check/version.rb +1 -1
- data/lib/theme_check.rb +11 -2
- metadata +31 -3
- data/lib/theme_check/language_server/diagnostics_tracker.rb +0 -66
|
@@ -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,12 +36,8 @@ 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
|
-
@error = SizedQueue.new(
|
|
40
|
+
@error = SizedQueue.new(number_of_threads)
|
|
57
41
|
|
|
58
42
|
@should_raise_errors = should_raise_errors
|
|
59
43
|
end
|
|
@@ -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 =
|
|
75
|
-
if message[
|
|
57
|
+
message = @bridge.read_message
|
|
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,46 @@ 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.class}: #{e.message}\n#{e.backtrace.join("\n")}")
|
|
111
98
|
2
|
|
112
99
|
end
|
|
113
100
|
|
|
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
101
|
private
|
|
134
102
|
|
|
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
103
|
def handle_message(message)
|
|
160
|
-
id = message[
|
|
161
|
-
method_name = message[
|
|
104
|
+
id = message[:id]
|
|
105
|
+
method_name = message[:method]
|
|
162
106
|
method_name &&= "on_#{to_snake_case(method_name)}"
|
|
163
|
-
params = message[
|
|
164
|
-
result = message['result']
|
|
107
|
+
params = message[:params]
|
|
165
108
|
|
|
166
|
-
if
|
|
167
|
-
@messenger.respond(id, result)
|
|
168
|
-
elsif @handler.respond_to?(method_name)
|
|
109
|
+
if @handler.respond_to?(method_name)
|
|
169
110
|
@handler.send(method_name, id, params)
|
|
170
111
|
end
|
|
171
|
-
end
|
|
172
112
|
|
|
173
|
-
|
|
174
|
-
|
|
113
|
+
rescue DoneStreaming => e
|
|
114
|
+
raise e
|
|
115
|
+
rescue StandardError => e
|
|
116
|
+
is_request = id
|
|
117
|
+
raise e unless is_request
|
|
118
|
+
# Errors obtained in request handlers should be sent
|
|
119
|
+
# back as internal errors instead of closing the program.
|
|
120
|
+
@bridge.log("#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}")
|
|
121
|
+
@bridge.send_internal_error(id, e)
|
|
175
122
|
end
|
|
176
123
|
|
|
177
|
-
def
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
124
|
+
def handle_response(message)
|
|
125
|
+
id = message[:id]
|
|
126
|
+
result = message[:result]
|
|
127
|
+
@bridge.receive_response(id, result)
|
|
189
128
|
end
|
|
190
129
|
|
|
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
|
|
130
|
+
def to_snake_case(method_name)
|
|
131
|
+
StringHelpers.underscore(method_name.gsub(/[^\w]/, '_'))
|
|
201
132
|
end
|
|
202
133
|
|
|
203
|
-
def cleanup
|
|
134
|
+
def cleanup(status_code)
|
|
204
135
|
# Stop listenting to RPC calls
|
|
205
|
-
@
|
|
136
|
+
@messenger.close_input
|
|
206
137
|
# Wait for rpc loop to close
|
|
207
138
|
@json_rpc_thread&.join if @json_rpc_thread&.alive?
|
|
208
139
|
# Close the queue
|
|
@@ -210,9 +141,22 @@ module ThemeCheck
|
|
|
210
141
|
# Give 10 seconds for the handlers to wrap up what they were
|
|
211
142
|
# doing/emptying the queue. 👀 unit tests.
|
|
212
143
|
@handlers.each { |thread| thread.join(10) if thread.alive? }
|
|
144
|
+
|
|
145
|
+
# Hijack the status_code if an error occurred while cleaning up.
|
|
146
|
+
# 👀 unit tests.
|
|
147
|
+
until @error.empty?
|
|
148
|
+
code = status_code_from_error(@error.pop)
|
|
149
|
+
# Promote the status_code to ERROR if one of the threads
|
|
150
|
+
# resulted in an error, otherwise leave the status_code as
|
|
151
|
+
# is. That's because one thread could end successfully in a
|
|
152
|
+
# DoneStreaming error while the other failed with an
|
|
153
|
+
# internal error. If we had an internal error, we should
|
|
154
|
+
# return with a status_code that fits.
|
|
155
|
+
status_code = code if code > status_code
|
|
156
|
+
end
|
|
157
|
+
status_code
|
|
213
158
|
ensure
|
|
214
|
-
@
|
|
215
|
-
@out.close
|
|
159
|
+
@messenger.close_output
|
|
216
160
|
end
|
|
217
161
|
end
|
|
218
162
|
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThemeCheck
|
|
4
|
+
class VersionedInMemoryStorage < InMemoryStorage
|
|
5
|
+
Version = Struct.new(:id, :version)
|
|
6
|
+
|
|
7
|
+
attr_reader :versions
|
|
8
|
+
|
|
9
|
+
def initialize(files, root = "/dev/null")
|
|
10
|
+
super(files, root)
|
|
11
|
+
@versions = {}
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Motivations:
|
|
16
|
+
# - Need way for LanguageServer to know on which version of a file
|
|
17
|
+
# the check was run on, because we need to know where the
|
|
18
|
+
# TextEdit goes. If the text changed, our TextEdit might not be
|
|
19
|
+
# in the right spot. e.g.
|
|
20
|
+
#
|
|
21
|
+
# Example:
|
|
22
|
+
#
|
|
23
|
+
# ```
|
|
24
|
+
# Hi
|
|
25
|
+
# {{world}}
|
|
26
|
+
# ```
|
|
27
|
+
#
|
|
28
|
+
# Would produce two "SpaceInsideBrace" errors:
|
|
29
|
+
#
|
|
30
|
+
# - One after {{ at index 5 to 6
|
|
31
|
+
# - One before }} at index 10 to 11
|
|
32
|
+
#
|
|
33
|
+
# If the user goes in and changes Hi to Sup, and _then_
|
|
34
|
+
# right clicks to apply the code edit at index 5 to 6, he'd
|
|
35
|
+
# get the following:
|
|
36
|
+
#
|
|
37
|
+
# ```
|
|
38
|
+
# Sup
|
|
39
|
+
# { {world}}
|
|
40
|
+
# ```
|
|
41
|
+
#
|
|
42
|
+
# Which is not a fix at all.
|
|
43
|
+
#
|
|
44
|
+
# Solution:
|
|
45
|
+
# - Have the LanguageServer store the version on textDocument/did{Open,Change,Close}
|
|
46
|
+
# - Have ThemeFile store the version right after @storage.read.
|
|
47
|
+
# - Add version to the diagnostic meta data
|
|
48
|
+
# - Use diagnostic meta data to determine if we can make a code edit or not
|
|
49
|
+
# - Only offer fixes on "clean" files (or offer the change but specify the version so the editor knows what to do with it)
|
|
50
|
+
def write(relative_path, content, version)
|
|
51
|
+
@mutex.synchronize do
|
|
52
|
+
@versions[relative_path] = version
|
|
53
|
+
super(relative_path, content)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def read_version(relative_path)
|
|
58
|
+
@mutex.synchronize { [read(relative_path), version(relative_path)] }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def versioned?
|
|
62
|
+
true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def version(relative_path)
|
|
66
|
+
@versions[relative_path.to_s]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -1,31 +1,53 @@
|
|
|
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/configuration"
|
|
5
|
+
require_relative "language_server/channel"
|
|
6
|
+
require_relative "language_server/messenger"
|
|
7
|
+
require_relative "language_server/io_messenger"
|
|
8
|
+
require_relative "language_server/bridge"
|
|
5
9
|
require_relative "language_server/uri_helper"
|
|
6
|
-
require_relative "language_server/handler"
|
|
7
10
|
require_relative "language_server/server"
|
|
8
11
|
require_relative "language_server/tokens"
|
|
9
12
|
require_relative "language_server/variable_lookup_finder"
|
|
13
|
+
require_relative "language_server/diagnostic"
|
|
14
|
+
require_relative "language_server/diagnostics_manager"
|
|
15
|
+
require_relative "language_server/diagnostics_engine"
|
|
16
|
+
require_relative "language_server/document_change_corrector"
|
|
17
|
+
require_relative "language_server/versioned_in_memory_storage"
|
|
18
|
+
require_relative "language_server/client_capabilities"
|
|
19
|
+
|
|
10
20
|
require_relative "language_server/completion_helper"
|
|
11
21
|
require_relative "language_server/completion_provider"
|
|
12
22
|
require_relative "language_server/completion_engine"
|
|
23
|
+
Dir[__dir__ + "/language_server/completion_providers/*.rb"].each do |file|
|
|
24
|
+
require file
|
|
25
|
+
end
|
|
26
|
+
|
|
13
27
|
require_relative "language_server/document_link_provider"
|
|
14
28
|
require_relative "language_server/document_link_engine"
|
|
15
|
-
|
|
29
|
+
Dir[__dir__ + "/language_server/document_link_providers/*.rb"].each do |file|
|
|
30
|
+
require file
|
|
31
|
+
end
|
|
16
32
|
|
|
17
|
-
|
|
33
|
+
require_relative "language_server/execute_command_provider"
|
|
34
|
+
require_relative "language_server/execute_command_engine"
|
|
35
|
+
Dir[__dir__ + "/language_server/execute_command_providers/*.rb"].each do |file|
|
|
18
36
|
require file
|
|
19
37
|
end
|
|
20
38
|
|
|
21
|
-
|
|
39
|
+
require_relative "language_server/code_action_provider"
|
|
40
|
+
require_relative "language_server/code_action_engine"
|
|
41
|
+
Dir[__dir__ + "/language_server/code_action_providers/*.rb"].each do |file|
|
|
22
42
|
require file
|
|
23
43
|
end
|
|
24
44
|
|
|
45
|
+
require_relative "language_server/handler"
|
|
46
|
+
|
|
25
47
|
module ThemeCheck
|
|
26
48
|
module LanguageServer
|
|
27
49
|
def self.start
|
|
28
|
-
Server.new.listen
|
|
50
|
+
Server.new(messenger: IOMessenger.new).listen
|
|
29
51
|
end
|
|
30
52
|
end
|
|
31
53
|
end
|
|
@@ -45,11 +45,47 @@ module ThemeCheck
|
|
|
45
45
|
def markup
|
|
46
46
|
if tag?
|
|
47
47
|
tag_markup
|
|
48
|
+
elsif literal?
|
|
49
|
+
value.to_s
|
|
48
50
|
elsif @value.instance_variable_defined?(:@markup)
|
|
49
51
|
@value.instance_variable_get(:@markup)
|
|
50
52
|
end
|
|
51
53
|
end
|
|
52
54
|
|
|
55
|
+
# The original source code of the node. Does contain wrapping braces.
|
|
56
|
+
def outer_markup
|
|
57
|
+
if literal?
|
|
58
|
+
markup
|
|
59
|
+
elsif variable_lookup?
|
|
60
|
+
''
|
|
61
|
+
elsif variable?
|
|
62
|
+
start_token + markup + end_token
|
|
63
|
+
elsif tag? && block?
|
|
64
|
+
start_index = block_start_start_index
|
|
65
|
+
end_index = block_start_end_index
|
|
66
|
+
end_index += inner_markup.size
|
|
67
|
+
end_index = find_block_delimiter(end_index)&.end(0)
|
|
68
|
+
source[start_index...end_index]
|
|
69
|
+
elsif tag?
|
|
70
|
+
source[block_start_start_index...block_start_end_index]
|
|
71
|
+
else
|
|
72
|
+
inner_markup
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def inner_markup
|
|
77
|
+
return '' unless block?
|
|
78
|
+
@inner_markup ||= source[block_start_end_index...block_end_start_index]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def inner_json
|
|
82
|
+
return nil unless schema?
|
|
83
|
+
@inner_json ||= JSON.parse(inner_markup)
|
|
84
|
+
rescue JSON::ParserError
|
|
85
|
+
# Handled by ValidSchema
|
|
86
|
+
@inner_json = nil
|
|
87
|
+
end
|
|
88
|
+
|
|
53
89
|
def markup=(markup)
|
|
54
90
|
if @value.instance_variable_defined?(:@markup)
|
|
55
91
|
@value.instance_variable_set(:@markup, markup)
|
|
@@ -70,10 +106,26 @@ module ThemeCheck
|
|
|
70
106
|
position.start_index
|
|
71
107
|
end
|
|
72
108
|
|
|
109
|
+
def start_row
|
|
110
|
+
position.start_row
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def start_column
|
|
114
|
+
position.start_column
|
|
115
|
+
end
|
|
116
|
+
|
|
73
117
|
def end_index
|
|
74
118
|
position.end_index
|
|
75
119
|
end
|
|
76
120
|
|
|
121
|
+
def end_row
|
|
122
|
+
position.end_row
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def end_column
|
|
126
|
+
position.end_column
|
|
127
|
+
end
|
|
128
|
+
|
|
77
129
|
# Literals are hard-coded values in the liquid file.
|
|
78
130
|
def literal?
|
|
79
131
|
@value.is_a?(String) || @value.is_a?(Integer)
|
|
@@ -88,6 +140,14 @@ module ThemeCheck
|
|
|
88
140
|
@value.is_a?(Liquid::Variable)
|
|
89
141
|
end
|
|
90
142
|
|
|
143
|
+
def assigned_or_echoed_variable?
|
|
144
|
+
variable? && start_token == ""
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def variable_lookup?
|
|
148
|
+
@value.is_a?(Liquid::VariableLookup)
|
|
149
|
+
end
|
|
150
|
+
|
|
91
151
|
# A {% comment %} block node?
|
|
92
152
|
def comment?
|
|
93
153
|
@value.is_a?(Liquid::Comment)
|
|
@@ -114,6 +174,10 @@ module ThemeCheck
|
|
|
114
174
|
block_tag? || block_body? || document?
|
|
115
175
|
end
|
|
116
176
|
|
|
177
|
+
def schema?
|
|
178
|
+
@value.is_a?(ThemeCheck::Tags::Schema)
|
|
179
|
+
end
|
|
180
|
+
|
|
117
181
|
# The `:under_score_name` of this type of node. Used to dispatch to the `on_<type_name>`
|
|
118
182
|
# and `after_<type_name>` check methods.
|
|
119
183
|
def type_name
|
|
@@ -124,6 +188,86 @@ module ThemeCheck
|
|
|
124
188
|
theme_file&.source
|
|
125
189
|
end
|
|
126
190
|
|
|
191
|
+
def block_start_markup
|
|
192
|
+
source[block_start_start_index...block_start_end_index]
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def block_start_start_index
|
|
196
|
+
@block_start_start_index ||= if inside_liquid_tag?
|
|
197
|
+
backtrack_on_whitespace(source, start_index, /[ \t]/)
|
|
198
|
+
elsif tag?
|
|
199
|
+
backtrack_on_whitespace(source, start_index) - start_token.length
|
|
200
|
+
else
|
|
201
|
+
position.start_index - start_token.length
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def block_start_end_index
|
|
206
|
+
@block_start_end_index ||= position.end_index + end_token.size
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def block_end_markup
|
|
210
|
+
source[block_end_start_index...block_end_end_index]
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def block_end_start_index
|
|
214
|
+
return block_start_end_index unless tag? && block?
|
|
215
|
+
@block_end_start_index ||= block_end_match&.begin(0) || block_start_end_index
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def block_end_end_index
|
|
219
|
+
return block_end_start_index unless tag? && block?
|
|
220
|
+
@block_end_end_index ||= block_end_match&.end(0) || block_start_end_index
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def outer_markup_start_index
|
|
224
|
+
outer_markup_position.start_index
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def outer_markup_end_index
|
|
228
|
+
outer_markup_position.end_index
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def outer_markup_start_row
|
|
232
|
+
outer_markup_position.start_row
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def outer_markup_start_column
|
|
236
|
+
outer_markup_position.start_column
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def outer_markup_end_row
|
|
240
|
+
outer_markup_position.end_row
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def outer_markup_end_column
|
|
244
|
+
outer_markup_position.end_column
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def inner_markup_start_index
|
|
248
|
+
inner_markup_position.start_index
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def inner_markup_end_index
|
|
252
|
+
inner_markup_position.end_index
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def inner_markup_start_row
|
|
256
|
+
inner_markup_position.start_row
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def inner_markup_start_column
|
|
260
|
+
inner_markup_position.start_column
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def inner_markup_end_row
|
|
264
|
+
inner_markup_position.end_row
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def inner_markup_end_column
|
|
268
|
+
inner_markup_position.end_column
|
|
269
|
+
end
|
|
270
|
+
|
|
127
271
|
WHITESPACE = /\s/
|
|
128
272
|
|
|
129
273
|
# Is this node inside a `{% liquid ... %}` block?
|
|
@@ -165,21 +309,37 @@ module ThemeCheck
|
|
|
165
309
|
end
|
|
166
310
|
|
|
167
311
|
def start_token
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
312
|
+
if inside_liquid_tag?
|
|
313
|
+
""
|
|
314
|
+
elsif variable? && source[start_index - 3..start_index - 1] == "{{-"
|
|
315
|
+
"{{-"
|
|
316
|
+
elsif variable? && source[start_index - 2..start_index - 1] == "{{"
|
|
317
|
+
"{{"
|
|
318
|
+
elsif tag? && whitespace_trimmed_start?
|
|
319
|
+
"{%-"
|
|
320
|
+
elsif tag?
|
|
321
|
+
"{%"
|
|
322
|
+
else
|
|
323
|
+
""
|
|
324
|
+
end
|
|
174
325
|
end
|
|
175
326
|
|
|
176
327
|
def end_token
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
328
|
+
if inside_liquid_tag? && source[end_index] == "\n"
|
|
329
|
+
"\n"
|
|
330
|
+
elsif inside_liquid_tag?
|
|
331
|
+
""
|
|
332
|
+
elsif variable? && source[end_index...end_index + 3] == "-}}"
|
|
333
|
+
"-}}"
|
|
334
|
+
elsif variable? && source[end_index...end_index + 2] == "}}"
|
|
335
|
+
"}}"
|
|
336
|
+
elsif tag? && whitespace_trimmed_end?
|
|
337
|
+
"-%}"
|
|
338
|
+
elsif tag?
|
|
339
|
+
"%}"
|
|
340
|
+
else # this could happen because we're in an assign statement (variable)
|
|
341
|
+
""
|
|
342
|
+
end
|
|
183
343
|
end
|
|
184
344
|
|
|
185
345
|
private
|
|
@@ -192,6 +352,22 @@ module ThemeCheck
|
|
|
192
352
|
)
|
|
193
353
|
end
|
|
194
354
|
|
|
355
|
+
def outer_markup_position
|
|
356
|
+
@outer_markup_position ||= StrictPosition.new(
|
|
357
|
+
outer_markup,
|
|
358
|
+
source,
|
|
359
|
+
block_start_start_index,
|
|
360
|
+
)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def inner_markup_position
|
|
364
|
+
@inner_markup_position ||= StrictPosition.new(
|
|
365
|
+
inner_markup,
|
|
366
|
+
source,
|
|
367
|
+
block_start_end_index,
|
|
368
|
+
)
|
|
369
|
+
end
|
|
370
|
+
|
|
195
371
|
# Here we're hacking around a glorious bug in Liquid that makes it so the
|
|
196
372
|
# line_number and markup of a tag is wrong if there's whitespace
|
|
197
373
|
# between the tag_name and the markup of the tag.
|
|
@@ -287,5 +463,72 @@ module ThemeCheck
|
|
|
287
463
|
# return the real raw content
|
|
288
464
|
@tag_markup = source[tag_start...markup_end]
|
|
289
465
|
end
|
|
466
|
+
|
|
467
|
+
# Returns the index of the leftmost consecutive whitespace
|
|
468
|
+
# starting from start going backwards.
|
|
469
|
+
#
|
|
470
|
+
# e.g. backtrack_on_whitespace("01 45", 4) would return 2.
|
|
471
|
+
# e.g. backtrack_on_whitespace("{% render %}", 5) would return 2.
|
|
472
|
+
def backtrack_on_whitespace(string, start, whitespace = WHITESPACE)
|
|
473
|
+
i = start
|
|
474
|
+
i -= 1 while string[i - 1] =~ whitespace && i > 0
|
|
475
|
+
i
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def find_block_delimiter(start_index)
|
|
479
|
+
return nil unless tag? && block?
|
|
480
|
+
|
|
481
|
+
tag_start, tag_end = if inside_liquid_tag?
|
|
482
|
+
[
|
|
483
|
+
/^\s*#{@value.tag_name}\s*/,
|
|
484
|
+
/^\s*end#{@value.tag_name}\s*/,
|
|
485
|
+
]
|
|
486
|
+
else
|
|
487
|
+
[
|
|
488
|
+
/#{Liquid::TagStart}-?\s*#{@value.tag_name}/mi,
|
|
489
|
+
/#{Liquid::TagStart}-?\s*end#{@value.tag_name}\s*-?#{Liquid::TagEnd}/mi,
|
|
490
|
+
]
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# This little algorithm below find the _correct_ block delimiter
|
|
494
|
+
# (endif, endcase, endcomment) for the current tag. What do I
|
|
495
|
+
# mean by correct? It means the one you'd expect. Making sure
|
|
496
|
+
# that we don't do the naive regex find. Since you can have
|
|
497
|
+
# nested ifs, fors, etc.
|
|
498
|
+
#
|
|
499
|
+
# It works by having a stack, pushing onto the stack when we
|
|
500
|
+
# open a tag of our type_name. And popping when we find a
|
|
501
|
+
# closing tag of our type_name.
|
|
502
|
+
#
|
|
503
|
+
# When the stack is empty, we return the end tag match.
|
|
504
|
+
index = start_index
|
|
505
|
+
stack = []
|
|
506
|
+
stack.push("open")
|
|
507
|
+
loop do
|
|
508
|
+
tag_start_match = tag_start.match(source, index)
|
|
509
|
+
tag_end_match = tag_end.match(source, index)
|
|
510
|
+
|
|
511
|
+
return nil unless tag_end_match
|
|
512
|
+
|
|
513
|
+
# We have found a tag_start and it appeared _before_ the
|
|
514
|
+
# tag_end that we found, thus we push it onto the stack.
|
|
515
|
+
if tag_start_match && tag_start_match.end(0) < tag_end_match.end(0)
|
|
516
|
+
stack.push("open")
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
# We have found a tag_end, therefore we pop
|
|
520
|
+
stack.pop
|
|
521
|
+
|
|
522
|
+
# Nothing left on the stack, we're done.
|
|
523
|
+
break tag_end_match if stack.empty?
|
|
524
|
+
|
|
525
|
+
# We keep looking from the end of the end tag we just found.
|
|
526
|
+
index = tag_end_match.end(0)
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def block_end_match
|
|
531
|
+
@block_end_match ||= find_block_delimiter(block_start_end_index)
|
|
532
|
+
end
|
|
290
533
|
end
|
|
291
534
|
end
|