steadfast 0.0.1 → 0.0.2
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 +7 -1
- data/README.md +24 -2
- data/lib/steadfast/sourcifier.rb +114 -25
- data/lib/steadfast/version.rb +1 -1
- data/lib/steadfast.rb +75 -4
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5d446eb7d3f0db23310b1ea5dfc4343ff7b82bfaae804de963ea42616977b2ac
|
|
4
|
+
data.tar.gz: c02e0997705985da9800da6db4a88ea9e5a652e4e6562a2a7aba276eadf8d536
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 080361ccb2c5f651657f429f82c01444c60cc208b1a4fe2bb2c390dd75d879491347a12aec7f43dcc28dc8e8eb0ee2d80d18e1aa6d6765a810d2ef3f5ce70d41
|
|
7
|
+
data.tar.gz: a67148f3b42ce798e8278f043be6021e6cfa6fc3d9c89febb69bc1b0b68041ae86ef449af30351a55ec795c38fa6a46e245349118f9dab16564edd23e2140521
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
# 0.0.2 2026-11-09
|
|
2
|
+
|
|
3
|
+
- Emit newline upon detecting a source change while sourcifying an AST
|
|
4
|
+
- Implement source mapping
|
|
5
|
+
- Implement `Steadfast.translate_backtrace`
|
|
6
|
+
|
|
1
7
|
# 0.0.1 2026-11-06
|
|
2
8
|
|
|
3
|
-
- Implement Steadfast.to_source
|
|
9
|
+
- Implement `Steadfast.to_source`
|
data/README.md
CHANGED
|
@@ -1,2 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
<h1 align="center">
|
|
2
|
+
<br>
|
|
3
|
+
Steadfast
|
|
4
|
+
</h1>
|
|
5
|
+
|
|
6
|
+
<h4 align="center">A Metaprogramming Gem for Ruby</h4>
|
|
7
|
+
|
|
8
|
+
<p align="center">
|
|
9
|
+
<a href="http://rubygems.org/gems/steadfast">
|
|
10
|
+
<img src="https://badge.fury.io/rb/steadfast.svg" alt="Ruby gem">
|
|
11
|
+
</a>
|
|
12
|
+
<a href="https://github.com/digital-fabric/steadfast/actions">
|
|
13
|
+
<img src="https://github.com/digital-fabric/steadfast/actions/workflows/test.yml/badge.svg" alt="Tests">
|
|
14
|
+
</a>
|
|
15
|
+
<a href="https://github.com/digital-fabric/steadfast/blob/master/LICENSE">
|
|
16
|
+
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License">
|
|
17
|
+
</a>
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
<p align="center">
|
|
21
|
+
<img src="./steadfast.png">
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
More information coming soon...
|
data/lib/steadfast/sourcifier.rb
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Steadfast
|
|
4
|
+
|
|
5
|
+
# A Sourcifier is used for converting an AST to source code. Sourcifier is
|
|
6
|
+
# implemented as an AST visitor that walks the AST, copying the relevant
|
|
7
|
+
# strings from each node into a buffer, with the addition of whitespace,
|
|
8
|
+
# commas, semicolons and newlines where applicable.
|
|
2
9
|
class Sourcifier < Ruby::Prism::BasicVisitor
|
|
10
|
+
# The following visit plans are used for generating code for node types that
|
|
11
|
+
# don't require special logic. For each node, a list of fields/elements to
|
|
12
|
+
# emit is given. For each node, a visit_xxx_node method is generated from
|
|
13
|
+
# its visit plan. Nodes requiring special logic have normal visit_xxx_node
|
|
14
|
+
# method below.
|
|
3
15
|
VISIT_PLANS = {
|
|
4
16
|
alias_global_variable: [:keyword_loc, :whitespace, :new_name, :whitespace, :old_name],
|
|
5
17
|
alias_method: [:keyword_loc, :whitespace, :new_name, :whitespace, :old_name],
|
|
@@ -42,8 +54,8 @@ module Steadfast
|
|
|
42
54
|
global_variable_target: :emit_verbatim,
|
|
43
55
|
global_variable_write: [:name_loc, :operator_loc, :value],
|
|
44
56
|
imaginary: :emit_verbatim,
|
|
45
|
-
implicit: :
|
|
46
|
-
implicit_rest: :
|
|
57
|
+
implicit: :nop,
|
|
58
|
+
implicit_rest: :nop,
|
|
47
59
|
in: [:in_loc, :whitespace, :pattern, :whitespace, :then_loc],
|
|
48
60
|
index_target: [:receiver, :opening_loc, :arguments, :closing_loc],
|
|
49
61
|
instance_variable_operator_write: [:name_loc, :whitespace, :binary_operator_loc, :whitespace, :value],
|
|
@@ -52,7 +64,7 @@ module Steadfast
|
|
|
52
64
|
instance_variable_target: :emit_verbatim,
|
|
53
65
|
instance_variable_write: [:name_loc, :operator_loc, :value],
|
|
54
66
|
integer: :emit_verbatim,
|
|
55
|
-
it_parameters: :
|
|
67
|
+
it_parameters: :nop,
|
|
56
68
|
it_local_variable_read: :emit_verbatim,
|
|
57
69
|
keyword_rest_parameter: [:operator_loc, :name_loc],
|
|
58
70
|
keyword_parameter: :emit_verbatim,
|
|
@@ -70,7 +82,7 @@ module Steadfast
|
|
|
70
82
|
next: [:keyword_loc, :whitespace, :arguments],
|
|
71
83
|
nil: :emit_verbatim,
|
|
72
84
|
no_keywords_parameter: :emit_verbatim,
|
|
73
|
-
numbered_parameters: :
|
|
85
|
+
numbered_parameters: :nop,
|
|
74
86
|
optional_parameter: [:name_loc, :whitespace, :operator_loc, :whitespace, :value],
|
|
75
87
|
optional_keyword_parameter: [:name_loc, :whitespace, :value],
|
|
76
88
|
or: [:left, :whitespace, :operator_loc, :whitespace, :right],
|
|
@@ -101,19 +113,30 @@ module Steadfast
|
|
|
101
113
|
|
|
102
114
|
attr_reader :buffer
|
|
103
115
|
|
|
104
|
-
|
|
105
|
-
|
|
116
|
+
# Initializes the Sourcifier instance.
|
|
117
|
+
#
|
|
118
|
+
# @param start_line []
|
|
119
|
+
# @return [void]
|
|
120
|
+
def initialize(source_location: nil, sourcemap: false)
|
|
121
|
+
@filename, @line = source_location
|
|
122
|
+
@line ||= 1
|
|
123
|
+
@sourcemap = sourcemap
|
|
124
|
+
|
|
125
|
+
raise Steadfast::Error, "No filename given, can't generate source map" if @sourcemap && !@filename
|
|
106
126
|
end
|
|
107
127
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
128
|
+
# Converts the given AST to source code.
|
|
129
|
+
#
|
|
130
|
+
# @param ast [Ruby::Prism::Node] AST
|
|
131
|
+
# @return [String] source code
|
|
132
|
+
def to_source(ast)
|
|
133
|
+
@buffer = +''
|
|
134
|
+
visit(ast)
|
|
111
135
|
@buffer
|
|
112
136
|
end
|
|
113
137
|
|
|
114
|
-
VISIT_PLANS.each do |
|
|
115
|
-
|
|
116
|
-
define_method(sym) { |n| visit_plan(plan, n) }
|
|
138
|
+
VISIT_PLANS.each do |kind, plan|
|
|
139
|
+
define_method(:"visit_#{kind}_node") { |n| visit_node_with_plan(n, plan) }
|
|
117
140
|
end
|
|
118
141
|
|
|
119
142
|
def visit_array_node(node)
|
|
@@ -577,10 +600,15 @@ module Steadfast
|
|
|
577
600
|
|
|
578
601
|
private
|
|
579
602
|
|
|
580
|
-
|
|
603
|
+
# Visits a node with the given visit plan.
|
|
604
|
+
#
|
|
605
|
+
# @param node [Ruby::Prism::Node]
|
|
606
|
+
# @param plan [Array<Symbol>, Symbol] visit plan
|
|
607
|
+
# @return [void]
|
|
608
|
+
def visit_node_with_plan(node, plan)
|
|
581
609
|
return send(plan, node) if plan.is_a?(Symbol)
|
|
582
610
|
|
|
583
|
-
plan.
|
|
611
|
+
plan.each do |sym|
|
|
584
612
|
case sym
|
|
585
613
|
when :newline
|
|
586
614
|
emit_newline
|
|
@@ -593,17 +621,28 @@ module Steadfast
|
|
|
593
621
|
end
|
|
594
622
|
end
|
|
595
623
|
|
|
624
|
+
# Visit an array of nodes, with commas emitted between them. The given comma
|
|
625
|
+
# flag determines whether to emit a comma before visiting the next node in
|
|
626
|
+
# the list, and is set to true after visiting a node.
|
|
627
|
+
#
|
|
628
|
+
# @param list [Array<Ruby::Prism::Node>, nil] node list or nil
|
|
629
|
+
# @param comma [bool] comma flag
|
|
630
|
+
# @return [bool] comma flag
|
|
596
631
|
def visit_comma_separated_nodes(list, comma = false)
|
|
597
|
-
if list
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
632
|
+
return false if !list
|
|
633
|
+
|
|
634
|
+
list.each do |child|
|
|
635
|
+
emit_comma if comma
|
|
636
|
+
emit_code(child)
|
|
637
|
+
comma = true
|
|
603
638
|
end
|
|
604
639
|
comma
|
|
605
640
|
end
|
|
606
641
|
|
|
642
|
+
# Visit an array of nodes separated by whitespace.
|
|
643
|
+
#
|
|
644
|
+
# @param list [Array<Ruby::Prism::Node>, nil] node list or nil
|
|
645
|
+
# @return [void]
|
|
607
646
|
def visit_space_separated_nodes(list)
|
|
608
647
|
return if !list
|
|
609
648
|
|
|
@@ -615,38 +654,88 @@ module Steadfast
|
|
|
615
654
|
end
|
|
616
655
|
end
|
|
617
656
|
|
|
657
|
+
# Appends a node's location verbatim into the source code buffer.
|
|
658
|
+
#
|
|
659
|
+
# @param node [Ruby::Prism::Node]
|
|
660
|
+
# @return [void]
|
|
618
661
|
def emit_verbatim(node)
|
|
619
662
|
emit_code(node.location)
|
|
620
663
|
end
|
|
621
664
|
|
|
665
|
+
# Appends a code location (or a node) into the source code buffer.
|
|
666
|
+
#
|
|
667
|
+
# @param loc [Ruby::Prism::Location, Ruby::Prism::Node, nil]
|
|
668
|
+
# @return [void]
|
|
622
669
|
def emit_code(loc)
|
|
623
670
|
return if !loc
|
|
624
671
|
return visit(loc) if loc.is_a?(Ruby::Prism::Node)
|
|
625
672
|
|
|
673
|
+
record_loc_source_mapping(loc) if @sourcemap
|
|
626
674
|
emit(loc.slice.chomp)
|
|
627
|
-
|
|
628
|
-
|
|
675
|
+
@last_loc = loc
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
# Adds an entry to the sourcemap hash when detecting a change of line or
|
|
679
|
+
# filename before emitting an AST location. If a change of filename is
|
|
680
|
+
# detected, a newline is emitted in order to more accurately be able to
|
|
681
|
+
# translate backtraces if needed.
|
|
682
|
+
#
|
|
683
|
+
# @param loc [Ruby::Prism::Location] code location
|
|
684
|
+
# @return [void]
|
|
685
|
+
def record_loc_source_mapping(loc)
|
|
686
|
+
source = loc.send(:source)
|
|
687
|
+
last_source = @last_loc&.send(:source)
|
|
688
|
+
different_source = source != last_source
|
|
689
|
+
changed = different_source || (@last_loc && (loc.start_line != @last_loc.start_line))
|
|
690
|
+
return if !changed
|
|
691
|
+
|
|
692
|
+
emit_newline if different_source && last_source
|
|
693
|
+
Steadfast.sourcemap["#{@filename}:#{@line}"] ||= "#{loc.send(:source).__filename__}:#{loc.start_line}"
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
# Appends the given string into the source code buffer.
|
|
697
|
+
#
|
|
698
|
+
# @param str [String]
|
|
699
|
+
# @return [void]
|
|
629
700
|
def emit(str)
|
|
630
701
|
@buffer << str
|
|
702
|
+
@line += str.count("\n")
|
|
631
703
|
end
|
|
632
704
|
|
|
705
|
+
# Appends a newline into the source code buffer.
|
|
706
|
+
#
|
|
707
|
+
# @return [void]
|
|
633
708
|
def emit_newline
|
|
634
|
-
|
|
709
|
+
return if @buffer[-1] == "\n"
|
|
710
|
+
|
|
711
|
+
@buffer << "\n"
|
|
712
|
+
@line += 1
|
|
635
713
|
end
|
|
636
714
|
|
|
715
|
+
# Appends a space character into the source code buffer.
|
|
716
|
+
#
|
|
717
|
+
# @return [void]
|
|
637
718
|
def emit_whitespace
|
|
638
719
|
@buffer << " "
|
|
639
720
|
end
|
|
640
721
|
|
|
722
|
+
# Appends a comma into the source code buffer.
|
|
723
|
+
#
|
|
724
|
+
# @return [void]
|
|
641
725
|
def emit_comma
|
|
642
726
|
@buffer << ","
|
|
643
727
|
end
|
|
644
728
|
|
|
729
|
+
# Appends a semicolon into the source code buffer.
|
|
730
|
+
#
|
|
731
|
+
# @return [void]
|
|
645
732
|
def emit_semicolon
|
|
646
733
|
@buffer << ';'
|
|
647
734
|
end
|
|
648
735
|
|
|
649
|
-
|
|
650
|
-
|
|
736
|
+
# A nop method for doing nothing.
|
|
737
|
+
#
|
|
738
|
+
# @return [void]
|
|
739
|
+
def nop(node); end
|
|
651
740
|
end
|
|
652
741
|
end
|
data/lib/steadfast/version.rb
CHANGED
data/lib/steadfast.rb
CHANGED
|
@@ -2,17 +2,57 @@
|
|
|
2
2
|
|
|
3
3
|
require 'steadfast/sourcifier'
|
|
4
4
|
|
|
5
|
+
# Tell Ruby to keep script lines in memory, so calling Proc#syntax_tree or
|
|
6
|
+
# Method#syntax_tree will work.
|
|
5
7
|
RubyVM.keep_script_lines = true
|
|
6
8
|
|
|
9
|
+
# Steadfast is a metaprogramming gem for Ruby. Steadfast provides a
|
|
10
|
+
# general-purpose mecehanism for generating and rewriting Ruby code at runtime.
|
|
7
11
|
module Steadfast
|
|
12
|
+
# Extensions to Ruby::Prism::Source
|
|
13
|
+
module PrismSourceExtensions
|
|
14
|
+
|
|
15
|
+
# The __filename__ attribute lets us track the source location for a given
|
|
16
|
+
# AST.
|
|
17
|
+
attr_accessor :__filename__
|
|
18
|
+
end
|
|
19
|
+
::Ruby::Prism::Source.include(PrismSourceExtensions)
|
|
20
|
+
|
|
21
|
+
# A base error class for Steadfast errors
|
|
8
22
|
class Error < StandardError
|
|
9
23
|
end
|
|
10
24
|
|
|
11
25
|
class << self
|
|
26
|
+
# Returns the global Steadfast source map hash. Currently *not* thread-safe.
|
|
27
|
+
#
|
|
28
|
+
# @return [Hash] source map
|
|
29
|
+
def sourcemap
|
|
30
|
+
@sourcemap ||= {}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Translates entries in the given backtrace according to the global source
|
|
34
|
+
# map.
|
|
35
|
+
#
|
|
36
|
+
# @param bt [Array<String>] backtrace
|
|
37
|
+
# @return [Array<String>] translated source map
|
|
38
|
+
def translate_backtrace(bt)
|
|
39
|
+
bt.map {
|
|
40
|
+
if (m = it.match(/^(.+)(\:in.+)$/)) && (loc = sourcemap[m[1]])
|
|
41
|
+
"#{sourcemap[m[1]]}#{m[2]}"
|
|
42
|
+
else
|
|
43
|
+
it
|
|
44
|
+
end
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Returns an AST for the given object.
|
|
49
|
+
#
|
|
50
|
+
# @param obj [Proc, Method, UnboundedMethod, Ruby::Prism::Node]
|
|
51
|
+
# @return [Ruby::Prism::Node]
|
|
12
52
|
def to_ast(obj)
|
|
13
53
|
case obj
|
|
14
54
|
when Proc, Method, UnboundMethod
|
|
15
|
-
obj.syntax_tree
|
|
55
|
+
obj.syntax_tree.tap { set_source_filename(it, obj) }
|
|
16
56
|
when Ruby::Prism::Node
|
|
17
57
|
obj
|
|
18
58
|
else
|
|
@@ -20,13 +60,29 @@ module Steadfast
|
|
|
20
60
|
end
|
|
21
61
|
end
|
|
22
62
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
63
|
+
# Returns the source code to the given object by sourcifying its AST.
|
|
64
|
+
#
|
|
65
|
+
# @param obj [Proc, Method, UnboundedMethod, Ruby::Prism::Node]
|
|
66
|
+
# @return [String]
|
|
67
|
+
def to_source(obj, **opts)
|
|
68
|
+
ast = to_ast(obj)
|
|
69
|
+
Sourcifier.new(**opts).to_source(ast)
|
|
26
70
|
end
|
|
27
71
|
|
|
28
72
|
private
|
|
29
73
|
|
|
74
|
+
# Sets
|
|
75
|
+
def set_source_filename(ast, obj)
|
|
76
|
+
source = ast.send(:source)
|
|
77
|
+
source.__filename__ ||= obj.source_location.first
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Finds the AST for the given proc. This method is currently not in use, but
|
|
81
|
+
# we may eventually use this code in order to provide backward compatibility
|
|
82
|
+
# for Ruby < 4.1.
|
|
83
|
+
#
|
|
84
|
+
# @param proc [Proc]
|
|
85
|
+
# @return [Ruby::Prism::Node]
|
|
30
86
|
def proc_ast(proc)
|
|
31
87
|
fn, lineno = proc.source_location
|
|
32
88
|
pr = Prism.parse(get_source(fn), filepath: fn)
|
|
@@ -53,6 +109,12 @@ module Steadfast
|
|
|
53
109
|
raise Sirop::Error, "Could not get source for proc"
|
|
54
110
|
end
|
|
55
111
|
|
|
112
|
+
# Finds the AST for the given method. This method is currently not in use,
|
|
113
|
+
# but we may eventually use this code in order to provide backward
|
|
114
|
+
# compatibility for Ruby < 4.1.
|
|
115
|
+
#
|
|
116
|
+
# @param method [Method, UnboundMethod]
|
|
117
|
+
# @return [Ruby::Prism::Node]
|
|
56
118
|
def method_ast(method)
|
|
57
119
|
fn, lineno = method.source_location
|
|
58
120
|
pr = Prism.parse(get_source(fn), filepath: fn)
|
|
@@ -66,12 +128,21 @@ module Steadfast
|
|
|
66
128
|
end
|
|
67
129
|
end
|
|
68
130
|
|
|
131
|
+
# Returns the content of the given filename. Returns the IRB line buffer if
|
|
132
|
+
# '(irb)' is given. This method is not currently used.
|
|
133
|
+
#
|
|
134
|
+
# @param fn [String]
|
|
135
|
+
# @return [String] source code
|
|
69
136
|
def get_source(fn)
|
|
70
137
|
return get_irb_source if fn == '(irb)'
|
|
71
138
|
|
|
72
139
|
IO.read(fn)
|
|
73
140
|
end
|
|
74
141
|
|
|
142
|
+
# Returns the IRB line buffer (the source code for the current IRB session.)
|
|
143
|
+
# This method is currently not used.
|
|
144
|
+
#
|
|
145
|
+
# @return [String] IRB line buffer
|
|
75
146
|
def get_irb_source
|
|
76
147
|
io = IRB.CurrentContext.io
|
|
77
148
|
last_line = io.instance_variable_get(:@line_no)
|