code-ruby 3.0.10 → 3.0.12

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: e59505a1d473ed64e03853dd3465ce958bb81ee2dfcd2ec38b0280a5ee3dd98a
4
- data.tar.gz: e13a2d15ba45209e81213bd7b5a3f1ea58a26cdcb9446f29f9f6b5906aa7e9fe
3
+ metadata.gz: eef80c2ebc4ef6d318ee0af151704caca0f0a370c698dc727814836d6e5146db
4
+ data.tar.gz: 6ac8ae69e473aa937eb4794466e710ec40444c53f5d1a12149368e03de0bc4c0
5
5
  SHA512:
6
- metadata.gz: 82cf388eb36848ff8f01f122177e436205ccebfd6f4e19488e65e1adecdff267246292da159a99b42cca23f5adefb505483ebe1357b928922ee824ac4c832086
7
- data.tar.gz: 3069cb6a421189bad2b0aba4eed4a3d50d5e24128f1fcd6fa6ee12db5110b5a53babf2b310175ba4701b59a5bb3447bb4fd1505282c5f3de44c5e2c3a530fc6d
6
+ metadata.gz: 6b99011ea5c71b86b7709949e2d4096f4708b2a4c985e1f30e704bac060c58c710922797c4c4ade47f91b31ad37a47153f66fd8e204783786d55da38234e0d79
7
+ data.tar.gz: 367d671e1fbb4aaacd278bd3e186a03a7304f7041f5381e502a54e44afe754048ed603eb83e6e21f2c8415916078b633fc352984ae6c5b4ab27f96c330394b19
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- code-ruby (3.0.10)
4
+ code-ruby (3.0.12)
5
5
  activesupport
6
6
  base64
7
7
  bigdecimal
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.0.10
1
+ 3.0.12
data/lib/code/format.rb CHANGED
@@ -57,13 +57,20 @@ class Code
57
57
  "begin\n#{body}\n#{INDENT * indent}end"
58
58
  end
59
59
 
60
+ def format_group(group, indent:)
61
+ formatted = format_code_inline(group, indent: indent)
62
+ return "(#{formatted})" unless formatted.include?("\n")
63
+
64
+ "(\n#{indent_lines(normalize_group_indentation(formatted, indent: indent), indent + 1)}\n#{INDENT * indent})"
65
+ end
66
+
60
67
  def format_statement(statement, indent:)
61
68
  if statement.is_a?(Hash) && statement.key?(:nothing)
62
69
  statement[:nothing].presence || "nothing"
63
70
  elsif statement.is_a?(Hash) && statement.key?(:boolean)
64
71
  statement[:boolean]
65
72
  elsif statement.is_a?(Hash) && statement.key?(:group)
66
- "(#{format_code_inline(statement[:group], indent: indent)})"
73
+ format_group(statement[:group], indent: indent)
67
74
  elsif statement.is_a?(Hash) && statement.key?(:call)
68
75
  format_call(statement[:call], indent: indent)
69
76
  elsif statement.is_a?(Hash) && statement.key?(:number)
@@ -431,19 +438,28 @@ class Code
431
438
 
432
439
  expression =
433
440
  if compact_operator?(operator)
434
- "#{expression}#{operator}#{right}"
441
+ "#{format_compact_receiver(expression, indent: indent)}#{operator}#{right}"
435
442
  else
436
443
  candidate = "#{expression} #{operator} #{right}"
437
- if multiline_collection_statement?(other[:statement]) &&
438
- right.include?("\n")
444
+ if right.include?("\n")
439
445
  first_line, *rest = right.lines(chomp: true)
440
- [ "#{expression} #{operator} #{first_line}", *rest ].join("\n")
446
+ if multiline_operand_statement?(other[:statement]) ||
447
+ !%w[and or].include?(operator)
448
+ [ "#{expression} #{operator} #{first_line.lstrip}", *rest ].join("\n")
449
+ else
450
+ [
451
+ "#{expression}\n#{INDENT * (indent + 1)}#{operator} #{first_line.lstrip}",
452
+ *rest
453
+ ].join("\n")
454
+ end
441
455
  elsif expression.include?("\n") || candidate.length > MAX_LINE_LENGTH
442
456
  right_lines =
443
457
  if right.include?("\n")
444
458
  right.lines(chomp: true).map(&:lstrip)
445
- else
459
+ elsif %w[and or].include?(operator)
446
460
  right.split(" #{operator} ")
461
+ else
462
+ [right]
447
463
  end
448
464
  continuation_lines =
449
465
  right_lines.map do |line|
