ast_transform 3.0.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: d42a8d0999b373707aeb231d0c8bcf50ef7011ef8fd2bc2e9de9e7fda131550b
4
- data.tar.gz: e7dccb5d7359e7fd974b5497ffbdf433458141ea278e25e937b3dda1c574dc2b
3
+ metadata.gz: fb38ad75ff6d31e4f8da1d1b272968b3d65393a3acaf92b914be2c0d35ebdb32
4
+ data.tar.gz: f2621b3116184b0f7b7eb87a9a4b4af184f685e4399e5b52761b1deb1f54c998
5
5
  SHA512:
6
- metadata.gz: ad852344a9213e684c9e8f05e98d7d0292d654ee2ca2a4af14aaa230428ecdb165579bf914fc0e751259e22f0712553e4a8c7b4ae65d776a946baa055a67cd50
7
- data.tar.gz: 5e20310267e2f0b233eb790cc10808436dfdbf5f6c7d8700935982ffefdeae1660a8fac0df1f4e933ed3704c96079dbea2526109ec3d8eb4e60f5054313793ea
6
+ metadata.gz: 3ff9177f5bdd86e95af2406aeb675a53cd93917a63faa2fdf26bcfb12bc6365e415dbadb576247f7d134a2a36a7866d46cca17626351fb648f156379e3ba5473
7
+ data.tar.gz: 054fe46ee27bc9cda5f18b163460f62a94c496cc3993407e79a3592079d3011c56c78d300db0639d685749e47a704a0a3e6d3a1915fbcbd044de7a7b6a6c173b
data/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ 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.1.0] - 2026-07-29
8
+ ### Added
9
+ - 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.
10
+ - Public parse seam: `ASTTransform::SourceParser` (`#parse(source, file_path:)` / `#parse_file(path)`) extracts the framework's Prism-backed parsing from `Transformer` so analysis-only consumers that never emit can instantiate it directly. `Transformer#build_ast` / `#build_ast_from_file` delegate to it (unchanged public behavior).
11
+ - Pass-taxonomy documentation on `AbstractTransformation`, `Transformation`, and the README: structural (`on_*` handlers, pattern matched anywhere), positional (node builders with the bare `run(node) → node` duck type, caller owns traversal), and sibling-annotation (marker statement + next sibling, matched in `process_node` where the child list is visible — `transform!` itself).
12
+
7
13
  ## [3.0.0] - 2026-07-24
8
14
  ### Added
9
15
  - 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`).
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ast_transform (3.0.0)
4
+ ast_transform (3.1.1)
5
5
  parser (>= 3.3)
6
6
  prism (>= 1.5)
7
7
  unparser (>= 0.8)
data/README.md CHANGED
@@ -130,6 +130,16 @@ class MyTransformation < ASTTransformation::AbstractTransformation
130
130
  end
131
131
  ```
132
132
 
