sirop 0.1 → 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: edde04771185a121f37a4a2df66569803c6f5b18d2a6739923c2a58cedacfc24
4
- data.tar.gz: 364af589b7a6ca122f32447c23dc281b296e6ade1b9f280dca3e6e51b50d456b
3
+ metadata.gz: 1641ee60187094fe9dcdeebefbfc5283c0d2ea535aa161b17f7bf6f720d63854
4
+ data.tar.gz: c9abaef1a8b7116760f92e143bcb2e58fbc4424fe3d4afe110bc10bf552852cf
5
5
  SHA512:
6
- metadata.gz: 6bb9fc18cefdbe071ba2b1d7f54fd0aa8111f97ffde49c362b4314e92d300e33075711a956384c15ee7880dab8bc85524d63de3ef5625278aa35917fce983066
7
- data.tar.gz: e35e58fc9ba7d223d68298a3a25c8cb30596a1f548e314298de10730b6ea49456486fff541bee1cca5df3f3b092cd59e98fe3e9634a92be75f1d5b4bf5532c6e
6
+ metadata.gz: 3900ece58c932df6181676abb597fa6deedfbd8801e28f4993cc36cce7584a188e88b73e6ef2b0fea33b5dbc8a8d284949159973cb108e149dd0d9b3ea5dd762
7
+ data.tar.gz: 7f848b0e842792a33e3fc7c9b7d6cf72e924040307e2db5ccb0cff16af1e947e7053ee81e54ab688c131532b66432dc1283651ceeef5820ded58c3b8447d193a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ # 2024-02-27 0.2
2
+
3
+ - Update README
4
+ - Remove support for Ruby < 3.2
5
+ - Implement general purpose Finder for finding nodes
6
+ - Implement DSL compiler (for tests)
7
+ - Implement Sirop.to_source
8
+
1
9
  # 2024-02-20 0.1
2
10
 
3
11
  - Find node for a given `Proc` object
data/README.md CHANGED
@@ -1,4 +1,113 @@
1
1
  # Sirop
2
2
 
3
- Sirop is a Ruby code rewriter. More information coming soon...
3
+ Sirop is a Ruby gem for manipulating Ruby source code. Sirop is very young, so
4
+ the following information might be incomplete, out of date, or simply wrong!
4
5
 