@@ -462,6 +478,34 @@ class Code
462
478
  expression
463
479
  end
464
480
 
481
+ def format_compact_receiver(expression, indent:)
482
+ return expression unless compact_receiver_needs_parentheses?(expression)
483
+
484
+ "(\n#{indent_lines(expression, indent + 1)}\n#{INDENT * indent})"
485
+ end
486
+
487
+ def compact_receiver_needs_parentheses?(expression)
488
+ return false unless expression.include?("\n")
489
+ return false if expression.lstrip.start_with?("(")
490
+
491
+ continuation_lines =
492
+ expression
493
+ .lines(chomp: true)[1..]
494
+ .to_a
495
+ .reject { |line| line.strip.empty? || line.strip.match?(/\A[\)\]\}]+\z/) }
496
+
497
+ return false if continuation_lines.empty?
498
+
499
+ base_indent =
500
+ continuation_lines.map { |line| line[/\A */].to_s.length }.min
501
+
502
+ continuation_lines.any? do |line|
503
+ next false unless line[/\A */].to_s.length == base_indent
504
+
505
+ line.lstrip.match?(/\A(\+|-|\*|\/|%|<<|>>|\||\^|&|and\b|or\b)/)
506
+ end
507
+ end
508
+
465
509
  def extract_string_concatenation_parts(statement)
466
510
  return nil unless statement.is_a?(Hash)
467
511
 
@@ -489,11 +533,64 @@ class Code
489
533
 
490
534
  def format_right_operation(operation, indent:)
491
535
  operator = operation[:operator].to_s
492
- left = format_nested_statement(operation[:left], indent: indent)
536
+ left =
537
+ if %w[if unless].include?(operator)
538
+ format_modifier_left(operation[:left], indent: indent)
539
+ else
540
+ format_nested_statement(operation[:left], indent: indent)
541
+ end
493
542
  right = format_nested_statement(operation[:right], indent: indent)
543
+
544
+ if right.include?("\n")
545
+ first_line, *rest = right.lines(chomp: true)
546
+ first_line = first_line.lstrip
547
+ return "#{left} #{operator} #{first_line}" if rest.empty?
548
+
549
+ return [
550
+ "#{left} #{operator} #{first_line}",
551
+ *rest
552
+ ].join("\n")
553
+ end
554
+
494
555
  "#{left} #{operator} #{right}"
495
556
  end
496
557
 
558
+ def format_modifier_left(statement, indent:)
559
+ if statement.is_a?(Hash) && statement.key?(:right_operation)
560
+ nested = statement[:right_operation]
561
+ nested_operator = nested[:operator].to_s
562
+ if nested_operator == "="
563
+ left = format_nested_statement(nested[:left], indent: indent)
564
+ right = format_nested_statement(nested[:right], indent: indent)
565
+ return "#{left} #{nested_operator} #{group_multiline_expression(right, indent: indent)}"
566
+ end
567
+ end
568
+
569
+ format_nested_statement(statement, indent: indent)
570
+ end
571
+
572
+ def group_multiline_expression(expression, indent:)
573
+ return expression unless expression.include?("\n")
574
+ return expression if expression.lstrip.start_with?("(")
575
+
576
+ normalized = normalize_group_indentation(expression, indent: indent)
577
+ "(\n#{indent_lines(normalized, indent + 1)}\n#{INDENT * indent})"
578
+ end
579
+
580
+ def normalize_group_indentation(expression, indent:)
581
+ prefix = INDENT * indent
582
+
583
+ expression
584
+ .lines(chomp: true)
585
+ .map
586
+ .with_index do |line, index|
587
+ next line if index.zero? || line.empty? || prefix.empty?
588
+
589
+ line.delete_prefix(prefix)
590
+ end
591
+ .join("\n")
592
+ end
593
+
497
594
  def compact_operator?(operator)
498
595
  %w[. :: &. .. ...].include?(operator)
499
596
  end
@@ -501,9 +598,11 @@ class Code
501
598
  def format_ternary(ternary, indent:)
502
599
  left = format_nested_statement(ternary[:left], indent: indent)
503
600
  middle = format_nested_statement(ternary[:middle], indent: indent)
601
+ middle = group_multiline_expression(middle, indent: indent)
504
602
  return "#{left} ? #{middle}" unless ternary.key?(:right)
505
603
 
506
604
  right = format_nested_statement(ternary[:right], indent: indent)
605
+ right = group_multiline_expression(right, indent: indent)
507
606
  "#{left} ? #{middle} : #{right}"
508
607
  end
509
608
 
