sirop 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
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