ast_transform 3.1.0 → 3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a74867dd7f6d788c78d286901e1e4c6a362e0a341300fb9bb22ccb92503a0223
4
- data.tar.gz: c2968b8000a875e169f89d85deaa38868ad49dcc8d193db6a78c88ea4eafb986
3
+ metadata.gz: fb38ad75ff6d31e4f8da1d1b272968b3d65393a3acaf92b914be2c0d35ebdb32
4
+ data.tar.gz: f2621b3116184b0f7b7eb87a9a4b4af184f685e4399e5b52761b1deb1f54c998
5
5
  SHA512:
6
- metadata.gz: 9f4e007503a09479a7df3f595097bf384eb0196fb192cd012855c63417582e34a32cfd318ae765f1a8122de408bbfd337c8e3adfe889d2fd1bc36b3a20c96599
7
- data.tar.gz: 900412939feb57ee329d2c3adca9b9712c1da8243038268104db916f9270ea09427d0d96cbf5aded11f86d919d4b45e6629be20372eb4dfc1d0531f05757b1e5
6
+ metadata.gz: 3ff9177f5bdd86e95af2406aeb675a53cd93917a63faa2fdf26bcfb12bc6365e415dbadb576247f7d134a2a36a7866d46cca17626351fb648f156379e3ba5473
7
+ data.tar.gz: 054fe46ee27bc9cda5f18b163460f62a94c496cc3993407e79a3592079d3011c56c78d300db0639d685749e47a704a0a3e6d3a1915fbcbd044de7a7b6a6c173b
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ast_transform (3.1.0)
4
+ ast_transform (3.1.1)
5
5
  parser (>= 3.3)
6
6
  prism (>= 1.5)
7
7
  unparser (>= 0.8)
@@ -9,6 +9,7 @@ module ASTTransform
9
9
  class Layout
10
10
  def initialize
11
11
  @lines = []
12
+ @sealed = false
12
13
  end
13
14
 
14
15
  # The line number currently being written; the next fresh line would be +cursor + 1+.
@@ -21,7 +22,10 @@ module ASTTransform
21
22
  # +column+ — cosmetic only (leading whitespace is never significant in emitted code), but it keeps the artifact
22
23
  # visually close to the source. Packed text ignores the column, as do continuation lines (they keep their own
23
24
  # relative indentation).
24
- def place(target_line, text, column: nil)
25
+ #
26
+ # +seal+ declares the text's last line unextendable (the caller's property — e.g. it terminates a heredoc,
27
+ # whose terminator must stand alone): a subsequent pack opens a fresh line instead of joining it.
28
+ def place(target_line, text, column: nil, seal: false)
25
29
  first, *rest = text.split("\n")
26
30
 
27
31
  if target_line && target_line > @lines.size
@@ -32,22 +36,27 @@ module ASTTransform
32
36
  end
33
37
 
34
38
  @lines.concat(rest)
39
+ @sealed = seal
35
40
  end
36
41
 
37
42
  # Appends +text+ on a new line unconditionally — for text that must never be `;`-packed after a statement
38
43
  # (e.g. keywords).
39
44
  def place_on_fresh_line(text)
40
45
  @lines << text
46
+ @sealed = false
41
47
  end
42
48
 
43
- # Appends +text+ to the current line with a `; ` separator. The last line is never blank here: padding blanks
44
- # are only created inside +place+, which immediately overwrites the padded line.
49
+ # Appends +text+ to the current line with a `; ` separator unless the current line is sealed, in which case
50
+ # the text opens a fresh line (alignment degrades by one more line; validity is preserved). The last line is
51
+ # never blank here: padding blanks are only created inside +place+, which immediately overwrites the padded
52
+ # line.
45
53
  def pack(text)
46
- if @lines.empty?
54
+ if @lines.empty? || @sealed
47
55
  @lines << text
48
56
  else
49
57
  @lines[-1] = "#{@lines.last}; #{text}"
50
58
  end
59
+ @sealed = false
51
60
  end
52
61
 
53
62
  # @return [String] the laid-out text, with a trailing newline.
@@ -71,7 +71,9 @@ module ASTTransform
71
71
  if recursive_container?(node)
72
72
  emit_container(node, layout, renderer)
73
73
  else
74
- layout.place(node.loc&.line, renderer.aligned_render(node), column: node.loc&.column)
74
+ render = renderer.aligned_render(node)
75
+ # A render in heredoc form seals its line: nothing may pack after the terminator (issue #16).
76
+ layout.place(node.loc&.line, render, column: node.loc&.column, seal: renderer.line_terminal?(render))
75
77
  end
76
78
  end
77
79
 
@@ -145,7 +147,8 @@ module ASTTransform
145
147
  # nested statements align.
146
148
  def emit_container(node, layout, renderer)
147
149
  opener, closer = container_delimiters(node, renderer)
148
- layout.place(node.loc&.line, opener, column: node.loc&.column)
150
+ # An opener can carry a heredoc too (e.g. a block call with a heredoc argument) — seal it like a statement.
151
+ layout.place(node.loc&.line, opener, column: node.loc&.column, seal: renderer.line_terminal?(opener))
149
152
  emit_body(container_body(node), layout, renderer)
150
153
  layout.place(closer_line(node), closer, column: closer_column(node))
151
154
  end
@@ -62,8 +62,30 @@ module ASTTransform
62
62
  compress_to_single_line(render) || render
63
63
  end
64
64
 
65
+ # Whether +render+ must be the last text on its final line: a render ending on a heredoc terminator (Unparser
66
+ # falls back to `<<-HEREDOC` form when its quoted-string round-trip fails) cannot have anything `;`-packed
67
+ # after it — the terminator must stand alone, so a join unterminates the heredoc. Probed by re-parse, the same
68
+ # verification style as +compress_to_single_line+, behind a cheap textual gate: only a heredoc makes a last
69
+ # line unextendable, and every heredoc opener contains +<<+. A false positive (the probe failing for another
70
+ # reason, e.g. an isolated container opener carrying a heredoc argument) costs one line of alignment, never
71
+ # correctness.
72
+ def line_terminal?(render)
73
+ return false unless render.include?('<<')
74
+
75
+ # chomp: heredoc renders end with a trailing newline; the probe must extend the last *line* (where the
76
+ # layout would pack), not append below it.
77
+ !parses?("#{render.chomp}; nil")
78
+ end
79
+
65
80
  private
66
81
 
82
+ def parses?(source)
83
+ Unparser.parse(source)
84
+ true
85
+ rescue Parser::SyntaxError
86
+ false
87
+ end
88
+
67
89
  def compress_to_single_line(render)
68
90
  candidate = render.split("\n").map(&:strip).join('; ')
69
91
  # Both sides parsed without scope context, so lvar/send ambiguity cancels out; equality means the newline
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ASTTransform
4
- VERSION = "3.1.0"
4
+ VERSION = "3.1.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ast_transform
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0
4
+ version: 3.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jean-Philippe Duchesne
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-29 00:00:00.000000000 Z
11
+ date: 2026-08-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: parser