@@ -598,9 +697,22 @@ class Code
598
697
  values.join(", ").length > MAX_INLINE_COLLECTION_LENGTH
599
698
  end
600
699
 
601
- def multiline_collection_statement?(statement)
602
- statement.is_a?(Hash) &&
603
- (statement.key?(:dictionnary) || statement.key?(:list))
700
+ def multiline_operand_statement?(statement)
701
+ return false unless statement.is_a?(Hash)
702
+
703
+ return true if statement.key?(:dictionnary) || statement.key?(:list)
704
+ return true if statement.key?(:call)
705
+ if statement.key?(:left_operation)
706
+ operation = statement[:left_operation]
707
+ others = Array(operation[:others])
708
+
709
+ return false if others.empty?
710
+ return false unless others.all? { |other| compact_operator?(other[:operator]) }
711
+
712
+ return multiline_operand_statement?(operation[:first])
713
+ end
714
+
715
+ false
604
716
  end
605
717
 
606
718
  def multiline_call_arguments?(raw_arguments, arguments)
@@ -623,7 +735,7 @@ class Code
623
735
 
624
736
  def indent_lines(value, indent)
625
737
  prefix = INDENT * indent
626
- value.split("\n").map { |line| "#{prefix}#{line}" }.join("\n")
738
+ value.split("\n").map { |line| line.empty? ? "" : "#{prefix}#{line}" }.join("\n")
627
739
  end
628
740
 
629
741
  def statement_separator(inline:, indent: nil)
@@ -692,7 +804,7 @@ class Code
692
804
  search_limit = [MAX_LINE_LENGTH, line.length - token.length].min
693
805
  index = line.rindex(token, search_limit)
694
806
  while index
695
- break if index.positive? && outside_string?(line, index)
807
+ break if index.positive? && outside_string_and_grouping?(line, index)
696
808
 
697
809
  index = line.rindex(token, index - 1)
698
810
  end
@@ -701,9 +813,10 @@ class Code
701
813
  [index, token]
702
814
  end
703
815
 
704
- def outside_string?(line, index)
816
+ def outside_string_and_grouping?(line, index)
705
817
  quote_count = 0
706
818
  escaped = false
819
+ grouping_depth = 0
707
820
  line[0...index].each_char do |char|
708
821
  if escaped
709
822
  escaped = false
@@ -714,10 +827,17 @@ class Code
714
827
  escaped = true
715
828
  elsif char == '"'
716
829
  quote_count += 1
830
+ elsif quote_count.even?
831
+ case char
832
+ when "(", "[", "{"
833
+ grouping_depth += 1
834
+ when ")", "]", "}"
835
+ grouping_depth -= 1 if grouping_depth.positive?
836
+ end
717
837
  end
718
838
  end
719
839
 
720
- quote_count.even?
840
+ quote_count.even? && grouping_depth.zero?
721
841
  end
722
842
  end
723
843
  end
