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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +368 -0
- data/CODE_OF_CONDUCT.md +76 -0
- data/CONTRIBUTING.md +143 -0
- data/LICENSE +21 -0
- data/README.md +151 -0
- data/bin/console +7 -0
- data/exe/rbprettier +8 -0
- data/lib/prettier.rb +18 -0
- data/lib/prettier/rake/task.rb +57 -0
- data/node_modules/prettier/bin-prettier.js +44620 -0
- data/node_modules/prettier/index.js +42565 -0
- data/node_modules/prettier/third-party.js +5326 -0
- data/package.json +39 -0
- data/src/builders.js +9 -0
- data/src/escapePattern.js +45 -0
- data/src/nodes.js +594 -0
- data/src/nodes/alias.js +32 -0
- data/src/nodes/arrays.js +162 -0
- data/src/nodes/blocks.js +66 -0
- data/src/nodes/calls.js +28 -0
- data/src/nodes/case.js +57 -0
- data/src/nodes/commands.js +70 -0
- data/src/nodes/conditionals.js +154 -0
- data/src/nodes/hashes.js +134 -0
- data/src/nodes/hooks.js +15 -0
- data/src/nodes/lambdas.js +59 -0
- data/src/nodes/loops.js +46 -0
- data/src/nodes/methods.js +42 -0
- data/src/nodes/params.js +75 -0
- data/src/nodes/regexp.js +18 -0
- data/src/nodes/rescue.js +77 -0
- data/src/nodes/strings.js +143 -0
- data/src/parse.js +16 -0
- data/src/print.js +23 -0
- data/src/ripper.rb +542 -0
- data/src/ruby.js +127 -0
- data/src/toProc.js +82 -0
- data/src/utils.js +150 -0
- metadata +109 -0
@@ -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
|
+
};
|
data/src/parse.js
ADDED
@@ -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
|
+
};
|
data/src/print.js
ADDED
@@ -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
|
+
};
|
data/src/ripper.rb
ADDED
@@ -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
|