ast_transform 2.1.4 → 3.0.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/.github/CODEOWNERS +1 -0
- data/.github/workflows/ci.yml +11 -1
- data/.gitignore +4 -1
- data/.rubocop.yml +5 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +16 -0
- data/Gemfile +12 -1
- data/Gemfile.lock +38 -11
- data/README.md +54 -2
- data/Rakefile +8 -7
- data/ast_transform.gemspec +11 -12
- data/bin/console +1 -0
- data/dependencies.rb +11 -0
- data/dev.yml +11 -0
- data/lib/ast_transform/abstract_transformation.rb +10 -1
- data/lib/ast_transform/instruction_sequence/bootsnap_mixin.rb +4 -4
- data/lib/ast_transform/instruction_sequence/mixin.rb +6 -5
- data/lib/ast_transform/instruction_sequence/mixin_utils.rb +1 -1
- data/lib/ast_transform/instruction_sequence.rb +3 -2
- data/lib/ast_transform/kwargs_builder.rb +10 -8
- data/lib/ast_transform/layout.rb +64 -0
- data/lib/ast_transform/line_aligned_emitter.rb +224 -0
- data/lib/ast_transform/node.rb +48 -0
- data/lib/ast_transform/statement_renderer.rb +76 -0
- data/lib/ast_transform/testing/assertions.rb +97 -0
- data/lib/ast_transform/thunk.rb +55 -0
- data/lib/ast_transform/thunk_lowering.rb +240 -0
- data/lib/ast_transform/transformation.rb +19 -22
- data/lib/ast_transform/transformation_helper.rb +115 -4
- data/lib/ast_transform/transformer.rb +19 -25
- data/lib/ast_transform/version.rb +3 -1
- data/lib/ast_transform.rb +11 -15
- metadata +19 -92
- data/lib/ast_transform/source_map.rb +0 -233
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d42a8d0999b373707aeb231d0c8bcf50ef7011ef8fd2bc2e9de9e7fda131550b
|
|
4
|
+
data.tar.gz: e7dccb5d7359e7fd974b5497ffbdf433458141ea278e25e937b3dda1c574dc2b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ad852344a9213e684c9e8f05e98d7d0292d654ee2ca2a4af14aaa230428ecdb165579bf914fc0e751259e22f0712553e4a8c7b4ae65d776a946baa055a67cd50
|
|
7
|
+
data.tar.gz: 5e20310267e2f0b233eb790cc10808436dfdbf5f6c7d8700935982ffefdeae1660a8fac0df1f4e933ed3704c96079dbea2526109ec3d8eb4e60f5054313793ea
|
data/.github/CODEOWNERS
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* @JPDuchesne
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -7,12 +7,22 @@ on:
|
|
|
7
7
|
branches: [main, master]
|
|
8
8
|
|
|
9
9
|
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: ruby/setup-ruby@v1
|
|
15
|
+
with:
|
|
16
|
+
ruby-version: '4.0'
|
|
17
|
+
bundler-cache: true
|
|
18
|
+
- run: bundle exec rubocop
|
|
19
|
+
|
|
10
20
|
test:
|
|
11
21
|
runs-on: ubuntu-latest
|
|
12
22
|
strategy:
|
|
13
23
|
fail-fast: false
|
|
14
24
|
matrix:
|
|
15
|
-
ruby: ['3.
|
|
25
|
+
ruby: ['3.3', '3.4', '4.0']
|
|
16
26
|
steps:
|
|
17
27
|
- uses: actions/checkout@v4
|
|
18
28
|
- uses: ruby/setup-ruby@v1
|
data/.gitignore
CHANGED
data/.rubocop.yml
ADDED
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4.0.6
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ 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
|
+
## [3.0.0] - 2026-07-24
|
|
8
|
+
### Added
|
|
9
|
+
- Line-aligned emission: transformed code is emitted with every loc-carrying statement on its original source line, making backtraces, breakpoints, and debugger display correct by construction (`LineAlignedEmitter`).
|
|
10
|
+
- Authoring toolkit in `TransformationHelper`: `s_at` (loc-anchored node construction), `thunk` (a single invariant-checked `Thunk` node spliced at the execution point; the lowering derives the hidden proc's textual placement from the body's source locations), and `run_after` (sequence-level execution reordering that preserves textual/source order). Thunks lower to a non-lambda proc, so `return` still returns from the enclosing method, and locals assigned by thunked statements are pre-declared to stay method-scope. Reusing one thunk node executes its body from several points.
|
|
11
|
+
- `ASTTransform::Node.register`: type-routed construction of custom IR node classes through `s`, with an emitter postcondition (`LineAlignedEmitter::UnloweredNodeTypeError`) rejecting custom types that were not lowered before emission.
|
|
12
|
+
- `ast_transform/testing/assertions` (test-only): `assert_line_aligned` and `assert_backtrace_lines` for transform authors' suites.
|
|
13
|
+
- Error types, each owned by its producer: `TransformationHelper::MissingLocationError`, `ThunkLowering::PlacementError`, `LineAlignedEmitter::UnloweredNodeTypeError`. Thunk construction invariants raise plain `ArgumentError`.
|
|
14
|
+
|
|
15
|
+
### Removed
|
|
16
|
+
- **Breaking:** `ASTTransform::SourceMap` and source-map registration. Line-aligned emission makes raw VM line numbers the source line numbers, so there is nothing left to map at display time.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- **Breaking:** `Transformer#transform` and `#transform_file_source` emit line-aligned output (source-anchored layout, always newline-terminated) instead of Unparser's re-normalized formatting.
|
|
20
|
+
- **Breaking:** dropped Ruby 3.2 support (EOL since March 2026); `required_ruby_version` is now `>= 3.3`.
|
|
21
|
+
- Dependency floors now reflect reality: `unparser >= 0.8` (the emitter uses `static_local_variables:`, a 0.7 interface, and 0.8's prism-based round-trip verification is required for Ruby >= 3.4 syntax) and `parser >= 3.3` (unparser's own floor; the declared `>= 3.0` could never resolve lower).
|
|
22
|
+
|
|
7
23
|
## [0.1.4] 2019-06-20
|
|
8
24
|
### Fixed
|
|
9
25
|
- Source mapping for transformations wrapping source nodes into virtual nodes now work.
|
data/Gemfile
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
source "https://rubygems.org"
|
|
2
4
|
|
|
3
|
-
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
|
|
5
|
+
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
|
|
4
6
|
|
|
5
7
|
# Specify your gem's dependencies in ast_transform.gemspec
|
|
6
8
|
gemspec
|
|
9
|
+
|
|
10
|
+
# Development dependencies
|
|
11
|
+
gem "bundler", ">= 2.1"
|
|
12
|
+
gem "minitest", "~> 5.14"
|
|
13
|
+
gem "minitest-reporters", "~> 1.4"
|
|
14
|
+
gem "pry", ">= 0.14"
|
|
15
|
+
gem "rake", "~> 13.0"
|
|
16
|
+
gem "rubocop-shopify", "~> 3.0", require: false
|
|
17
|
+
gem "simplecov", "~> 0.22"
|
data/Gemfile.lock
CHANGED
|
@@ -1,29 +1,33 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
ast_transform (
|
|
5
|
-
parser (>= 3.
|
|
4
|
+
ast_transform (3.0.0)
|
|
5
|
+
parser (>= 3.3)
|
|
6
6
|
prism (>= 1.5)
|
|
7
|
-
unparser (>= 0.
|
|
7
|
+
unparser (>= 0.8)
|
|
8
8
|
|
|
9
9
|
GEM
|
|
10
10
|
remote: https://rubygems.org/
|
|
11
11
|
specs:
|
|
12
|
-
ansi (1.
|
|
12
|
+
ansi (1.6.0)
|
|
13
13
|
ast (2.4.3)
|
|
14
14
|
builder (3.3.0)
|
|
15
15
|
coderay (1.1.3)
|
|
16
|
-
diff-lcs (
|
|
16
|
+
diff-lcs (2.0.0)
|
|
17
17
|
docile (1.4.1)
|
|
18
18
|
io-console (0.8.2)
|
|
19
|
+
json (2.21.1)
|
|
20
|
+
language_server-protocol (3.17.0.6)
|
|
21
|
+
lint_roller (1.1.0)
|
|
19
22
|
method_source (1.1.0)
|
|
20
23
|
minitest (5.27.0)
|
|
21
|
-
minitest-reporters (1.
|
|
24
|
+
minitest-reporters (1.8.0)
|
|
22
25
|
ansi
|
|
23
26
|
builder
|
|
24
|
-
minitest (>= 5.0)
|
|
27
|
+
minitest (>= 5.0, < 7)
|
|
25
28
|
ruby-progressbar
|
|
26
|
-
|
|
29
|
+
parallel (2.1.0)
|
|
30
|
+
parser (3.3.12.0)
|
|
27
31
|
ast (~> 2.4.1)
|
|
28
32
|
racc
|
|
29
33
|
prism (1.9.0)
|
|
@@ -32,9 +36,28 @@ GEM
|
|
|
32
36
|
method_source (~> 1.0)
|
|
33
37
|
reline (>= 0.6.0)
|
|
34
38
|
racc (1.8.1)
|
|
35
|
-
|
|
39
|
+
rainbow (3.1.1)
|
|
40
|
+
rake (13.4.2)
|
|
41
|
+
regexp_parser (2.12.0)
|
|
36
42
|
reline (0.6.3)
|
|
37
43
|
io-console (~> 0.5)
|
|
44
|
+
rubocop (1.88.2)
|
|
45
|
+
json (~> 2.3)
|
|
46
|
+
language_server-protocol (~> 3.17.0.2)
|
|
47
|
+
lint_roller (~> 1.1.0)
|
|
48
|
+
parallel (>= 1.10)
|
|
49
|
+
parser (>= 3.3.0.2)
|
|
50
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
51
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
|
52
|
+
rubocop-ast (>= 1.49.0, < 2.0)
|
|
53
|
+
ruby-progressbar (~> 1.7)
|
|
54
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
|
55
|
+
rubocop-ast (1.50.0)
|
|
56
|
+
parser (>= 3.3.7.2)
|
|
57
|
+
prism (~> 1.7)
|
|
58
|
+
rubocop-shopify (3.0.1)
|
|
59
|
+
lint_roller
|
|
60
|
+
rubocop (~> 1.72, >= 1.72.1)
|
|
38
61
|
ruby-progressbar (1.13.0)
|
|
39
62
|
simplecov (0.22.0)
|
|
40
63
|
docile (~> 1.1)
|
|
@@ -42,8 +65,11 @@ GEM
|
|
|
42
65
|
simplecov_json_formatter (~> 0.1)
|
|
43
66
|
simplecov-html (0.13.2)
|
|
44
67
|
simplecov_json_formatter (0.1.4)
|
|
45
|
-
|
|
46
|
-
|
|
68
|
+
unicode-display_width (3.2.0)
|
|
69
|
+
unicode-emoji (~> 4.1)
|
|
70
|
+
unicode-emoji (4.2.0)
|
|
71
|
+
unparser (0.9.0)
|
|
72
|
+
diff-lcs (>= 1.6, < 3)
|
|
47
73
|
parser (>= 3.3.0)
|
|
48
74
|
prism (>= 1.5.1)
|
|
49
75
|
|
|
@@ -58,6 +84,7 @@ DEPENDENCIES
|
|
|
58
84
|
minitest-reporters (~> 1.4)
|
|
59
85
|
pry (>= 0.14)
|
|
60
86
|
rake (~> 13.0)
|
|
87
|
+
rubocop-shopify (~> 3.0)
|
|
61
88
|
simplecov (~> 0.22)
|
|
62
89
|
|
|
63
90
|
BUNDLED WITH
|
data/README.md
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
ASTTransform is an Abstract Syntax Tree (AST) transformation framework. It hooks into the compilation process and allows to perform AST transformations using an annotation: `transform!`.
|
|
7
7
|
|
|
8
|
+
Transformed code is emitted **line-aligned**: every statement carrying a source location is placed on its original source line. Backtraces, failure messages, `break file:line` breakpoints, and debugger display are therefore correct by construction — no source maps, no backtrace filtering, no debugger integration required.
|
|
9
|
+
|
|
8
10
|
## Installation
|
|
9
11
|
|
|
10
12
|
Add this line to your application's Gemfile:
|
|
@@ -160,6 +162,54 @@ In the above, `node#updated` allows updating the node, either its type or its ch
|
|
|
160
162
|
|
|
161
163
|
The [ast gem](https://github.com/whitequark/ast) uses a pattern in which a Transformation may implement a method matching a node type, i.e. `on_class`, `on_send`, `on_lvar`, etc... This is very useful when transformations should process all nodes of this type.
|
|
162
164
|
|
|
165
|
+
### Line-aligned emission and the authoring contract
|
|
166
|
+
|
|
167
|
+
ASTTransform owns text and lines; transform authors own semantics and execution order. The contract:
|
|
168
|
+
|
|
169
|
+
* A node **with** a source location is emitted at that location's line (the emitter pads with blank lines to reach it, and packs with `;` when a line is already occupied).
|
|
170
|
+
* A node **without** a source location is synthetic: it packs onto the current line and inherits its neighbors' line number.
|
|
171
|
+
* Textual order is source order. If your transform needs code to *execute* in a different order than it *appears*, use a thunk (below) instead of moving nodes.
|
|
172
|
+
|
|
173
|
+
`ASTTransform::TransformationHelper` (included by `AbstractTransformation`) provides the authoring toolkit:
|
|
174
|
+
|
|
175
|
+
* `s(type, *children)` — builds a loc-less (synthetic) node. Registered custom types (see below) construct their registered class.
|
|
176
|
+
* `s_at(anchor, type, *children)` — builds a node anchored at `anchor`'s source location, so it is emitted at `anchor`'s line. Raises `TransformationHelper::MissingLocationError` if the anchor has no location.
|
|
177
|
+
* `thunk(*statements)` — wraps statements in a single `Thunk` node: splice it wherever the statements must *run*, in statement position or composed inside an expression (e.g. an `assert_raises` block body). The wrapped statements keep their own locations, and the lowering derives the hidden proc's textual placement from them — the body still emits on its source lines even though execution waits. Reuse the same node to execute one body from several points. Thunk construction is invariant-checked (`ArgumentError` on a missing id or empty body); a body whose source lines fall after its execution point fails lowering with `ThunkLowering::PlacementError` (a thunk can only delay execution, never text).
|
|
178
|
+
* `run_after(statements, run:, after:)` — the paved road over `thunk`: returns a reordered copy of `statements` where the contiguous `run` executes after `after`, while remaining at its source position textually. Elements are matched by object identity.
|
|
179
|
+
|
|
180
|
+
Thunked statements keep their original meaning as far as Ruby's closure semantics allow:
|
|
181
|
+
|
|
182
|
+
* `return` still returns from the enclosing method — the hidden closure is a non-lambda proc, and the proc and its call always share one method activation (placements never cross a `def` boundary).
|
|
183
|
+
* Locals assigned by the thunked statements stay method-scope: the lowering pre-declares each one (`result = result`) before the proc, so code after the execution point can read them. Before the thunk runs they are `nil` — exactly what an unexecuted assignment yields.
|
|
184
|
+
* Jump keywords whose owner lies *outside* the thunked statements keep Ruby's native behavior: `break`/`retry` raise `LocalJumpError` at the jump's own source line, while `next`/`redo` silently end or restart the thunk body. ASTTransform does not validate this — what a transform surface allows users to thunk is the transform author's call.
|
|
185
|
+
|
|
186
|
+
#### Gotcha: location-only rewrites are silently dropped
|
|
187
|
+
|
|
188
|
+
`Parser::AST::Node#updated` returns `self` when the new children compare `==` to the old ones — and `AST::Node#==` ignores source locations. A `Processor` pass that replaces a node with an equal-valued one (e.g. the same call rebuilt loc-less, hoping to change its emitted line) is a no-op: every `node.updated(nil, process_all(node))` up the tree discards the replacement. Location is part of a node's *emission*, not its *value* — to change where a node emits, change what it is (`s_at` an anchored rebuild with different children), or restructure the parent explicitly rather than relying on `updated`.
|
|
189
|
+
|
|
190
|
+
#### Custom node types (intermediate representation)
|
|
191
|
+
|
|
192
|
+
Transformations that parse a DSL can build their own IR by registering node classes:
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
class InteractionNode < ASTTransform::Node
|
|
196
|
+
register :my_interaction
|
|
197
|
+
|
|
198
|
+
def cardinality = children[0]
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
s(:my_interaction, ...) # => InteractionNode, with domain accessors
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Custom node types are IR **between stages that understand them** — the stage that owns a type must lower it to plain Ruby nodes before emission. The emitter enforces this: any registered or `ast_`-prefixed type reaching emission raises `LineAlignedEmitter::UnloweredNodeTypeError`.
|
|
205
|
+
|
|
206
|
+
#### Testing your transformation
|
|
207
|
+
|
|
208
|
+
`require 'ast_transform/testing/assertions'` (test-only) provides `ASTTransform::Testing::Assertions`, a Minitest-flavored module to include in your test class:
|
|
209
|
+
|
|
210
|
+
* `assert_line_aligned(source, *transformations)` — transforms `source` through the real pipeline and asserts every surviving statement is emitted at its source line.
|
|
211
|
+
* `assert_backtrace_lines(source, path:, raise_at:)` — compiles and executes `source`, asserting the raw first backtrace frame cites `path:raise_at` with no filtering.
|
|
212
|
+
|
|
163
213
|
### Parameterizable transformations
|
|
164
214
|
|
|
165
215
|
If you want your transformation to be customizable, accept the parameters in the constructor. The annotation can the be changed accordingly:
|
|
@@ -183,6 +233,8 @@ end
|
|
|
183
233
|
|
|
184
234
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
185
235
|
|
|
236
|
+
If you use the [d3mlabs dev tool](https://github.com/d3mlabs/dev), `dev up` provisions the pinned Ruby (see `.ruby-version`) with a per-project shadowenv, and `dev test` runs the suite — plain Bundler as above works just as well.
|
|
237
|
+
|
|
186
238
|
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
187
239
|
|
|
188
240
|
## Releasing a New Version
|
|
@@ -191,7 +243,7 @@ There are two ways to create a release. Both require that `version.rb` has alrea
|
|
|
191
243
|
|
|
192
244
|
### Via GitHub UI
|
|
193
245
|
|
|
194
|
-
1. Update `VERSION` in `lib/ast_transform/version.rb`, commit, open a PR, and merge to main
|
|
246
|
+
1. Update `VERSION` in `lib/ast_transform/version.rb` and run `bundle install` to regenerate `Gemfile.lock`, commit, open a PR, and merge to main
|
|
195
247
|
2. Go to the repo on GitHub → **Releases** → **Draft a new release**
|
|
196
248
|
3. Enter a new tag (e.g. `v2.0.0`), select `main` as the target branch
|
|
197
249
|
4. Add a title and release notes (GitHub can auto-generate these from merged PRs)
|
|
@@ -199,7 +251,7 @@ There are two ways to create a release. Both require that `version.rb` has alrea
|
|
|
199
251
|
|
|
200
252
|
### Via CLI
|
|
201
253
|
|
|
202
|
-
1. Update `VERSION` in `lib/ast_transform/version.rb`, commit, open a PR, and merge to main
|
|
254
|
+
1. Update `VERSION` in `lib/ast_transform/version.rb` and run `bundle install` to regenerate `Gemfile.lock`, commit, open a PR, and merge to main
|
|
203
255
|
2. Tag and push:
|
|
204
256
|
```
|
|
205
257
|
git checkout main && git pull
|
data/Rakefile
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rake/testtask"
|
|
4
5
|
|
|
5
6
|
Rake::TestTask.new(:test) do |t|
|
|
6
7
|
# Ensure we load test_loader first for ASTTransform.install
|
|
7
|
-
filepath = File.expand_path(
|
|
8
|
+
filepath = File.expand_path("test/test_loader.rb", __dir__)
|
|
8
9
|
t.ruby_opts << "-r #{filepath}"
|
|
9
10
|
t.warning = false
|
|
10
11
|
|
|
11
|
-
t.libs <<
|
|
12
|
-
t.libs <<
|
|
13
|
-
t.test_files = FileList[
|
|
12
|
+
t.libs << "test"
|
|
13
|
+
t.libs << "lib"
|
|
14
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
|
14
15
|
end
|
|
15
16
|
|
|
16
|
-
task :
|
|
17
|
+
task default: :test
|
data/ast_transform.gemspec
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
|
|
2
3
|
lib = File.expand_path("../lib", __FILE__)
|
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
@@ -9,28 +10,26 @@ Gem::Specification.new do |spec|
|
|
|
9
10
|
spec.authors = ["Jean-Philippe Duchesne"]
|
|
10
11
|
spec.email = ["jpduchesne89@gmail.com"]
|
|
11
12
|
|
|
12
|
-
spec.summary =
|
|
13
|
+
spec.summary = "An AST transformation framework."
|
|
13
14
|
spec.description = spec.summary
|
|
14
15
|
spec.homepage = "https://github.com/rspockframework/ast-transform"
|
|
15
16
|
spec.license = "MIT"
|
|
16
|
-
spec.files =
|
|
17
|
+
spec.files = %x(git ls-files -z).split("\x0").reject do |f|
|
|
17
18
|
f.match(%r{^(test|spec|features)/})
|
|
18
19
|
end
|
|
19
20
|
spec.bindir = "exe"
|
|
20
21
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
21
22
|
spec.require_paths = ["lib"]
|
|
22
|
-
spec.required_ruby_version = '>= 3.
|
|
23
|
+
spec.required_ruby_version = '>= 3.3'
|
|
23
24
|
|
|
24
|
-
# Development dependencies
|
|
25
|
-
spec.add_development_dependency "bundler", ">= 2.1"
|
|
26
|
-
spec.add_development_dependency "rake", "~> 13.0"
|
|
27
|
-
spec.add_development_dependency "minitest", "~> 5.14"
|
|
28
|
-
spec.add_development_dependency "minitest-reporters", "~> 1.4"
|
|
29
|
-
spec.add_development_dependency "pry", ">= 0.14"
|
|
30
|
-
spec.add_development_dependency "simplecov", "~> 0.22"
|
|
25
|
+
# Development dependencies live in the Gemfile (Gemspec/DevelopmentDependencies).
|
|
31
26
|
|
|
32
27
|
# Runtime dependencies
|
|
33
|
-
|
|
28
|
+
# parser provides the runtime AST vocabulary (Parser::AST::Node/Processor,
|
|
29
|
+
# Source::Buffer/Map); parsing itself goes through prism's translation layer.
|
|
30
|
+
spec.add_runtime_dependency "parser", ">= 3.3"
|
|
34
31
|
spec.add_runtime_dependency "prism", ">= 1.5"
|
|
35
|
-
|
|
32
|
+
# unparser >= 0.8: static_local_variables: (0.7 interface) + the prism-based
|
|
33
|
+
# round-trip verification parser required for Ruby >= 3.4 syntax.
|
|
34
|
+
spec.add_runtime_dependency "unparser", ">= 0.8"
|
|
36
35
|
end
|
data/bin/console
CHANGED
data/dependencies.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Toolchain-only manifest for d3mlabs' dev tool: it provisions this exact
|
|
4
|
+
# Ruby (rbenv + shadowenv) for `dev` commands. Gems stay bundler-managed
|
|
5
|
+
# through the hand-written gemspec/Gemfile; contributors without dev can
|
|
6
|
+
# ignore this file and use .ruby-version.
|
|
7
|
+
require "dev/deps"
|
|
8
|
+
|
|
9
|
+
Dev::Deps.define do
|
|
10
|
+
ruby "4.0.6"
|
|
11
|
+
end
|
data/dev.yml
ADDED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
require "ast_transform/transformation_helper"
|
|
3
4
|
|
|
4
5
|
module ASTTransform
|
|
5
6
|
class AbstractTransformation < Parser::AST::Processor
|
|
@@ -22,6 +23,14 @@ module ASTTransform
|
|
|
22
23
|
process_node(node)
|
|
23
24
|
end
|
|
24
25
|
|
|
26
|
+
# Thunks are framework-owned IR: descend into the body so passes that don't know about thunks still process the
|
|
27
|
+
# wrapped statements. Without this, Processor's handler_missing default would pass the node through opaquely,
|
|
28
|
+
# hiding the body from every later transformation. The token (first child) is not a node and passes through
|
|
29
|
+
# untouched.
|
|
30
|
+
def on_ast_thunk(node)
|
|
31
|
+
node.updated(nil, [node.children[0], *process_all(node.children.drop(1))])
|
|
32
|
+
end
|
|
33
|
+
|
|
25
34
|
private
|
|
26
35
|
|
|
27
36
|
# Processes the given +node+.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
3
|
+
require "ast_transform/transformer"
|
|
4
|
+
require "ast_transform/instruction_sequence/mixin_utils"
|
|
5
|
+
require "pathname"
|
|
6
6
|
|
|
7
7
|
module ASTTransform
|
|
8
8
|
module InstructionSequence
|
|
@@ -14,7 +14,7 @@ module ASTTransform
|
|
|
14
14
|
iseq = ASTTransform::InstructionSequence.source_to_transformed_iseq(source, source_path)
|
|
15
15
|
iseq.to_binary
|
|
16
16
|
rescue SyntaxError
|
|
17
|
-
raise ::Bootsnap::CompileCache::Uncompilable,
|
|
17
|
+
raise ::Bootsnap::CompileCache::Uncompilable, "syntax error"
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
end
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "ast_transform/transformer"
|
|
5
|
+
require "ast_transform/transformation"
|
|
6
|
+
require "ast_transform/instruction_sequence/mixin_utils"
|
|
6
7
|
|
|
7
8
|
module ASTTransform
|
|
8
9
|
module InstructionSequence
|
|
@@ -15,7 +16,7 @@ module ASTTransform
|
|
|
15
16
|
# via magic comments, so we never need to set it ourselves.
|
|
16
17
|
source = File.binread(source_path)
|
|
17
18
|
|
|
18
|
-
return ASTTransform::MixinUtils.try_super(self, :load_iseq, source_path) unless source.include?(
|
|
19
|
+
return ASTTransform::MixinUtils.try_super(self, :load_iseq, source_path) unless source.include?("transform!".b)
|
|
19
20
|
|
|
20
21
|
ASTTransform::InstructionSequence.source_to_transformed_iseq(source, source_path)
|
|
21
22
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module ASTTransform
|
|
3
4
|
module InstructionSequence
|
|
4
5
|
class << self
|
|
@@ -22,12 +23,12 @@ module ASTTransform
|
|
|
22
23
|
def write_pathname(file_path)
|
|
23
24
|
project_path = File.expand_path("")
|
|
24
25
|
relative_source_file_pathname = Pathname.new(file_path).relative_path_from(Pathname.new(project_path))
|
|
25
|
-
Pathname.new("").join(project_path,
|
|
26
|
+
Pathname.new("").join(project_path, "tmp", "ast_transform", relative_source_file_pathname)
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
def write(string, pathname)
|
|
29
30
|
FileUtils.mkdir_p(pathname.dirname)
|
|
30
|
-
File.open(pathname,
|
|
31
|
+
File.open(pathname, "w") do |file|
|
|
31
32
|
file.write(string)
|
|
32
33
|
end
|
|
33
34
|
end
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "prism/translation/parser"
|
|
4
4
|
|
|
5
5
|
module ASTTransform
|
|
6
|
-
# Extends the default Prism parser builder to distinguish keyword arguments
|
|
7
|
-
# from hash literals in the AST.
|
|
6
|
+
# Extends the default Prism parser builder to distinguish keyword arguments from hash literals in the AST.
|
|
8
7
|
#
|
|
9
|
-
# The upstream builder always emits :hash nodes for both `foo(bar: 1)` and
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# AST
|
|
8
|
+
# The upstream builder always emits :hash nodes for both `foo(bar: 1)` and `foo({ bar: 1 })`. Unparser uses the
|
|
9
|
+
# node type to decide whether to emit braces: :hash gets `{}`, :kwargs does not. Since Ruby 3.0+ treats these as
|
|
10
|
+
# semantically different (strict keyword/positional separation), we need the AST to preserve the distinction.
|
|
11
|
+
#
|
|
12
|
+
# NOTE: parsed nodes deliberately stay plain Parser::AST::Node. Custom node classes exist only for registered
|
|
13
|
+
# custom types (see ASTTransform::Node), which are IR and never reach Unparser: AST::Node#eql? compares class, and
|
|
14
|
+
# Unparser verifies dynamic-string emission by re-parsing and comparing eql? against the freshly parsed
|
|
15
|
+
# (plain-class) node — custom-class nodes of standard types would fail that verification.
|
|
14
16
|
class KwargsBuilder < Prism::Translation::Parser::Builder
|
|
15
17
|
def associate(begin_t, pairs, end_t)
|
|
16
18
|
node = super
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ASTTransform
|
|
4
|
+
# Line-addressed output: text is placed at absolute line numbers, top to bottom, and the cursor never rewinds.
|
|
5
|
+
# When a placement's target line is already behind the cursor, the text is packed (`; `) onto the current line
|
|
6
|
+
# instead — Ruby lets statements share a physical line, so alignment degrades locally and the next placement
|
|
7
|
+
# whose target is still ahead re-anchors. Knows nothing about Ruby structure or ASTs; callers decide WHAT goes
|
|
8
|
+
# on WHICH line, the layout owns the pad-or-pack mechanics.
|
|
9
|
+
class Layout
|
|
10
|
+
def initialize
|
|
11
|
+
@lines = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# The line number currently being written; the next fresh line would be +cursor + 1+.
|
|
15
|
+
def cursor
|
|
16
|
+
@lines.size
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Places +text+ at +target_line+ when the cursor hasn't passed it; otherwise packs onto the current line.
|
|
20
|
+
# Multi-line text advances the cursor by its height. When opening a fresh line, the first line is indented to
|
|
21
|
+
# +column+ — cosmetic only (leading whitespace is never significant in emitted code), but it keeps the artifact
|
|
22
|
+
# visually close to the source. Packed text ignores the column, as do continuation lines (they keep their own
|
|
23
|
+
# relative indentation).
|
|
24
|
+
def place(target_line, text, column: nil)
|
|
25
|
+
first, *rest = text.split("\n")
|
|
26
|
+
|
|
27
|
+
if target_line && target_line > @lines.size
|
|
28
|
+
@lines << '' while @lines.size < target_line
|
|
29
|
+
@lines[-1] = indented(first, column)
|
|
30
|
+
else
|
|
31
|
+
pack(first)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@lines.concat(rest)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Appends +text+ on a new line unconditionally — for text that must never be `;`-packed after a statement
|
|
38
|
+
# (e.g. keywords).
|
|
39
|
+
def place_on_fresh_line(text)
|
|
40
|
+
@lines << text
|
|
41
|
+
end
|
|
42
|
+
|
|
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.
|
|
45
|
+
def pack(text)
|
|
46
|
+
if @lines.empty?
|
|
47
|
+
@lines << text
|
|
48
|
+
else
|
|
49
|
+
@lines[-1] = "#{@lines.last}; #{text}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [String] the laid-out text, with a trailing newline.
|
|
54
|
+
def to_source
|
|
55
|
+
"#{@lines.join("\n")}\n"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def indented(text, column)
|
|
61
|
+
column && column.positive? ? "#{' ' * column}#{text}" : text
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|