Astel
Astel provides fast, reusable building blocks for Ruby source analysis and transformation on top of Prism. It parses each source once and provides APIs for AST traversal, declarative node matching, and non-destructive source rewriting.
Installation
Install Astel with Bundler:
bundle add astel
Or install it directly with RubyGems:
gem install astel
Astel requires Ruby 3.3 or newer and Prism 0.30 or newer, but earlier than 2.0.
Usage
Require Astel before using its APIs:
require "astel"
Parse source
Parse a file from disk:
source = Astel::SourceFile.parse(path: "example.rb")
source.ast # => Prism::ProgramNode
source.comments # => Prism comments
source.errors # => Prism parse errors
source.valid? # => true when there are no parse errors
Use Astel::SourceFile.from_string when the source is already in memory:
source = Astel::SourceFile.from_string("value = 1\n", path: "example.rb")
Traverse the AST
Astel::Dispatcher walks the tree once and invokes callbacks registered for
specific Prism node types:
source = Astel::SourceFile.from_string(<<~RUBY)
puts "hello"
"value".freeze
RUBY
dispatcher = Astel::Dispatcher.new
dispatcher.on(:call_node) { |node| puts node.name }
dispatcher.run(source.ast)
Multiple callbacks can be registered for the same node type.
Match nodes
Astel::NodePattern compiles a declarative pattern that can be reused across
nodes:
source = Astel::SourceFile.from_string('"value".freeze')
node = source.ast.statements.body.first
pattern = Astel::NodePattern.compile(<<~PATTERN)
(call_node receiver: (string_node) name: :freeze)
PATTERN
pattern.match?(node) # => true
Prefix a subpattern with $ to capture its matched value:
pattern = Astel::NodePattern.compile(
"(call_node receiver: $(string_node) name: $:freeze)"
)
captures = pattern.match(node)
captures.first # => Prism::StringNode
captures.last # => :freeze
Patterns support node types, named fields, _ for any non-nil value, nil,
symbol, string, integer, and boolean literals, { ... } alternatives, and $
captures.
Rewrite source
Astel::Rewriter records edits without modifying the original SourceFile:
source = Astel::SourceFile.from_string("old_name\n")
node = source.ast.statements.body.first
rewriter = Astel::Rewriter.new(source)
rewriter.replace(node.location, "new_name")
rewriter.rewrite # => "new_name\n"
source.source # => "old_name\n"
The rewriter supports replace, remove, insert_before, and insert_after.
Overlapping edits raise Astel::Rewriter::ConflictError, and
Astel::Rewriter#edits returns an immutable snapshot of registered edits.
Performance and concurrency
Compile node patterns once and reuse them for every candidate node. Astel also keeps a bounded cache of compiler output for repeated pattern strings.
For repository-wide tools, process independent files in worker processes at
the application layer. Keep each SourceFile and its Prism AST inside the
worker that parsed it; Astel intentionally does not own a process pool or move
ASTs between workers.
Benchmarks for dispatching, node patterns, and rewriting are available under
benchmark/:
bundle exec ruby benchmark/dispatch_bench.rb
bundle exec ruby benchmark/node_pattern_bench.rb
bundle exec ruby benchmark/rewriter_bench.rb
Development
After checking out the repository, install dependencies and run the test suite:
bundle install
bundle exec rake
Verify the packaged gem with:
bundle exec ruby script/package_smoke.rb
Contributing
Bug reports and pull requests are welcome on GitHub. Please include tests for behavior changes and keep the existing test and performance gates passing.
See CHANGELOG.md for notable changes.
License
The gem is available as open source under the terms of the MIT License.