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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1f71684c4dc7d49cba70fd38bcd64ad838a0f3c303dbb492364e2cd7a8d66d55
4
- data.tar.gz: 1debf69960b9da25c30f2523aa64df84e7ee0d6fc5b4965d7ceb956c66244a3f
3
+ metadata.gz: 103fdb30c9cb28f7a771add31e5b21d71e2f397476b117b3ae90ab99d7ed902d
4
+ data.tar.gz: 6a3ab9cbff25405b6179ee7bd7e0b316cd4930e906abb13eef575b5bc3b4dc59
5
5
  SHA512:
6
- metadata.gz: 7dfb5cc19d6334ca14906b74be16ce26d6bec269e6f78030ad3ce6ef00fa0ded8ade6b6af3c6fb0c0e39aa8849cc64425fec6ee95e669ab8f50cd9d5dd866066
7
- data.tar.gz: abe4ea08744984d2f84f099dedb81e31b041b3d19a97fa173655f727cdda435d78a790bd5b8601f068ed3c953a7c28ebc7624a2a3ca573de1456ab113af36927
6
+ metadata.gz: ce5a8cabe4a1adaad31e04d101d1a883731c71b60b0b3279186a74b9571b041f10792ed16767245352ac5cc7905402d2eec4f5ba6ed952ae8e7000f333d9976f
7
+ data.tar.gz: a80c30b44e0ff16742a0c9620a43ed51e2ee8083dfeb09809b4ac5a2458816654f708855049e75c21310f5b779bb78c013033d34bc8c417dd2e4f4db8db9c306
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- code-ruby (1.8.15)
4
+ code-ruby (1.8.16)
5
5
  activesupport
6
6
  base64
7
7
  bigdecimal
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.8.15
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 = 'loop { print("> ") puts(evaluate(read)) }' if input.blank?
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 Exception => e
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(
@@ -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 { |input| it(input) { described_class.evaluate(input) } }
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
- described_class.evaluate(<<~INPUT)
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
- described_class.evaluate(<<~INPUT)
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.15
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