6
+ ## Use Cases
7
+
8
+ Some of the use cases addressed by Sirop are:
9
+
10
+ - Compile DSLs into optimized Ruby code. This is especially interesting for HTML
11
+ templating DSLs in libraries like Phlex, Papercraft etc.
12
+ [Example](https://github.com/digital-fabric/sirop/blob/main/test/dsl_compiler.rb)
13
+ - Get the source of a given block or method.
14
+ - Rewrite parts of Ruby code, for implementing Ruby macros (and why not?).
15
+
16
+ ## Limitations
17
+
18
+ - Sirop supports Ruby 3.2 or newer.
19
+ - Sirop can be used only on blocks and methods defined in a file, so cannot
20
+ really be used on dynamically `eval`'d Ruby code, or in an IRB/Pry session.
21
+
22
+ ## Getting the AST/source of a Ruby proc or method
23
+
24
+ To get the AST of a proc or a method, use `Sirop.to_ast`:
25
+
26
+ ```ruby
27
+ # for a proc
28
+ mul = ->(x, y) { x * y }
29
+ Sirop.to_ast(mul) #=> ...
30
+
31
+ # for a method
32
+ def foo; :bar; end
33
+ Sirop.to_ast(method(:foo)) #=> ...
34
+ ```
35
+
36
+ To get the source of a proc or a method, use `Sirop.to_source`:
37
+
38
+ ```ruby
39
+ mul = ->(x, y) { x * y }
40
+ Sirop.to_source(mul) #=> "->(x, y) { x * y }"
41
+
42
+ def foo; :bar; end
43
+ Sirop.to_source(method(:foo)) #=> "def foo; :bar; end"
44
+ ```
45
+
46
+ ## Rewriting Ruby code
47
+
48
+ You can consult the [DSL compiler
49
+ example](https://github.com/digital-fabric/sirop/blob/main/test/dsl_compiler.rb). This example intercepts method calls by defining a `visit_call_node` method:
50
+
51
+ ```ruby
52
+ # Annotated with some explanations
53
+ def visit_call_node(node)
54
+ # don't rewrite if the call has a receiver
55
+ return super if node.receiver
56
+
57
+ # set HTML location start
58
+ @html_location_start ||= node.location
59
+ # get method arguments...
60
+ inner_text, attrs = tag_args(node)
61
+ # and block
62
+ block = node.block
63
+
64
+ # emit HTML tag according to given arguments
65
+ if inner_text
66
+ emit_tag_open(node, attrs)
67
+ emit_tag_inner_text(inner_text)
68
+ emit_tag_close(node)
69
+ elsif block
70
+ emit_tag_open(node, attrs)
71
+ visit(block.body)
72
+ emit_tag_close(node)
73
+ else
74
+ emit_tag_open_close(node, attrs)
75
+ end
76
+ # set HTML location end
77
+ @html_location_end = node.location
78
+ end
79
+ ```
80
+
81
+ ## Future directions
82
+
83
+ - Implement a macro expander with support for `quote`/`unquote`:
84
+
85
+ ```ruby
86
+ trace_macro = Sirop.macro do |ast|
87
+ source = Sirop.to_source(ast)
88
+ quote do
89
+ result = unquote(ast)
90
+ puts "The result of #{source} is: #{result}"
91
+ result
92
+ end
93
+ end
94
+
95
+ def add(x, y)
96
+ trace(x + y)
97
+ end
98
+
99
+ Sirop.expand_macros(method(:add), trace: trace_macro)
100
+ ```
101
+
102
+ - Implement a DSL compiler with hooks for easier usage in DSL libraries.
103
+
104
+ ## Contributing
105
+
106
+ We gladly welcome contributions from anyone! Some areas that need work currently
107
+ are:
108
+
109
+ - Documentation
110
+ - More test cases for Ruby syntax in the Sirop tests. Look here:
111
+ https://github.com/digital-fabric/sirop/tree/main/test/fixtures
112
+
113
+ Please feel free to contribute PR's and issues
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'prism'
4
+
5
+ module Sirop
6
+ class Finder < Prism::BasicVisitor
7
+ def self.find(*, &)
8
+ finder = self.new
9
+ finder.find(*, &)
10
+ end
11
+
12
+ def find(root, key, &)
13
+ instance_exec(&)
14
+ @key = key
15
+ catch(key) do
16
+ visit(root)
17
+ nil
18
+ end
19
+ end
20
+
21
+ def found!(node)
22
+ throw(@key, node)
23
+ end
24
+
25
+ def method_missing(sym, node, *args)
26
+ visit_child_nodes(node)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'prism'
4
+
5
+ class Prism::BasicVisitor
6
+ def on(type, &)
7
+ singleton_class.define_method(:"visit_#{type}_node", &)
8
+ self
9
+ end
10
+ end
@@ -0,0 +1,290 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'prism'
4
+
5
+ module Sirop
6
+ #
7
+ class Sourcifier < Prism::BasicVisitor
8
+ attr_reader :buffer
9
+
10
+ def initialize
11
+ @buffer = +''
12
+ end
13
+
14
+ def to_source(node)
15
+ @buffer.clear
16
+ visit(node)
17
+ @buffer
18
+ end
19
+
20
+ def loc_start(loc)
21
+ [loc.start_line, loc.start_column]
22
+ end
23
+
24
+ def loc_end(loc)
25
+ [loc.end_line, loc.end_column]
26
+ end
27
+
28
+ def emit(str)
29
+ @buffer << str
30
+ end
31
+
32
+ def adjust_whitespace(loc)
33
+ if @last_loc_start
34
+ if @last_loc_end.first != loc.start_line
35
+ @buffer << "\n" * (loc.start_line - @last_loc_end.first)
36
+ @buffer << ' ' * loc.start_column
37
+ else
38
+ ofs = loc.start_column - @last_loc_end.last
39
+ if ofs > 0
40
+ @buffer << ' ' * ofs
41
+ end
42
+ end
43
+ else
44
+ # empty buffer
45
+ @buffer << ' ' * loc.start_column
46
+ end
47
+ @last_loc = loc
48
+ @last_loc_start = loc_start(loc)
49
+ @last_loc_end = loc_end(loc)
50
+ end
51
+
52
+ def emit_code(loc, semicolon: false)
53
+ return if !loc
54
+
55
+ emit_semicolon(loc) if semicolon
56
+ return visit(loc) if loc.is_a?(Prism::Node)
57
+
58
+ adjust_whitespace(loc)
59
+ emit(loc.slice)
60
+ end
61
+
62
+ def emit_verbatim(node)
63
+ emit_code(node.location)
64
+ end
65
+
66
+ def emit_str(str)
67
+ emit(str)
68
+ @last_loc_end[1] += str.size
69
+ end
70
+
71
+ def emit_comma
72
+ emit_str(',')
73
+ end
74
+
75
+ def emit_semicolon(loc)
76
+ loc = loc.location if loc.is_a?(Prism::Node)
77
+ emit_str(';') if loc.start_line == @last_loc.end_line
78
+ end
79
+
80
+ def method_missing(sym, node, *args)
81
+ puts '!' * 40
82
+ p node
83
+ raise NotImplementedError, "Don't know how to handle #{sym}"
84
+ visit_child_nodes(node)
85
+ end
86
+
87
+ VISIT_PLANS = {
88
+ and: [:left, :operator_loc, :right],
89
+ assoc: :visit_child_nodes,
90
+ assoc_splat: [:operator_loc, :value],
91
+ block: [:opening_loc, :parameters, :body, :closing_loc],
92
+ block_argument: [:operator_loc, :expression],
93
+ block_parameter: [:operator_loc, :name_loc],
94
+ block_parameters: [:opening_loc, :parameters, :closing_loc],
95
+ break: [:keyword_loc, :arguments],
96
+ constant_path: [:parent, :delimiter_loc, :child],
97
+ constant_read: :emit_verbatim,
98
+ else: [:else_keyword_loc, :statements],
99
+ embedded_statements: [:opening_loc, :statements, :closing_loc],
100
+ false: :emit_verbatim,
101
+ integer: :emit_verbatim,
102
+ keyword_rest_parameter: [:operator_loc, :name_loc],
103
+ lambda: [:operator_loc, :parameters, :opening_loc, :body,
104
+ :closing_loc],
105
+ local_variable_read: :emit_verbatim,
106
+ local_variable_write: [:name_loc, :operator_loc, :value],
107
+ next: [:keyword_loc, :arguments],
108
+ nil: :emit_verbatim,
109
+ optional_parameter: [:name_loc, :operator_loc, :value],
110
+ or: [:left, :operator_loc, :right],
111
+ parentheses: [:opening_loc, :body, :closing_loc],
112
+ required_parameter: :emit_verbatim,
113
+ rest_parameter: [:operator_loc, :name_loc],
114
+ splat: [:operator_loc, :expression],
115
+ statements: :visit_child_nodes,
116
+ string: :emit_verbatim,
117
+ symbol: :emit_verbatim,
118
+ true: :emit_verbatim,
119
+ yield: [:keyword_loc, :lparen_loc, :arguments, :rparen_loc],
120
+ }
121
+
122
+ VISIT_PLANS.each do |key, plan|
123
+ sym = :"visit_#{key}_node"
124
+ define_method(sym) { |n| visit_plan(plan, n) }
125
+ end
126
+
127
+ def visit_plan(plan, node)
128
+ return send(plan, node) if plan.is_a?(Symbol)
129
+
130
+ insert_semicolon = false
131
+ plan.each_with_index do |sym, idx|
132
+ if sym == :semicolon
133
+ insert_semicolon = true
134
+ next
135
+ end
136
+
137
+ obj = node.send(sym)
138
+ emit_code(obj, semicolon: insert_semicolon)
139
+ insert_semicolon = false
140
+ end
141
+ end
142
+
143
+ def visit_comma_separated_nodes(list, comma = false)
144
+ if list
145
+ list.each_with_index do |child, idx|
146
+ emit_comma if comma
147
+ emit_code(child)
148
+ comma = true
149
+ end
150
+ end
151
+ comma
152
+ end
153
+
154
+ def visit_parameters_node(node)
155
+ comma = visit_comma_separated_nodes(node.requireds)
156
+ comma = visit_comma_separated_nodes(node.optionals, comma)
157
+ comma = visit_comma_separated_nodes(node.posts, comma)
158
+ if node.rest
159
+ emit_comma if comma
160
+ comma = true
161
+ emit_code(node.rest)
162
+ end
163
+ if node.keyword_rest
164
+ emit_comma if comma
165
+ comma = true
166
+ emit_code(node.keyword_rest)
167
+ end
168
+ if node.block
169
+ emit_comma if comma
170
+ comma = true
171
+ emit_code(node.block)
172
+ end
173
+ end
174
+
175
+ def visit_arguments_node(node)
176
+ visit_comma_separated_nodes(node.arguments)
177
+ end
178
+
179
+ def visit_keyword_hash_node(node)
180
+ visit_comma_separated_nodes(node.elements)
181
+ end
182
+
183
+ def visit_if_node(node)
184
+ if !node.if_keyword_loc
185
+ return visit_if_node_ternary(node)
186
+ elsif !node.end_keyword_loc
187
+ return visit_if_node_guard(node)
188
+ end
189
+
190
+ emit_code(node.if_keyword_loc)
191
+ emit_code(node.predicate)
192
+ emit_code(node.then_keyword_loc)
193
+ emit_code(node.statements)
194
+ emit_code(node.consequent) if node.consequent
195
+ emit_code(node.end_keyword_loc) if node.if_keyword_loc.slice == 'if'
196
+ end
197
+
198
+ def visit_if_node_ternary(node)
199
+ emit_code(node.predicate)
200
+ emit_code(node.then_keyword_loc)
201
+ emit_code(node.statements)
202
+ emit_code(node.consequent)
203
+ end
204
+
205
+ def visit_if_node_guard(node)
206
+ emit_code(node.statements)
207
+ emit_code(node.if_keyword_loc)
208
+ emit_code(node.predicate)
209
+ end
210
+
211
+ def visit_case_node(node)
212
+ emit_code(node.case_keyword_loc)
213
+ emit_code(node.predicate)
214
+ node.conditions.each { |c| emit_code(c) }
215
+ emit_code(node.consequent)
216
+ emit_code(node.end_keyword_loc)
217
+ end
218
+
219
+ def visit_when_node(node)
220
+ emit_code(node.keyword_loc)
221
+ visit_comma_separated_nodes(node.conditions)
222
+ emit_code(node.statements)
223
+ end
224
+
225
+ def visit_interpolated_symbol_node(node)
226
+ emit_code(node.opening_loc)
227
+ node.parts.each { |p| emit_code(p) }
228
+ emit_code(node.closing_loc)
229
+ end
230
+ alias_method :visit_interpolated_string_node, :visit_interpolated_symbol_node
231
+
232
+ def visit_def_node(node)
233
+ emit_code(node.def_keyword_loc)
234
+ emit_code(node.name_loc)
235
+ last_loc = node.name_loc
236
+
237
+ if node.parameters
238
+ emit_str('(')
239
+ emit_code(node.parameters)
240
+ emit_str(')')
241
+ last_loc = node.parameters.location
242
+ end
243
+
244
+ emit_code(node.body, semicolon: true)
245
+ emit_code(node.end_keyword_loc, semicolon: true)
246
+ end
247
+
248
+ def visit_call_node(node)
249
+ if node.receiver && !node.call_operator_loc && !node.arguments
250
+ return visit_call_node_unary_op(node)
251
+ end
252
+
253
+ block = node.block
254
+
255
+ emit_code(node.receiver)
256
+ emit_code(node.call_operator_loc)
257
+ emit_code(node.message_loc)
258
+ emit_code(node.opening_loc)
259
+ emit_code(node.arguments)
260
+
261
+ if block.is_a?(Prism::BlockArgumentNode)
262
+ emit_comma if node.arguments&.arguments.size > 0
263
+ emit_code(block)
264
+ block = nil
265
+ end
266
+ emit_code(node.closing_loc)
267
+ emit_code(block)
268
+ end
269
+
270
+ def visit_call_node_unary_op(node)
271
+ emit_code(node.message_loc)
272
+ emit_code(node.receiver)
273
+ end
274
+
275
+ def visit_while_node(node)
276
+ return visit_while_node_guard(node) if !node.closing_loc
277
+
278
+ emit_code(node.keyword_loc)
279
+ emit_code(node.predicate)
280
+ emit_code(node.statements, semicolon: true)
281
+ emit_code(node.closing_loc, semicolon: true)
282
+ end
283
+
284
+ def visit_while_node_guard(node)
285
+ emit_code(node.statements)
286
+ emit_code(node.keyword_loc)
287
+ emit_code(node.predicate)
288
+ end
289
+ end
290
+ end
data/lib/sirop/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Sirop
2
2
  # Sirop version
3
- VERSION = '0.1'
3
+ VERSION = '0.2'
4
4
  end
data/lib/sirop.rb CHANGED
@@ -1,27 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'prism'
4
- require 'sirop/block_finder'
4
+ require 'sirop/prism_ext'
5
+ require 'sirop/finder'
6
+ require 'sirop/sourcifier'
5
7
 
6
8
  module Sirop
7
9
  class << self
8
- def find(obj)
10
+ def to_ast(obj)
9
11
  case obj
10
12
  when Proc
11
- find_proc(obj)
13
+ proc_ast(obj)
14
+ when UnboundMethod, Method
15
+ method_ast(obj)
12
16
  else
13
17
  raise ArgumentError, "Invalid object type"
14
18
  end
15
19
  end
16
20
 
17
- def find_proc(proc)
21
+ def to_source(obj)
22
+ obj = to_ast(obj) if !obj.is_a?(Prism::Node)
23
+ Sourcifier.new.to_source(obj)
24
+ end
25
+
26
+ private
27
+
28
+ def proc_ast(proc)
18
29
  fn, lineno = proc.source_location
19
30
  pr = Prism.parse(IO.read(fn), filepath: fn)
20
31
  program = pr.value
21
-
22
- finder = Sirop::BlockFinder.new(proc, lineno)
23
- finder.find(program)
32
+
33
+ Finder.find(program, proc) do
34
+ on(:lambda) do |node|
35
+ found!(node) if node.location.start_line == lineno
36
+ super(node)
37
+ end
38
+ on(:call) do |node|
39
+ case node.name
40
+ when :proc, :lambda
41
+ found!(node) if node.block && node.block.location.start_line == lineno
42
+ end
43
+ super(node)
44
+ end
45
+ end
46
+ end
47
+
48
+ def method_ast(method)
49
+ fn, lineno = method.source_location
50
+ pr = Prism.parse(IO.read(fn), filepath: fn)
51
+ program = pr.value
52
+
53
+ Finder.find(program, method) do
54
+ on(:def) do |node|
55
+ found!(node) if node.name == method.name && node.location.start_line == lineno
56
+ super(node)
57
+ end
58
+ end
24
59
  end
25
-
26
60
  end
27
61
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sirop
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.1'
4
+ version: '0.2'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-02-20 00:00:00.000000000 Z
11
+ date: 2024-02-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: prism
@@ -48,15 +48,16 @@ files:
48
48
  - CHANGELOG.md
49
49
  - README.md
50
50
  - lib/sirop.rb
51
- - lib/sirop/block_finder.rb
51
+ - lib/sirop/finder.rb
52
+ - lib/sirop/prism_ext.rb
53
+ - lib/sirop/sourcifier.rb
52
54
  - lib/sirop/version.rb
53
55
  homepage: http://github.com/digital-fabric/sirop
54
56
  licenses:
55
57
  - MIT
56
58
  metadata:
57
- source_code_uri: https://github.com/digital-fabric/sirop
58
- documentation_uri: https://www.rubydoc.info/gems/sirop
59
59
  homepage_uri: https://github.com/digital-fabric/sirop
60
+ documentation_uri: https://www.rubydoc.info/gems/sirop
60
61
  changelog_uri: https://github.com/digital-fabric/sirop/blob/main/CHANGELOG.md
61
62
  post_install_message:
62
63
  rdoc_options:
@@ -70,7 +71,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
70
71
  requirements:
71
72
  - - ">="
72
73
  - !ruby/object:Gem::Version
73
- version: '3.0'
74
+ version: '3.2'
74
75
  required_rubygems_version: !ruby/object:Gem::Requirement
75
76
  requirements:
76
77
  - - ">="
@@ -1,42 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Sirop
4
- class BlockFinder < Prism::BasicVisitor
5
- attr_accessor :block_node
6
-
7
- def initialize(proc, lineno)
8
- @proc = proc
9
- @lineno = lineno
10
- end
11
-
12
- def find(program)
13
- # p program
14
- # puts
15
- catch(@proc) {
16
- visit(program)
17
- nil
18
- }
19
- end
20
-
21
- def visit_lambda_node(node)
22
- if node.location.start_line == @lineno
23
- throw @proc, node
24
- else
25
- visit_child_nodes(node)
26
- end
27
- end
28
-
29
- def visit_call_node(node)
30
- case node.name
31
- when :proc, :lambda
32
- if node.block && node.block.location.start_line == @lineno
33
- throw @proc, node
34
- end
35
- end
36
- end
37
-
38
- def method_missing(sym, node, *args)
39
- visit_child_nodes(node)
40
- end
41
- end
42
- end