kapusta 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,10 +13,16 @@ module Kapusta
13
13
  def initialize(argv)
14
14
  @mode = :stdout
15
15
  @files = []
16
+ @version = false
16
17
  parse_args(argv)
17
18
  end
18
19
 
19
20
  def run
21
+ if @version
22
+ puts "kapfmt #{Kapusta::VERSION}"
23
+ return 0
24
+ end
25
+
20
26
  validate_args!
21
27
 
22
28
  formatted = @files.map do |path|
@@ -56,6 +62,8 @@ module Kapusta
56
62
  ensure_mode!(:fix)
57
63
  when '--check'
58
64
  ensure_mode!(:check)
65
+ when '--version', '-v'
66
+ @version = true
59
67
  when '--help', '-h'
60
68
  print_help
61
69
  exit 0
@@ -92,16 +100,14 @@ module Kapusta
92
100
  end
93
101
 
94
102
  def format_source(source)
95
- reject_comments!(source)
96
-
97
- forms = Reader.read_all(source)
98
- rendered = forms.map { |form| render(form, 0, top_level: true) }
99
- return '' if rendered.empty?
103
+ forms = Reader.read_all(source, preserve_comments: true)
104
+ entries = top_level_entries(forms)
105
+ return '' if entries.empty?
100
106
 
101
107
  output = +''
102
- rendered.each_with_index do |form, index|
103
- output << separator_for(forms[index - 1], forms[index]) unless index.zero?
104
- output << form
108
+ entries.each_with_index do |entry, index|
109
+ output << separator_for_entries(entries[index - 1], entry) unless index.zero?
110
+ output << render_top_level_entry(entry)
105
111
  end
106
112
  output << "\n"
107
113
  rescue StandardError => e
@@ -117,37 +123,37 @@ module Kapusta
117
123
  end
118
124
  end
119
125
 
120
- def reject_comments!(source)
121
- index = 0
122
-
123
- while index < source.length
124
- char = source[index]
126
+ def top_level_entries(forms)
127
+ entries = []
128
+ leading_comments = []
125
129
 
126
- if char == '"'
127
- index = consume_string(source, index)
128
- elsif char == ';'
129
- raise Error, 'kapfmt does not support comments yet.'
130
+ forms.each do |form|
131
+ if comment?(form)
132
+ leading_comments << form
130
133
  else
131
- index += 1
134
+ entries << { comments: leading_comments, form: }
135
+ leading_comments = []
132
136
  end
133
137
  end
138
+
139
+ entries << { comments: leading_comments, form: nil } unless leading_comments.empty?
140
+ entries
134
141
  end
135
142
 
136
- def consume_string(source, start)
137
- index = start + 1
143
+ def separator_for_entries(previous, current)
144
+ return "\n" unless previous[:form] && current[:form]
138
145
 
139
- while index < source.length
140
- char = source[index]
141
- index += 1
146
+ separator_for(previous[:form], current[:form])
147
+ end
142
148
 
143
- if char == '\\'
144
- index += 1
145
- elsif char == '"'
146
- break
147
- end
148
- end
149
+ def render_top_level_entry(entry)
150
+ parts = entry[:comments].map { |comment| render(comment, 0) }
151
+ parts << render(entry[:form], 0, top_level: true) if entry[:form]
152
+ parts.join("\n")
153
+ end
149
154
 
150
- index
155
+ def comment?(form)
156
+ form.is_a?(Comment)
151
157
  end
152
158
 
153
159
  def render(form, indent, layout: nil, top_level: false, force_expand: false)
@@ -155,6 +161,7 @@ module Kapusta
155
161
  return flat if !force_expand && flat && fits?(flat, indent) && allow_flat?(form, top_level:, layout:)
156
162
 
157
163
  case form
164
+ when Comment then form.text
158
165
  when List then render_list(form, indent, top_level:)
159
166
  when Vec then render_vec(form, indent, layout:, top_level:, force_expand:)
160
167
  when HashLit then render_hash(form, indent)
@@ -165,14 +172,21 @@ module Kapusta
165
172
 