@@ -51,11 +51,41 @@ RSpec.describe Code::Format do
51
51
  [
52
52
  "blocks << { title: \"hello world\", description: \"lorem ipsum dolor es sit\", position: 1 }",
53
53
  "blocks << {\n title: \"hello world\",\n description: \"lorem ipsum dolor es sit\",\n position: 1\n}"
54
+ ],
55
+ [
56
+ "sections << Html.join([Html.p { Html.b { \"{index + 1}. {title}\" } }, Html.p { query } if query.presence, Html.p { Html.a(href: link || inline_url) { :source } } if (link || inline_url), Html.p { Html.a(href: inline_url) { Html.img(src: inline_url, alt: title) } }, Html.p { Html.a(href: attachment_url) { \"télécharger\" } }].compact)",
57
+ "sections << Html.join(\n [\n Html.p { Html.b { \"{index + 1}. {title}\" } },\n Html.p { query } if query.presence,\n Html.p {\n Html.a(href: link || inline_url) { :source }\n } if (link || inline_url),\n Html.p {\n Html.a(href: inline_url) { Html.img(src: inline_url, alt: title) }\n },\n Html.p {\n Html.a(href: attachment_url) { \"télécharger\" }\n }\n ].compact\n)"
58
+ ],
59
+ [
60
+ "safe = post.present? and !post[:over_18] and post[:post_hint] == :image and post[:url].to_string.strip.presence and (post[:url].to_string.strip.ends_with?(\".jpg\") or post[:url].to_string.strip.ends_with?(\".jpeg\") or post[:url].to_string.strip.ends_with?(\".png\") or post[:url].to_string.strip.include?(\"i.redd.it\"))",
61
+ "safe = post.present?\n and !post[:over_18]\n and post[:post_hint] == :image\n and post[:url].to_string.strip.presence\n and (\n post[:url].to_string.strip.ends_with?(\".jpg\")\n or post[:url].to_string.strip.ends_with?(\".jpeg\")\n or post[:url].to_string.strip.ends_with?(\".png\")\n or post[:url].to_string.strip.include?(\"i.redd.it\")\n)"
62
+ ],
63
+ [
64
+ "items.each { |item, index| proxied_image_url = if image_url proxy_url(image_url) else nothing end }",
65
+ "items.each { |item, index|\n proxied_image_url = if image_url\n proxy_url(image_url)\n else\n nothing\n end\n}"
66
+ ],
67
+ [
68
+ "lines << \"vacances scolaires france {zone} (prochains {months_ahead} mois) :\".downcase",
69
+ "lines << (\n \"vacances scolaires france {zone} (prochains \"\n + \"{months_ahead} mois) :\"\n).downcase"
70
+ ],
71
+ [
72
+ "src = \"https://proxy.dorianmarie.com?\" + \"{{ url: src, disposition: :inline }.to_query}\" if src",
73
+ "src = (\n \"https://proxy.dorianmarie.com?\"\n + \"{{ url: src, disposition: :inline }.to_query}\"\n) if src"
74
+ ],
75
+ [
76
+ "inline_image = image ? \"https://proxy.dorianmarie.com?\" + \"{inline_params.to_query}\" : nothing",
77
+ "inline_image = image ? (\n \"https://proxy.dorianmarie.com?\"\n + \"{inline_params.to_query}\"\n) : nothing"
54
78
  ]
55
79
  ].each do |input, expected|
56
80
  it "formats #{input.inspect}" do
57
81
  expect(described_class.format(Code.parse(input))).to eq(expected)
58
82
  end
83
+
84
+ it "formats #{input.inspect} idempotently" do
85
+ formatted = described_class.format(Code.parse(input))
86
+
87
+ expect(described_class.format(Code.parse(formatted))).to eq(formatted)
88
+ end
59
89
  end
60
90
 
61
91
  it "round-trips parse and evaluation semantics for formatted code" do
@@ -65,5 +95,50 @@ RSpec.describe Code::Format do
65
95
  expect(Code.parse(formatted)).to be_present
66
96
  expect(Code.evaluate(formatted)).to eq(Code.evaluate(input))
67
97
  end
98
+
99
+ it "keeps grouped multiline receivers stable" do
100
+ input = <<~CODE.chomp
101
+ lines << (
102
+ "vacances scolaires france {zone} (prochains "
103
+ + "{months_ahead} mois) :"
104
+ ).downcase
105
+ CODE
106
+
107
+ expect(described_class.format(Code.parse(input))).to eq(input)
108
+ end
109
+
110
+ it "does not split operators inside string interpolations when wrapping" do
111
+ input =
112
+ %q(body_text = "{title}\n\n{description}\n\ningrédients :\n" + ingredients.map { |ingredient| "- {ingredient}" }.join("\n") + "\n\ninstructions :\n" + instructions.map { |instruction, index| "{index + 1}. {instruction}" }.join("\n"))
113
+
114
+ formatted = described_class.format(Code.parse(input))
115
+
116
+ expect(formatted).to include(
117
+ 'instructions.map { |instruction, index| "{index + 1}. {instruction}" }'
118
+ )
119
+ expect(formatted).not_to include(
120
+ "\"{index\n + 1}. {instruction}\""
121
+ )
122
+ end
123
+
124
+ it "does not emit whitespace-only blank lines" do
125
+ input = <<~CODE.chomp
126
+ body = Html.join(elements.map { |element| title = element.at_css(".x") value = element.at_css(".y") title }, Html.br)
127
+ CODE
128
+
129
+ formatted = described_class.format(Code.parse(input))
130
+
131
+ expect(formatted.lines).not_to include(match(/\A[ \t]+\n\z/))
132
+ end
133
+
134
+ it "keeps lines within 80 characters" do
135
+ input = <<~CODE.chomp
136
+ src = "https://proxy.dorianmarie.com?" + "{{ url: src, disposition: :inline }.to_query}" if src
137
+ CODE
138
+
139
+ formatted = described_class.format(Code.parse(input))
140
+
141
+ expect(formatted.lines.map(&:chomp).map(&:length).max).to be <= 80
142
+ end
68
143
  end
69
144
  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: 3.0.10
4
+ version: 3.0.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dorian Marié