prettier 0.12.2

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