166
173
  def flat_render(form)
167
174
  case form
175
+ when Comment
176
+ nil
168
177
  when Sym
169
178
  form.name
170
179
  when Vec
180
+ return nil if contains_comments?(form.items)
181
+
171
182
  "[#{form.items.map { |item| flat_render(item) }.join(' ')}]"
172
183
  when HashLit
184
+ return nil if contains_comments?(form.entries)
185
+
173
186
  "{#{form.pairs.map { |key, value| flat_hash_pair(key, value) }.join(' ')}}"
174
187
  when List
175
- return "##{flat_render(form.items[1])}" if hashfn_literal?(form)
188
+ return nil if contains_comments?(form.items)
189
+ return "##{flat_render(semantic_items(form.items)[1])}" if hashfn_literal?(form)
176
190
 
177
191
  "(#{form.items.map { |item| flat_render(item) }.join(' ')})"
178
192
  when String, Numeric, true, false, nil
@@ -183,55 +197,66 @@ module Kapusta
183
197
  end
184
198
 
185
199
  def render_list(list, indent, top_level: false)
186
- return '()' if list.empty?
187
- return "##{render(list.items[1], indent, top_level:)}" if hashfn_literal?(list)
200
+ return '()' if list.items.empty?
201
+ return "##{render(semantic_items(list.items)[1], indent, top_level:)}" if hashfn_literal?(list)
202
+
203
+ head = list_head(list)
204
+ return render_generic_list(list, indent) unless head
188
205
 
189
- head = list.head
190
206
  head_name = head.is_a?(Sym) ? head.name : nil
207
+ raw_args = list_raw_rest(list)
191
208
 
192
209
  case head_name
193
210
  when 'fn', 'lambda', 'λ' then render_fn(head_name, list, indent, top_level:)
194
211
  when 'let' then render_let(list, indent)
195
- when 'do', 'finally' then render_prefix_body_form(head_name, [], list.rest, indent)
212
+ when 'do', 'finally' then render_prefix_body_form(head_name, [], raw_args, indent)
196
213
  when 'try' then render_try(list, indent)
197
214
  when 'while', 'when', 'unless', 'for', 'each', 'icollect', 'collect', 'fcollect', 'accumulate', 'faccumulate'
198
- render_prefix_body_form(head_name, list.rest.take(1), list.rest.drop(1), indent)
199
- when 'module' then render_prefix_body_form('module', list.rest.take(1), list.rest.drop(1), indent)
215
+ raw_prefix, raw_body = split_raw_items(raw_args, 1)
216
+ render_prefix_body_form(head_name, raw_prefix, raw_body, indent)
217
+ when 'module'
218
+ raw_prefix, raw_body = split_raw_items(raw_args, 1)
219
+ render_prefix_body_form('module', raw_prefix, raw_body, indent)
200
220
  when 'class' then render_class(list, indent)
201
221
  when 'catch' then render_catch(list, indent)
202
222
  when 'if' then render_if(list, indent)
203
- when 'case', 'match' then render_case(head_name, list.rest, indent)
204
- when *PIPELINE_FORMS then render_pipeline(head_name, list.rest, indent)
223
+ when 'case', 'match'
224
+ if contains_comments?(raw_args)
225
+ render_sequential_head_form(head_name, raw_args, indent)
226
+ else
227
+ render_case(head_name, list_rest(list), indent)
228
+ end
229
+ when *PIPELINE_FORMS then render_pipeline(head_name, raw_args, indent)
205
230
  else
206
231
  render_call(list, indent)
207
232
  end
208
233
  end
209
234
 
210
235
  def render_fn(head, list, indent, top_level: false)
211
- args = list.rest
212
- if args[0].is_a?(Sym) && args[1].is_a?(Vec)
213
- render_prefix_body_form(head, args.take(2), args.drop(2), indent, force_body_multiline: top_level)
214
- else
215
- render_prefix_body_form(head, args.take(1), args.drop(1), indent, force_body_multiline: top_level)
216
- end
236
+ args = list_rest(list)
237
+ raw_args = list_raw_rest(list)
238
+ prefix_length = args[0].is_a?(Sym) && args[1].is_a?(Vec) ? 2 : 1
239
+ raw_prefix, raw_body = split_raw_items(raw_args, prefix_length)
240
+ render_prefix_body_form(head, raw_prefix, raw_body, indent, force_body_multiline: top_level)
217
241
  end
