theme-check 0.3.0 → 0.5.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/.rubocop.yml +7 -0
- data/CHANGELOG.md +50 -0
- data/CONTRIBUTING.md +5 -2
- data/README.md +9 -4
- data/RELEASING.md +2 -2
- data/config/default.yml +7 -0
- data/data/shopify_liquid/tags.yml +27 -0
- data/docs/checks/CHECK_DOCS_TEMPLATE.md +47 -0
- data/docs/checks/asset_size_javascript.md +79 -0
- data/docs/checks/convert_include_to_render.md +48 -0
- data/docs/checks/default_locale.md +46 -0
- data/docs/checks/deprecated_filter.md +46 -0
- data/docs/checks/liquid_tag.md +65 -0
- data/docs/checks/matching_schema_translations.md +93 -0
- data/docs/checks/matching_translations.md +72 -0
- data/docs/checks/missing_enable_comment.md +50 -0
- data/docs/checks/missing_required_template_files.md +26 -0
- data/docs/checks/missing_template.md +40 -0
- data/docs/checks/nested_snippet.md +69 -0
- data/docs/checks/parser_blocking_javascript.md +97 -0
- data/docs/checks/required_directories.md +25 -0
- data/docs/checks/required_layout_theme_object.md +28 -0
- data/docs/checks/space_inside_braces.md +63 -0
- data/docs/checks/syntax_error.md +49 -0
- data/docs/checks/template_length.md +50 -0
- data/docs/checks/translation_key_exists.md +63 -0
- data/docs/checks/undefined_object.md +53 -0
- data/docs/checks/unknown_filter.md +45 -0
- data/docs/checks/unused_assign.md +47 -0
- data/docs/checks/unused_snippet.md +32 -0
- data/docs/checks/valid_html_translation.md +53 -0
- data/docs/checks/valid_json.md +60 -0
- data/docs/checks/valid_schema.md +50 -0
- data/lib/theme_check.rb +4 -0
- data/lib/theme_check/asset_file.rb +34 -0
- data/lib/theme_check/check.rb +19 -9
- data/lib/theme_check/checks/asset_size_javascript.rb +74 -0
- data/lib/theme_check/checks/convert_include_to_render.rb +1 -1
- data/lib/theme_check/checks/default_locale.rb +1 -0
- data/lib/theme_check/checks/deprecated_filter.rb +1 -1
- data/lib/theme_check/checks/liquid_tag.rb +3 -3
- data/lib/theme_check/checks/matching_schema_translations.rb +1 -0
- data/lib/theme_check/checks/matching_translations.rb +1 -0
- data/lib/theme_check/checks/missing_enable_comment.rb +1 -0
- data/lib/theme_check/checks/missing_required_template_files.rb +1 -2
- data/lib/theme_check/checks/missing_template.rb +1 -0
- data/lib/theme_check/checks/nested_snippet.rb +1 -0
- data/lib/theme_check/checks/parser_blocking_javascript.rb +2 -1
- data/lib/theme_check/checks/required_directories.rb +1 -1
- data/lib/theme_check/checks/required_layout_theme_object.rb +1 -1
- data/lib/theme_check/checks/space_inside_braces.rb +1 -0
- data/lib/theme_check/checks/syntax_error.rb +1 -0
- data/lib/theme_check/checks/template_length.rb +1 -0
- data/lib/theme_check/checks/translation_key_exists.rb +1 -0
- data/lib/theme_check/checks/undefined_object.rb +29 -10
- data/lib/theme_check/checks/unknown_filter.rb +1 -0
- data/lib/theme_check/checks/unused_assign.rb +5 -3
- data/lib/theme_check/checks/unused_snippet.rb +1 -0
- data/lib/theme_check/checks/valid_html_translation.rb +1 -0
- data/lib/theme_check/checks/valid_json.rb +1 -0
- data/lib/theme_check/checks/valid_schema.rb +1 -0
- data/lib/theme_check/cli.rb +22 -6
- data/lib/theme_check/config.rb +2 -2
- data/lib/theme_check/in_memory_storage.rb +1 -1
- data/lib/theme_check/language_server.rb +10 -0
- data/lib/theme_check/language_server/completion_engine.rb +38 -0
- data/lib/theme_check/language_server/completion_helper.rb +25 -0
- data/lib/theme_check/language_server/completion_provider.rb +24 -0
- data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +47 -0
- data/lib/theme_check/language_server/completion_providers/object_completion_provider.rb +31 -0
- data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +31 -0
- data/lib/theme_check/language_server/handler.rb +62 -6
- data/lib/theme_check/language_server/position_helper.rb +27 -0
- data/lib/theme_check/language_server/protocol.rb +41 -0
- data/lib/theme_check/language_server/server.rb +6 -1
- data/lib/theme_check/language_server/tokens.rb +55 -0
- data/lib/theme_check/offense.rb +51 -14
- data/lib/theme_check/regex_helpers.rb +15 -0
- data/lib/theme_check/remote_asset_file.rb +44 -0
- data/lib/theme_check/shopify_liquid.rb +1 -0
- data/lib/theme_check/shopify_liquid/tag.rb +16 -0
- data/lib/theme_check/theme.rb +7 -1
- data/lib/theme_check/version.rb +1 -1
- metadata +44 -2
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class CompletionEngine
|
6
|
+
include PositionHelper
|
7
|
+
|
8
|
+
def initialize(storage)
|
9
|
+
@storage = storage
|
10
|
+
@providers = CompletionProvider.all.map(&:new)
|
11
|
+
end
|
12
|
+
|
13
|
+
def completions(name, line, col)
|
14
|
+
buffer = @storage.read(name)
|
15
|
+
cursor = from_line_column_to_index(buffer, line, col)
|
16
|
+
token = find_token(buffer, cursor)
|
17
|
+
return [] if token.nil?
|
18
|
+
|
19
|
+
@providers.flat_map do |p|
|
20
|
+
p.completions(
|
21
|
+
token.content,
|
22
|
+
cursor - token.start
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def find_token(buffer, cursor)
|
28
|
+
Tokens.new(buffer).find do |token|
|
29
|
+
# Here we include the next character and exclude the first
|
30
|
+
# one becase when we want to autocomplete inside a token
|
31
|
+
# and at most 1 outside it since the cursor could be placed
|
32
|
+
# at the end of the token.
|
33
|
+
token.start < cursor && cursor <= token.end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
module CompletionHelper
|
6
|
+
WORD = /\w+/
|
7
|
+
|
8
|
+
def cursor_on_start_content?(content, cursor, regex)
|
9
|
+
content.slice(0, cursor).match?(/#{regex}(?:\s|\n)*$/m)
|
10
|
+
end
|
11
|
+
|
12
|
+
def cursor_on_first_word?(content, cursor)
|
13
|
+
word = content.match(WORD)
|
14
|
+
return false if word.nil?
|
15
|
+
word_start = word.begin(0)
|
16
|
+
word_end = word.end(0)
|
17
|
+
word_start <= cursor && cursor <= word_end
|
18
|
+
end
|
19
|
+
|
20
|
+
def first_word(content)
|
21
|
+
return content.match(WORD)[0] if content.match?(WORD)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class CompletionProvider
|
6
|
+
include CompletionHelper
|
7
|
+
include RegexHelpers
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def all
|
11
|
+
@all ||= []
|
12
|
+
end
|
13
|
+
|
14
|
+
def inherited(subclass)
|
15
|
+
all << subclass
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def completions(content, cursor)
|
20
|
+
raise NotImplementedError
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class FilterCompletionProvider < CompletionProvider
|
6
|
+
NAMED_FILTER = /#{Liquid::FilterSeparator}\s*(\w+)/o
|
7
|
+
|
8
|
+
def completions(content, cursor)
|
9
|
+
return [] unless can_complete?(content, cursor)
|
10
|
+
ShopifyLiquid::Filter.labels
|
11
|
+
.select { |w| w.starts_with?(partial(content, cursor)) }
|
12
|
+
.map { |filter| filter_to_completion(filter) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def can_complete?(content, cursor)
|
16
|
+
content.match?(Liquid::FilterSeparator) && (
|
17
|
+
cursor_on_start_content?(content, cursor, Liquid::FilterSeparator) ||
|
18
|
+
cursor_on_filter?(content, cursor)
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def cursor_on_filter?(content, cursor)
|
25
|
+
return false unless content.match?(NAMED_FILTER)
|
26
|
+
matches(content, NAMED_FILTER).any? do |match|
|
27
|
+
match.begin(1) <= cursor && cursor < match.end(1) + 1 # including next character
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def partial(content, cursor)
|
32
|
+
return '' unless content.match?(NAMED_FILTER)
|
33
|
+
partial_match = matches(content, NAMED_FILTER).find do |match|
|
34
|
+
match.begin(1) <= cursor && cursor < match.end(1) + 1 # including next character
|
35
|
+
end
|
36
|
+
partial_match[1]
|
37
|
+
end
|
38
|
+
|
39
|
+
def filter_to_completion(filter)
|
40
|
+
{
|
41
|
+
label: filter,
|
42
|
+
kind: CompletionItemKinds::FUNCTION,
|
43
|
+
}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class ObjectCompletionProvider < CompletionProvider
|
6
|
+
def completions(content, cursor)
|
7
|
+
return [] unless can_complete?(content, cursor)
|
8
|
+
partial = first_word(content) || ''
|
9
|
+
ShopifyLiquid::Object.labels
|
10
|
+
.select { |w| w.starts_with?(partial) }
|
11
|
+
.map { |object| object_to_completion(object) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def can_complete?(content, cursor)
|
15
|
+
content.match?(Liquid::VariableStart) && (
|
16
|
+
cursor_on_first_word?(content, cursor) ||
|
17
|
+
cursor_on_start_content?(content, cursor, Liquid::VariableStart)
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def object_to_completion(object)
|
24
|
+
{
|
25
|
+
label: object,
|
26
|
+
kind: CompletionItemKinds::VARIABLE,
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class TagCompletionProvider < CompletionProvider
|
6
|
+
def completions(content, cursor)
|
7
|
+
return [] unless can_complete?(content, cursor)
|
8
|
+
partial = first_word(content) || ''
|
9
|
+
ShopifyLiquid::Tag.labels
|
10
|
+
.select { |w| w.starts_with?(partial) }
|
11
|
+
.map { |tag| tag_to_completion(tag) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def can_complete?(content, cursor)
|
15
|
+
content.starts_with?(Liquid::TagStart) && (
|
16
|
+
cursor_on_first_word?(content, cursor) ||
|
17
|
+
cursor_on_start_content?(content, cursor, Liquid::TagStart)
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def tag_to_completion(tag)
|
24
|
+
{
|
25
|
+
label: tag,
|
26
|
+
kind: CompletionItemKinds::KEYWORD,
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -4,9 +4,13 @@ module ThemeCheck
|
|
4
4
|
module LanguageServer
|
5
5
|
class Handler
|
6
6
|
CAPABILITIES = {
|
7
|
+
completionProvider: {
|
8
|
+
triggerCharacters: ['.', '{{ ', '{% '],
|
9
|
+
context: true,
|
10
|
+
},
|
7
11
|
textDocumentSync: {
|
8
12
|
openClose: true,
|
9
|
-
change:
|
13
|
+
change: TextDocumentSyncKind::FULL,
|
10
14
|
willSave: false,
|
11
15
|
save: true,
|
12
16
|
},
|
@@ -15,6 +19,8 @@ module ThemeCheck
|
|
15
19
|
def initialize(server)
|
16
20
|
@server = server
|
17
21
|
@previously_reported_files = Set.new
|
22
|
+
@storage = InMemoryStorage.new
|
23
|
+
@completion_engine = CompletionEngine.new(@storage)
|
18
24
|
end
|
19
25
|
|
20
26
|
def on_initialize(id, params)
|
@@ -31,14 +37,52 @@ module ThemeCheck
|
|
31
37
|
def on_exit(_id, _params)
|
32
38
|
close!
|
33
39
|
end
|
40
|
+
alias_method :on_shutdown, :on_exit
|
41
|
+
|
42
|
+
def on_text_document_did_change(_id, params)
|
43
|
+
uri = text_document_uri(params)
|
44
|
+
@storage.write(uri, content_changes_text(params))
|
45
|
+
end
|
46
|
+
|
47
|
+
def on_text_document_did_close(_id, params)
|
48
|
+
uri = text_document_uri(params)
|
49
|
+
@storage.write(uri, nil)
|
50
|
+
end
|
34
51
|
|
35
52
|
def on_text_document_did_open(_id, params)
|
36
|
-
|
53
|
+
uri = text_document_uri(params)
|
54
|
+
@storage.write(uri, text_document_text(params))
|
55
|
+
analyze_and_send_offenses(uri)
|
56
|
+
end
|
57
|
+
|
58
|
+
def on_text_document_did_save(_id, params)
|
59
|
+
analyze_and_send_offenses(text_document_uri(params))
|
60
|
+
end
|
61
|
+
|
62
|
+
def on_text_document_completion(id, params)
|
63
|
+
uri = text_document_uri(params)
|
64
|
+
line = params.dig('position', 'line')
|
65
|
+
col = params.dig('position', 'character')
|
66
|
+
send_response(
|
67
|
+
id: id,
|
68
|
+
result: completions(uri, line, col)
|
69
|
+
)
|
37
70
|
end
|
38
|
-
alias_method :on_text_document_did_save, :on_text_document_did_open
|
39
71
|
|
40
72
|
private
|
41
73
|
|
74
|
+
def text_document_uri(params)
|
75
|
+
params.dig('textDocument', 'uri').sub('file://', '')
|
76
|
+
end
|
77
|
+
|
78
|
+
def text_document_text(params)
|
79
|
+
params.dig('textDocument', 'text')
|
80
|
+
end
|
81
|
+
|
82
|
+
def content_changes_text(params)
|
83
|
+
params.dig('contentChanges', 0, 'text')
|
84
|
+
end
|
85
|
+
|
42
86
|
def analyze_and_send_offenses(file_path)
|
43
87
|
root = ThemeCheck::Config.find(file_path) || @root_path
|
44
88
|
config = ThemeCheck::Config.from_path(root)
|
@@ -60,6 +104,10 @@ module ThemeCheck
|
|
60
104
|
analyzer.offenses
|
61
105
|
end
|
62
106
|
|
107
|
+
def completions(uri, line, col)
|
108
|
+
@completion_engine.completions(uri, line, col)
|
109
|
+
end
|
110
|
+
|
63
111
|
def send_diagnostics(offenses)
|
64
112
|
reported_files = Set.new
|
65
113
|
|
@@ -90,12 +138,20 @@ module ThemeCheck
|
|
90
138
|
end
|
91
139
|
|
92
140
|
def offense_to_diagnostic(offense)
|
93
|
-
{
|
141
|
+
diagnostic = {
|
142
|
+
code: offense.code_name,
|
143
|
+
message: offense.message,
|
94
144
|
range: range(offense),
|
95
145
|
severity: severity(offense),
|
96
|
-
code: offense.code_name,
|
97
146
|
source: "theme-check",
|
98
|
-
|
147
|
+
}
|
148
|
+
diagnostic["codeDescription"] = code_description(offense) unless offense.doc.nil?
|
149
|
+
diagnostic
|
150
|
+
end
|
151
|
+
|
152
|
+
def code_description(offense)
|
153
|
+
{
|
154
|
+
href: offense.doc,
|
99
155
|
}
|
100
156
|
end
|
101
157
|
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# Note: Everything is 0-indexed here.
|
3
|
+
|
4
|
+
module ThemeCheck
|
5
|
+
module LanguageServer
|
6
|
+
module PositionHelper
|
7
|
+
def from_line_column_to_index(content, row, col)
|
8
|
+
i = 0
|
9
|
+
result = 0
|
10
|
+
lines = content.lines
|
11
|
+
while i < row
|
12
|
+
result += lines[i].size
|
13
|
+
i += 1
|
14
|
+
end
|
15
|
+
result += col
|
16
|
+
result
|
17
|
+
end
|
18
|
+
|
19
|
+
def from_index_to_line_column(content, index)
|
20
|
+
lines = content[0..index].lines
|
21
|
+
row = lines.size - 1
|
22
|
+
col = lines.last.size - 1
|
23
|
+
[row, col]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# Here we define the Language Server Protocol Constants we're using.
|
3
|
+
# For complete docs, see the following:
|
4
|
+
# https://microsoft.github.io/language-server-protocol/specifications/specification-current
|
5
|
+
module ThemeCheck
|
6
|
+
module LanguageServer
|
7
|
+
module CompletionItemKinds
|
8
|
+
TEXT = 1
|
9
|
+
METHOD = 2
|
10
|
+
FUNCTION = 3
|
11
|
+
CONSTRUCTOR = 4
|
12
|
+
FIELD = 5
|
13
|
+
VARIABLE = 6
|
14
|
+
CLASS = 7
|
15
|
+
INTERFACE = 8
|
16
|
+
MODULE = 9
|
17
|
+
PROPERTY = 10
|
18
|
+
UNIT = 11
|
19
|
+
VALUE = 12
|
20
|
+
ENUM = 13
|
21
|
+
KEYWORD = 14
|
22
|
+
SNIPPET = 15
|
23
|
+
COLOR = 16
|
24
|
+
FILE = 17
|
25
|
+
REFERENCE = 18
|
26
|
+
FOLDER = 19
|
27
|
+
ENUM_MEMBER = 20
|
28
|
+
CONSTANT = 21
|
29
|
+
STRUCT = 22
|
30
|
+
EVENT = 23
|
31
|
+
OPERATOR = 24
|
32
|
+
TYPE_PARAMETER = 25
|
33
|
+
end
|
34
|
+
|
35
|
+
module TextDocumentSyncKind
|
36
|
+
NONE = 0
|
37
|
+
FULL = 1
|
38
|
+
INCREMENTAL = 2
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -10,11 +10,13 @@ module ThemeCheck
|
|
10
10
|
|
11
11
|
class Server
|
12
12
|
attr_reader :handler
|
13
|
+
attr_reader :should_raise_errors
|
13
14
|
|
14
15
|
def initialize(
|
15
16
|
in_stream: STDIN,
|
16
17
|
out_stream: STDOUT,
|
17
|
-
err_stream: $DEBUG ? File.open('/tmp/lsp.log', 'a') : STDERR
|
18
|
+
err_stream: $DEBUG ? File.open('/tmp/lsp.log', 'a') : STDERR,
|
19
|
+
should_raise_errors: false
|
18
20
|
)
|
19
21
|
validate!([in_stream, out_stream, err_stream])
|
20
22
|
|
@@ -25,6 +27,8 @@ module ThemeCheck
|
|
25
27
|
|
26
28
|
@out.sync = true # do not buffer
|
27
29
|
@err.sync = true # do not buffer
|
30
|
+
|
31
|
+
@should_raise_errors = should_raise_errors
|
28
32
|
end
|
29
33
|
|
30
34
|
def listen
|
@@ -37,6 +41,7 @@ module ThemeCheck
|
|
37
41
|
return 0
|
38
42
|
|
39
43
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
44
|
+
raise e if should_raise_errors
|
40
45
|
log(e)
|
41
46
|
log(e.backtrace)
|
42
47
|
return 1
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
Token = Struct.new(
|
5
|
+
:content,
|
6
|
+
:start, # inclusive
|
7
|
+
:end, # exclusive
|
8
|
+
)
|
9
|
+
|
10
|
+
TAG_START = Liquid::TagStart
|
11
|
+
TAG_END = Liquid::TagEnd
|
12
|
+
VARIABLE_START = Liquid::VariableStart
|
13
|
+
VARIABLE_END = Liquid::VariableEnd
|
14
|
+
SPLITTER = %r{
|
15
|
+
(?=(?:#{TAG_START}|#{VARIABLE_START}))| # positive lookahead on tag/variable start
|
16
|
+
(?<=(?:#{TAG_END}|#{VARIABLE_END})) # positive lookbehind on tag/variable end
|
17
|
+
}xom
|
18
|
+
|
19
|
+
# Implemented as an Enumerable so we stop iterating on the find once
|
20
|
+
# we have what we want. Kind of a perf thing.
|
21
|
+
class Tokens
|
22
|
+
include Enumerable
|
23
|
+
|
24
|
+
def initialize(buffer)
|
25
|
+
@buffer = buffer
|
26
|
+
end
|
27
|
+
|
28
|
+
def each(&block)
|
29
|
+
return to_enum(:each) unless block_given?
|
30
|
+
|
31
|
+
chunks = @buffer.split(SPLITTER)
|
32
|
+
chunks.shift if chunks[0]&.empty?
|
33
|
+
|
34
|
+
prev = Token.new('', 0, 0)
|
35
|
+
curr = Token.new('', 0, 0)
|
36
|
+
|
37
|
+
while (content = chunks.shift)
|
38
|
+
|
39
|
+
curr.start = prev.end
|
40
|
+
curr.end = curr.start + content.size
|
41
|
+
|
42
|
+
block.call(Token.new(
|
43
|
+
content,
|
44
|
+
curr.start,
|
45
|
+
curr.end,
|
46
|
+
))
|
47
|
+
|
48
|
+
# recycling structs
|
49
|
+
tmp = prev
|
50
|
+
prev = curr
|
51
|
+
curr = tmp
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|