parsejs 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +1 -0
- data/.travis.yml +9 -0
- data/Gemfile +5 -0
- data/README.markdown +85 -0
- data/Rakefile +15 -0
- data/lib/parsejs.rb +14 -0
- data/lib/parsejs/ast.rb +84 -0
- data/lib/parsejs/docs.rb +237 -0
- data/lib/parsejs/grammar.kpeg +575 -0
- data/lib/parsejs/grammar.kpeg.rb +9460 -0
- data/lib/parsejs/scope.rb +183 -0
- data/lib/parsejs/stringifier.rb +450 -0
- data/lib/parsejs/version.rb +3 -0
- data/lib/parsejs/visitor.rb +257 -0
- data/parsejs.gemspec +24 -0
- data/spec/fixtures/jquery-1.6.js +8982 -0
- data/spec/fixtures/jquery-1.7.js +9289 -0
- data/spec/fixtures/jquery-ajax.js +1000 -0
- data/spec/fixtures/jquery-attributes.js +650 -0
- data/spec/fixtures/jquery-traversing.js +330 -0
- data/spec/fixtures/metamorph.js +324 -0
- data/spec/fixtures/sizzle.js +1442 -0
- data/spec/fixtures/sproutcore-core.js +195 -0
- data/spec/fixtures/sproutcore-each-proxy.js +218 -0
- data/spec/fixtures/sproutcore-native-array.js +139 -0
- data/spec/fixtures/sproutcore.js +14319 -0
- data/spec/scope_spec.rb +111 -0
- data/spec/stringify_spec.rb +587 -0
- data/test.rb +76 -0
- metadata +145 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 41b39d4f01ad6648dbb147d8cf758bb0b5cd45ff
|
4
|
+
data.tar.gz: 3877eb40098d127ed7a2a036f2ceee54983e2c6b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e4fe5a4c22dea6bb05393054ea312a3eedf38bc76b083b24a73cc0b23736889fb9ca2a8019dd7e8a5cfd0cdd2cbd90d8a87264086746d7bc68f3bd16d13bcd1f
|
7
|
+
data.tar.gz: e56c48ca152263de721598472e4b03ac2145455193dd38e4d37dcee4fd66f79a402963e1cd90ec843142050e2e5d4731b937a477f7b972ad78a02fad7d910a49
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
-cfs
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.markdown
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# ParseJS
|
2
|
+
|
3
|
+
ParseJS is a JavaScript parser written in Ruby using the KPeg parser generator. It also
|
4
|
+
takes a JavaScript parse tree and emits semantically identical JavaScript. The parser is
|
5
|
+
tested by parsing large, popular JavaScript libraries (such as jQuery) and confirming that
|
6
|
+
the minified output after a round-trip through ParseJS is the same as minifying the original
|
7
|
+
source.
|
8
|
+
|
9
|
+
The ParseJS stringifier does not guarantee equivalent whitespace and comments in a
|
10
|
+
round-trip, but it should guarantee semantic equivalence.
|
11
|
+
|
12
|
+
The ParseJS parser maintains comments in most cases where comments would represent inline
|
13
|
+
documentation.
|
14
|
+
|
15
|
+
* top-level statement: a ParseJS top-level statement node contains any comment that
|
16
|
+
immediately preceded it.
|
17
|
+
* property: in a property list (object literal), a property node contains any comment
|
18
|
+
that immediately preceded it.
|
19
|
+
|
20
|
+
There is a work-in-progress AST walker that associates comments with particular structures.
|
21
|
+
The ultimate goal of this walker is to identify JavaScript structures that represent
|
22
|
+
"classes" or similar structures and associate their comments with information extracted
|
23
|
+
from the code.
|
24
|
+
|
25
|
+
# Usage
|
26
|
+
|
27
|
+
ParseJS is provided as a Rubygem. At the moment, you can use it in your Gemfile by using
|
28
|
+
Bundler's git feature.
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
gem "parsejs", :git => "git://github.com/wycats/parsejs.git"
|
32
|
+
```
|
33
|
+
|
34
|
+
You can parse a String of JavaScript and receive an AST by using
|
35
|
+
`ParseJS.parse`.
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
ast = ParseJS.parse(some_data)
|
39
|
+
```
|
40
|
+
|
41
|
+
You can convert the AST back into a JavaScript String using the
|
42
|
+
stringifier. You can mutate the AST before converting it into a String
|
43
|
+
if you wish.
|
44
|
+
|
45
|
+
```
|
46
|
+
ParseJS::Stringifier.to_string(ast)
|
47
|
+
```
|
48
|
+
|
49
|
+
You can write your own AST walker without implementing visitors for all
|
50
|
+
nodes by subclassing `ParseJS::Visitor`. Take a look
|
51
|
+
[at that class](https://github.com/wycats/parsejs/blob/master/lib/parsejs/visitor.rb)
|
52
|
+
to see the default visitor behavior for a particular node.
|
53
|
+
|
54
|
+
By default, nodes `accept` their children. Nodes that have children that
|
55
|
+
are `Array`s of nodes (e.g. `FunctionDeclaration`, which has an `Array` of
|
56
|
+
parameters and an `Array` of statements as children) default to looping
|
57
|
+
over the `Array`s and accepting their members.
|
58
|
+
|
59
|
+
You can take a look at the in-progress
|
60
|
+
[docs extractor](https://github.com/wycats/parsejs/blob/master/lib/parsejs/docs.rb)
|
61
|
+
or the [stringifier](https://github.com/wycats/parsejs/blob/master/lib/parsejs/stringifier.rb)
|
62
|
+
for examples of subclasses of `ParseJS::Visitor`.
|
63
|
+
|
64
|
+
# LICENSE
|
65
|
+
|
66
|
+
Copyright (C) 2012 Yehuda Katz
|
67
|
+
|
68
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
69
|
+
this software and associated documentation files (the "Software"), to deal in
|
70
|
+
the Software without restriction, including without limitation the rights to
|
71
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
72
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
73
|
+
so, subject to the following conditions:
|
74
|
+
|
75
|
+
The above copyright notice and this permission notice shall be included in all
|
76
|
+
copies or substantial portions of the Software.
|
77
|
+
|
78
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
79
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
80
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
81
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
82
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
83
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
84
|
+
SOFTWARE.
|
85
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
require "bundler/setup"
|
4
|
+
|
5
|
+
file "lib/parsejs/grammar.kpeg.rb" => "lib/parsejs/grammar.kpeg" do
|
6
|
+
sh "kpeg -f lib/parsejs/grammar.kpeg --stand-alone --debug"
|
7
|
+
end
|
8
|
+
|
9
|
+
task :compile => "lib/parsejs/grammar.kpeg.rb"
|
10
|
+
|
11
|
+
task :spec => :compile do
|
12
|
+
sh "rspec -cfs spec"
|
13
|
+
end
|
14
|
+
|
15
|
+
task :default => :compile
|
data/lib/parsejs.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require "parsejs/version"
|
2
|
+
require "parsejs/grammar.kpeg"
|
3
|
+
require "parsejs/stringifier"
|
4
|
+
require "parsejs/ast"
|
5
|
+
require "parsejs/scope"
|
6
|
+
|
7
|
+
module ParseJS
|
8
|
+
def self.parse(string)
|
9
|
+
string = string.force_encoding('BINARY') if string.respond_to?(:force_encoding)
|
10
|
+
parser = ParseJS::Parser.new(string)
|
11
|
+
parser.parse
|
12
|
+
parser.result
|
13
|
+
end
|
14
|
+
end
|
data/lib/parsejs/ast.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
require "parsejs/scope"
|
2
|
+
|
3
|
+
module ParseJS
|
4
|
+
module AST
|
5
|
+
class Node
|
6
|
+
def type?(string)
|
7
|
+
self.class.name.split("::").last == string
|
8
|
+
end
|
9
|
+
|
10
|
+
def cuddly?
|
11
|
+
false
|
12
|
+
end
|
13
|
+
|
14
|
+
def statement?
|
15
|
+
false
|
16
|
+
end
|
17
|
+
|
18
|
+
def needs_newline?
|
19
|
+
false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Comment
|
24
|
+
def multiline?
|
25
|
+
@type == 'multiline'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class SequenceExpression
|
30
|
+
attr_accessor :parens
|
31
|
+
end
|
32
|
+
|
33
|
+
class BlockStatement
|
34
|
+
attr_accessor :cuddly
|
35
|
+
|
36
|
+
def cuddle!
|
37
|
+
@cuddly = true
|
38
|
+
end
|
39
|
+
|
40
|
+
def needs_newline?
|
41
|
+
!@cuddly
|
42
|
+
end
|
43
|
+
|
44
|
+
def cuddly?
|
45
|
+
true
|
46
|
+
end
|
47
|
+
|
48
|
+
def statement?
|
49
|
+
true
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class FunctionExpression
|
54
|
+
include ParseJS::AST::Scope
|
55
|
+
end
|
56
|
+
|
57
|
+
class FunctionDeclaration
|
58
|
+
include ParseJS::AST::Scope
|
59
|
+
end
|
60
|
+
|
61
|
+
class Program
|
62
|
+
include ParseJS::AST::Scope
|
63
|
+
end
|
64
|
+
|
65
|
+
statements = [VariableDeclaration, EmptyStatement, ExpressionStatement, IfStatement]
|
66
|
+
statements += [WhileStatement, ForStatement, ForInStatement, DoWhileStatement]
|
67
|
+
statements += [ContinueStatement, BreakStatement, ReturnStatement, WithStatement]
|
68
|
+
statements += [LabeledStatement, SwitchStatement, ThrowStatement, TryStatement]
|
69
|
+
statements += [DebuggerStatement, FunctionDeclaration]
|
70
|
+
|
71
|
+
statements.each do |statement|
|
72
|
+
statement.class_eval do
|
73
|
+
def needs_newline?
|
74
|
+
true
|
75
|
+
end
|
76
|
+
|
77
|
+
def statement?
|
78
|
+
true
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
data/lib/parsejs/docs.rb
ADDED
@@ -0,0 +1,237 @@
|
|
1
|
+
require "parsejs/visitor"
|
2
|
+
require "yard"
|
3
|
+
|
4
|
+
module ParseJS
|
5
|
+
class CommentScanner < Visitor
|
6
|
+
include ParseJS::AST
|
7
|
+
|
8
|
+
def initialize(*)
|
9
|
+
@current_comment = nil
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def comment?
|
14
|
+
return false unless @current_comment
|
15
|
+
return false if @current_comment.empty?
|
16
|
+
return @current_comment.any?(&:multiline?)
|
17
|
+
end
|
18
|
+
|
19
|
+
def stringify(node)
|
20
|
+
Stringifier.to_string(Marshal.load(Marshal.dump(node)))
|
21
|
+
end
|
22
|
+
|
23
|
+
def visit_AssignmentExpression(expr)
|
24
|
+
process_comment(expr)
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
28
|
+
def visit_CommentedStatement(comment)
|
29
|
+
@current_comment = comment.comments
|
30
|
+
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
def visit_Property(property)
|
35
|
+
@current_comment = property.comments
|
36
|
+
process_comment(property)
|
37
|
+
|
38
|
+
super
|
39
|
+
end
|
40
|
+
|
41
|
+
def process_comment(node)
|
42
|
+
print_comment(node)
|
43
|
+
end
|
44
|
+
|
45
|
+
def print_comment(node)
|
46
|
+
return unless comment?
|
47
|
+
|
48
|
+
puts
|
49
|
+
puts "-" * 10 + node.class.name + "-" * 10
|
50
|
+
puts @current_comment.map { |c| next unless c.multiline?; c.body }.join("\n")
|
51
|
+
puts "-" * (10 + node.class.name.size + 10)
|
52
|
+
puts
|
53
|
+
|
54
|
+
puts Stringifier.to_string(Marshal.load(Marshal.dump(node)))
|
55
|
+
puts
|
56
|
+
ensure
|
57
|
+
@current_comment = nil
|
58
|
+
end
|
59
|
+
|
60
|
+
def strip_leading_whitespace(string)
|
61
|
+
min = string.scan(/^[ \t]*(?=\S)/).min
|
62
|
+
size = min ? min.size : 0
|
63
|
+
string.gsub(/^[ \t]{#{size}}/, '')
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class ExtractDocs < CommentScanner
|
68
|
+
include AST
|
69
|
+
|
70
|
+
def initialize(*)
|
71
|
+
@current_variables = []
|
72
|
+
@current_class = []
|
73
|
+
end
|
74
|
+
|
75
|
+
def visit_Program(program)
|
76
|
+
with_variables(program, []) { super }
|
77
|
+
end
|
78
|
+
|
79
|
+
def visit_FunctionDeclaration(decl)
|
80
|
+
with_variables(decl, decl.params.list.map(&:val)) { super }
|
81
|
+
end
|
82
|
+
|
83
|
+
def visit_FunctionExpression(expr)
|
84
|
+
with_variables(expr, expr.params.list.map(&:val)) { super }
|
85
|
+
end
|
86
|
+
|
87
|
+
def with_variables(expr, params=expr.params.map(&:val))
|
88
|
+
locals = FindVars.find_variables(expr)
|
89
|
+
@current_variables.push(locals | params)
|
90
|
+
yield
|
91
|
+
ensure
|
92
|
+
@current_variables.pop
|
93
|
+
end
|
94
|
+
|
95
|
+
def variable?(name)
|
96
|
+
@current_variables.find do |list|
|
97
|
+
list.include?(name)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def member_left(expr)
|
102
|
+
object = expr
|
103
|
+
|
104
|
+
while object
|
105
|
+
if object.is_a?(Identifier)
|
106
|
+
return object.val
|
107
|
+
elsif object.is_a?(ThisExpression)
|
108
|
+
return "this"
|
109
|
+
elsif object.respond_to?(:object)
|
110
|
+
object = object.object
|
111
|
+
else
|
112
|
+
return nil
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def visit_AssignmentExpression(expr)
|
118
|
+
right = expr.right
|
119
|
+
in_class = false
|
120
|
+
|
121
|
+
if right.is_a?(CallExpression)
|
122
|
+
callee = expr.right.callee
|
123
|
+
if callee.is_a?(MemberExpression) && !callee.computed
|
124
|
+
property = callee.property.val
|
125
|
+
if property == "extend"
|
126
|
+
if expr.left.is_a?(MemberExpression)
|
127
|
+
left_id = member_left(expr.left)
|
128
|
+
extends = stringify(expr.right.callee).sub(/\.extend$/, '')
|
129
|
+
|
130
|
+
if variable?(left_id)
|
131
|
+
# found private class
|
132
|
+
else
|
133
|
+
klass = stringify(expr.left)
|
134
|
+
|
135
|
+
left_string = stringify(expr.left)
|
136
|
+
namespace_obj = build_namespace left_string.split(".")[0...-1].join("::")
|
137
|
+
|
138
|
+
class_name = left_string.split(".")[-1]
|
139
|
+
|
140
|
+
obj = YARD::CodeObjects::ClassObject.new(namespace_obj, class_name)
|
141
|
+
obj.docstring = stripped_comment
|
142
|
+
|
143
|
+
@current_class.push [klass, extends, obj]
|
144
|
+
in_class = true
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
super
|
152
|
+
|
153
|
+
ensure
|
154
|
+
@current_class.pop if in_class
|
155
|
+
end
|
156
|
+
|
157
|
+
def build_namespace(namespace)
|
158
|
+
if namespace.empty?
|
159
|
+
YARD::Registry.root
|
160
|
+
elsif ns = YARD::Registry.at(namespace)
|
161
|
+
ns
|
162
|
+
else
|
163
|
+
name = namespace.gsub('::', '.')
|
164
|
+
YARD::CodeObjects::NamespaceObject.new(:root, name)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def current_class_name
|
169
|
+
klass, parent = @current_class.last
|
170
|
+
"#{klass} < #{parent}"
|
171
|
+
end
|
172
|
+
|
173
|
+
def current_yard_class
|
174
|
+
@current_class.last[2]
|
175
|
+
end
|
176
|
+
|
177
|
+
def visit_Property(property)
|
178
|
+
return if @current_class.empty?
|
179
|
+
|
180
|
+
@current_comment = property.comments
|
181
|
+
return if stripped_comment.empty?
|
182
|
+
|
183
|
+
case property.value
|
184
|
+
when FunctionDeclaration, FunctionExpression
|
185
|
+
obj = YARD::CodeObjects::MethodObject.new(current_yard_class, property.key.val)
|
186
|
+
obj.docstring = stripped_comment
|
187
|
+
else
|
188
|
+
# found a non-method property
|
189
|
+
end
|
190
|
+
|
191
|
+
super
|
192
|
+
end
|
193
|
+
|
194
|
+
def stripped_comment
|
195
|
+
comments = @current_comment.map do |c|
|
196
|
+
next unless c.multiline?
|
197
|
+
c.body
|
198
|
+
end
|
199
|
+
|
200
|
+
string = comments.join("\n")
|
201
|
+
string = string.gsub(/\A\**\n/, '')
|
202
|
+
strip_leading_whitespace(string)
|
203
|
+
end
|
204
|
+
|
205
|
+
def process_comment(*)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
require "set"
|
210
|
+
|
211
|
+
class FindVars < Visitor
|
212
|
+
def self.find_variables(ast)
|
213
|
+
ast = Marshal.load(Marshal.dump(ast))
|
214
|
+
|
215
|
+
finder = new
|
216
|
+
finder.accept(ast)
|
217
|
+
finder.variables
|
218
|
+
end
|
219
|
+
|
220
|
+
attr_reader :variables
|
221
|
+
|
222
|
+
def initialize(*)
|
223
|
+
@variables = Set.new
|
224
|
+
super
|
225
|
+
end
|
226
|
+
|
227
|
+
def visit_VariableDeclarator(expr)
|
228
|
+
@variables << expr.id.val
|
229
|
+
end
|
230
|
+
|
231
|
+
def visit_FunctionDeclaration(*)
|
232
|
+
end
|
233
|
+
|
234
|
+
def visit_FunctionExpression(*)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|