218
242
 
219
243
  def render_catch(list, indent)
220
- args = list.rest
221
- prefix = args.take(2)
222
- body = args.drop(2)
223
- render_prefix_body_form('catch', prefix, body, indent)
244
+ raw_prefix, raw_body = split_raw_items(list_raw_rest(list), 2)
245
+ render_prefix_body_form('catch', raw_prefix, raw_body, indent)
224
246
  end
225
247
 
226
248
  def render_class(list, indent)
227
- args = list.rest
249
+ args = list_rest(list)
250
+ raw_args = list_raw_rest(list)
228
251
  prefix = args[1].is_a?(Vec) ? args.take(2) : args.take(1)
229
- body = args.drop(prefix.length)
230
- render_prefix_body_form('class', prefix, body, indent)
252
+ raw_prefix, raw_body = split_raw_items(raw_args, prefix.length)
253
+ render_prefix_body_form('class', raw_prefix, raw_body, indent)
231
254
  end
232
255
 
233
256
  def render_try(list, indent)
234
- args = list.rest
257
+ args = list_rest(list)
258
+ return render_sequential_head_form('try', list_raw_rest(list), indent) if contains_comments?(list_raw_rest(list))
259
+
235
260
  lines = ['(try']
236
261
 
237
262
  if args.any?
@@ -252,10 +277,17 @@ module Kapusta
252
277
  end
253
278
 
254
279
  def render_let(list, indent)
255
- bindings = list.rest.first
256
- body = list.rest.drop(1)
280
+ bindings = list_rest(list).first
281
+ raw_args = list_raw_rest(list)
282
+ raw_prefix, raw_body = split_raw_items(raw_args, 1)
283
+ body = list_rest(list).drop(1)
257
284
  unless bindings.is_a?(Vec)
