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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8abea2bc78eddf9d3a4785cbb385799f1ea1ec21e11b794cfc32f8b13b5a8639
4
- data.tar.gz: 2ddf099e67ee8725df09a644bca9a25a26031c17363b10bdecea7b8e73511487
3
+ metadata.gz: 5d446eb7d3f0db23310b1ea5dfc4343ff7b82bfaae804de963ea42616977b2ac
4
+ data.tar.gz: c02e0997705985da9800da6db4a88ea9e5a652e4e6562a2a7aba276eadf8d536
5
5
  SHA512:
6
- metadata.gz: 93299385d7b4098bdb409495d57c142de725e6328cf185f5e0a27d087b8e2e35656c31dd433d3324396f6959b8ec4c0114bbf69911788f2aefcedcb6b12d77fb
7
- data.tar.gz: 04c5a30cc22f2ff6268a6fb0345c39a74632ccd559b215280dac020a5717f34802d9e8e669588a23ee21bdca040dca9d308a6153b25bcbf9896fcb50d63b397d
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
- # steadfast
2
- A modern meta-programming gem for Ruby
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...
@@ -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: :emit_nothing,
46
- implicit_rest: :emit_nothing,
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: :emit_nothing,
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: :emit_nothing,
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
- def initialize(**)
105
- @buffer = +''
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
- def to_source(node)
109
- @buffer.clear
110
- visit(node)
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 |key, plan|
115
- sym = :"visit_#{key}_node"
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
- def visit_plan(plan, node)
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.each_with_index do |sym, idx|
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
- list.each do |child|
599
- emit_comma if comma
600
- emit_code(child)
601
- comma = true
602
- end
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
- end
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
- @buffer << "\n" if @buffer[-1] != "\n"
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
- def emit_nothing(node)
650
- end
736
+ # A nop method for doing nothing.
737
+ #
738
+ # @return [void]
739
+ def nop(node); end
651
740
  end
652
741
  end
@@ -1,4 +1,4 @@
1
1
  module Steadfast
2
2
  # Steadfast version
3
- VERSION = '0.0.1'
3
+ VERSION = '0.0.2'
4
4
  end
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
- def to_source(obj, **)
24
- obj = to_ast(obj)
25
- Sourcifier.new(**).to_source(obj)
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)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: steadfast
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner