code-ruby 1.8.15 → 1.8.16
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/Gemfile.lock +1 -1
- data/VERSION +1 -1
- data/applies +0 -0
- data/bin/code +12 -2
- data/lib/code/format.rb +630 -0
- data/lib/code.rb +11 -0
- data/spec/bin/code_spec.rb +16 -0
- data/spec/code/format_spec.rb +45 -0
- data/spec/code_spec.rb +39 -3
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 103fdb30c9cb28f7a771add31e5b21d71e2f397476b117b3ae90ab99d7ed902d
|
|
4
|
+
data.tar.gz: 6a3ab9cbff25405b6179ee7bd7e0b316cd4930e906abb13eef575b5bc3b4dc59
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ce5a8cabe4a1adaad31e04d101d1a883731c71b60b0b3279186a74b9571b041f10792ed16767245352ac5cc7905402d2eec4f5ba6ed952ae8e7000f333d9976f
|
|
7
|
+
data.tar.gz: a80c30b44e0ff16742a0c9620a43ed51e2ee8083dfeb09809b4ac5a2458816654f708855049e75c21310f5b779bb78c013033d34bc8c417dd2e4f4db8db9c306
|
data/Gemfile.lock
CHANGED
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.8.
|
|
1
|
+
1.8.16
|
data/applies
ADDED
|
File without changes
|
data/bin/code
CHANGED
|
@@ -6,6 +6,10 @@ require "dorian/arguments"
|
|
|
6
6
|
|
|
7
7
|
parsed =
|
|
8
8
|
Dorian::Arguments.parse(
|
|
9
|
+
format: {
|
|
10
|
+
type: :boolean,
|
|
11
|
+
alias: :f
|
|
12
|
+
},
|
|
9
13
|
input: {
|
|
10
14
|
type: :string,
|
|
11
15
|
alias: :i
|
|
@@ -43,14 +47,20 @@ require "ruby-prof" if profile
|
|
|
43
47
|
|
|
44
48
|
RubyProf.start if profile
|
|
45
49
|
|
|
46
|
-
input =
|
|
50
|
+
input = STDIN.each_line.to_a.join if input.empty?
|
|
47
51
|
|
|
48
52
|
if parsed.options.parse
|
|
49
53
|
begin
|
|
50
54
|
pp Code::Parser.parse(input).to_raw
|
|
51
|
-
rescue
|
|
55
|
+
rescue StandardError => e
|
|
52
56
|
warn e.message
|
|
53
57
|
end
|
|
58
|
+
elsif parsed.options.format
|
|
59
|
+
begin
|
|
60
|
+
print(Code.format(input, timeout: parsed.options.timeout))
|
|
61
|
+
rescue Code::Error => e
|
|
62
|
+
warn "#{e.class}: #{e.message}"
|
|
63
|
+
end
|
|
54
64
|
else
|
|
55
65
|
begin
|
|
56
66
|
print(
|
data/lib/code/format.rb
ADDED
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Code
|
|
4
|
+
class Format
|
|
5
|
+
INDENT = " "
|
|
6
|
+
MAX_LINE_LENGTH = 80
|
|
7
|
+
MIN_WIDTH = 20
|
|
8
|
+
MAX_INLINE_STRING_LENGTH = 50
|
|
9
|
+
MAX_INLINE_COLLECTION_LENGTH = 40
|
|
10
|
+
MAX_INLINE_COLLECTION_ITEMS = 3
|
|
11
|
+
MAX_INLINE_CALL_ARGUMENTS_LENGTH = 80
|
|
12
|
+
MAX_INLINE_BLOCK_BODY_LENGTH = 40
|
|
13
|
+
CONTINUATION_PADDING = 4
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def format(parsed)
|
|
17
|
+
new(parsed).format
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(parsed)
|
|
22
|
+
@parsed = parsed || []
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def format
|
|
26
|
+
enforce_line_width(format_code(@parsed, indent: 0))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def format_code(code, indent:, inline: false)
|
|
32
|
+
statements = Array(code)
|
|
33
|
+
return "" if statements.empty?
|
|
34
|
+
|
|
35
|
+
separator = statement_separator(inline: inline, indent: indent)
|
|
36
|
+
|
|
37
|
+
statements
|
|
38
|
+
.map do |statement|
|
|
39
|
+
formatted = format_statement(statement, indent: indent)
|
|
40
|
+
pre_indented_statement?(statement) ? formatted : "#{INDENT * indent}#{formatted}"
|
|
41
|
+
end
|
|
42
|
+
.join(separator)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def format_code_inline(code, indent:)
|
|
46
|
+
statements = Array(code)
|
|
47
|
+
return "nothing" if statements.empty?
|
|
48
|
+
return format_statement(statements.first, indent: indent) if statements.one?
|
|
49
|
+
|
|
50
|
+
body = format_code(statements, indent: indent + 1)
|
|
51
|
+
"begin\n#{body}\n#{INDENT * indent}end"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def format_statement(statement, indent:)
|
|
55
|
+
if statement.is_a?(Hash) && statement.key?(:nothing)
|
|
56
|
+
statement[:nothing].presence || "nothing"
|
|
57
|
+
elsif statement.is_a?(Hash) && statement.key?(:boolean)
|
|
58
|
+
statement[:boolean]
|
|
59
|
+
elsif statement.is_a?(Hash) && statement.key?(:group)
|
|
60
|
+
"(#{format_code_inline(statement[:group], indent: indent)})"
|
|
61
|
+
elsif statement.is_a?(Hash) && statement.key?(:call)
|
|
62
|
+
format_call(statement[:call], indent: indent)
|
|
63
|
+
elsif statement.is_a?(Hash) && statement.key?(:number)
|
|
64
|
+
format_number(statement[:number], indent: indent)
|
|
65
|
+
elsif statement.is_a?(Hash) && statement.key?(:string)
|
|
66
|
+
format_string(statement[:string], indent: indent)
|
|
67
|
+
elsif statement.is_a?(Hash) && statement.key?(:list)
|
|
68
|
+
format_list(statement[:list], indent: indent)
|
|
69
|
+
elsif statement.is_a?(Hash) && statement.key?(:dictionnary)
|
|
70
|
+
format_dictionary(statement[:dictionnary], indent: indent)
|
|
71
|
+
elsif statement.is_a?(Hash) && statement.key?(:left_operation)
|
|
72
|
+
format_left_operation(statement[:left_operation], indent: indent)
|
|
73
|
+
elsif statement.is_a?(Hash) && statement.key?(:right_operation)
|
|
74
|
+
format_right_operation(statement[:right_operation], indent: indent)
|
|
75
|
+
elsif statement.is_a?(Hash) && statement.key?(:function)
|
|
76
|
+
format_function(statement[:function], indent: indent)
|
|
77
|
+
elsif statement.is_a?(Hash) && statement.key?(:negation)
|
|
78
|
+
format_prefixed(statement[:negation], indent: indent)
|
|
79
|
+
elsif statement.is_a?(Hash) && statement.key?(:unary_minus)
|
|
80
|
+
format_prefixed(statement[:unary_minus], indent: indent)
|
|
81
|
+
elsif statement.is_a?(Hash) && statement.key?(:ternary)
|
|
82
|
+
format_ternary(statement[:ternary], indent: indent)
|
|
83
|
+
elsif statement.is_a?(Hash) && statement.key?(:not)
|
|
84
|
+
format_not(statement[:not], indent: indent)
|
|
85
|
+
elsif statement.is_a?(Hash) && statement.key?(:if)
|
|
86
|
+
format_if(statement[:if], indent: indent)
|
|
87
|
+
elsif statement.is_a?(Hash) && statement.key?(:while)
|
|
88
|
+
format_while(statement[:while], indent: indent)
|
|
89
|
+
elsif statement.is_a?(Hash) && statement.key?(:splat)
|
|
90
|
+
format_splat(statement[:splat], indent: indent)
|
|
91
|
+
elsif statement.is_a?(Hash) && statement.key?(:square_bracket)
|
|
92
|
+
format_square_bracket(statement[:square_bracket], indent: indent)
|
|
93
|
+
else
|
|
94
|
+
"nothing"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def format_number(number, indent:)
|
|
99
|
+
return "0" unless number.is_a?(Hash)
|
|
100
|
+
|
|
101
|
+
if number.key?(:decimal)
|
|
102
|
+
format_decimal(number[:decimal], indent: indent)
|
|
103
|
+
elsif number.key?(:base_16)
|
|
104
|
+
"0x#{number[:base_16]}"
|
|
105
|
+
elsif number.key?(:base_8)
|
|
106
|
+
"0o#{number[:base_8]}"
|
|
107
|
+
elsif number.key?(:base_2)
|
|
108
|
+
"0b#{number[:base_2]}"
|
|
109
|
+
elsif number.key?(:base_10)
|
|
110
|
+
format_base_10(number[:base_10], indent: indent)
|
|
111
|
+
else
|
|
112
|
+
"0"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def format_base_10(base_10, indent:)
|
|
117
|
+
return "0" unless base_10.is_a?(Hash)
|
|
118
|
+
|
|
119
|
+
whole = base_10[:whole] || "0"
|
|
120
|
+
exponent = base_10[:exponent]
|
|
121
|
+
return whole unless exponent
|
|
122
|
+
|
|
123
|
+
"#{whole}e#{format_nested_statement(exponent, indent: indent)}"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def format_decimal(decimal, indent:)
|
|
127
|
+
return "0.0" unless decimal.is_a?(Hash)
|
|
128
|
+
|
|
129
|
+
raw = decimal[:decimal] || "0.0"
|
|
130
|
+
exponent = decimal[:exponent]
|
|
131
|
+
return raw unless exponent
|
|
132
|
+
|
|
133
|
+
"#{raw}e#{format_nested_statement(exponent, indent: indent)}"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def format_string(parts, indent:)
|
|
137
|
+
return '""' if parts == "" || parts.nil?
|
|
138
|
+
symbol = symbolizable_string(parts)
|
|
139
|
+
return symbol if symbol
|
|
140
|
+
|
|
141
|
+
# Always use double-quoted strings for a single canonical output.
|
|
142
|
+
components =
|
|
143
|
+
Array(parts).map do |part|
|
|
144
|
+
if part.is_a?(Hash) && part.key?(:text)
|
|
145
|
+
{ type: :text, value: escape_string_text(part[:text].to_s) }
|
|
146
|
+
elsif part.is_a?(Hash) && part.key?(:code)
|
|
147
|
+
{ type: :code, value: "{#{format_code_inline(part[:code], indent: indent)}}" }
|
|
148
|
+
else
|
|
149
|
+
{ type: :text, value: escape_string_text(part.to_s) }
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
content = components.map { |component| component[:value] }.join
|
|
153
|
+
|
|
154
|
+
format_string_literal(
|
|
155
|
+
content,
|
|
156
|
+
components: components,
|
|
157
|
+
indent: indent,
|
|
158
|
+
allow_split: true
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def symbolizable_string(parts)
|
|
163
|
+
nodes = Array(parts)
|
|
164
|
+
return nil unless nodes.one?
|
|
165
|
+
|
|
166
|
+
node = nodes.first
|
|
167
|
+
return nil unless node.is_a?(Hash) && node.key?(:text)
|
|
168
|
+
|
|
169
|
+
text = node[:text].to_s
|
|
170
|
+
return nil unless text.match?(/\A[a-z_][a-z0-9_]*\z/)
|
|
171
|
+
|
|
172
|
+
":#{text}"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def escape_string_text(text)
|
|
176
|
+
text
|
|
177
|
+
.gsub("\\", "\\\\")
|
|
178
|
+
.gsub('"', '\"')
|
|
179
|
+
.gsub("{", "\\{")
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def format_string_literal(content, components:, indent:, allow_split:)
|
|
183
|
+
literal = %("#{content}")
|
|
184
|
+
return literal if literal.length <= string_inline_limit(indent)
|
|
185
|
+
return literal unless allow_split
|
|
186
|
+
|
|
187
|
+
split_string_literal(components, indent: indent)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def split_string_literal(components, indent:)
|
|
191
|
+
chunks = split_string_chunks(components, chunk_limit(indent))
|
|
192
|
+
return %("#{chunks.first}") if chunks.one?
|
|
193
|
+
|
|
194
|
+
continuation_indent = INDENT * (indent + 1)
|
|
195
|
+
lines = chunks.map { |chunk| %("#{chunk}") }
|
|
196
|
+
|
|
197
|
+
([lines.first] +
|
|
198
|
+
lines[1..].to_a.map { |line| "#{continuation_indent}+ #{line}" })
|
|
199
|
+
.join("\n")
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def split_string_chunks(components, limit)
|
|
203
|
+
units =
|
|
204
|
+
components.flat_map do |component|
|
|
205
|
+
value = component[:value].to_s
|
|
206
|
+
if component[:type] == :code
|
|
207
|
+
[value]
|
|
208
|
+
else
|
|
209
|
+
value.split(/(\s+)/)
|
|
210
|
+
end
|
|
211
|
+
end.reject(&:empty?)
|
|
212
|
+
|
|
213
|
+
chunks = [""]
|
|
214
|
+
units.each do |unit|
|
|
215
|
+
if unit.length > limit
|
|
216
|
+
if chunks.last.empty?
|
|
217
|
+
segments = unit.scan(/.{1,#{limit}}/m)
|
|
218
|
+
chunks[-1] = segments.shift.to_s
|
|
219
|
+
segments.each { |segment| chunks << segment }
|
|
220
|
+
else
|
|
221
|
+
chunks << unit
|
|
222
|
+
end
|
|
223
|
+
next
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
current = chunks.last
|
|
227
|
+
candidate = "#{current}#{unit}"
|
|
228
|
+
if !current.empty? && candidate.length > limit
|
|
229
|
+
chunks[-1] = current
|
|
230
|
+
chunks << unit
|
|
231
|
+
else
|
|
232
|
+
chunks[-1] = candidate
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
chunks.reject(&:empty?)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def string_inline_limit(indent)
|
|
240
|
+
[MIN_WIDTH, [MAX_INLINE_STRING_LENGTH, MAX_LINE_LENGTH - (INDENT * indent).length].min].max
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def chunk_limit(indent)
|
|
244
|
+
[MIN_WIDTH,
|
|
245
|
+
[MAX_INLINE_STRING_LENGTH,
|
|
246
|
+
MAX_LINE_LENGTH - (INDENT * (indent + 1)).length - CONTINUATION_PADDING].min].max
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def format_list(elements, indent:)
|
|
250
|
+
return "[]" if elements == "" || elements.nil?
|
|
251
|
+
|
|
252
|
+
values =
|
|
253
|
+
Array(elements).map { |element| format_code_inline(element, indent: 0) }
|
|
254
|
+
return "[#{values.join(', ')}]" unless multiline_collection?(values)
|
|
255
|
+
|
|
256
|
+
body = values.map { |value| indent_lines(value, indent + 1) }.join(",\n")
|
|
257
|
+
"[\n#{body}\n#{INDENT * indent}]"
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def format_dictionary(key_values, indent:)
|
|
261
|
+
return "{}" if key_values == "" || key_values.nil?
|
|
262
|
+
|
|
263
|
+
values =
|
|
264
|
+
Array(key_values).map do |key_value|
|
|
265
|
+
if key_value.is_a?(Hash) && key_value.key?(:name_code)
|
|
266
|
+
format_dictionary_name_code(key_value[:name_code])
|
|
267
|
+
elsif key_value.is_a?(Hash) && key_value.key?(:statement_code)
|
|
268
|
+
format_dictionary_statement_code(key_value[:statement_code])
|
|
269
|
+
elsif key_value.is_a?(Hash) && key_value.key?(:code)
|
|
270
|
+
format_code_inline(key_value[:code], indent: 0)
|
|
271
|
+
else
|
|
272
|
+
format_code_inline([key_value], indent: 0)
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
return "{ #{values.join(', ')} }" unless multiline_collection?(values)
|
|
277
|
+
|
|
278
|
+
body = values.map { |value| indent_lines(value, indent + 1) }.join(",\n")
|
|
279
|
+
"{\n#{body}\n#{INDENT * indent}}"
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def format_dictionary_name_code(name_code)
|
|
283
|
+
name = name_code[:name]
|
|
284
|
+
return "#{name}:" unless name_code.key?(:code)
|
|
285
|
+
|
|
286
|
+
value = format_code_inline(name_code[:code], indent: 0)
|
|
287
|
+
"#{name}: #{value}"
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def format_dictionary_statement_code(statement_code)
|
|
291
|
+
key = format_nested_statement(statement_code[:statement], indent: 0)
|
|
292
|
+
return key unless statement_code.key?(:code)
|
|
293
|
+
|
|
294
|
+
value = format_code_inline(statement_code[:code], indent: 0)
|
|
295
|
+
"#{key}: #{value}"
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def format_call(call, indent:)
|
|
299
|
+
name = call[:name]
|
|
300
|
+
raw_arguments = Array(call[:arguments])
|
|
301
|
+
arguments = raw_arguments.map { |arg| format_call_argument(arg) }
|
|
302
|
+
statement =
|
|
303
|
+
if arguments.empty?
|
|
304
|
+
name.to_s
|
|
305
|
+
elsif multiline_call_arguments?(raw_arguments, arguments)
|
|
306
|
+
body = arguments.map { |arg| indent_lines(arg, indent + 1) }.join(",\n")
|
|
307
|
+
"#{name}(\n#{body}\n#{INDENT * indent})"
|
|
308
|
+
else
|
|
309
|
+
"#{name}(#{arguments.join(', ')})"
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
return statement unless call.key?(:block)
|
|
313
|
+
|
|
314
|
+
"#{statement} #{format_block(call[:block], indent: indent)}"
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def format_call_argument(argument)
|
|
318
|
+
value = format_code_inline(argument[:value], indent: 0)
|
|
319
|
+
return value unless argument.key?(:name)
|
|
320
|
+
|
|
321
|
+
"#{argument[:name]}: #{value}"
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def format_block(block, indent:)
|
|
325
|
+
parameters = Array(block[:parameters]).map { |parameter| format_parameter(parameter, indent: indent) }
|
|
326
|
+
inline_body = format_inline_block_body(block[:body], indent: indent)
|
|
327
|
+
if inline_body
|
|
328
|
+
prefix = parameters.empty? ? "" : " |#{parameters.join(', ')}|"
|
|
329
|
+
return "{#{prefix} #{inline_body} }"
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
header = parameters.empty? ? "{" : "{ |#{parameters.join(', ')}|"
|
|
333
|
+
body = format_code(Array(block[:body]), indent: indent + 1)
|
|
334
|
+
"#{header}\n#{body}\n#{INDENT * indent}}"
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def format_function(function, indent:)
|
|
338
|
+
parameters = Array(function[:parameters]).map { |parameter| format_parameter(parameter, indent: indent) }
|
|
339
|
+
body = format_code(Array(function[:body]), indent: indent + 1)
|
|
340
|
+
"(#{parameters.join(', ')}) => {\n#{body}\n#{INDENT * indent}}"
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def format_parameter(parameter, indent:)
|
|
344
|
+
return "" unless parameter.is_a?(Hash)
|
|
345
|
+
|
|
346
|
+
prefix =
|
|
347
|
+
if parameter.key?(:keyword_splat)
|
|
348
|
+
parameter[:keyword_splat]
|
|
349
|
+
elsif parameter.key?(:regular_splat)
|
|
350
|
+
parameter[:regular_splat]
|
|
351
|
+
elsif parameter.key?(:spread)
|
|
352
|
+
parameter[:spread]
|
|
353
|
+
elsif parameter.key?(:block)
|
|
354
|
+
parameter[:block]
|
|
355
|
+
else
|
|
356
|
+
""
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
name = parameter[:name].to_s
|
|
360
|
+
left = "#{prefix}#{name}"
|
|
361
|
+
|
|
362
|
+
if parameter.key?(:keyword)
|
|
363
|
+
default = parameter[:default]
|
|
364
|
+
return "#{name}:" unless default
|
|
365
|
+
|
|
366
|
+
return "#{name}: #{format_code_inline(default, indent: indent)}"
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
default = parameter[:default]
|
|
370
|
+
return left unless default
|
|
371
|
+
|
|
372
|
+
"#{left} = #{format_code_inline(default, indent: indent)}"
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def format_left_operation(operation, indent:)
|
|
376
|
+
expression = format_nested_statement(operation[:first], indent: indent)
|
|
377
|
+
|
|
378
|
+
Array(operation[:others]).each do |other|
|
|
379
|
+
right = format_nested_statement(other[:statement], indent: indent)
|
|
380
|
+
operator = other[:operator]
|
|
381
|
+
|
|
382
|
+
expression =
|
|
383
|
+
if compact_operator?(operator)
|
|
384
|
+
"#{expression}#{operator}#{right}"
|
|
385
|
+
else
|
|
386
|
+
candidate = "#{expression} #{operator} #{right}"
|
|
387
|
+
if expression.include?("\n") || candidate.length > MAX_LINE_LENGTH
|
|
388
|
+
right_parts = right.split(" #{operator} ")
|
|
389
|
+
continuation_lines =
|
|
390
|
+
right_parts.map do |part|
|
|
391
|
+
"#{INDENT * (indent + 1)}#{operator} #{part}"
|
|
392
|
+
end
|
|
393
|
+
"#{expression}\n#{continuation_lines.join("\n")}"
|
|
394
|
+
else
|
|
395
|
+
candidate
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
expression
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def format_right_operation(operation, indent:)
|
|
404
|
+
operator = operation[:operator].to_s
|
|
405
|
+
left = format_nested_statement(operation[:left], indent: indent)
|
|
406
|
+
right = format_nested_statement(operation[:right], indent: indent)
|
|
407
|
+
"#{left} #{operator} #{right}"
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def compact_operator?(operator)
|
|
411
|
+
[".", "::", "&.", "..", "..."].include?(operator)
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def format_ternary(ternary, indent:)
|
|
415
|
+
left = format_nested_statement(ternary[:left], indent: indent)
|
|
416
|
+
middle = format_nested_statement(ternary[:middle], indent: indent)
|
|
417
|
+
return "#{left} ? #{middle}" unless ternary.key?(:right)
|
|
418
|
+
|
|
419
|
+
right = format_nested_statement(ternary[:right], indent: indent)
|
|
420
|
+
"#{left} ? #{middle} : #{right}"
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def format_not(not_statement, indent:)
|
|
424
|
+
right = format_nested_statement(not_statement[:right], indent: indent)
|
|
425
|
+
"#{not_statement[:operator]} #{right}"
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def format_prefixed(statement, indent:)
|
|
429
|
+
right = format_nested_statement(statement[:right], indent: indent)
|
|
430
|
+
"#{statement[:operator]}#{right}"
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def format_splat(statement, indent:)
|
|
434
|
+
right = format_nested_statement(statement[:right], indent: indent)
|
|
435
|
+
"#{statement[:operator]}#{right}"
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def format_square_bracket(square_bracket, indent:)
|
|
439
|
+
left = format_nested_statement(square_bracket[:left], indent: indent)
|
|
440
|
+
suffix =
|
|
441
|
+
Array(square_bracket[:statements]).map do |statement|
|
|
442
|
+
"[#{format_nested_statement(statement, indent: indent)}]"
|
|
443
|
+
end.join
|
|
444
|
+
"#{left}#{suffix}"
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def format_if(if_statement, indent:)
|
|
448
|
+
lines = []
|
|
449
|
+
first_operator = if_statement[:first_operator]
|
|
450
|
+
first_statement =
|
|
451
|
+
format_nested_statement(if_statement[:first_statement], indent: indent)
|
|
452
|
+
lines << "#{INDENT * indent}#{first_operator} #{first_statement}"
|
|
453
|
+
lines << format_code(if_statement[:first_body], indent: indent + 1)
|
|
454
|
+
|
|
455
|
+
Array(if_statement[:elses]).each do |branch|
|
|
456
|
+
lines.concat(format_if_branch(branch, indent: indent))
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
lines << "#{INDENT * indent}end"
|
|
460
|
+
lines.join("\n")
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def format_if_branch(branch, indent:)
|
|
464
|
+
operator = branch[:operator]
|
|
465
|
+
|
|
466
|
+
case operator
|
|
467
|
+
when "elsif", "elsunless"
|
|
468
|
+
statement = format_nested_statement(branch[:statement], indent: indent)
|
|
469
|
+
[
|
|
470
|
+
"#{INDENT * indent}#{operator} #{statement}",
|
|
471
|
+
format_code(branch[:body], indent: indent + 1)
|
|
472
|
+
]
|
|
473
|
+
when "if", "unless"
|
|
474
|
+
statement = format_nested_statement(branch[:statement], indent: indent)
|
|
475
|
+
[
|
|
476
|
+
"#{INDENT * indent}else #{operator} #{statement}",
|
|
477
|
+
format_code(branch[:body], indent: indent + 1)
|
|
478
|
+
]
|
|
479
|
+
else
|
|
480
|
+
["#{INDENT * indent}else", format_code(branch[:body], indent: indent + 1)]
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def format_while(while_statement, indent:)
|
|
485
|
+
operator = while_statement[:operator]
|
|
486
|
+
|
|
487
|
+
if operator == "loop"
|
|
488
|
+
body = format_code(while_statement[:body], indent: indent + 1)
|
|
489
|
+
return "#{INDENT * indent}loop {\n#{body}\n#{INDENT * indent}}"
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
statement = format_nested_statement(while_statement[:statement], indent: indent)
|
|
493
|
+
body = format_code(while_statement[:body], indent: indent + 1)
|
|
494
|
+
"#{INDENT * indent}#{operator} #{statement}\n#{body}\n#{INDENT * indent}end"
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def format_nested_statement(statement, indent:)
|
|
498
|
+
format_statement(statement, indent: indent)
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def multiline_collection?(values)
|
|
502
|
+
return true if values.any? { |value| value.include?("\n") }
|
|
503
|
+
return true if values.size > MAX_INLINE_COLLECTION_ITEMS
|
|
504
|
+
|
|
505
|
+
values.join(", ").length > MAX_INLINE_COLLECTION_LENGTH
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def multiline_call_arguments?(raw_arguments, arguments)
|
|
509
|
+
return true if arguments.any? { |argument| argument.include?("\n") }
|
|
510
|
+
return true if arguments.size > MAX_INLINE_COLLECTION_ITEMS
|
|
511
|
+
return true if arguments.join(", ").length > MAX_INLINE_CALL_ARGUMENTS_LENGTH
|
|
512
|
+
|
|
513
|
+
raw_arguments.any? do |argument|
|
|
514
|
+
named_value = argument[:value]
|
|
515
|
+
next false unless named_value.is_a?(Array)
|
|
516
|
+
|
|
517
|
+
named_value.any? do |statement|
|
|
518
|
+
statement.is_a?(Hash) &&
|
|
519
|
+
(statement.key?(:dictionnary) || statement.key?(:list))
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def indent_lines(value, indent)
|
|
525
|
+
prefix = INDENT * indent
|
|
526
|
+
value.split("\n").map { |line| "#{prefix}#{line}" }.join("\n")
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def statement_separator(inline:, indent: _indent)
|
|
530
|
+
return " " if inline
|
|
531
|
+
"\n\n"
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def pre_indented_statement?(statement)
|
|
535
|
+
statement.is_a?(Hash) && (statement.key?(:if) || statement.key?(:while))
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def format_inline_block_body(body, indent:)
|
|
539
|
+
statements = Array(body)
|
|
540
|
+
return nil unless statements.one?
|
|
541
|
+
|
|
542
|
+
formatted = format_code_inline(statements, indent: indent)
|
|
543
|
+
return nil if formatted.include?("\n")
|
|
544
|
+
return nil if formatted.length > MAX_INLINE_BLOCK_BODY_LENGTH
|
|
545
|
+
|
|
546
|
+
formatted
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def enforce_line_width(formatted)
|
|
550
|
+
formatted
|
|
551
|
+
.split("\n")
|
|
552
|
+
.flat_map { |line| wrap_line(line) }
|
|
553
|
+
.join("\n")
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
def wrap_line(line)
|
|
557
|
+
return [line] if line.length <= MAX_LINE_LENGTH
|
|
558
|
+
|
|
559
|
+
indent = line[/\A */].to_s
|
|
560
|
+
split =
|
|
561
|
+
find_split(line, " and ") ||
|
|
562
|
+
find_split(line, " or ") ||
|
|
563
|
+
find_split(line, " ? ") ||
|
|
564
|
+
find_split(line, " : ") ||
|
|
565
|
+
find_split(line, " <=> ") ||
|
|
566
|
+
find_split(line, " >= ") ||
|
|
567
|
+
find_split(line, " <= ") ||
|
|
568
|
+
find_split(line, " == ") ||
|
|
569
|
+
find_split(line, " = ") ||
|
|
570
|
+
find_split(line, ".")
|
|
571
|
+
|
|
572
|
+
return [line] unless split
|
|
573
|
+
|
|
574
|
+
index, token = split
|
|
575
|
+
left = line[0...index]
|
|
576
|
+
right = line[(index + token.length)..]
|
|
577
|
+
continuation = "#{indent}#{INDENT}#{right.lstrip}"
|
|
578
|
+
|
|
579
|
+
first_line =
|
|
580
|
+
if token == "."
|
|
581
|
+
left
|
|
582
|
+
else
|
|
583
|
+
"#{left}#{token.rstrip}"
|
|
584
|
+
end
|
|
585
|
+
second_line =
|
|
586
|
+
if token == "."
|
|
587
|
+
"#{indent}#{INDENT}.#{right}"
|
|
588
|
+
else
|
|
589
|
+
continuation
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
[first_line, *wrap_line(second_line)]
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def find_split(line, token)
|
|
596
|
+
return nil unless line.length > MAX_LINE_LENGTH
|
|
597
|
+
|
|
598
|
+
search_limit = [MAX_LINE_LENGTH, line.length - token.length].min
|
|
599
|
+
index = line.rindex(token, search_limit)
|
|
600
|
+
while index
|
|
601
|
+
break if index.positive? && outside_string?(line, index)
|
|
602
|
+
|
|
603
|
+
index = line.rindex(token, index - 1)
|
|
604
|
+
end
|
|
605
|
+
return nil unless index
|
|
606
|
+
|
|
607
|
+
[index, token]
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
def outside_string?(line, index)
|
|
611
|
+
quote_count = 0
|
|
612
|
+
escaped = false
|
|
613
|
+
line[0...index].each_char do |char|
|
|
614
|
+
if escaped
|
|
615
|
+
escaped = false
|
|
616
|
+
next
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
if char == "\\"
|
|
620
|
+
escaped = true
|
|
621
|
+
elsif char == '"'
|
|
622
|
+
quote_count += 1
|
|
623
|
+
end
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
quote_count.even?
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
end
|
|
630
|
+
end
|
data/lib/code.rb
CHANGED
|
@@ -32,6 +32,17 @@ class Code
|
|
|
32
32
|
new(...).evaluate
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
def self.format(source_or_tree, timeout: DEFAULT_TIMEOUT)
|
|
36
|
+
parse_tree =
|
|
37
|
+
if source_or_tree.is_a?(::String)
|
|
38
|
+
parse(source_or_tree, timeout: timeout)
|
|
39
|
+
else
|
|
40
|
+
source_or_tree
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
Format.format(parse_tree)
|
|
44
|
+
end
|
|
45
|
+
|
|
35
46
|
def evaluate
|
|
36
47
|
Timeout.timeout(timeout) do
|
|
37
48
|
Node::Code.new(Code.parse(source)).evaluate(
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "spec_helper"
|
|
5
|
+
|
|
6
|
+
RSpec.describe "bin/code" do
|
|
7
|
+
let(:bin) { File.expand_path("../../bin/code", __dir__) }
|
|
8
|
+
|
|
9
|
+
it "formats input with -f" do
|
|
10
|
+
stdout, stderr, status = Open3.capture3(bin, "-f", "{a:1}")
|
|
11
|
+
|
|
12
|
+
expect(status.success?).to be(true)
|
|
13
|
+
expect(stderr).to eq("")
|
|
14
|
+
expect(stdout).to eq("{ a: 1 }")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Code::Format do
|
|
7
|
+
describe ".format" do
|
|
8
|
+
[
|
|
9
|
+
["Time.now.second", "Time.now.second"],
|
|
10
|
+
["{}", "{}"],
|
|
11
|
+
["[]", "[]"],
|
|
12
|
+
['""', '""'],
|
|
13
|
+
["{a:1}", "{ a: 1 }"],
|
|
14
|
+
["[1,2,3]", "[1, 2, 3]"],
|
|
15
|
+
[
|
|
16
|
+
"[1, 2, 3].select { |n| n.even? }",
|
|
17
|
+
"[1, 2, 3].select { |n|\n n.even?\n}"
|
|
18
|
+
],
|
|
19
|
+
[
|
|
20
|
+
"if true 1 elsif false 2 else 3 end",
|
|
21
|
+
"if true\n 1\nelsif false\n 2\nelse\n 3\nend"
|
|
22
|
+
],
|
|
23
|
+
[
|
|
24
|
+
"sum = (a, b: 2) => { a + b } sum(1)",
|
|
25
|
+
"sum = (a, b: 2) => {\n a + b\n}\n\nsum(1)"
|
|
26
|
+
],
|
|
27
|
+
[
|
|
28
|
+
"Http.post(\"https://api.openai.com/v1/chat/completions\", headers: { authorization: \"Bearer {open_ai_api_key}\", \"content-type\": \"application/json\" }, body: { model: model, messages: [{ role: \"system\", content: \"hello\" }, { role: \"user\", content: \"world\" }] }.to_json)",
|
|
29
|
+
"Http.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers: {\n authorization: \"Bearer {open_ai_api_key}\",\n \"content-type\": \"application/json\"\n },\n body: {\n model: model,\n messages: [\n { role: \"system\", content: \"hello\" },\n { role: \"user\", content: \"world\" }\n ]\n }.to_json\n)"
|
|
30
|
+
]
|
|
31
|
+
].each do |input, expected|
|
|
32
|
+
it "formats #{input.inspect}" do
|
|
33
|
+
expect(described_class.format(Code.parse(input))).to eq(expected)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "round-trips parse and evaluation semantics for formatted code" do
|
|
38
|
+
input = "user = {name: :Dorian, age: 31} user.age"
|
|
39
|
+
formatted = described_class.format(Code.parse(input))
|
|
40
|
+
|
|
41
|
+
expect(Code.parse(formatted)).to be_present
|
|
42
|
+
expect(Code.evaluate(formatted)).to eq(Code.evaluate(input))
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/spec/code_spec.rb
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
require "spec_helper"
|
|
4
4
|
|
|
5
5
|
RSpec.describe Code do
|
|
6
|
+
def format_input(input)
|
|
7
|
+
Code::Format.format(described_class.parse(input))
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def evaluate_with_output(input)
|
|
11
|
+
output = StringIO.new
|
|
12
|
+
result = described_class.evaluate(input, output: output)
|
|
13
|
+
[result, output.string]
|
|
14
|
+
end
|
|
15
|
+
|
|
6
16
|
(
|
|
7
17
|
[
|
|
8
18
|
"{ a: 1, b: 2 }.transform_values { |key| key.upcase }",
|
|
@@ -143,7 +153,16 @@ RSpec.describe Code do
|
|
|
143
153
|
Json.parse('random-string')
|
|
144
154
|
{}["".to_string]
|
|
145
155
|
] + ["Time.hour >= 6 and Time.hour <= 23"]
|
|
146
|
-
).each
|
|
156
|
+
).each do |input|
|
|
157
|
+
it(input) do
|
|
158
|
+
original_result, original_output = evaluate_with_output(input)
|
|
159
|
+
formatted = format_input(input)
|
|
160
|
+
formatted_result, formatted_output = evaluate_with_output(formatted)
|
|
161
|
+
|
|
162
|
+
expect(formatted_result.class).to eq(original_result.class)
|
|
163
|
+
expect(formatted_output).to eq(original_output)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
147
166
|
|
|
148
167
|
[
|
|
149
168
|
[
|
|
@@ -444,11 +463,18 @@ RSpec.describe Code do
|
|
|
444
463
|
["", ""]
|
|
445
464
|
].each do |input, expected|
|
|
446
465
|
it "#{input} == #{expected}" do
|
|
466
|
+
formatted = format_input(input)
|
|
467
|
+
|
|
447
468
|
output = StringIO.new
|
|
448
469
|
code_input = described_class.evaluate(input, output: output)
|
|
449
470
|
code_expected = described_class.evaluate(expected)
|
|
450
471
|
expect(code_input).to eq(code_expected)
|
|
451
472
|
expect(output.string).to eq("")
|
|
473
|
+
|
|
474
|
+
formatted_output = StringIO.new
|
|
475
|
+
formatted_result = described_class.evaluate(formatted, output: formatted_output)
|
|
476
|
+
expect(formatted_result).to eq(code_input)
|
|
477
|
+
expect(formatted_output.string).to eq("")
|
|
452
478
|
next if code_input.is_a?(Code::Object::Decimal)
|
|
453
479
|
|
|
454
480
|
expect(code_input.to_json).to eq(code_expected.to_json)
|
|
@@ -459,14 +485,20 @@ RSpec.describe Code do
|
|
|
459
485
|
|
|
460
486
|
[["puts(true)", "true\n"], %w[print(false) false]].each do |input, expected|
|
|
461
487
|
it "#{input} prints #{expected}" do
|
|
488
|
+
formatted = format_input(input)
|
|
489
|
+
|
|
462
490
|
output = StringIO.new
|
|
463
491
|
described_class.evaluate(input, output: output)
|
|
464
492
|
expect(output.string).to eq(expected)
|
|
493
|
+
|
|
494
|
+
formatted_output = StringIO.new
|
|
495
|
+
described_class.evaluate(formatted, output: formatted_output)
|
|
496
|
+
expect(formatted_output.string).to eq(expected)
|
|
465
497
|
end
|
|
466
498
|
end
|
|
467
499
|
|
|
468
500
|
it "doesn't crash with dictionnary as parameter" do
|
|
469
|
-
|
|
501
|
+
input = <<~INPUT
|
|
470
502
|
[
|
|
471
503
|
{
|
|
472
504
|
videos: [{}]
|
|
@@ -478,10 +510,12 @@ RSpec.describe Code do
|
|
|
478
510
|
post.videos.map { |video| }
|
|
479
511
|
end
|
|
480
512
|
INPUT
|
|
513
|
+
described_class.evaluate(input)
|
|
514
|
+
described_class.evaluate(format_input(input))
|
|
481
515
|
end
|
|
482
516
|
|
|
483
517
|
it "doesn't crash with functions" do
|
|
484
|
-
|
|
518
|
+
input = <<~INPUT
|
|
485
519
|
send! = (subject:) => { subject }
|
|
486
520
|
|
|
487
521
|
send!(subject: "pomodoro start")
|
|
@@ -489,5 +523,7 @@ RSpec.describe Code do
|
|
|
489
523
|
send!(subject: "pomodoro start")
|
|
490
524
|
send!(subject: "pomodoro break")
|
|
491
525
|
INPUT
|
|
526
|
+
described_class.evaluate(input)
|
|
527
|
+
described_class.evaluate(format_input(input))
|
|
492
528
|
end
|
|
493
529
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: code-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.8.
|
|
4
|
+
version: 1.8.16
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dorian Marié
|
|
@@ -215,6 +215,7 @@ files:
|
|
|
215
215
|
- README.md
|
|
216
216
|
- Rakefile
|
|
217
217
|
- VERSION
|
|
218
|
+
- applies
|
|
218
219
|
- bin/bundle
|
|
219
220
|
- bin/bundle-audit
|
|
220
221
|
- bin/bundler-audit
|
|
@@ -230,6 +231,7 @@ files:
|
|
|
230
231
|
- lib/code/concerns.rb
|
|
231
232
|
- lib/code/concerns/shared.rb
|
|
232
233
|
- lib/code/error.rb
|
|
234
|
+
- lib/code/format.rb
|
|
233
235
|
- lib/code/node.rb
|
|
234
236
|
- lib/code/node/base_10.rb
|
|
235
237
|
- lib/code/node/base_16.rb
|
|
@@ -335,6 +337,8 @@ files:
|
|
|
335
337
|
- lib/code/version.rb
|
|
336
338
|
- package-lock.json
|
|
337
339
|
- package.json
|
|
340
|
+
- spec/bin/code_spec.rb
|
|
341
|
+
- spec/code/format_spec.rb
|
|
338
342
|
- spec/code/node/call_spec.rb
|
|
339
343
|
- spec/code/object/boolean_spec.rb
|
|
340
344
|
- spec/code/object/cryptography_spec.rb
|