-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathsyntactic.js
297 lines (240 loc) · 9.52 KB
/
syntactic.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
'use strict';
(function(global) {
/* syntactic.js provides an API for several operations over an abstract
* syntax tree that can be useful for automated testing of a students
* javascript code.
*
* Dependencies: esprima.js, q.js
*
*/
var syntactic = {};
global.syntactic = syntactic;
// When simplifying the AST, removing these nodes tends to make branches
// shorter while still preserving the overall structure
var REDUNDANT_TOKENS = [
'VariableDeclarator',
'BlockStatement',
'Identifier',
'Literal',
'Line',
'ExpressionStatement'
];
// Modifications of Esprima methods
// ================================
// This returns a simplified version of the esprima abstract syntax tree
// 1. For every branch it removes the root node if it has only one child,
// and makes the child the new root node. This repeats until the root
// node has multiple children or is empty
// 2. It removes any node that doesn't have a type attribute
// 3. It renames the node to the value of it's type attribute
syntactic.parse = function(text){
var bag = {};
var comments = [];
// Recursively walk through the ast
var _parse = function(branch, bag){
// collect the node if it has a type attribute
if(branch.hasOwnProperty('type')){
// Capture comments to be used as hints
if(branch.type === 'Line'){
comments[branch.loc.end.line + 1] = branch.value;
} else {
// Collect the node
var lineNo = branch.loc ? branch.loc.start.line : undefined;
bag[branch.type] = {line:lineNo};
bag = bag[branch.type];
}
}
// repeat the above for any child objects of this node.
// ignore child attributes, they are just metadata
for(var i in branch){
if(branch.hasOwnProperty(i)){
if(branch[i] && typeof branch[i] === 'object'){
_parse(branch[i], bag);
}
}
}
};
var ast = esprima.parse(text, {loc:true, comment:true});
_parse(ast, bag);
return {structure: bag, comments:comments};
};
// Asynchronous
// This method returns the esprima tokens list as a map
// {Keyword:{for:[ {end: {column:7, line:2}, start: {column:4, line:2}}]}}
syntactic.tokenize = function(text){
var deferred = Q.defer();
var dict = {};
var tokens = esprima.tokenize(text, {loc:true});
for(var i = tokens.length - 1; i >= 0; --i){
var token = tokens[i];
if(!dict.hasOwnProperty(token.type)){
dict[token.type] = {};
}
if( dict[token.type].hasOwnProperty(token.value) ){
dict[token.type][token.value].push(token.loc);
} else {
dict[token.type][token.value] = [token.loc];
}
}
deferred.resolve(dict);
return deferred.promise;
};
// Whitelist / Blacklist Specification
// ===================================
// This Requirements object is composed of
// 1. the whitelist/blacklist
// 2. a "flags" object which contains copies of those items in the
// whitelist/blacklist which were not conformed to
// 3. a status attribute which is true if the requirements were met and
// false if the requirements were not met
function Requirements(options){
this.specify(options);
this.flags = {};
this.status = true;
}
// Steps through the whitelist/blacklist and applies the provided function
Requirements.prototype._traverse = function(list, fn){
var tokenTypes = Object.keys(list);
for (var i = tokenTypes.length - 1; i >= 0; --i) {
var tokens = list[tokenTypes[i]];
for (var j = tokens.length - 1; j >= 0; --j) {
fn(tokenTypes[i],tokens[j]);
}
}
};
// This method is for flagging a missing/offending token type
Requirements.prototype._flag = function(listType, type, value, locs){
this.status = false;
if(!this.flags[listType]){
this.flags[listType] = {};
}
if(!this.flags[listType].hasOwnProperty(type)){
this.flags[listType][type] = {};
}
this.flags[listType][type][value] = locs;
};
// This instantiates the object with a whitelist and/or blacklist which
// will remain cached in the object
Requirements.prototype.specify = function(options){
if(options.hasOwnProperty('whitelist')){
this.whitelist = options.whitelist;
}
if(options.hasOwnProperty('blacklist')){
this.blacklist = options.blacklist;
}
};
// Asynchronous
// accepts a block of text which it tokenizes and compares to it's
// whitelist / blacklist.
Requirements.prototype.verify = function(text){
var self = this;
var deferred = Q.defer();
// Reset object state
this.flags = {};
this.status = true;
// Verify
syntactic.tokenize(text).then(function(summary){
if(self.whitelist){
self._traverse(self.whitelist, function(type, value){
if(!summary[type] || !summary[type][value]){
self._flag('whitelist', type, value);
}
});
}
if(self.blacklist){
self._traverse(self.blacklist, function(type, value){
if(summary[type] && summary[type][value]){
self._flag('blacklist', type, value, summary[type][value]);
}
});
}
deferred.resolve(self);
});
return deferred.promise;
};
// Returns a new requirments object
syntactic.specify = function(options){
return new Requirements(options);
};
// Comparing code against a template structure
// ===========================================
// This class parses text using the syntactic.parse method and then
// serializes the resulting object into string representations of each
// branch.
function SerializedStructure(text, options){
var parsed = syntactic.parse(text);
this.structure = parsed.structure;
this.hints = parsed.comments;
this.ignore = options && options.ignore ? options.ignore : null;
this.serialized = {};
// do the serialization
this._serialize(this.structure, '');
}
SerializedStructure.prototype._serialize = function(branch, branchName){
var stems = Object.keys(branch);
for (var i = stems.length - 1; i >= 0; --i) {
var stemName = stems[i];
// ignore line numbers, they aren't structure
if(stemName !== 'line'){
var name = branchName;
if(this.ignore.indexOf(stemName) === -1){
name = branchName === '' ? stemName :
branchName + '.' + stemName;
}
// end of the branch, store serialized branch name
if(Object.keys(branch[stemName]).length === 1){
// ignore comments, they aren't structure
if(stemName !== 'Line'){
var line = branch[stemName].line;
if(! this.serialized.hasOwnProperty(name)){
this.serialized[name] = {};
}
this.serialized[name][line] = this.hints[line];
}
} else {
// recurse
this._serialize(branch[stemName], name);
}
}
}
};
// Checks to see if a new block of codes matches the structure of this one.
SerializedStructure.prototype.verify = function(text){
// serialize the users code
// filter the same nodes as the template
var userCode = new SerializedStructure(text, {
ignore: this.ignore
});
// This is what gets returned
var info = {
status: true,
missing:[],
hints:[]
};
// iterate over the template structure, checking that each branch
// exists in the users structure
for (var branchName in this.serialized) {
// if user code is missing the branch
if(!userCode.serialized.hasOwnProperty(branchName)){
// record the problem
info.status = false;
info.missing.push(branchName);
// get the hint
var lines = this.serialized[branchName];
for (var i in lines) {
var hint = lines[i];
if(hint){
info.hints[i] = hint;
}
}
}
}
return info;
};
// Public API method
syntactic.outline = function(example){
// This could be a public argument, but probably doesn't need to be
var options = {ignore:REDUNDANT_TOKENS};
return new SerializedStructure(example, options);
};
})(typeof window !== 'undefined' ? window : global);