258
- return render_prefix_body_form('let', list.rest.take(1), body, indent,
285
+ return render_prefix_body_form('let', raw_prefix, raw_body, indent,
286
+ layouts: [:pairwise])
287
+ end
288
+
289
+ if contains_comments?(raw_args) || contains_comments?(bindings.items)
290
+ return render_prefix_body_form('let', raw_prefix, raw_body, indent,
259
291
  layouts: [:pairwise])
260
292
  end
261
293
 
@@ -272,20 +304,35 @@ module Kapusta
272
304
  line = "(#{head}"
273
305
  lines = [line]
274
306
  current_first_line = line
307
+ layout_index = 0
308
+ inline_prefix = true
309
+
310
+ prefix_forms.each do |form|
311
+ if comment?(form)
312
+ lines << indent_block(render(form, indent + INDENT), INDENT)
313
+ inline_prefix = false
314
+ next
315
+ end
275
316
 
276
- prefix_forms.each_with_index do |form, index|
277
- rendered = render(form, indent + INDENT, layout: layouts[index])
317
+ rendered = render(form, indent + INDENT, layout: layouts[layout_index])
318
+ layout_index += 1
278
319
  candidate = "#{current_first_line} #{rendered}"
279
320
 
280
- if single_line?(rendered) && fits?(candidate, indent)
321
+ if inline_prefix && single_line?(rendered) && fits?(candidate, indent)
281
322
  current_first_line = candidate
282
323
  lines[0] = current_first_line
283
324
  else
284
325
  lines << indent_block(rendered, INDENT)
326
+ inline_prefix = false
285
327
  end
286
328
  end
287
329
 
288
330
  body_forms.each do |form|
331
+ if comment?(form)
332
+ lines << indent_block(render(form, indent + INDENT), INDENT)
333
+ next
334
+ end
335
+
289
336
  body = render(
290
337
  form,
291
338
  indent + INDENT,
@@ -298,7 +345,9 @@ module Kapusta
298
345
  end
299
346
 
300
347
  def render_if(list, indent)
301
- args = list.rest
348
+ args = list_rest(list)
349
+ return render_sequential_head_form('if', list_raw_rest(list), indent) if contains_comments?(list_raw_rest(list))
350
+
302
351
  lines = []
303
352
  hanging = ' ' * '(if '.length
304
353
 
@@ -384,9 +433,15 @@ module Kapusta
384
433
  lines = [base]
385
434
  hanging = ' ' * (base.length + 1)
386
435
 
387
- args.each_with_index do |form, index|
436
+ semantic_index = 0
437
+ args.each do |form|
438
+ if comment?(form)
439
+ lines << "#{hanging}#{render(form, indent + base.length + 1)}"
440
+ next
441
+ end
442
+
388
443
  rendered = render(form, indent + base.length + 1)
389
- if index.zero?
444
+ if semantic_index.zero?
390
445
  first_line, *rest = rendered.lines(chomp: true)
391
446
  candidate = "#{base} #{first_line}"
392
447
  if fits?(candidate, indent)
@@ -398,39 +453,49 @@ module Kapusta
398
453
  else
399
454
  lines << "#{hanging}#{rendered}"
400
455
  end
456
+ semantic_index += 1
401
457
  end
402
458
 
403
459
  append_suffix(lines, ')')
404
460
  end
405
461
 
406
462
  def render_call(list, indent)
407
- head = flat_render(list.head)
408
- raise Error, "cannot format form head: #{list.head.inspect}" unless head
463
+ head = flat_render(list_head(list))
464
+ raise Error, "cannot format form head: #{list_head(list).inspect}" unless head
409
465
 
410
466
  base = "(#{head}"
411
467
  lines = [base]
412
- args = list.rest
468
+ args = list_raw_rest(list)
469
+ semantic_length = semantic_items(args).length
413
470
 
414
- unless args.empty?
415
- first = render(
416
- args.first,
417
- indent + base.length + 1,
418
- force_expand: args.length == 1 && fn_form?(args.first)
419
- )
420
- first_line, *rest = first.lines(chomp: true)
421
- candidate = "#{base} #{first_line}"
422
-
423
- if fits?(candidate, indent)
424
- lines[0] = candidate
425
- hanging = ' ' * (base.length + 1)
426
- rest.each { |line| lines << "#{hanging}#{line}" }
427
- else
428
- lines << indent_block(first, INDENT)
471
+ semantic_index = 0
472
+ args.each do |arg|
473
+ if comment?(arg)
474
+ lines << indent_block(render(arg, indent + INDENT), INDENT)
475
+ next
429
476
  end
430
477
 
431
- args.drop(1).each do |arg|
478
+ if semantic_index.zero?
479
+ first = render(
480
+ arg,
481
+ indent + base.length + 1,
482
+ force_expand: semantic_length == 1 && fn_form?(arg)
483
+ )
484
+ first_line, *rest = first.lines(chomp: true)
485
+ candidate = "#{base} #{first_line}"
486
+
487
+ if lines.length == 1 && fits?(candidate, indent)
488
+ lines[0] = candidate
489
+ hanging = ' ' * (base.length + 1)
490
+ rest.each { |line| lines << "#{hanging}#{line}" }
491
+ else
492
+ lines << indent_block(first, INDENT)
493
+ end
494
+ else
432
495
  lines << indent_block(render(arg, indent + INDENT), INDENT)
433
496
  end
497
+
498
+ semantic_index += 1
434
499
  end
435
500
 
436
501
  append_suffix(lines, ')')
@@ -440,15 +505,14 @@ module Kapusta
440
505
  flat = flat_render(vec)
441
506
  return flat if !force_expand && flat && fits?(flat, indent) && allow_flat?(vec, top_level:, layout:)
442
507
 
443
- if layout == :pairwise
508
+ if layout == :pairwise && !contains_comments?(vec.items)
444
509
  render_pairwise_vec(vec, indent)
445
510
  else
446
511
  lines = ['[']
447
512
  vec.items.each do |item|
448
513
  lines << indent_block(render(item, indent + INDENT), INDENT)
449
514
  end
450
- lines << ']'
451
- lines.join("\n")
515
+ append_suffix(lines, ']')
452
516
  end
453
517
  end
454
518
 
@@ -474,6 +538,7 @@ module Kapusta
474
538
  end
475
539
 
476
540
  def render_let_bindings(bindings, indent)
541
+ return render(bindings, indent + '(let '.length, force_expand: true) if contains_comments?(bindings.items)
477
542
  return render(bindings, indent + '(let '.length, layout: :pairwise) if bindings.items.length <= 2
478
543
 
479
544
  hanging = render_hanging_pairwise_vec(bindings)
@@ -504,18 +569,22 @@ module Kapusta
504
569
 
505
570
  lines = ['{']
506
571
 
507
- hash.pairs.each do |key, value|
508
- pair = flat_hash_pair(key, value)
509
- if fits?(pair, indent + INDENT)
510
- lines << indent_block(pair, INDENT)
572
+ hash.entries.each do |entry|
573
+ if comment?(entry)
574
+ lines << indent_block(render(entry, indent + INDENT), INDENT)
511
575
  else
512
- lines << indent_block(render_hash_key(key), INDENT)
513
- lines << indent_block(render(value, indent + INDENT), INDENT)
576
+ key, value = entry
577
+ pair = flat_hash_pair(key, value)
578
+ if pair && fits?(pair, indent + INDENT)
579
+ lines << indent_block(pair, INDENT)
580
+ else
581
+ lines << indent_block(render_hash_key(key), INDENT)
582
+ lines << indent_block(render(value, indent + INDENT), INDENT)
583
+ end
514
584
  end
515
585
  end
516
586
 
517
- lines << '}'
518
- lines.join("\n")
587
+ append_suffix(lines, '}')
519
588
  end
520
589
 
521
590
  def flat_hash_pair(key, value)
@@ -564,17 +633,19 @@ module Kapusta
564
633
  end
565
634
 
566
635
  def hashfn_literal?(form)
567
- form.is_a?(List) &&
568
- form.items.length == 2 &&
569
- form.items[0].is_a?(Sym) &&
570
- form.items[0].name == 'hashfn'
636
+ return false unless form.is_a?(List)
637
+
638
+ items = semantic_items(form.items)
639
+ items.length == 2 &&
640
+ items[0].is_a?(Sym) &&
641
+ items[0].name == 'hashfn'
571
642
  end
572
643
 
573
644
  def allow_flat?(form, top_level:, layout:)
574
- return false if layout == :pairwise && form.is_a?(Vec) && form.items.length > 2
645
+ return false if layout == :pairwise && form.is_a?(Vec) && semantic_items(form.items).length > 2
575
646
  return true unless form.is_a?(List)
576
647
 
577
- head = form.head
648
+ head = list_head(form)
578
649
  return true unless head.is_a?(Sym)
579
650
 
580
651
  case head.name
@@ -587,9 +658,12 @@ module Kapusta
587
658
  end
588
659
 
589
660
  def force_multiline_body?(form)
590
- return false unless form.is_a?(List) && form.head.is_a?(Sym)
661
+ return false unless form.is_a?(List)
591
662
 
592
- case form.head.name
663
+ head = list_head(form)
664
+ return false unless head.is_a?(Sym)
665
+
666
+ case head.name
593
667
  when 'if', 'case', 'match', 'let', 'try', 'catch', 'finally', 'do', 'for', '->', '->>', '-?>', '-?>>', 'doto',
594
668
  'fn', 'lambda', 'λ'
595
669
  true
@@ -600,7 +674,7 @@ module Kapusta
600
674
  end
601
675
 
602
676
  def fn_body(form)
603
- args = form.rest
677
+ args = list_rest(form)
604
678
  if args[0].is_a?(Sym) && args[1].is_a?(Vec)
605
679
  args.drop(2)
606
680
  else
@@ -616,23 +690,27 @@ module Kapusta
616
690
  return true if require_form?(form)
617
691
  return false unless form.is_a?(List) && flat_render(form)
618
692
 
619
- head = form.head
693
+ head = list_head(form)
620
694
  return false unless head.is_a?(Sym)
621
695
 
622
696
  !%w[fn module class let].include?(head.name)
623
697
  end
624
698
 
625
699
  def require_form?(form)
626
- form.is_a?(List) &&
627
- form.items.length == 2 &&
628
- form.head.is_a?(Sym) &&
629
- form.head.name == 'require'
700
+ return false unless form.is_a?(List)
701
+
702
+ items = semantic_items(form.items)
703
+ items.length == 2 &&
704
+ items[0].is_a?(Sym) &&
705
+ items[0].name == 'require'
630
706
  end
631
707
 
632
708
  def fn_form?(form)
633
- form.is_a?(List) &&
634
- form.head.is_a?(Sym) &&
635
- %w[fn lambda λ].include?(form.head.name)
709
+ return false unless form.is_a?(List)
710
+
711
+ head = list_head(form)
712
+ head.is_a?(Sym) &&
713
+ %w[fn lambda λ].include?(head.name)
636
714
  end
637
715
 
638
716
  def inline_three_arg_if?(args)
@@ -670,10 +748,84 @@ module Kapusta
670
748
 
671
749
  def append_suffix(lines, suffix)
672
750
  updated = lines.dup
673
- updated[-1] = "#{updated[-1]}#{suffix}"
751
+ if updated[-1].lstrip.start_with?(';')
752
+ updated << suffix
753
+ else
754
+ updated[-1] = "#{updated[-1]}#{suffix}"
755
+ end
674
756
  updated.join("\n")
675
757
  end
676
758
 
759
+ def render_generic_list(list, indent)
760
+ lines = ['(']
761
+ list.items.each do |item|
762
+ lines << indent_block(render(item, indent + INDENT), INDENT)
763
+ end
764
+ append_suffix(lines, ')')
765
+ end
766
+
767
+ def render_sequential_head_form(head, raw_items, indent)
768
+ lines = ["(#{head}"]
769
+ semantic_index = 0
770
+
771
+ raw_items.each do |item|
772
+ if comment?(item)
773
+ lines << indent_block(render(item, indent + INDENT), INDENT)
774
+ next
775
+ end
776
+
777
+ rendered = render(item, indent + INDENT)
778
+ if semantic_index.zero?
779
+ candidate = "(#{head} #{rendered}"
780
+ if lines.length == 1 && single_line?(rendered) && fits?(candidate, indent)
781
+ lines[0] = candidate
782
+ else
783
+ lines << indent_block(rendered, INDENT)
784
+ end
785
+ else
786
+ lines << indent_block(rendered, INDENT)
787
+ end
788
+ semantic_index += 1
789
+ end
790
+
791
+ append_suffix(lines, ')')
792
+ end
793
+
794
+ def contains_comments?(items)
795
+ items.any? { |item| comment?(item) }
796
+ end
797
+
798
+ def semantic_items(items)
799
+ items.reject { |item| comment?(item) }
800
+ end
801
+
802
+ def list_head(list)
803
+ semantic_items(list.items).first
804
+ end
805
+
806
+ def list_rest(list)
807
+ semantic_items(list.items).drop(1)
808
+ end
809
+
810
+ def list_raw_rest(list)
811
+ index = list.items.index { |item| !comment?(item) }
812
+ return list.items if index.nil?
813
+
814
+ list.items[(index + 1)..] || []
815
+ end
816
+
817
+ def split_raw_items(items, semantic_count)
818
+ split_index = 0
819
+ seen = 0
820
+
821
+ while split_index < items.length && seen < semantic_count
822
+ seen += 1 unless comment?(items[split_index])
823
+ split_index += 1
824
+ end
825
+
826
+ [items.take(split_index), items.drop(split_index)]
827
+ end
828
+
677
829
  def print_help
678
830
  puts 'Usage: kapfmt [--fix] [--check] FILENAME...'
679
831
  puts