/[jscoverage]/trunk/instrument-js.cpp
ViewVC logotype

Contents of /trunk/instrument-js.cpp

Parent Directory Parent Directory | Revision Log Revision Log


Revision 190 - (show annotations)
Tue Sep 23 03:49:01 2008 UTC (10 years ago) by siliconforks
Original Path: trunk/instrument-js.c
File MIME type: text/plain
File size: 41332 byte(s)
Do not deflate lines looking for conditionals.
1 /*
2 instrument-js.c - JavaScript instrumentation routines
3 Copyright (C) 2007, 2008 siliconforks.com
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License along
16 with this program; if not, write to the Free Software Foundation, Inc.,
17 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20 #include <config.h>
21
22 #include "instrument-js.h"
23
24 #include <assert.h>
25 #include <stdlib.h>
26 #include <string.h>
27
28 #include <jsapi.h>
29 #include <jsatom.h>
30 #include <jsfun.h>
31 #include <jsinterp.h>
32 #include <jsparse.h>
33 #include <jsregexp.h>
34 #include <jsscope.h>
35 #include <jsstr.h>
36
37 #include "encoding.h"
38 #include "global.h"
39 #include "highlight.h"
40 #include "resource-manager.h"
41 #include "util.h"
42
43 static JSRuntime * runtime = NULL;
44 static JSContext * context = NULL;
45 static JSObject * global = NULL;
46
47 /*
48 JSParseNode objects store line numbers starting from 1.
49 The lines array stores line numbers starting from 0.
50 */
51 static const char * file_id = NULL;
52 static char * lines = NULL;
53
54 void jscoverage_init(void) {
55 runtime = JS_NewRuntime(8L * 1024L * 1024L);
56 if (runtime == NULL) {
57 fatal("cannot create runtime");
58 }
59
60 context = JS_NewContext(runtime, 8192);
61 if (context == NULL) {
62 fatal("cannot create context");
63 }
64
65 global = JS_NewObject(context, NULL, NULL, NULL);
66 if (global == NULL) {
67 fatal("cannot create global object");
68 }
69
70 if (! JS_InitStandardClasses(context, global)) {
71 fatal("cannot initialize standard classes");
72 }
73 }
74
75 void jscoverage_cleanup(void) {
76 JS_DestroyContext(context);
77 JS_DestroyRuntime(runtime);
78 }
79
80 static void print_string(JSString * s, Stream * f) {
81 size_t length = JSSTRING_LENGTH(s);
82 jschar * characters = JSSTRING_CHARS(s);
83 for (size_t i = 0; i < length; i++) {
84 jschar c = characters[i];
85 if (32 <= c && c <= 126) {
86 switch (c) {
87 case '"':
88 Stream_write_string(f, "\\\"");
89 break;
90 /*
91 case '\'':
92 Stream_write_string(f, "\\'");
93 break;
94 */
95 case '\\':
96 Stream_write_string(f, "\\\\");
97 break;
98 default:
99 Stream_write_char(f, c);
100 break;
101 }
102 }
103 else {
104 switch (c) {
105 case 0x8:
106 Stream_write_string(f, "\\b");
107 break;
108 case 0x9:
109 Stream_write_string(f, "\\t");
110 break;
111 case 0xa:
112 Stream_write_string(f, "\\n");
113 break;
114 case 0xb:
115 Stream_write_string(f, "\\v");
116 break;
117 case 0xc:
118 Stream_write_string(f, "\\f");
119 break;
120 case 0xd:
121 Stream_write_string(f, "\\r");
122 break;
123 default:
124 Stream_printf(f, "\\u%04x", c);
125 break;
126 }
127 }
128 }
129 }
130
131 static void print_string_atom(JSAtom * atom, Stream * f) {
132 assert(ATOM_IS_STRING(atom));
133 JSString * s = ATOM_TO_STRING(atom);
134 print_string(s, f);
135 }
136
137 static void print_regex(jsval value, Stream * f) {
138 assert(JSVAL_IS_STRING(value));
139 JSString * s = JSVAL_TO_STRING(value);
140 size_t length = JSSTRING_LENGTH(s);
141 jschar * characters = JSSTRING_CHARS(s);
142 for (size_t i = 0; i < length; i++) {
143 jschar c = characters[i];
144 if (32 <= c && c <= 126) {
145 Stream_write_char(f, c);
146 }
147 else {
148 Stream_printf(f, "\\u%04x", c);
149 }
150 }
151 }
152
153 static void print_quoted_string_atom(JSAtom * atom, Stream * f) {
154 assert(ATOM_IS_STRING(atom));
155 JSString * s = ATOM_TO_STRING(atom);
156 Stream_write_char(f, '"');
157 print_string(s, f);
158 Stream_write_char(f, '"');
159 }
160
161 static const char * get_op(uint8 op) {
162 switch(op) {
163 case JSOP_BITOR:
164 return "|";
165 case JSOP_BITXOR:
166 return "^";
167 case JSOP_BITAND:
168 return "&";
169 case JSOP_EQ:
170 return "==";
171 case JSOP_NE:
172 return "!=";
173 case JSOP_NEW_EQ:
174 return "===";
175 case JSOP_NEW_NE:
176 return "!==";
177 case JSOP_LT:
178 return "<";
179 case JSOP_LE:
180 return "<=";
181 case JSOP_GT:
182 return ">";
183 case JSOP_GE:
184 return ">=";
185 case JSOP_LSH:
186 return "<<";
187 case JSOP_RSH:
188 return ">>";
189 case JSOP_URSH:
190 return ">>>";
191 case JSOP_ADD:
192 return "+";
193 case JSOP_SUB:
194 return "-";
195 case JSOP_MUL:
196 return "*";
197 case JSOP_DIV:
198 return "/";
199 case JSOP_MOD:
200 return "%";
201 default:
202 abort();
203 }
204 }
205
206 static void instrument_expression(JSParseNode * node, Stream * f);
207 static void instrument_statement(JSParseNode * node, Stream * f, int indent);
208
209 enum FunctionType {
210 FUNCTION_NORMAL,
211 FUNCTION_GETTER_OR_SETTER
212 };
213
214 static void instrument_function(JSParseNode * node, Stream * f, int indent, enum FunctionType type) {
215 assert(node->pn_arity == PN_FUNC);
216 assert(ATOM_IS_OBJECT(node->pn_funAtom));
217 JSObject * object = ATOM_TO_OBJECT(node->pn_funAtom);
218 assert(JS_ObjectIsFunction(context, object));
219 JSFunction * function = (JSFunction *) JS_GetPrivate(context, object);
220 assert(function);
221 assert(object == function->object);
222 Stream_printf(f, "%*s", indent, "");
223 if (type == FUNCTION_NORMAL) {
224 Stream_write_string(f, "function");
225 }
226
227 /* function name */
228 if (function->atom) {
229 Stream_write_char(f, ' ');
230 print_string_atom(function->atom, f);
231 }
232
233 /* function parameters */
234 Stream_write_string(f, "(");
235 JSAtom ** params = xnew(JSAtom *, function->nargs);
236 for (int i = 0; i < function->nargs; i++) {
237 /* initialize to NULL for sanity check */
238 params[i] = NULL;
239 }
240 JSScope * scope = OBJ_SCOPE(object);
241 for (JSScopeProperty * scope_property = SCOPE_LAST_PROP(scope); scope_property != NULL; scope_property = scope_property->parent) {
242 if (scope_property->getter != js_GetArgument) {
243 continue;
244 }
245 assert(scope_property->flags & SPROP_HAS_SHORTID);
246 assert((uint16) scope_property->shortid < function->nargs);
247 assert(JSID_IS_ATOM(scope_property->id));
248 params[(uint16) scope_property->shortid] = JSID_TO_ATOM(scope_property->id);
249 }
250 for (int i = 0; i < function->nargs; i++) {
251 assert(params[i] != NULL);
252 if (i > 0) {
253 Stream_write_string(f, ", ");
254 }
255 if (ATOM_IS_STRING(params[i])) {
256 print_string_atom(params[i], f);
257 }
258 }
259 Stream_write_string(f, ") {\n");
260 free(params);
261
262 /* function body */
263 instrument_statement(node->pn_body, f, indent + 2);
264
265 Stream_write_string(f, "}\n");
266 }
267
268 static void instrument_function_call(JSParseNode * node, Stream * f) {
269 instrument_expression(node->pn_head, f);
270 Stream_write_char(f, '(');
271 for (struct JSParseNode * p = node->pn_head->pn_next; p != NULL; p = p->pn_next) {
272 if (p != node->pn_head->pn_next) {
273 Stream_write_string(f, ", ");
274 }
275 instrument_expression(p, f);
276 }
277 Stream_write_char(f, ')');
278 }
279
280 /*
281 See <Expressions> in jsparse.h.
282 TOK_FUNCTION is handled as a statement and as an expression.
283 TOK_DBLDOT is not handled (XML op).
284 TOK_DEFSHARP and TOK_USESHARP are not handled.
285 TOK_ANYNAME is not handled (XML op).
286 TOK_AT is not handled (XML op).
287 TOK_DBLCOLON is not handled.
288 TOK_XML* are not handled.
289 There seem to be some undocumented expressions:
290 TOK_INSTANCEOF binary
291 TOK_IN binary
292 */
293 static void instrument_expression(JSParseNode * node, Stream * f) {
294 switch (node->pn_type) {
295 case TOK_FUNCTION:
296 instrument_function(node, f, 0, FUNCTION_NORMAL);
297 break;
298 case TOK_COMMA:
299 for (struct JSParseNode * p = node->pn_head; p != NULL; p = p->pn_next) {
300 if (p != node->pn_head) {
301 Stream_write_string(f, ", ");
302 }
303 instrument_expression(p, f);
304 }
305 break;
306 case TOK_ASSIGN:
307 instrument_expression(node->pn_left, f);
308 Stream_write_char(f, ' ');
309 switch (node->pn_op) {
310 case JSOP_ADD:
311 case JSOP_SUB:
312 case JSOP_MUL:
313 case JSOP_MOD:
314 case JSOP_LSH:
315 case JSOP_RSH:
316 case JSOP_URSH:
317 case JSOP_BITAND:
318 case JSOP_BITOR:
319 case JSOP_BITXOR:
320 case JSOP_DIV:
321 Stream_printf(f, "%s", get_op(node->pn_op));
322 break;
323 default:
324 /* do nothing - it must be a simple assignment */
325 break;
326 }
327 Stream_write_string(f, "= ");
328 instrument_expression(node->pn_right, f);
329 break;
330 case TOK_HOOK:
331 instrument_expression(node->pn_kid1, f);
332 Stream_write_string(f, "? ");
333 instrument_expression(node->pn_kid2, f);
334 Stream_write_string(f, ": ");
335 instrument_expression(node->pn_kid3, f);
336 break;
337 case TOK_OR:
338 instrument_expression(node->pn_left, f);
339 Stream_write_string(f, " || ");
340 instrument_expression(node->pn_right, f);
341 break;
342 case TOK_AND:
343 instrument_expression(node->pn_left, f);
344 Stream_write_string(f, " && ");
345 instrument_expression(node->pn_right, f);
346 break;
347 case TOK_BITOR:
348 case TOK_BITXOR:
349 case TOK_BITAND:
350 case TOK_EQOP:
351 case TOK_RELOP:
352 case TOK_SHOP:
353 case TOK_PLUS:
354 case TOK_MINUS:
355 case TOK_STAR:
356 case TOK_DIVOP:
357 switch (node->pn_arity) {
358 case PN_BINARY:
359 instrument_expression(node->pn_left, f);
360 Stream_printf(f, " %s ", get_op(node->pn_op));
361 instrument_expression(node->pn_right, f);
362 break;
363 case PN_LIST:
364 for (struct JSParseNode * p = node->pn_head; p != NULL; p = p->pn_next) {
365 if (p != node->pn_head) {
366 Stream_printf(f, " %s ", get_op(node->pn_op));
367 }
368 instrument_expression(p, f);
369 }
370 break;
371 default:
372 abort();
373 }
374 break;
375 case TOK_UNARYOP:
376 switch (node->pn_op) {
377 case JSOP_NEG:
378 Stream_write_char(f, '-');
379 instrument_expression(node->pn_kid, f);
380 break;
381 case JSOP_POS:
382 Stream_write_char(f, '+');
383 instrument_expression(node->pn_kid, f);
384 break;
385 case JSOP_NOT:
386 Stream_write_char(f, '!');
387 instrument_expression(node->pn_kid, f);
388 break;
389 case JSOP_BITNOT:
390 Stream_write_char(f, '~');
391 instrument_expression(node->pn_kid, f);
392 break;
393 case JSOP_TYPEOF:
394 Stream_write_string(f, "typeof ");
395 instrument_expression(node->pn_kid, f);
396 break;
397 case JSOP_VOID:
398 Stream_write_string(f, "void ");
399 instrument_expression(node->pn_kid, f);
400 break;
401 default:
402 abort();
403 break;
404 }
405 break;
406 case TOK_INC:
407 case TOK_DEC:
408 /*
409 This is not documented, but node->pn_op tells whether it is pre- or post-increment.
410 */
411 switch (node->pn_op) {
412 case JSOP_INCNAME:
413 case JSOP_INCPROP:
414 case JSOP_INCELEM:
415 Stream_write_string(f, "++");
416 instrument_expression(node->pn_kid, f);
417 break;
418 case JSOP_DECNAME:
419 case JSOP_DECPROP:
420 case JSOP_DECELEM:
421 Stream_write_string(f, "--");
422 instrument_expression(node->pn_kid, f);
423 break;
424 case JSOP_NAMEINC:
425 case JSOP_PROPINC:
426 case JSOP_ELEMINC:
427 instrument_expression(node->pn_kid, f);
428 Stream_write_string(f, "++");
429 break;
430 case JSOP_NAMEDEC:
431 case JSOP_PROPDEC:
432 case JSOP_ELEMDEC:
433 instrument_expression(node->pn_kid, f);
434 Stream_write_string(f, "--");
435 break;
436 default:
437 abort();
438 break;
439 }
440 break;
441 case TOK_NEW:
442 Stream_write_string(f, "new ");
443 instrument_function_call(node, f);
444 break;
445 case TOK_DELETE:
446 Stream_write_string(f, "delete ");
447 instrument_expression(node->pn_kid, f);
448 break;
449 case TOK_DOT:
450 /*
451 This may have originally been x['foo-bar']. Because the string 'foo-bar'
452 contains illegal characters, we have to use the subscript syntax instead of
453 the dot syntax.
454 */
455 instrument_expression(node->pn_expr, f);
456 assert(ATOM_IS_STRING(node->pn_atom));
457 {
458 JSString * s = ATOM_TO_STRING(node->pn_atom);
459 /* XXX - semantics changed in 1.7 */
460 if (! ATOM_KEYWORD(node->pn_atom) && js_IsIdentifier(s)) {
461 Stream_write_char(f, '.');
462 print_string_atom(node->pn_atom, f);
463 }
464 else {
465 Stream_write_char(f, '[');
466 print_quoted_string_atom(node->pn_atom, f);
467 Stream_write_char(f, ']');
468 }
469 }
470 break;
471 case TOK_LB:
472 instrument_expression(node->pn_left, f);
473 Stream_write_char(f, '[');
474 instrument_expression(node->pn_right, f);
475 Stream_write_char(f, ']');
476 break;
477 case TOK_LP:
478 instrument_function_call(node, f);
479 break;
480 case TOK_RB:
481 Stream_write_char(f, '[');
482 for (struct JSParseNode * p = node->pn_head; p != NULL; p = p->pn_next) {
483 if (p != node->pn_head) {
484 Stream_write_string(f, ", ");
485 }
486 /* TOK_COMMA is a special case: a hole in the array */
487 if (p->pn_type != TOK_COMMA) {
488 instrument_expression(p, f);
489 }
490 }
491 if (node->pn_extra == PNX_ENDCOMMA) {
492 Stream_write_char(f, ',');
493 }
494 Stream_write_char(f, ']');
495 break;
496 case TOK_RC:
497 Stream_write_char(f, '{');
498 for (struct JSParseNode * p = node->pn_head; p != NULL; p = p->pn_next) {
499 assert(p->pn_type == TOK_COLON);
500 if (p != node->pn_head) {
501 Stream_write_string(f, ", ");
502 }
503
504 /* check whether this is a getter or setter */
505 switch (p->pn_op) {
506 case JSOP_GETTER:
507 Stream_write_string(f, "get ");
508 instrument_expression(p->pn_left, f);
509 instrument_function(p->pn_right, f, 0, FUNCTION_GETTER_OR_SETTER);
510 break;
511 case JSOP_SETTER:
512 Stream_write_string(f, "set ");
513 instrument_expression(p->pn_left, f);
514 instrument_function(p->pn_right, f, 0, FUNCTION_GETTER_OR_SETTER);
515 break;
516 default:
517 instrument_expression(p->pn_left, f);
518 Stream_write_string(f, ": ");
519 instrument_expression(p->pn_right, f);
520 break;
521 }
522 }
523 Stream_write_char(f, '}');
524 break;
525 case TOK_RP:
526 Stream_write_char(f, '(');
527 instrument_expression(node->pn_kid, f);
528 Stream_write_char(f, ')');
529 break;
530 case TOK_NAME:
531 print_string_atom(node->pn_atom, f);
532 break;
533 case TOK_STRING:
534 print_quoted_string_atom(node->pn_atom, f);
535 break;
536 case TOK_OBJECT:
537 switch (node->pn_op) {
538 case JSOP_OBJECT:
539 /* I assume this is JSOP_REGEXP */
540 abort();
541 break;
542 case JSOP_REGEXP:
543 assert(ATOM_IS_OBJECT(node->pn_atom));
544 {
545 JSObject * object = ATOM_TO_OBJECT(node->pn_atom);
546 jsval result;
547 js_regexp_toString(context, object, 0, NULL, &result);
548 print_regex(result, f);
549 }
550 break;
551 default:
552 abort();
553 break;
554 }
555 break;
556 case TOK_NUMBER:
557 /*
558 A 64-bit IEEE 754 floating point number has a 52-bit fraction.
559 2^(-52) = 2.22 x 10^(-16)
560 Thus there are 16 significant digits.
561 To keep the output simple, special-case zero.
562 */
563 if (node->pn_dval == 0.0) {
564 Stream_write_string(f, "0");
565 }
566 else {
567 Stream_printf(f, "%.15g", node->pn_dval);
568 }
569 break;
570 case TOK_PRIMARY:
571 switch (node->pn_op) {
572 case JSOP_TRUE:
573 Stream_write_string(f, "true");
574 break;
575 case JSOP_FALSE:
576 Stream_write_string(f, "false");
577 break;
578 case JSOP_NULL:
579 Stream_write_string(f, "null");
580 break;
581 case JSOP_THIS:
582 Stream_write_string(f, "this");
583 break;
584 /* jsscan.h mentions `super' ??? */
585 default:
586 abort();
587 }
588 break;
589 case TOK_INSTANCEOF:
590 instrument_expression(node->pn_left, f);
591 Stream_write_string(f, " instanceof ");
592 instrument_expression(node->pn_right, f);
593 break;
594 case TOK_IN:
595 instrument_expression(node->pn_left, f);
596 Stream_write_string(f, " in ");
597 instrument_expression(node->pn_right, f);
598 break;
599 default:
600 fatal("unsupported node type in file %s: %d", file_id, node->pn_type);
601 }
602 }
603
604 static void instrument_var_statement(JSParseNode * node, Stream * f, int indent) {
605 assert(node->pn_arity == PN_LIST);
606 Stream_printf(f, "%*s", indent, "");
607 Stream_write_string(f, "var ");
608 for (struct JSParseNode * p = node->pn_u.list.head; p != NULL; p = p->pn_next) {
609 assert(p->pn_type == TOK_NAME);
610 assert(p->pn_arity == PN_NAME);
611 if (p != node->pn_head) {
612 Stream_write_string(f, ", ");
613 }
614 print_string_atom(p->pn_atom, f);
615 if (p->pn_expr != NULL) {
616 Stream_write_string(f, " = ");
617 instrument_expression(p->pn_expr, f);
618 }
619 }
620 }
621
622 static void output_statement(JSParseNode * node, Stream * f, int indent) {
623 switch (node->pn_type) {
624 case TOK_FUNCTION:
625 instrument_function(node, f, indent, FUNCTION_NORMAL);
626 break;
627 case TOK_LC:
628 assert(node->pn_arity == PN_LIST);
629 /*
630 Stream_write_string(f, "{\n");
631 */
632 for (struct JSParseNode * p = node->pn_u.list.head; p != NULL; p = p->pn_next) {
633 instrument_statement(p, f, indent);
634 }
635 /*
636 Stream_printf(f, "%*s", indent, "");
637 Stream_write_string(f, "}\n");
638 */
639 break;
640 case TOK_IF:
641 assert(node->pn_arity == PN_TERNARY);
642 Stream_printf(f, "%*s", indent, "");
643 Stream_write_string(f, "if (");
644 instrument_expression(node->pn_kid1, f);
645 Stream_write_string(f, ") {\n");
646 instrument_statement(node->pn_kid2, f, indent + 2);
647 Stream_printf(f, "%*s", indent, "");
648 Stream_write_string(f, "}\n");
649 if (node->pn_kid3) {
650 Stream_printf(f, "%*s", indent, "");
651 Stream_write_string(f, "else {\n");
652 instrument_statement(node->pn_kid3, f, indent + 2);
653 Stream_printf(f, "%*s", indent, "");
654 Stream_write_string(f, "}\n");
655 }
656 break;
657 case TOK_SWITCH:
658 assert(node->pn_arity == PN_BINARY);
659 Stream_printf(f, "%*s", indent, "");
660 Stream_write_string(f, "switch (");
661 instrument_expression(node->pn_left, f);
662 Stream_write_string(f, ") {\n");
663 for (struct JSParseNode * p = node->pn_right->pn_head; p != NULL; p = p->pn_next) {
664 Stream_printf(f, "%*s", indent, "");
665 switch (p->pn_type) {
666 case TOK_CASE:
667 Stream_write_string(f, "case ");
668 instrument_expression(p->pn_left, f);
669 Stream_write_string(f, ":\n");
670 break;
671 case TOK_DEFAULT:
672 Stream_write_string(f, "default:\n");
673 break;
674 default:
675 abort();
676 break;
677 }
678 instrument_statement(p->pn_right, f, indent + 2);
679 }
680 Stream_printf(f, "%*s", indent, "");
681 Stream_write_string(f, "}\n");
682 break;
683 case TOK_CASE:
684 case TOK_DEFAULT:
685 abort();
686 break;
687 case TOK_WHILE:
688 assert(node->pn_arity == PN_BINARY);
689 Stream_printf(f, "%*s", indent, "");
690 Stream_write_string(f, "while (");
691 instrument_expression(node->pn_left, f);
692 Stream_write_string(f, ") {\n");
693 instrument_statement(node->pn_right, f, indent + 2);
694 Stream_write_string(f, "}\n");
695 break;
696 case TOK_DO:
697 assert(node->pn_arity == PN_BINARY);
698 Stream_printf(f, "%*s", indent, "");
699 Stream_write_string(f, "do {\n");
700 instrument_statement(node->pn_left, f, indent + 2);
701 Stream_write_string(f, "}\n");
702 Stream_printf(f, "%*s", indent, "");
703 Stream_write_string(f, "while (");
704 instrument_expression(node->pn_right, f);
705 Stream_write_string(f, ");\n");
706 break;
707 case TOK_FOR:
708 assert(node->pn_arity == PN_BINARY);
709 Stream_printf(f, "%*s", indent, "");
710 Stream_write_string(f, "for (");
711 switch (node->pn_left->pn_type) {
712 case TOK_IN:
713 /* for/in */
714 assert(node->pn_left->pn_arity == PN_BINARY);
715 switch (node->pn_left->pn_left->pn_type) {
716 case TOK_VAR:
717 instrument_var_statement(node->pn_left->pn_left, f, 0);
718 break;
719 case TOK_NAME:
720 instrument_expression(node->pn_left->pn_left, f);
721 break;
722 default:
723 /* this is undocumented: for (x.value in y) */
724 instrument_expression(node->pn_left->pn_left, f);
725 break;
726 /*
727 default:
728 fprintf(stderr, "unexpected node type: %d\n", node->pn_left->pn_left->pn_type);
729 abort();
730 break;
731 */
732 }
733 Stream_write_string(f, " in ");
734 instrument_expression(node->pn_left->pn_right, f);
735 break;
736 case TOK_RESERVED:
737 /* for (;;) */
738 assert(node->pn_left->pn_arity == PN_TERNARY);
739 if (node->pn_left->pn_kid1) {
740 if (node->pn_left->pn_kid1->pn_type == TOK_VAR) {
741 instrument_var_statement(node->pn_left->pn_kid1, f, 0);
742 }
743 else {
744 instrument_expression(node->pn_left->pn_kid1, f);
745 }
746 }
747 Stream_write_string(f, ";");
748 if (node->pn_left->pn_kid2) {
749 Stream_write_char(f, ' ');
750 instrument_expression(node->pn_left->pn_kid2, f);
751 }
752 Stream_write_string(f, ";");
753 if (node->pn_left->pn_kid3) {
754 Stream_write_char(f, ' ');
755 instrument_expression(node->pn_left->pn_kid3, f);
756 }
757 break;
758 default:
759 abort();
760 break;
761 }
762 Stream_write_string(f, ") {\n");
763 instrument_statement(node->pn_right, f, indent + 2);
764 Stream_write_string(f, "}\n");
765 break;
766 case TOK_THROW:
767 assert(node->pn_arity == PN_UNARY);
768 Stream_printf(f, "%*s", indent, "");
769 Stream_write_string(f, "throw ");
770 instrument_expression(node->pn_u.unary.kid, f);
771 Stream_write_string(f, ";\n");
772 break;
773 case TOK_TRY:
774 Stream_printf(f, "%*s", indent, "");
775 Stream_write_string(f, "try {\n");
776 instrument_statement(node->pn_kid1, f, indent + 2);
777 Stream_printf(f, "%*s", indent, "");
778 Stream_write_string(f, "}\n");
779 {
780 for (JSParseNode * catch = node->pn_kid2; catch != NULL; catch = catch->pn_kid2) {
781 assert(catch->pn_type == TOK_CATCH);
782 Stream_printf(f, "%*s", indent, "");
783 Stream_write_string(f, "catch (");
784 assert(catch->pn_kid1->pn_arity == PN_NAME);
785 print_string_atom(catch->pn_kid1->pn_atom, f);
786 if (catch->pn_kid1->pn_expr) {
787 Stream_write_string(f, " if ");
788 instrument_expression(catch->pn_kid1->pn_expr, f);
789 }
790 Stream_write_string(f, ") {\n");
791 instrument_statement(catch->pn_kid3, f, indent + 2);
792 Stream_printf(f, "%*s", indent, "");
793 Stream_write_string(f, "}\n");
794 }
795 }
796 if (node->pn_kid3) {
797 Stream_printf(f, "%*s", indent, "");
798 Stream_write_string(f, "finally {\n");
799 instrument_statement(node->pn_kid3, f, indent + 2);
800 Stream_printf(f, "%*s", indent, "");
801 Stream_write_string(f, "}\n");
802 }
803 break;
804 case TOK_CATCH:
805 abort();
806 break;
807 case TOK_BREAK:
808 case TOK_CONTINUE:
809 assert(node->pn_arity == PN_NAME || node->pn_arity == PN_NULLARY);
810 Stream_printf(f, "%*s", indent, "");
811 Stream_write_string(f, node->pn_type == TOK_BREAK? "break": "continue");
812 JSAtom * atom = node->pn_u.name.atom;
813 if (atom != NULL) {
814 Stream_write_char(f, ' ');
815 print_string_atom(node->pn_atom, f);
816 }
817 Stream_write_string(f, ";\n");
818 break;
819 case TOK_WITH:
820 assert(node->pn_arity == PN_BINARY);
821 Stream_printf(f, "%*s", indent, "");
822 Stream_write_string(f, "with (");
823 instrument_expression(node->pn_left, f);
824 Stream_write_string(f, ") {\n");
825 instrument_statement(node->pn_right, f, indent + 2);
826 Stream_printf(f, "%*s", indent, "");
827 Stream_write_string(f, "}\n");
828 break;
829 case TOK_VAR:
830 instrument_var_statement(node, f, indent);
831 Stream_write_string(f, ";\n");
832 break;
833 case TOK_RETURN:
834 assert(node->pn_arity == PN_UNARY);
835 Stream_printf(f, "%*s", indent, "");
836 Stream_write_string(f, "return");
837 if (node->pn_kid != NULL) {
838 Stream_write_char(f, ' ');
839 instrument_expression(node->pn_kid, f);
840 }
841 Stream_write_string(f, ";\n");
842 break;
843 case TOK_SEMI:
844 assert(node->pn_arity == PN_UNARY);
845 Stream_printf(f, "%*s", indent, "");
846 if (node->pn_kid != NULL) {
847 instrument_expression(node->pn_kid, f);
848 }
849 Stream_write_string(f, ";\n");
850 break;
851 case TOK_COLON:
852 assert(node->pn_arity == PN_NAME);
853 /*
854 This one is tricky: can't output instrumentation between the label and the
855 statement it's supposed to label ...
856 */
857 Stream_printf(f, "%*s", indent < 2? 0: indent - 2, "");
858 print_string_atom(node->pn_atom, f);
859 Stream_write_string(f, ":\n");
860 /*
861 ... use output_statement instead of instrument_statement.
862 */
863 output_statement(node->pn_expr, f, indent);
864 break;
865 default:
866 fatal("unsupported node type in file %s: %d", file_id, node->pn_type);
867 }
868 }
869
870 /*
871 See <Statements> in jsparse.h.
872 TOK_FUNCTION is handled as a statement and as an expression.
873 TOK_EXPORT, TOK_IMPORT are not handled.
874 */
875 static void instrument_statement(JSParseNode * node, Stream * f, int indent) {
876 if (node->pn_type != TOK_LC) {
877 int line = node->pn_pos.begin.lineno;
878 /* the root node has line number 0 */
879 if (line != 0) {
880 Stream_printf(f, "%*s", indent, "");
881 Stream_printf(f, "_$jscoverage['%s'][%d]++;\n", file_id, line);
882 lines[line - 1] = 1;
883 }
884 }
885 output_statement(node, f, indent);
886 }
887
888 static bool characters_start_with(const jschar * characters, size_t line_start, size_t line_end, const char * prefix) {
889 const jschar * characters_end = characters + line_end;
890 const jschar * cp = characters + line_start;
891 const char * bp = prefix;
892 for (;;) {
893 if (*bp == '\0') {
894 return true;
895 }
896 else if (cp == characters_end) {
897 return false;
898 }
899 else if (*cp != *bp) {
900 return false;
901 }
902 bp++;
903 cp++;
904 }
905 }
906
907 void jscoverage_instrument_js(const char * id, const uint16_t * characters, size_t num_characters, Stream * output) {
908 file_id = id;
909
910 /* scan the javascript */
911 JSTokenStream * token_stream = js_NewTokenStream(context, characters, num_characters, NULL, 1, NULL);
912 if (token_stream == NULL) {
913 fatal("cannot create token stream from file: %s", file_id);
914 }
915
916 /* parse the javascript */
917 JSParseNode * node = js_ParseTokenStream(context, global, token_stream);
918 if (node == NULL) {
919 fatal("parse error in file: %s", file_id);
920 }
921 int num_lines = node->pn_pos.end.lineno;
922 lines = xmalloc(num_lines);
923 for (int i = 0; i < num_lines; i++) {
924 lines[i] = 0;
925 }
926
927 /*
928 An instrumented JavaScript file has 3 sections:
929 1. initialization
930 2. instrumented source code
931 3. original source code
932 */
933
934 Stream * instrumented = Stream_new(0);
935 instrument_statement(node, instrumented, 0);
936
937 /* write line number info to the output */
938 Stream_write_string(output, "/* automatically generated by JSCoverage - do not edit */\n");
939 Stream_write_string(output, "if (! top._$jscoverage) {\n top._$jscoverage = {};\n}\n");
940 Stream_write_string(output, "var _$jscoverage = top._$jscoverage;\n");
941 Stream_printf(output, "if (! _$jscoverage['%s']) {\n", file_id);
942 Stream_printf(output, " _$jscoverage['%s'] = [];\n", file_id);
943 for (int i = 0; i < num_lines; i++) {
944 if (lines[i]) {
945 Stream_printf(output, " _$jscoverage['%s'][%d] = 0;\n", file_id, i + 1);
946 }
947 }
948 Stream_write_string(output, "}\n");
949 free(lines);
950 lines = NULL;
951
952 /* copy the instrumented source code to the output */
953 Stream_write(output, instrumented->data, instrumented->length);
954
955 /* conditionals */
956 bool has_conditionals = false;
957 size_t line_number = 0;
958 size_t i = 0;
959 while (i < num_characters) {
960 line_number++;
961 size_t line_start = i;
962 jschar c;
963 bool done = false;
964 while (! done && i < num_characters) {
965 c = characters[i];
966 switch (c) {
967 case '\r':
968 case '\n':
969 case 0x2028:
970 case 0x2029:
971 done = true;
972 break;
973 default:
974 i++;
975 break;
976 }
977 }
978 size_t line_end = i;
979 if (i < num_characters) {
980 i++;
981 if (c == '\r' && i < num_characters && characters[i] == '\n') {
982 i++;
983 }
984 }
985 if (characters_start_with(characters, line_start, line_end, "//#JSCOVERAGE_IF")) {
986 if (! has_conditionals) {
987 has_conditionals = true;
988 Stream_printf(output, "_$jscoverage['%s'].conditionals = [];\n", file_id);
989 }
990 Stream_write_string(output, "if (!(");
991 for (size_t j = line_start + 16; j < line_end; j++) {
992 jschar c = characters[j];
993 if (c == '\t' || (32 <= c && c <= 126)) {
994 Stream_write_char(output, c);
995 }
996 else {
997 Stream_printf(output, "\\u%04x", c);
998 }
999 }
1000 Stream_write_string(output, ")) {\n");
1001 Stream_printf(output, " _$jscoverage['%s'].conditionals[%d] = ", file_id, line_number);
1002 }
1003 else if (characters_start_with(characters, line_start, line_end, "//#JSCOVERAGE_ENDIF")) {
1004 Stream_printf(output, "%d;\n", line_number);
1005 Stream_printf(output, "}\n");
1006 }
1007 }
1008
1009 /* copy the original source to the output */
1010 Stream_printf(output, "_$jscoverage['%s'].source = ", file_id);
1011 jscoverage_write_source(id, characters, num_characters, output);
1012 Stream_printf(output, ";\n");
1013
1014 Stream_delete(instrumented);
1015
1016 file_id = NULL;
1017 }
1018
1019 void jscoverage_write_source(const char * id, const jschar * characters, size_t num_characters, Stream * output) {
1020 Stream_write_string(output, "[");
1021 if (jscoverage_highlight) {
1022 Stream * highlighted_stream = Stream_new(num_characters);
1023 jscoverage_highlight_js(context, id, characters, num_characters, highlighted_stream);
1024 size_t i = 0;
1025 while (i < highlighted_stream->length) {
1026 if (i > 0) {
1027 Stream_write_char(output, ',');
1028 }
1029
1030 Stream_write_char(output, '"');
1031 bool done = false;
1032 while (! done) {
1033 char c = highlighted_stream->data[i];
1034 switch (c) {
1035 case 0x8:
1036 /* backspace */
1037 Stream_write_string(output, "\\b");
1038 break;
1039 case 0x9:
1040 /* horizontal tab */
1041 Stream_write_string(output, "\\t");
1042 break;
1043 case 0xa:
1044 /* line feed (new line) */
1045 done = true;
1046 break;
1047 case 0xb:
1048 /* vertical tab */
1049 Stream_write_string(output, "\\v");
1050 break;
1051 case 0xc:
1052 /* form feed */
1053 Stream_write_string(output, "\\f");
1054 break;
1055 case 0xd:
1056 /* carriage return */
1057 done = true;
1058 if (i + 1 < highlighted_stream->length && highlighted_stream->data[i + 1] == '\n') {
1059 i++;
1060 }
1061 break;
1062 case '"':
1063 Stream_write_string(output, "\\\"");
1064 break;
1065 case '\\':
1066 Stream_write_string(output, "\\\\");
1067 break;
1068 default:
1069 Stream_write_char(output, c);
1070 break;
1071 }
1072 i++;
1073 if (i >= highlighted_stream->length) {
1074 done = true;
1075 }
1076 }
1077 Stream_write_char(output, '"');
1078 }
1079 Stream_delete(highlighted_stream);
1080 }
1081 else {
1082 size_t i = 0;
1083 while (i < num_characters) {
1084 if (i > 0) {
1085 Stream_write_char(output, ',');
1086 }
1087
1088 Stream_write_char(output, '"');
1089 bool done = false;
1090 while (! done) {
1091 jschar c = characters[i];
1092 switch (c) {
1093 case 0x8:
1094 /* backspace */
1095 Stream_write_string(output, "\\b");
1096 break;
1097 case 0x9:
1098 /* horizontal tab */
1099 Stream_write_string(output, "\\t");
1100 break;
1101 case 0xa:
1102 /* line feed (new line) */
1103 done = true;
1104 break;
1105 case 0xb:
1106 /* vertical tab */
1107 Stream_write_string(output, "\\v");
1108 break;
1109 case 0xc:
1110 /* form feed */
1111 Stream_write_string(output, "\\f");
1112 break;
1113 case 0xd:
1114 /* carriage return */
1115 done = true;
1116 if (i + 1 < num_characters && characters[i + 1] == '\n') {
1117 i++;
1118 }
1119 break;
1120 case '"':
1121 Stream_write_string(output, "\\\"");
1122 break;
1123 case '\\':
1124 Stream_write_string(output, "\\\\");
1125 break;
1126 case '&':
1127 Stream_write_string(output, "&amp;");
1128 break;
1129 case '<':
1130 Stream_write_string(output, "&lt;");
1131 break;
1132 case '>':
1133 Stream_write_string(output, "&gt;");
1134 break;
1135 case 0x2028:
1136 case 0x2029:
1137 done = true;
1138 break;
1139 default:
1140 if (32 <= c && c <= 126) {
1141 Stream_write_char(output, c);
1142 }
1143 else {
1144 Stream_printf(output, "&#%d;", c);
1145 }
1146 break;
1147 }
1148 i++;
1149 if (i >= num_characters) {
1150 done = true;
1151 }
1152 }
1153 Stream_write_char(output, '"');
1154 }
1155 }
1156 Stream_write_string(output, "]");
1157 }
1158
1159 void jscoverage_copy_resources(const char * destination_directory) {
1160 copy_resource("jscoverage.html", destination_directory);
1161 copy_resource("jscoverage.css", destination_directory);
1162 copy_resource("jscoverage.js", destination_directory);
1163 copy_resource("jscoverage-ie.css", destination_directory);
1164 copy_resource("jscoverage-throbber.gif", destination_directory);
1165 copy_resource("jscoverage-highlight.css", destination_directory);
1166 }
1167
1168 /*
1169 coverage reports
1170 */
1171
1172 struct FileCoverageList {
1173 FileCoverage * file_coverage;
1174 struct FileCoverageList * next;
1175 };
1176
1177 struct Coverage {
1178 JSHashTable * coverage_table;
1179 struct FileCoverageList * coverage_list;
1180 };
1181
1182 static int compare_strings(const void * p1, const void * p2) {
1183 return strcmp(p1, p2) == 0;
1184 }
1185
1186 Coverage * Coverage_new(void) {
1187 Coverage * result = xmalloc(sizeof(Coverage));
1188 result->coverage_table = JS_NewHashTable(1024, JS_HashString, compare_strings, NULL, NULL, NULL);
1189 if (result->coverage_table == NULL) {
1190 fatal("cannot create hash table");
1191 }
1192 result->coverage_list = NULL;
1193 return result;
1194 }
1195
1196 void Coverage_delete(Coverage * coverage) {
1197 JS_HashTableDestroy(coverage->coverage_table);
1198 struct FileCoverageList * p = coverage->coverage_list;
1199 while (p != NULL) {
1200 free(p->file_coverage->coverage_lines);
1201 if (p->file_coverage->source_lines != NULL) {
1202 for (uint32 i = 0; i < p->file_coverage->num_source_lines; i++) {
1203 free(p->file_coverage->source_lines[i]);
1204 }
1205 free(p->file_coverage->source_lines);
1206 }
1207 free(p->file_coverage->id);
1208 free(p->file_coverage);
1209 struct FileCoverageList * q = p;
1210 p = p->next;
1211 free(q);
1212 }
1213 free(coverage);
1214 }
1215
1216 struct EnumeratorArg {
1217 CoverageForeachFunction f;
1218 void * p;
1219 };
1220
1221 static intN enumerator(JSHashEntry * entry, intN i, void * arg) {
1222 struct EnumeratorArg * enumerator_arg = arg;
1223 enumerator_arg->f(entry->value, i, enumerator_arg->p);
1224 return 0;
1225 }
1226
1227 void Coverage_foreach_file(Coverage * coverage, CoverageForeachFunction f, void * p) {
1228 struct EnumeratorArg enumerator_arg;
1229 enumerator_arg.f = f;
1230 enumerator_arg.p = p;
1231 JS_HashTableEnumerateEntries(coverage->coverage_table, enumerator, &enumerator_arg);
1232 }
1233
1234 int jscoverage_parse_json(Coverage * coverage, const uint8_t * json, size_t length) {
1235 jschar * base = js_InflateString(context, (char *) json, &length);
1236 if (base == NULL) {
1237 fatal("out of memory");
1238 }
1239
1240 jschar * parenthesized_json = xnew(jschar, addst(length, 2));
1241 parenthesized_json[0] = '(';
1242 memcpy(parenthesized_json + 1, base, mulst(length, sizeof(jschar)));
1243 parenthesized_json[length + 1] = ')';
1244
1245 JS_free(context, base);
1246
1247 JSTokenStream * token_stream = js_NewTokenStream(context, parenthesized_json, length + 2, NULL, 1, NULL);
1248 if (token_stream == NULL) {
1249 fatal("cannot create token stream");
1250 }
1251
1252 JSParseNode * root = js_ParseTokenStream(context, global, token_stream);
1253 free(parenthesized_json);
1254 if (root == NULL) {
1255 return -1;
1256 }
1257
1258 /* root node must be TOK_LC */
1259 if (root->pn_type != TOK_LC) {
1260 return -1;
1261 }
1262 JSParseNode * semi = root->pn_u.list.head;
1263
1264 /* the list must be TOK_SEMI and it must contain only one element */
1265 if (semi->pn_type != TOK_SEMI || semi->pn_next != NULL) {
1266 return -1;
1267 }
1268 JSParseNode * parenthesized = semi->pn_kid;
1269
1270 /* this must be a parenthesized expression */
1271 if (parenthesized->pn_type != TOK_RP) {
1272 return -1;
1273 }
1274 JSParseNode * object = parenthesized->pn_kid;
1275
1276 /* this must be an object literal */
1277 if (object->pn_type != TOK_RC) {
1278 return -1;
1279 }
1280
1281 for (JSParseNode * p = object->pn_head; p != NULL; p = p->pn_next) {
1282 /* every element of this list must be TOK_COLON */
1283 if (p->pn_type != TOK_COLON) {
1284 return -1;
1285 }
1286
1287 /* the key must be a string representing the file */
1288 JSParseNode * key = p->pn_left;
1289 if (key->pn_type != TOK_STRING || ! ATOM_IS_STRING(key->pn_atom)) {
1290 return -1;
1291 }
1292 char * id_bytes = JS_GetStringBytes(ATOM_TO_STRING(key->pn_atom));
1293
1294 /* the value must be an object literal OR an array */
1295 JSParseNode * value = p->pn_right;
1296 if (! (value->pn_type == TOK_RC || value->pn_type == TOK_RB)) {
1297 return -1;
1298 }
1299
1300 JSParseNode * array = NULL;
1301 JSParseNode * source = NULL;
1302 if (value->pn_type == TOK_RB) {
1303 /* an array */
1304 array = value;
1305 }
1306 else if (value->pn_type == TOK_RC) {
1307 /* an object literal */
1308 if (value->pn_count != 2) {
1309 return -1;
1310 }
1311 for (JSParseNode * element = value->pn_head; element != NULL; element = element->pn_next) {
1312 if (element->pn_type != TOK_COLON) {
1313 return -1;
1314 }
1315 JSParseNode * left = element->pn_left;
1316 if (left->pn_type != TOK_STRING || ! ATOM_IS_STRING(left->pn_atom)) {
1317 return -1;
1318 }
1319 const char * s = JS_GetStringBytes(ATOM_TO_STRING(left->pn_atom));
1320 if (strcmp(s, "coverage") == 0) {
1321 array = element->pn_right;
1322 if (array->pn_type != TOK_RB) {
1323 return -1;
1324 }
1325 }
1326 else if (strcmp(s, "source") == 0) {
1327 source = element->pn_right;
1328 if (source->pn_type != TOK_RB) {
1329 return -1;
1330 }
1331 }
1332 else {
1333 return -1;
1334 }
1335 }
1336 }
1337 else {
1338 return -1;
1339 }
1340
1341 if (array == NULL) {
1342 return -1;
1343 }
1344
1345 /* look up the file in the coverage table */
1346 FileCoverage * file_coverage = JS_HashTableLookup(coverage->coverage_table, id_bytes);
1347 if (file_coverage == NULL) {
1348 /* not there: create a new one */
1349 char * id = xstrdup(id_bytes);
1350 file_coverage = xmalloc(sizeof(FileCoverage));
1351 file_coverage->id = id;
1352 file_coverage->num_coverage_lines = array->pn_count;
1353 file_coverage->coverage_lines = xnew(int, array->pn_count);
1354 if (source == NULL) {
1355 file_coverage->source_lines = NULL;
1356 }
1357 else {
1358 file_coverage->num_source_lines = source->pn_count;
1359 file_coverage->source_lines = xnew(char *, source->pn_count);
1360 uint32 i = 0;
1361 for (JSParseNode * element = source->pn_head; element != NULL; element = element->pn_next, i++) {
1362 if (element->pn_type != TOK_STRING) {
1363 return -1;
1364 }
1365 file_coverage->source_lines[i] = xstrdup(JS_GetStringBytes(ATOM_TO_STRING(element->pn_atom)));
1366 }
1367 assert(i == source->pn_count);
1368 }
1369
1370 /* set coverage for all lines */
1371 uint32 i = 0;
1372 for (JSParseNode * element = array->pn_head; element != NULL; element = element->pn_next, i++) {
1373 if (element->pn_type == TOK_NUMBER) {
1374 file_coverage->coverage_lines[i] = (int) element->pn_dval;
1375 }
1376 else if (element->pn_type == TOK_PRIMARY && element->pn_op == JSOP_NULL) {
1377 file_coverage->coverage_lines[i] = -1;
1378 }
1379 else {
1380 return -1;
1381 }
1382 }
1383 assert(i == array->pn_count);
1384
1385 /* add to the hash table */
1386 JS_HashTableAdd(coverage->coverage_table, id, file_coverage);
1387 struct FileCoverageList * coverage_list = xmalloc(sizeof(struct FileCoverageList));
1388 coverage_list->file_coverage = file_coverage;
1389 coverage_list->next = coverage->coverage_list;
1390 coverage->coverage_list = coverage_list;
1391 }
1392 else {
1393 /* sanity check */
1394 assert(strcmp(file_coverage->id, id_bytes) == 0);
1395 if (file_coverage->num_coverage_lines != array->pn_count) {
1396 return -2;
1397 }
1398
1399 /* merge the coverage */
1400 uint32 i = 0;
1401 for (JSParseNode * element = array->pn_head; element != NULL; element = element->pn_next, i++) {
1402 if (element->pn_type == TOK_NUMBER) {
1403 if (file_coverage->coverage_lines[i] == -1) {
1404 return -2;
1405 }
1406 file_coverage->coverage_lines[i] += (int) element->pn_dval;
1407 }
1408 else if (element->pn_type == TOK_PRIMARY && element->pn_op == JSOP_NULL) {
1409 if (file_coverage->coverage_lines[i] != -1) {
1410 return -2;
1411 }
1412 }
1413 else {
1414 return -1;
1415 }
1416 }
1417 assert(i == array->pn_count);
1418
1419 /* if this JSON file has source, use it */
1420 if (file_coverage->source_lines == NULL && source != NULL) {
1421 file_coverage->num_source_lines = source->pn_count;
1422 file_coverage->source_lines = xnew(char *, source->pn_count);
1423 uint32 i = 0;
1424 for (JSParseNode * element = source->pn_head; element != NULL; element = element->pn_next, i++) {
1425 if (element->pn_type != TOK_STRING) {
1426 return -1;
1427 }
1428 file_coverage->source_lines[i] = xstrdup(JS_GetStringBytes(ATOM_TO_STRING(element->pn_atom)));
1429 }
1430 assert(i == source->pn_count);
1431 }
1432 }
1433 }
1434
1435 return 0;
1436 }

  ViewVC Help
Powered by ViewVC 1.1.24