prettier 0.12.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.
@@ -0,0 +1,143 @@
1
+ const {
2
+ concat,
3
+ group,
4
+ hardline,
5
+ indent,
6
+ join,
7
+ literalline,
8
+ softline
9
+ } = require("../builders");
10
+ const { concatBody, empty, makeList, surround } = require("../utils");
11
+ const escapePattern = require("../escapePattern");
12
+
13
+ // If there is some part of this string that matches an escape sequence or that
14
+ // contains the interpolation pattern ("#{"), then we are locked into whichever
15
+ // quote the user chose. (If they chose single quotes, then double quoting
16
+ // would activate the escape sequence, and if they chose double quotes, then
17
+ // single quotes would deactivate it.)
18
+ const isQuoteLocked = string =>
19
+ string.body.some(
20
+ part =>
21
+ part.type === "@tstring_content" &&
22
+ (escapePattern.test(part.body) || part.body.includes("#{"))
23
+ );
24
+
25
+ // A string is considered to be able to use single quotes if it contains only
26
+ // plain string content and that content does not contain a single quote.
27
+ const isSingleQuotable = string =>
28
+ string.body.every(
29
+ part => part.type === "@tstring_content" && !part.body.includes("'")
30
+ );
31
+
32
+ const getStringQuote = (string, preferSingleQuotes) => {
33
+ if (isQuoteLocked(string)) {
34
+ return string.quote;
35
+ }
36
+
37
+ return preferSingleQuotes && isSingleQuotable(string) ? "'" : '"';
38
+ };
39
+
40
+ const quotePattern = new RegExp("\\\\([\\s\\S])|(['\"])", "g");
41
+
42
+ const makeString = (content, enclosingQuote) => {
43
+ const otherQuote = enclosingQuote === '"' ? "'" : enclosingQuote;
44
+
45
+ // Escape and unescape single and double quotes as needed to be able to
46
+ // enclose `content` with `enclosingQuote`.
47
+ return content.replace(quotePattern, (match, escaped, quote) => {
48
+ if (escaped === otherQuote) {
49
+ return escaped;
50
+ }
51
+
52
+ if (quote === enclosingQuote) {
53
+ return `\\${quote}`;
54
+ }
55
+
56
+ if (quote) {
57
+ return quote;
58
+ }
59
+
60
+ return `\\${escaped}`;
61
+ });
62
+ };
63
+
64
+ module.exports = {
65
+ "@CHAR": (path, { preferSingleQuotes }, _print) => {
66
+ const { body } = path.getValue();
67
+
68
+ if (body.length !== 2) {
69
+ return body;
70
+ }
71
+
72
+ const quote = preferSingleQuotes ? "'" : '"';
73
+ return body.length === 2 ? concat([quote, body.slice(1), quote]) : body;
74
+ },
75
+ heredoc: (path, opts, print) => {
76
+ const { beging, ending } = path.getValue();
77
+
78
+ return concat([
79
+ beging,
80
+ concat([literalline].concat(path.map(print, "body"))),
81
+ ending
82
+ ]);
83
+ },
84
+ string: makeList,
85
+ string_concat: (path, opts, print) =>
86
+ group(
87
+ concat([
88
+ path.call(print, "body", 0),
89
+ " \\",
90
+ indent(concat([hardline, path.call(print, "body", 1)]))
91
+ ])
92
+ ),
93
+ string_dvar: surround("#{", "}"),
94
+ string_embexpr: (path, opts, print) =>
95
+ group(
96
+ concat([
97
+ "#{",
98
+ indent(concat([softline, path.call(print, "body", 0)])),
99
+ concat([softline, "}"])
100
+ ])
101
+ ),
102
+ string_literal: (path, { preferSingleQuotes }, print) => {
103
+ const string = path.getValue().body[0];
104
+
105
+ // If this string is actually a heredoc, bail out and return to the print
106
+ // function for heredocs
107
+ if (string.type === "heredoc") {
108
+ return path.call(print, "body", 0);
109
+ }
110
+
111
+ // If the string is empty, it will not have any parts, so just print out the
112
+ // quotes corresponding to the config
113
+ if (string.body.length === 0) {
114
+ return preferSingleQuotes ? "''" : '""';
115
+ }
116
+
117
+ const quote = getStringQuote(string, preferSingleQuotes);
118
+ const parts = [];
119
+
120
+ string.body.forEach((part, index) => {
121
+ if (part.type === "@tstring_content") {
122
+ // In this case, the part of the string is just regular string content
123
+ parts.push(makeString(part.body, quote));
124
+ } else {
125
+ // In this case, the part of the string is an embedded expression
126
+ parts.push(path.call(print, "body", 0, "body", index));
127
+ }
128
+ });
129
+
130
+ return concat([quote].concat(parts).concat([quote]));
131
+ },
132
+ word_add: concatBody,
133
+ word_new: empty,
134
+ xstring: makeList,
135
+ xstring_literal: (path, opts, print) =>
136
+ group(
137
+ concat([
138
+ "`",
139
+ indent(concat([softline, join(softline, path.call(print, "body", 0))])),
140
+ concat([softline, "`"])
141
+ ])
142
+ )
143
+ };
@@ -0,0 +1,16 @@
1
+ const { spawnSync } = require("child_process");
2
+ const path = require("path");
3
+
4
+ module.exports = (text, _parsers, _opts) => {
5
+ const child = spawnSync("ruby", [path.join(__dirname, "./ripper.rb")], {
6
+ input: text
7
+ });
8
+
9
+ const error = child.stderr.toString();
10
+ if (error) {
11
+ throw new Error(error);
12
+ }
13
+
14
+ const response = child.stdout.toString();
15
+ return JSON.parse(response);
16
+ };
@@ -0,0 +1,23 @@
1
+ const { printComments } = require("./utils");
2
+ const nodes = require("./nodes");
3
+
4
+ module.exports = (path, opts, print) => {
5
+ const { type, body, comments, start } = path.getValue();
6
+
7
+ if (type in nodes) {
8
+ const printed = nodes[type](path, opts, print);
9
+
10
+ if (comments) {
11
+ return printComments(printed, start, comments);
12
+ }
13
+ return printed;
14
+ }
15
+
16
+ if (type[0] === "@") {
17
+ return body;
18
+ }
19
+
20
+ throw new Error(
21
+ `Unsupported node encountered: ${type}\n${JSON.stringify(body, null, 2)}`
22
+ );
23
+ };
@@ -0,0 +1,542 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ REQUIRED_VERSION = Gem::Version.new('2.5')
4
+ if Gem::Version.new(RUBY_VERSION) < REQUIRED_VERSION
5
+ raise "Ruby version #{RUBY_VERSION} not supported. " \
6
+ "Please upgrade to #{REQUIRED_VERSION} or above."
7
+ end
8
+
9
+ require 'json' unless defined?(JSON)
10
+ require 'ripper'
11
+
12
+ module Layer
13
+ # Some nodes are lists that come back from the parser. They always start with
14
+ # a *_new node (or in the case of string, *_content) and each additional node
15
+ # in the list is a *_add node. This layer takes those nodes and turns them
16
+ # into one node with an array body.
17
+ module Lists
18
+ events = %i[
19
+ args
20
+ mlhs
21
+ mrhs
22
+ qsymbols
23
+ qwords
24
+ regexp
25
+ stmts
26
+ string
27
+ symbols
28
+ words
29
+ xstring
30
+ ]
31
+
32
+ private
33
+
34
+ events.each do |event|
35
+ suffix = event == :string ? 'content' : 'new'
36
+
37
+ define_method(:"on_#{event}_#{suffix}") do
38
+ { type: event, body: [], start: lineno, end: lineno }
39
+ end
40
+
41
+ define_method(:"on_#{event}_add") do |parts, part|
42
+ parts.tap do |node|
43
+ node[:body] << part
44
+ node[:end] = lineno
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ # For most nodes, it's enough to look at the child nodes to determine the
51
+ # start of the parent node. However, for some nodes it's necessary to keep
52
+ # track of the keywords as they come in from the lexer and to modify the start
53
+ # node once we have it.
54
+ module StartLine
55
+ events = %i[begin else elsif ensure rescue until while]
56
+
57
+ def initialize(*args)
58
+ super(*args)
59
+ @keywords = []
60
+ end
61
+
62
+ def self.prepended(base)
63
+ base.attr_reader :keywords
64
+ end
65
+
66
+ private
67
+
68
+ def find_start(body)
69
+ keywords[keywords.rindex { |keyword| keyword[:body] == body }][:start]
70
+ end
71
+
72
+ events.each do |event|
73
+ keyword = event.to_s
74
+
75
+ define_method(:"on_#{event}") do |*body|
76
+ super(*body).tap { |sexp| sexp.merge!(start: find_start(keyword)) }
77
+ end
78
+ end
79
+
80
+ def on_kw(body)
81
+ super(body).tap { |sexp| keywords << sexp }
82
+ end
83
+
84
+ def on_program(*body)
85
+ super(*body).tap { |sexp| sexp.merge!(start: 1) }
86
+ end
87
+ end
88
+
89
+ # Nodes that are always on their own line occur when the lexer is in the
90
+ # EXPR_BEG node. Those comments are tracked within the @block_comments
91
+ # instance variable. Then for each node that could contain them, we attach
92
+ # them after the node has been built.
93
+ module BlockComments
94
+ events = {
95
+ begin: [0, :body, 0],
96
+ bodystmt: [0],
97
+ class: [2, :body, 0],
98
+ def: [2, :body, 0],
99
+ defs: [4, :body, 0],
100
+ else: [0],
101
+ elsif: [1],
102
+ ensure: [0],
103
+ if: [1],
104
+ program: [0],
105
+ rescue: [2],
106
+ sclass: [1, :body, 0],
107
+ unless: [1],
108
+ until: [1],
109
+ when: [1],
110
+ while: [1]
111
+ }
112
+
113
+ def initialize(*args)
114
+ super(*args)
115
+ @block_comments = []
116
+ @current_embdoc = nil
117
+ end
118
+
119
+ def self.prepended(base)
120
+ base.attr_reader :block_comments, :current_embdoc
121
+ end
122
+
123
+ private
124
+
125
+ def attach_comments(sexp, stmts)
126
+ range = sexp[:start]..sexp[:end]
127
+ comments =
128
+ block_comments.group_by { |comment| range.include?(comment[:start]) }
129
+
130
+ if comments[true]
131
+ stmts[:body] =
132
+ (stmts[:body] + comments[true]).sort_by { |node| node[:start] }
133
+
134
+ @block_comments = comments.fetch(false) { [] }
135
+ end
136
+ end
137
+
138
+ events.each do |event, path|
139
+ define_method(:"on_#{event}") do |*body|
140
+ super(*body).tap { |sexp| attach_comments(sexp, body.dig(*path)) }
141
+ end
142
+ end
143
+
144
+ def on_comment(body)
145
+ super(body).tap do |sexp|
146
+ block_comments << sexp if RipperJS.lex_state_name(state) == 'EXPR_BEG'
147
+ end
148
+ end
149
+
150
+ def on_embdoc_beg(comment)
151
+ @current_embdoc = {
152
+ type: :embdoc, body: comment, start: lineno, end: lineno
153
+ }
154
+ end
155
+
156
+ def on_embdoc(comment)
157
+ @current_embdoc[:body] << comment
158
+ end
159
+
160
+ def on_embdoc_end(comment)
161
+ @current_embdoc[:body] << comment.chomp
162
+ @block_comments << @current_embdoc
163
+ @current_embdoc = nil
164
+ end
165
+
166
+ def on_method_add_block(*body)
167
+ super(*body).tap do |sexp|
168
+ stmts = body[1][:body][1]
169
+ stmts = stmts[:type] == :stmts ? stmts : body[1][:body][1][:body][0]
170
+
171
+ attach_comments(sexp, stmts)
172
+ end
173
+ end
174
+ end
175
+
176
+ # Tracking heredocs in somewhat interesting. Straight-line heredocs are
177
+ # reported as strings, whereas squiggly-line heredocs are reported as
178
+ # heredocs.
179
+ module Heredocs
180
+ def initialize(*args)
181
+ super(*args)
182
+ @heredoc_stack = []
183
+ end
184
+
185
+ def self.prepended(base)
186
+ base.attr_reader :heredoc_stack
187
+ end
188
+
189
+ private
190
+
191
+ def on_embexpr_beg(body)
192
+ super(body).tap { |sexp| heredoc_stack << sexp }
193
+ end
194
+
195
+ def on_embexpr_end(body)
196
+ super(body).tap { heredoc_stack.pop }
197
+ end
198
+
199
+ def on_heredoc_beg(beging)
200
+ heredoc = { type: :heredoc, beging: beging, start: lineno, end: lineno }
201
+ heredoc_stack << heredoc
202
+ end
203
+
204
+ def on_heredoc_end(ending)
205
+ heredoc_stack[-1].merge!(ending: ending.chomp, end: lineno)
206
+ end
207
+
208
+ def on_heredoc_dedent(string, _width)
209
+ heredoc = heredoc_stack.pop
210
+ string.merge!(heredoc.slice(:type, :beging, :ending, :start, :end))
211
+ end
212
+
213
+ def on_string_literal(string)
214
+ heredoc = heredoc_stack[-1]
215
+
216
+ if heredoc && string[:type] != :heredoc && heredoc[:type] == :heredoc
217
+ heredoc_stack.pop
218
+ string.merge!(heredoc.slice(:type, :beging, :ending, :start, :end))
219
+ else
220
+ super
221
+ end
222
+ end
223
+ end
224
+
225
+ # These are the event types that contain _actual_ string content. If there is
226
+ # an encoding magic comment at the top of the file, ripper will actually
227
+ # change into that encoding for the storage of the string. This will break
228
+ # everything, so we need to force the encoding back into UTF-8 so that
229
+ # the JSON library won't break.
230
+ module Encoding
231
+ events = %w[comment ident tstring_content]
232
+
233
+ events.each do |event|
234
+ define_method(:"on_#{event}") do |body|
235
+ super(body.force_encoding('UTF-8'))
236
+ end
237
+ end
238
+ end
239
+
240
+ # This layer keeps track of inline comments as they come in. Ripper itself
241
+ # doesn't attach comments to the AST, so we need to do it manually. In this
242
+ # case, inline comments are defined as any comments wherein the lexer state is
243
+ # not equal to EXPR_BEG (tracked in the BlockComments layer).
244
+ module InlineComments
245
+ # Certain events needs to steal the comments from their children in order
246
+ # for them to display properly.
247
+ events = {
248
+ args_add_block: [:body, 0],
249
+ break: [:body, 0],
250
+ command: [:body, 1],
251
+ command_call: [:body, 3],
252
+ regexp_literal: [:body, 0],
253
+ string_literal: [:body, 0],
254
+ symbol_literal: [:body, 0]
255
+ }
256
+
257
+ def initialize(*args)
258
+ super(*args)
259
+ @inline_comments = []
260
+ @last_sexp = nil
261
+ end
262
+
263
+ def self.prepended(base)
264
+ base.attr_reader :inline_comments, :last_sexp
265
+ end
266
+
267
+ private
268
+
269
+ events.each do |event, path|
270
+ define_method(:"on_#{event}") do |*body|
271
+ @last_sexp =
272
+ super(*body).tap do |sexp|
273
+ comments = (sexp.dig(*path) || {}).delete(:comments)
274
+ sexp.merge!(comments: comments) if comments
275
+ end
276
+ end
277
+ end
278
+
279
+ SPECIAL_LITERALS = %i[qsymbols qwords symbols words].freeze
280
+
281
+ # Special array literals are handled in different ways and so their comments
282
+ # need to be passed up to their parent array node.
283
+ def on_array(*body)
284
+ @last_sexp =
285
+ super(*body).tap do |sexp|
286
+ next unless SPECIAL_LITERALS.include?(body.dig(0, :type))
287
+
288
+ comments = sexp.dig(:body, 0).delete(:comments)
289
+ sexp.merge!(comments: comments) if comments
290
+ end
291
+ end
292
+
293
+ # Handling this specially because we want to pull the comments out of both
294
+ # child nodes.
295
+ def on_assoc_new(*body)
296
+ @last_sexp =
297
+ super(*body).tap do |sexp|
298
+ comments =
299
+ (sexp.dig(:body, 0).delete(:comments) || []) +
300
+ (sexp.dig(:body, 1).delete(:comments) || [])
301
+
302
+ sexp.merge!(comments: comments) if comments.any?
303
+ end
304
+ end
305
+
306
+ # Most scanner events don't stand on their own a s-expressions, but the CHAR
307
+ # scanner event is effectively just a string, so we need to track it as a
308
+ # s-expression.
309
+ def on_CHAR(body)
310
+ @last_sexp = super(body)
311
+ end
312
+
313
+ # We need to know exactly where the comment is, switching off the current
314
+ # lexer state. In Ruby 2.7.0-dev, that's defined as:
315
+ #
316
+ # enum lex_state_bits {
317
+ # EXPR_BEG_bit, /* ignore newline, +/- is a sign. */
318
+ # EXPR_END_bit, /* newline significant, +/- is an operator. */
319
+ # EXPR_ENDARG_bit, /* ditto, and unbound braces. */
320
+ # EXPR_ENDFN_bit, /* ditto, and unbound braces. */
321
+ # EXPR_ARG_bit, /* newline significant, +/- is an operator. */
322
+ # EXPR_CMDARG_bit, /* newline significant, +/- is an operator. */
323
+ # EXPR_MID_bit, /* newline significant, +/- is an operator. */
324
+ # EXPR_FNAME_bit, /* ignore newline, no reserved words. */
325
+ # EXPR_DOT_bit, /* right after `.' or `::', no reserved words. */
326
+ # EXPR_CLASS_bit, /* immediate after `class', no here document. */
327
+ # EXPR_LABEL_bit, /* flag bit, label is allowed. */
328
+ # EXPR_LABELED_bit, /* flag bit, just after a label. */
329
+ # EXPR_FITEM_bit, /* symbol literal as FNAME. */
330
+ # EXPR_MAX_STATE
331
+ # };
332
+ def on_comment(body)
333
+ sexp = { type: :@comment, body: body.chomp, start: lineno, end: lineno }
334
+
335
+ case RipperJS.lex_state_name(state)
336
+ when 'EXPR_END', 'EXPR_ARG|EXPR_LABELED', 'EXPR_ENDFN'
337
+ last_sexp.merge!(comments: [sexp])
338
+ when 'EXPR_CMDARG', 'EXPR_END|EXPR_ENDARG', 'EXPR_ENDARG', 'EXPR_ARG',
339
+ 'EXPR_FNAME|EXPR_FITEM', 'EXPR_CLASS', 'EXPR_END|EXPR_LABEL'
340
+ inline_comments << sexp
341
+ when 'EXPR_BEG|EXPR_LABEL', 'EXPR_MID'
342
+ inline_comments << sexp.merge!(break: true)
343
+ when 'EXPR_DOT'
344
+ last_sexp.merge!(comments: [sexp.merge!(break: true)])
345
+ end
346
+
347
+ sexp
348
+ end
349
+
350
+ defined_events = private_instance_methods(false).grep(/\Aon_/) { $'.to_sym }
351
+
352
+ (Ripper::PARSER_EVENTS - defined_events).each do |event|
353
+ define_method(:"on_#{event}") do |*body|
354
+ super(*body).tap do |sexp|
355
+ @last_sexp = sexp
356
+ next if inline_comments.empty?
357
+
358
+ sexp[:comments] = inline_comments.reverse
359
+ @inline_comments = []
360
+ end
361
+ end
362
+ end
363
+ end
364
+
365
+ # Handles __END__ syntax, which allows individual scripts to keep content
366
+ # after the main ruby code that can be read through DATA.
367
+ module Ending
368
+ def initialize(source, *args)
369
+ super(source, *args)
370
+ @source = source
371
+ @ending = nil
372
+ end
373
+
374
+ def self.prepended(base)
375
+ base.attr_reader :source, :ending
376
+ end
377
+
378
+ private
379
+
380
+ def on___end__(body)
381
+ @ending = super(source.split("\n")[lineno..-1].join("\n"))
382
+ end
383
+
384
+ def on_program(*body)
385
+ super(*body).tap { |sexp| sexp[:body][0][:body] << ending if ending }
386
+ end
387
+ end
388
+
389
+ # Adds the used quote type onto string nodes.
390
+ module Strings
391
+ private
392
+
393
+ def on_tstring_end(quote)
394
+ last_sexp.merge!(quote: quote)
395
+ end
396
+
397
+ def on_label_end(quote)
398
+ last_sexp.merge!(quote: quote[0]) # quote is ": or ':
399
+ end
400
+ end
401
+
402
+ # Normally access controls are reported as vcall nodes. This module creates a
403
+ # new node type to explicitly track those nodes instead.
404
+ module AccessControls
405
+ def initialize(source, *args)
406
+ super(source, *args)
407
+ @lines = source.split("\n")
408
+ end
409
+
410
+ def self.prepended(base)
411
+ base.attr_reader :lines
412
+ end
413
+
414
+ private
415
+
416
+ def on_vcall(ident)
417
+ super(ident).tap do |sexp|
418
+ if !%w[private protected public].include?(ident[:body]) ||
419
+ ident[:body] != lines[lineno - 1].strip
420
+ next
421
+ end
422
+
423
+ sexp.merge!(type: :access_ctrl)
424
+ end
425
+ end
426
+ end
427
+ end
428
+
429
+ class RipperJS < Ripper
430
+ private
431
+
432
+ SCANNER_EVENTS.each do |event|
433
+ define_method(:"on_#{event}") do |body|
434
+ { type: :"@#{event}", body: body, start: lineno, end: lineno }
435
+ end
436
+ end
437
+
438
+ PARSER_EVENTS.each do |event|
439
+ define_method(:"on_#{event}") do |*body|
440
+ min = body.map { |part| part.is_a?(Hash) ? part[:start] : lineno }.min
441
+ { type: event, body: body, start: min || lineno, end: lineno }
442
+ end
443
+ end
444
+
445
+ prepend Layer::Lists
446
+ prepend Layer::StartLine
447
+ prepend Layer::InlineComments
448
+ prepend Layer::BlockComments
449
+ prepend Layer::Heredocs
450
+ prepend Layer::Encoding
451
+ prepend Layer::Ending
452
+ prepend Layer::Strings
453
+ prepend Layer::AccessControls
454
+
455
+ # When the only statement inside of a `def` node is a `begin` node, then you
456
+ # can safely replace the body of the `def` with the body of the `begin`. For
457
+ # example:
458
+ #
459
+ # def foo
460
+ # begin
461
+ # try_something
462
+ # rescue SomeError => error
463
+ # handle_error(error)
464
+ # end
465
+ # end
466
+ #
467
+ # can get transformed into:
468
+ #
469
+ # def foo
470
+ # try_something
471
+ # rescue SomeError => error
472
+ # handle_error(error)
473
+ # end
474
+ #
475
+ # This module handles this by hoisting up the `bodystmt` node from the inner
476
+ # `begin` up to the `def`.
477
+ prepend(
478
+ Module.new do
479
+ private
480
+
481
+ def on_def(ident, params, bodystmt)
482
+ def_bodystmt = bodystmt
483
+ stmts, *other_parts = bodystmt[:body]
484
+
485
+ if !other_parts.any? && stmts[:body].length == 1 &&
486
+ stmts.dig(:body, 0, :type) == :begin
487
+ def_bodystmt = stmts.dig(:body, 0, :body, 0)
488
+ end
489
+
490
+ super(ident, params, def_bodystmt)
491
+ end
492
+ end
493
+ )
494
+
495
+ # By default, Ripper parses the expression `lambda { foo }` as a
496
+ # `method_add_block` node, so we can't turn it back into `-> { foo }`. This
497
+ # module overrides that behavior and reports it back as a `lambda` node
498
+ # instead.
499
+ prepend(
500
+ Module.new do
501
+ private
502
+
503
+ def on_method_add_block(invocation, block)
504
+ # It's possible to hit a `method_add_block` node without going through
505
+ # `method_add_arg` node, ex: `super {}`. In that case we're definitely
506
+ # not going to transform into a lambda.
507
+ return super if invocation[:type] != :method_add_arg
508
+
509
+ fcall, args = invocation[:body]
510
+
511
+ # If there are arguments to the `lambda`, that means `lambda` has been
512
+ # overridden as a function so we cannot transform it into a `lambda`
513
+ # node.
514
+ if fcall[:type] != :fcall || args[:type] != :args || args[:body].any?
515
+ return super
516
+ end
517
+
518
+ ident = fcall.dig(:body, 0)
519
+ return super if ident[:type] != :@ident || ident[:body] != 'lambda'
520
+
521
+ super.tap do |sexp|
522
+ params, stmts = block[:body]
523
+ params ||= { type: :params, body: [] }
524
+
525
+ sexp.merge!(type: :lambda, body: [params, stmts])
526
+ end
527
+ end
528
+ end
529
+ )
530
+ end
531
+
532
+ if $0 == __FILE__
533
+ builder = RipperJS.new($stdin.read)
534
+ response = builder.parse
535
+
536
+ if !response && builder.error?
537
+ STDERR.puts 'Invalid ruby'
538
+ exit 1
539
+ end
540
+
541
+ puts JSON.fast_generate(response)
542
+ end