ast_transform 3.1.0 → 3.2.0
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/CHANGELOG.md +13 -0
- data/Gemfile.lock +1 -1
- data/lib/ast_transform/layout.rb +13 -4
- data/lib/ast_transform/line_aligned_emitter.rb +5 -2
- data/lib/ast_transform/modern_builder.rb +28 -0
- data/lib/ast_transform/source_parser.rb +4 -4
- data/lib/ast_transform/statement_renderer.rb +22 -0
- data/lib/ast_transform/version.rb +1 -1
- metadata +3 -3
- data/lib/ast_transform/kwargs_builder.rb +0 -23
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4bd9891c7329570100d1f706978508d03249b188472557bbe2a6ffef3813ccf3
|
|
4
|
+
data.tar.gz: d2f1620ea7475a63a255c41085db71deb6716e3a28b0dd8a1970a46884a1d742
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1212107491dedeffe09b7d1f54303cf784aef2ccc3c9da16b9fe81d1f98c377e32bf14a484d4089b3dc16d9b9087d30a1d6b0a5a5caeb61ca965c88cd2218902
|
|
7
|
+
data.tar.gz: 0af32ba36db47e621cb661879763b1aa8092b114f0f8fe49ba50bcf49612da879a99ad873523f9f9d75d704a508d9303f73bd8d09f259d78c0aa85497a934097
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [3.2.0] - 2026-11-08
|
|
10
|
+
### Fixed
|
|
11
|
+
- One-parameter blocks no longer auto-splat their argument after transformation: `[[1, 2]].each { |a| }` emitted as `{ |a,| }` (the trailing-comma form), so `a` saw `1` instead of `[1, 2]`. The parser builder opted into `emit_kwargs` alone and otherwise produced the parser gem's legacy AST, in which a lone `s(:arg)` under `s(:args)` is what Unparser (written against the modern AST) reads as `|a,|`. Lambdas were affected the same way (`->(a) {}` emitted as `lambda { |a,| }`), and forwarding defs lost their header (`def m(...)` emitted as `def m`). (#21)
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- `ASTTransform::KwargsBuilder` is now `ASTTransform::ModernBuilder` and opts into the parser gem's full `modernize` set rather than `emit_kwargs` alone. Transformations and analyses now see the modern node shapes: `s(:args, s(:procarg0, s(:arg, :a)))` for `|a|`, `:index`/`:indexasgn` for `foo[1]`/`foo[1] = 2` (previously `:send` with `:[]`/`:[]=`), `:lambda` for `->() {}` (previously `s(:send, nil, :lambda)`), `s(:args, s(:forward_arg))` for `def m(...)` (previously `:forward_args`), and `s(:__ENCODING__)` for `__ENCODING__` (previously the `Encoding` constant). `:kwargs` and `:match_pattern`/`:match_pattern_p` are unchanged: the former was already on, and Prism's translation emits the latter regardless of the flag. Passes that pattern-match the legacy shapes must update.
|
|
15
|
+
|
|
16
|
+
## [3.1.1] - 2026-08-03
|
|
17
|
+
### Fixed
|
|
18
|
+
- Line-aligned emission no longer packs displaced statements or container closers onto a heredoc terminator line. Unparser legitimately falls back to `<<-HEREDOC` emission when a dstr fails its quoted-string round-trip; the emitter now treats such a render as line-terminal and opens a fresh line instead, trading one line of alignment for valid Ruby. (#16)
|
|
19
|
+
|
|
7
20
|
## [3.1.0] - 2026-07-29
|
|
8
21
|
### Added
|
|
9
22
|
- Analysis passes: `ASTTransform::AbstractAnalysis`, the read-only counterpart of `AbstractTransformation`. Both leaves share the extracted traversal core `ASTTransform::AbstractProcessor` (sealed `process`, `process_node` hook, thunk descent, `TransformationHelper`) and differ only in what `run` returns — the rebuilt tree for transformations, the analysis instance (result readers) for analyses.
|
data/Gemfile.lock
CHANGED
data/lib/ast_transform/layout.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
44
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism/translation/parser"
|
|
4
|
+
|
|
5
|
+
module ASTTransform
|
|
6
|
+
# The framework's parser builder: the stock builder with the parser gem's `modernize` opt-ins, which produce the
|
|
7
|
+
# modern AST (`:procarg0`, `:index`/`:indexasgn`, `:lambda`, `:forward_args`, `:kwargs`, `:match_pattern`, ...).
|
|
8
|
+
#
|
|
9
|
+
# Unparser is written against the modern AST, so parsing in the gem's legacy compatibility shapes changes meaning
|
|
10
|
+
# at emission. Two examples that matter to any transformed source:
|
|
11
|
+
#
|
|
12
|
+
# - Block parameters: in the modern AST `{ |a| }` is `s(:args, s(:procarg0, s(:arg, :a)))`, and a lone bare
|
|
13
|
+
# `s(:arg)` means the trailing-comma form `{ |a,| }`. The legacy shape for `{ |a| }` is that lone `s(:arg)`, so
|
|
14
|
+
# Unparser emitted `|a,|` and every one-parameter block auto-splatted its argument (#21).
|
|
15
|
+
# - Keyword arguments: with `emit_kwargs` off, `foo(bar: 1)` and `foo({ bar: 1 })` both parse as :hash; Unparser
|
|
16
|
+
# braces :hash and not :kwargs, and Ruby 3+ treats the two as different calls.
|
|
17
|
+
#
|
|
18
|
+
# The flags are class-level ivars, so modernizing here opts in this builder only — the global
|
|
19
|
+
# Parser::Builders::Default stays untouched.
|
|
20
|
+
#
|
|
21
|
+
# NOTE: parsed nodes deliberately stay plain Parser::AST::Node. Custom node classes exist only for registered
|
|
22
|
+
# custom types (see ASTTransform::Node), which are IR and never reach Unparser: AST::Node#eql? compares class, and
|
|
23
|
+
# Unparser verifies dynamic-string emission by re-parsing and comparing eql? against the freshly parsed
|
|
24
|
+
# (plain-class) node — custom-class nodes of standard types would fail that verification.
|
|
25
|
+
class ModernBuilder < Prism::Translation::Parser::Builder
|
|
26
|
+
modernize
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
4
|
require "prism/translation/parser"
|
|
5
|
-
require "ast_transform/
|
|
5
|
+
require "ast_transform/modern_builder"
|
|
6
6
|
|
|
7
7
|
module ASTTransform
|
|
8
8
|
# Owns source → AST parsing: Prism's C parser through its whitequark translation layer, so every consumer gets the
|
|
@@ -11,10 +11,10 @@ module ASTTransform
|
|
|
11
11
|
class SourceParser
|
|
12
12
|
# Constructs a new SourceParser instance.
|
|
13
13
|
#
|
|
14
|
-
#
|
|
15
|
-
# on the
|
|
14
|
+
# ModernBuilder is a required implementation detail, not an injection seam: the framework's emission depends
|
|
15
|
+
# on the modern AST shapes it produces, so every parse goes through it.
|
|
16
16
|
def initialize
|
|
17
|
-
@builder =
|
|
17
|
+
@builder = ModernBuilder.new
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
# Parses the given +source+.
|
|
@@ -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
|
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.
|
|
4
|
+
version: 3.2.0
|
|
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-
|
|
11
|
+
date: 2026-11-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: parser
|
|
@@ -87,9 +87,9 @@ files:
|
|
|
87
87
|
- lib/ast_transform/instruction_sequence/bootsnap_mixin.rb
|
|
88
88
|
- lib/ast_transform/instruction_sequence/mixin.rb
|
|
89
89
|
- lib/ast_transform/instruction_sequence/mixin_utils.rb
|
|
90
|
-
- lib/ast_transform/kwargs_builder.rb
|
|
91
90
|
- lib/ast_transform/layout.rb
|
|
92
91
|
- lib/ast_transform/line_aligned_emitter.rb
|
|
92
|
+
- lib/ast_transform/modern_builder.rb
|
|
93
93
|
- lib/ast_transform/node.rb
|
|
94
94
|
- lib/ast_transform/source_parser.rb
|
|
95
95
|
- lib/ast_transform/statement_renderer.rb
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "prism/translation/parser"
|
|
4
|
-
|
|
5
|
-
module ASTTransform
|
|
6
|
-
# The framework's parser builder: the stock builder with the parser gem's `emit_kwargs` opt-in, which
|
|
7
|
-
# distinguishes keyword arguments from hash literals in the AST.
|
|
8
|
-
#
|
|
9
|
-
# With the flag off (the gem's compatibility default) both `foo(bar: 1)` and `foo({ bar: 1 })` emit :hash nodes.
|
|
10
|
-
# Unparser uses the node type to decide whether to emit braces: :hash gets `{}`, :kwargs does not. Since Ruby 3.0+
|
|
11
|
-
# treats these as semantically different (strict keyword/positional separation), the AST must preserve the
|
|
12
|
-
# distinction. `emit_kwargs` rewrites at the call sites where kwargs semantics live (method calls, index,
|
|
13
|
-
# super/yield); the flag is a class-level ivar, so setting it here opts in this builder only — the global
|
|
14
|
-
# Parser::Builders::Default stays untouched.
|
|
15
|
-
#
|
|
16
|
-
# NOTE: parsed nodes deliberately stay plain Parser::AST::Node. Custom node classes exist only for registered
|
|
17
|
-
# custom types (see ASTTransform::Node), which are IR and never reach Unparser: AST::Node#eql? compares class, and
|
|
18
|
-
# Unparser verifies dynamic-string emission by re-parsing and comparing eql? against the freshly parsed
|
|
19
|
-
# (plain-class) node — custom-class nodes of standard types would fail that verification.
|
|
20
|
-
class KwargsBuilder < Prism::Translation::Parser::Builder
|
|
21
|
-
self.emit_kwargs = true
|
|
22
|
-
end
|
|
23
|
-
end
|