133
+ #### Choosing a pass shape
134
+
135
+ Passes come in three shapes, distinguished by how the rewrite site is found:
136
+
137
+ * **Structural** — the pattern alone identifies the site. Subclass `AbstractTransformation` and write `on_*` handlers; the pass rewrites the pattern wherever it occurs in the tree.
138
+ * **Positional (node builders)** — the *caller* owns traversal and has already located the site. Implement a plain `run(node) → node` object (include `TransformationHelper` for the `s(...)` vocabulary); the builder maps a single node to its replacement subtree and never walks. Anything with that duck type composes with `Transformer` and other passes.
139
+ * **Sibling-annotation** — the pattern is a marker statement plus its NEXT sibling. Match in `process_node`, where a node's child list is visible: an `on_*` handler sees one node, never its siblings, and cannot delete itself from its parent. `ASTTransform::Transformation` (the `transform!` detector) is the canonical example.
140
+
141
+ For read-only passes that harvest information instead of rewriting, see [Analysis passes](#analysis-passes).
142
+
133
143
  #### Transformation discoverability
134
144
 
135
145
  ASTTransform automatically loads your transformations at compile time. As such, we expect your files to be located at a known path.
@@ -162,6 +172,48 @@ In the above, `node#updated` allows updating the node, either its type or its ch
162
172
 
163
173
  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.
164
174
 
175
+ ### Analysis passes
176
+
177
+ Not every pass rewrites. An analysis walks the tree and harvests information — `Parser::AST::Processor` has no read-only mode (every walk functionally rebuilds the tree), so an analysis is simply a walk whose rebuilt tree is discarded. Derive from `ASTTransform::AbstractAnalysis`: it shares the traversal engine with `AbstractTransformation` (including thunk descent and the `s(...)` matching vocabulary) and differs only in what `run` returns — the analysis instance, so callers chain result readers off the run.
178
+
179
+ ```ruby
180
+ require 'ast_transform/abstract_analysis'
181
+
182
+ class TypeAliasRanges < ASTTransform::AbstractAnalysis
183
+ attr_reader :ranges
184
+
185
+ def initialize
186
+ @ranges = []
187
+ super
188
+ end
189
+
190
+ def on_block(node)
191
+ send_node = node.children.first
192
+ @ranges << (node.loc.expression.first_line..node.loc.expression.last_line) if type_alias?(send_node)
193
+ super
194
+ end
195
+
196
+ private
197
+
198
+ def type_alias?(send_node)
199
+ send_node == s(:send, s(:const, nil, :T), :type_alias)
200
+ end
201
+ end
202
+ ```
203
+
204
+ Harvest state in `on_*` handlers and always call `super` so traversal continues. Node equality ignores source locations, so `s(...)` patterns match structurally.
205
+
206
+ Analysis-only consumers don't need a `Transformer`; instantiate the parsing seam directly (`Transformer` uses the same class internally) and keep the instance for as many parses as you need:
207
+
208
+ ```ruby
209
+ parser = ASTTransform::SourceParser.new
210
+
211
+ parser.parse(source) # => Parser::AST::Node
212
+ parser.parse_file(file_path) # => Parser::AST::Node
213
+
214
+ TypeAliasRanges.new.run(parser.parse(source)).ranges
215
+ ```
216
+
165
217
  ### Line-aligned emission and the authoring contract
166
218
 
167
219
  ASTTransform owns text and lines; transform authors own semantics and execution order. The contract:
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ast_transform/abstract_processor"
4
+
5
+ module ASTTransform
6
+ # Base class for read-only analysis passes: subclass, harvest state in +on_*+ handlers (always call +super+ so
7
+ # traversal continues), and expose results through readers. The walk still functionally rebuilds the tree —
8
+ # Parser::AST::Processor has no read-only mode — but +run+ discards the rebuilt tree, so handlers never need to
9
+ # care what they return.
10
+ #
11
+ # class SendCounter < ASTTransform::AbstractAnalysis
12
+ # attr_reader :count
13
+ #
14
+ # def initialize
15
+ # @count = 0
16
+ # super
17
+ # end
18
+ #
19
+ # def on_send(node)
20
+ # @count += 1
21
+ # super
22
+ # end
23
+ # end
24
+ #
25
+ # SendCounter.new.run(SourceParser.new.parse(source)).count
26
+ class AbstractAnalysis < AbstractProcessor
27
+ # Runs this analysis on +node+, discarding the rebuilt tree.
28
+ # Note: If you want to add one-time setup or result finalization, override this, then call super.
29
+ #
30
+ # @param node [Parser::AST::Node] The node to be analyzed.
31
+ #
32
+ # @return [ASTTransform::AbstractAnalysis] self, so callers can chain result readers off the run.
33
+ def run(node)
34
+ process(node)
35
+ self
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ast_transform/transformation_helper"
4
+
5
+ module ASTTransform
6
+ # Shared traversal core for tree passes. Parser::AST::Processor is a rewriting walker — every visit functionally
7
+ # rebuilds the tree — so transformation and analysis share one engine and differ only in what they keep:
8
+ # AbstractTransformation's +run+ returns the rebuilt tree, AbstractAnalysis's +run+ discards it and returns the
9
+ # harvested results. Subclass one of those leaves rather than this class.
10
+ class AbstractProcessor < Parser::AST::Processor
11
+ include TransformationHelper
12
+
13
+ # Used internally by Parser::AST::Processor to process each node. DO NOT OVERRIDE.
14
+ def process(node)
15
+ return node unless node.is_a?(Parser::AST::Node)
16
+
17
+ process_node(node)
18
+ end
19
+
20
+ # Thunks are framework-owned IR: descend into the body so passes that don't know about thunks still process the
21
+ # wrapped statements. Without this, Processor's handler_missing default would pass the node through opaquely,
22
+ # hiding the body from every later pass. The token (first child) is not a node and passes through untouched.
23
+ def on_ast_thunk(node)
24
+ node.updated(nil, [node.children[0], *process_all(node.children.drop(1))])
25
+ end
26
+
27
+ private
28
+
29
+ # Processes the given +node+.
30
+ # Note: If you want to do processing on each node, override this.
31
+ #
32
+ # @param node [Parser::AST::Node] The node being visited.
33
+ #
34
+ # @return [Parser::AST::Node] The rebuilt node.
35
+ def process_node(node)
36
+ method(:process).super_method.call(node)
37
+ end
38
+ end
39
+ end
@@ -1,12 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "ast_transform/transformation_helper"
3
+ require "ast_transform/abstract_processor"
4
4
 
5
5
  module ASTTransform
6
- class AbstractTransformation < Parser::AST::Processor
7
- include TransformationHelper
8
-
9
- # Runs this transformation on +node+.
6
+ # Base class for structural transformations: subclass and write +on_*+ handlers to rewrite a pattern wherever it
7
+ # occurs in the tree. This is one of three authoring shapes — pick by how the rewrite site is found:
8
+ #
9
+ # - Structural (this class): the pattern alone identifies the site, so +on_*+ handlers rewrite it anywhere in
10
+ # the tree.
11
+ # - Positional (node builders): a plain object with +run(node) → node+, including only TransformationHelper. The
12
+ # caller owns traversal and applies the builder at a site it already located; the builder never walks. Anything
13
+ # with that duck type composes with Transformer and other passes — deriving from this class is just one way to
14
+ # implement it.
15
+ # - Sibling-annotation: the pattern is a marker statement plus its NEXT sibling, matched in +process_node+ where
16
+ # the child list is visible — an +on_*+ handler sees one node, never its siblings, and cannot delete itself
17
+ # from its parent. See ASTTransform::Transformation (the +transform!+ detector) for the canonical example.
18
+ #
19
+ # For read-only passes that harvest information instead of rewriting, see ASTTransform::AbstractAnalysis.
20
+ class AbstractTransformation < AbstractProcessor
21
+ # Runs this transformation on +node+ and returns the rebuilt tree.
10
22
  # Note: If you want to add one-time checks to the transformation, override this, then call super.
11
23
  #
12
24
  # @param node [Parser::AST::Node] The node to be transformed.
@@ -15,32 +27,5 @@ module ASTTransform
15
27
  def run(node)
16
28
  process(node)
17
29
  end
18
-
19
- # Used internally by Parser::AST::Processor to process each node. DO NOT OVERRIDE.
20
- def process(node)
21
- return node unless node.is_a?(Parser::AST::Node)
22
-
23
- process_node(node)
24
- end
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
-
34
- private
35
-
36
- # Processes the given +node+.
37
- # Note: If you want to do processing on each node, override this.
38
- #
39
- # @param node [Parser::AST::Node] The node to be transformed.
40
- #
41
- # @return [Parser::AST::Node] The transformed node.
42
- def process_node(node)
43
- method(:process).super_method.call(node)
44
- end
45
30
  end
46
31
  end
@@ -3,22 +3,21 @@
3
3
  require "prism/translation/parser"
4
4
 
5
5
  module ASTTransform
6
- # Extends the default Prism parser builder to distinguish keyword arguments from hash literals in the AST.
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.
7
8
  #
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.
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.
11
15
  #
12
16
  # NOTE: parsed nodes deliberately stay plain Parser::AST::Node. Custom node classes exist only for registered
13
17
  # custom types (see ASTTransform::Node), which are IR and never reach Unparser: AST::Node#eql? compares class, and
14
18
  # Unparser verifies dynamic-string emission by re-parsing and comparing eql? against the freshly parsed
15
19
  # (plain-class) node — custom-class nodes of standard types would fail that verification.
16
20
  class KwargsBuilder < Prism::Translation::Parser::Builder
17
- def associate(begin_t, pairs, end_t)
18
- node = super
19
- return node unless begin_t.nil? && end_t.nil?
20
-
21
- node.updated(:kwargs)
22
- end
21
+ self.emit_kwargs = true
23
22
  end
24
23
  end
@@ -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
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require "prism/translation/parser"
5
+ require "ast_transform/kwargs_builder"
6
+
7
+ module ASTTransform
8
+ # Owns source → AST parsing: Prism's C parser through its whitequark translation layer, so every consumer gets the
9
+ # node vocabulary Parser::AST::Processor understands. Transformer parses through this seam; analysis-only
10
+ # consumers that never emit instantiate it directly and keep the instance for as many parses as they need.
11
+ class SourceParser
12
+ # Constructs a new SourceParser instance.
13
+ #
14
+ # KwargsBuilder is a required implementation detail, not an injection seam: the framework's emission depends
15
+ # on the kwargs/hash distinction it preserves, so every parse goes through it.
16
+ def initialize
17
+ @builder = KwargsBuilder.new
18
+ end
19
+
20
+ # Parses the given +source+.
21
+ #
22
+ # @param source [String] The input source code.
23
+ # @param file_path [String] The file path recorded on source locations. This is important for source mapping
24
+ # in backtraces.
25
+ #
26
+ # @return [Parser::AST::Node] The AST.
27
+ def parse(source, file_path: "tmp")
28
+ # A fresh parser per parse: parser instances accumulate per-run state (lexer position, diagnostics), and
29
+ # constructing one is trivial next to the parse itself.
30
+ parser = Prism::Translation::Parser.new(@builder)
31
+ parser.parse(create_buffer(source, file_path, parser.default_encoding))
32
+ end
33
+
34
+ # Parses the source in the given +file_path+.
35
+ #
36
+ # @param file_path [String] The input file path.
37
+ #
38
+ # @return [Parser::AST::Node] The AST.
39
+ def parse_file(file_path)
40
+ parse(File.read(file_path), file_path: file_path)
41
+ end
42
+
43
+ private
44
+
45
+ # Builds a source buffer over +source+ in the given +encoding+.
46
+ #
47
+ # @param source [String] The input source code.
48
+ # @param file_path [String] The file path recorded on the buffer.
49
+ # @param encoding [Encoding] The encoding the buffer's source is coerced to.
50
+ #
51
+ # @return [Parser::Source::Buffer] The buffer.
52
+ def create_buffer(source, file_path, encoding)
53
+ buffer = Parser::Source::Buffer.new(file_path)
54
+ buffer.source = source.dup.force_encoding(encoding)
55
+
56
+ buffer
57
+ end
58
+ end
59
+ 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
@@ -6,6 +6,11 @@ require "ast_transform/transformer"
6
6
  require "unparser"
7
7
 
8
8
  module ASTTransform
9
+ # The +transform!+ detector — the canonical sibling-annotation pass (see AbstractTransformation for the taxonomy).
10
+ # A +transform!(...)+ statement is a pragma on the NEXT sibling: the transformations it names are applied to the
11
+ # following class definition or constant assignment, and the marker itself is deleted from the child list. Both
12
+ # effects need the parent's child list in view, which is why matching happens in +process_node+ — an +on_send+
13
+ # handler would see the marker node alone, with no access to its next sibling and no way to remove itself.
9
14
  class Transformation < ASTTransform::AbstractTransformation
10
15
  TRANSFORM_AST = s(:send, nil, :transform!)
11
16
 
@@ -1,10 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'prism'
4
- require 'prism/translation/parser'
5
- require 'unparser'
6
- require 'ast_transform/kwargs_builder'
7
3
  require 'ast_transform/line_aligned_emitter'
4
+ require 'ast_transform/source_parser'
8
5
 
9
6
  module ASTTransform
10
7
  class Transformer
@@ -15,6 +12,7 @@ module ASTTransform
15
12
  def initialize(*transformations, emitter: LineAlignedEmitter.new)
16
13
  @transformations = transformations
17
14
  @emitter = emitter
15
+ @source_parser = SourceParser.new
18
16
  end
19
17
 
20
18
  # Builds the AST for the given +source+.
@@ -24,8 +22,7 @@ module ASTTransform
24
22
  #
25
23
  # @return [Parser::AST::Node] The AST.
26
24
  def build_ast(source, file_path: "tmp")
27
- buffer = create_buffer(source, file_path)
28
- parser.parse(buffer)
25
+ @source_parser.parse(source, file_path: file_path)
29
26
  end
30
27
 
31
28
  # Builds the AST for the given +file_path+.
@@ -34,8 +31,7 @@ module ASTTransform
34
31
  #
35
32
  # @return [Parser::AST::Node] The AST.
36
33
  def build_ast_from_file(file_path)
37
- source = File.read(file_path)
38
- build_ast(source, file_path: file_path)
34
+ @source_parser.parse_file(file_path)
39
35
  end
40
36
 
41
37
  # Transforms the given +source+.
@@ -89,19 +85,5 @@ module ASTTransform
89
85
  transformation.run(ast)
90
86
  end
91
87
  end
92
-
93
- private
94
-
95
- def create_buffer(source, file_path)
96
- buffer = Parser::Source::Buffer.new(file_path)
97
- buffer.source = source.dup.force_encoding(parser.default_encoding)
98
-
99
- buffer
100
- end
101
-
102
- def parser
103
- @parser&.reset
104
- @parser ||= Prism::Translation::Parser.new(ASTTransform::KwargsBuilder.new)
105
- end
106
88
  end
107
89
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ASTTransform
4
- VERSION = "3.0.0"
4
+ VERSION = "3.1.1"
5
5
  end
data/lib/ast_transform.rb CHANGED
@@ -4,6 +4,7 @@ require "ast_transform/version"
4
4
  require "ast_transform/instruction_sequence"
5
5
  require "ast_transform/instruction_sequence/mixin"
6
6
  require "ast_transform/instruction_sequence/bootsnap_mixin"
7
+ require "ast_transform/source_parser"
7
8
 
8
9
  module ASTTransform
9
10
  DEFAULT_OUTPUT_PATH = Pathname.new("").join("tmp", "ast_transform").to_s
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.0.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-24 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
@@ -80,6 +80,8 @@ files:
80
80
  - dependencies.rb
81
81
  - dev.yml
82
82
  - lib/ast_transform.rb
83
+ - lib/ast_transform/abstract_analysis.rb
84
+ - lib/ast_transform/abstract_processor.rb
83
85
  - lib/ast_transform/abstract_transformation.rb
84
86
  - lib/ast_transform/instruction_sequence.rb
85
87
  - lib/ast_transform/instruction_sequence/bootsnap_mixin.rb
@@ -89,6 +91,7 @@ files:
89
91
  - lib/ast_transform/layout.rb
90
92
  - lib/ast_transform/line_aligned_emitter.rb
91
93
  - lib/ast_transform/node.rb
94
+ - lib/ast_transform/source_parser.rb
92
95
  - lib/ast_transform/statement_renderer.rb
93
96
  - lib/ast_transform/testing/assertions.rb
94
97
  - lib/ast_transform/thunk.rb