toolkit/devtools/server/actors/call-watcher.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4 "use strict";
michael@0 5
michael@0 6 const {Cc, Ci, Cu, Cr} = require("chrome");
michael@0 7 const events = require("sdk/event/core");
michael@0 8 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
michael@0 9 const protocol = require("devtools/server/protocol");
michael@0 10 const {ContentObserver} = require("devtools/content-observer");
michael@0 11
michael@0 12 const {on, once, off, emit} = events;
michael@0 13 const {method, Arg, Option, RetVal} = protocol;
michael@0 14
michael@0 15 exports.register = function(handle) {
michael@0 16 handle.addTabActor(CallWatcherActor, "callWatcherActor");
michael@0 17 };
michael@0 18
michael@0 19 exports.unregister = function(handle) {
michael@0 20 handle.removeTabActor(CallWatcherActor);
michael@0 21 };
michael@0 22
michael@0 23 /**
michael@0 24 * Type describing a single function call in a stack trace.
michael@0 25 */
michael@0 26 protocol.types.addDictType("call-stack-item", {
michael@0 27 name: "string",
michael@0 28 file: "string",
michael@0 29 line: "number"
michael@0 30 });
michael@0 31
michael@0 32 /**
michael@0 33 * Type describing an overview of a function call.
michael@0 34 */
michael@0 35 protocol.types.addDictType("call-details", {
michael@0 36 type: "number",
michael@0 37 name: "string",
michael@0 38 stack: "array:call-stack-item"
michael@0 39 });
michael@0 40
michael@0 41 /**
michael@0 42 * This actor contains information about a function call, like the function
michael@0 43 * type, name, stack, arguments, returned value etc.
michael@0 44 */
michael@0 45 let FunctionCallActor = protocol.ActorClass({
michael@0 46 typeName: "function-call",
michael@0 47
michael@0 48 /**
michael@0 49 * Creates the function call actor.
michael@0 50 *
michael@0 51 * @param DebuggerServerConnection conn
michael@0 52 * The server connection.
michael@0 53 * @param DOMWindow window
michael@0 54 * The content window.
michael@0 55 * @param string global
michael@0 56 * The name of the global object owning this function, like
michael@0 57 * "CanvasRenderingContext2D" or "WebGLRenderingContext".
michael@0 58 * @param object caller
michael@0 59 * The object owning the function when it was called.
michael@0 60 * For example, in `foo.bar()`, the caller is `foo`.
michael@0 61 * @param number type
michael@0 62 * Either METHOD_FUNCTION, METHOD_GETTER or METHOD_SETTER.
michael@0 63 * @param string name
michael@0 64 * The called function's name.
michael@0 65 * @param array stack
michael@0 66 * The called function's stack, as a list of { name, file, line } objects.
michael@0 67 * @param array args
michael@0 68 * The called function's arguments.
michael@0 69 * @param any result
michael@0 70 * The value returned by the function call.
michael@0 71 */
michael@0 72 initialize: function(conn, [window, global, caller, type, name, stack, args, result]) {
michael@0 73 protocol.Actor.prototype.initialize.call(this, conn);
michael@0 74
michael@0 75 this.details = {
michael@0 76 window: window,
michael@0 77 caller: caller,
michael@0 78 type: type,
michael@0 79 name: name,
michael@0 80 stack: stack,
michael@0 81 args: args,
michael@0 82 result: result
michael@0 83 };
michael@0 84
michael@0 85 this.meta = {
michael@0 86 global: -1,
michael@0 87 previews: { caller: "", args: "" }
michael@0 88 };
michael@0 89
michael@0 90 if (global == "WebGLRenderingContext") {
michael@0 91 this.meta.global = CallWatcherFront.CANVAS_WEBGL_CONTEXT;
michael@0 92 } else if (global == "CanvasRenderingContext2D") {
michael@0 93 this.meta.global = CallWatcherFront.CANVAS_2D_CONTEXT;
michael@0 94 } else if (global == "window") {
michael@0 95 this.meta.global = CallWatcherFront.UNKNOWN_SCOPE;
michael@0 96 } else {
michael@0 97 this.meta.global = CallWatcherFront.GLOBAL_SCOPE;
michael@0 98 }
michael@0 99
michael@0 100 this.meta.previews.caller = this._generateCallerPreview();
michael@0 101 this.meta.previews.args = this._generateArgsPreview();
michael@0 102 },
michael@0 103
michael@0 104 /**
michael@0 105 * Customize the marshalling of this actor to provide some generic information
michael@0 106 * directly on the Front instance.
michael@0 107 */
michael@0 108 form: function() {
michael@0 109 return {
michael@0 110 actor: this.actorID,
michael@0 111 type: this.details.type,
michael@0 112 name: this.details.name,
michael@0 113 file: this.details.stack[0].file,
michael@0 114 line: this.details.stack[0].line,
michael@0 115 callerPreview: this.meta.previews.caller,
michael@0 116 argsPreview: this.meta.previews.args
michael@0 117 };
michael@0 118 },
michael@0 119
michael@0 120 /**
michael@0 121 * Gets more information about this function call, which is not necessarily
michael@0 122 * available on the Front instance.
michael@0 123 */
michael@0 124 getDetails: method(function() {
michael@0 125 let { type, name, stack } = this.details;
michael@0 126
michael@0 127 // Since not all calls on the stack have corresponding owner files (e.g.
michael@0 128 // callbacks of a requestAnimationFrame etc.), there's no benefit in
michael@0 129 // returning them, as the user can't jump to the Debugger from them.
michael@0 130 for (let i = stack.length - 1;;) {
michael@0 131 if (stack[i].file) {
michael@0 132 break;
michael@0 133 }
michael@0 134 stack.pop();
michael@0 135 i--;
michael@0 136 }
michael@0 137
michael@0 138 // XXX: Use grips for objects and serialize them properly, in order
michael@0 139 // to add the function's caller, arguments and return value. Bug 978957.
michael@0 140 return {
michael@0 141 type: type,
michael@0 142 name: name,
michael@0 143 stack: stack
michael@0 144 };
michael@0 145 }, {
michael@0 146 response: { info: RetVal("call-details") }
michael@0 147 }),
michael@0 148
michael@0 149 /**
michael@0 150 * Serializes the caller's name so that it can be easily be transferred
michael@0 151 * as a string, but still be useful when displayed in a potential UI.
michael@0 152 *
michael@0 153 * @return string
michael@0 154 * The caller's name as a string.
michael@0 155 */
michael@0 156 _generateCallerPreview: function() {
michael@0 157 let global = this.meta.global;
michael@0 158 if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
michael@0 159 return "gl";
michael@0 160 }
michael@0 161 if (global == CallWatcherFront.CANVAS_2D_CONTEXT) {
michael@0 162 return "ctx";
michael@0 163 }
michael@0 164 return "";
michael@0 165 },
michael@0 166
michael@0 167 /**
michael@0 168 * Serializes the arguments so that they can be easily be transferred
michael@0 169 * as a string, but still be useful when displayed in a potential UI.
michael@0 170 *
michael@0 171 * @return string
michael@0 172 * The arguments as a string.
michael@0 173 */
michael@0 174 _generateArgsPreview: function() {
michael@0 175 let { caller, args } = this.details;
michael@0 176 let { global } = this.meta;
michael@0 177
michael@0 178 // XXX: All of this sucks. Make this smarter, so that the frontend
michael@0 179 // can inspect each argument, be it object or primitive. Bug 978960.
michael@0 180 let serializeArgs = () => args.map(arg => {
michael@0 181 if (typeof arg == "undefined") {
michael@0 182 return "undefined";
michael@0 183 }
michael@0 184 if (typeof arg == "function") {
michael@0 185 return "Function";
michael@0 186 }
michael@0 187 if (typeof arg == "object") {
michael@0 188 return "Object";
michael@0 189 }
michael@0 190 if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
michael@0 191 // XXX: This doesn't handle combined bitmasks. Bug 978964.
michael@0 192 return getEnumsLookupTable("webgl", caller)[arg] || arg;
michael@0 193 }
michael@0 194 if (global == CallWatcherFront.CANVAS_2D_CONTEXT) {
michael@0 195 return getEnumsLookupTable("2d", caller)[arg] || arg;
michael@0 196 }
michael@0 197 return arg;
michael@0 198 });
michael@0 199
michael@0 200 return serializeArgs().join(", ");
michael@0 201 }
michael@0 202 });
michael@0 203
michael@0 204 /**
michael@0 205 * The corresponding Front object for the FunctionCallActor.
michael@0 206 */
michael@0 207 let FunctionCallFront = protocol.FrontClass(FunctionCallActor, {
michael@0 208 initialize: function(client, form) {
michael@0 209 protocol.Front.prototype.initialize.call(this, client, form);
michael@0 210 },
michael@0 211
michael@0 212 /**
michael@0 213 * Adds some generic information directly to this instance,
michael@0 214 * to avoid extra roundtrips.
michael@0 215 */
michael@0 216 form: function(form) {
michael@0 217 this.actorID = form.actor;
michael@0 218 this.type = form.type;
michael@0 219 this.name = form.name;
michael@0 220 this.file = form.file;
michael@0 221 this.line = form.line;
michael@0 222 this.callerPreview = form.callerPreview;
michael@0 223 this.argsPreview = form.argsPreview;
michael@0 224 }
michael@0 225 });
michael@0 226
michael@0 227 /**
michael@0 228 * This actor observes function calls on certain objects or globals.
michael@0 229 */
michael@0 230 let CallWatcherActor = exports.CallWatcherActor = protocol.ActorClass({
michael@0 231 typeName: "call-watcher",
michael@0 232 initialize: function(conn, tabActor) {
michael@0 233 protocol.Actor.prototype.initialize.call(this, conn);
michael@0 234 this.tabActor = tabActor;
michael@0 235 this._onGlobalCreated = this._onGlobalCreated.bind(this);
michael@0 236 this._onGlobalDestroyed = this._onGlobalDestroyed.bind(this);
michael@0 237 this._onContentFunctionCall = this._onContentFunctionCall.bind(this);
michael@0 238 },
michael@0 239 destroy: function(conn) {
michael@0 240 protocol.Actor.prototype.destroy.call(this, conn);
michael@0 241 this.finalize();
michael@0 242 },
michael@0 243
michael@0 244 /**
michael@0 245 * Starts waiting for the current tab actor's document global to be
michael@0 246 * created, in order to instrument the specified objects and become
michael@0 247 * aware of everything the content does with them.
michael@0 248 */
michael@0 249 setup: method(function({ tracedGlobals, tracedFunctions, startRecording, performReload }) {
michael@0 250 if (this._initialized) {
michael@0 251 return;
michael@0 252 }
michael@0 253 this._initialized = true;
michael@0 254
michael@0 255 this._functionCalls = [];
michael@0 256 this._tracedGlobals = tracedGlobals || [];
michael@0 257 this._tracedFunctions = tracedFunctions || [];
michael@0 258 this._contentObserver = new ContentObserver(this.tabActor);
michael@0 259
michael@0 260 on(this._contentObserver, "global-created", this._onGlobalCreated);
michael@0 261 on(this._contentObserver, "global-destroyed", this._onGlobalDestroyed);
michael@0 262
michael@0 263 if (startRecording) {
michael@0 264 this.resumeRecording();
michael@0 265 }
michael@0 266 if (performReload) {
michael@0 267 this.tabActor.window.location.reload();
michael@0 268 }
michael@0 269 }, {
michael@0 270 request: {
michael@0 271 tracedGlobals: Option(0, "nullable:array:string"),
michael@0 272 tracedFunctions: Option(0, "nullable:array:string"),
michael@0 273 startRecording: Option(0, "boolean"),
michael@0 274 performReload: Option(0, "boolean")
michael@0 275 },
michael@0 276 oneway: true
michael@0 277 }),
michael@0 278
michael@0 279 /**
michael@0 280 * Stops listening for document global changes and puts this actor
michael@0 281 * to hibernation. This method is called automatically just before the
michael@0 282 * actor is destroyed.
michael@0 283 */
michael@0 284 finalize: method(function() {
michael@0 285 if (!this._initialized) {
michael@0 286 return;
michael@0 287 }
michael@0 288 this._initialized = false;
michael@0 289
michael@0 290 this._contentObserver.stopListening();
michael@0 291 off(this._contentObserver, "global-created", this._onGlobalCreated);
michael@0 292 off(this._contentObserver, "global-destroyed", this._onGlobalDestroyed);
michael@0 293
michael@0 294 this._tracedGlobals = null;
michael@0 295 this._tracedFunctions = null;
michael@0 296 this._contentObserver = null;
michael@0 297 }, {
michael@0 298 oneway: true
michael@0 299 }),
michael@0 300
michael@0 301 /**
michael@0 302 * Returns whether the instrumented function calls are currently recorded.
michael@0 303 */
michael@0 304 isRecording: method(function() {
michael@0 305 return this._recording;
michael@0 306 }, {
michael@0 307 response: RetVal("boolean")
michael@0 308 }),
michael@0 309
michael@0 310 /**
michael@0 311 * Starts recording function calls.
michael@0 312 */
michael@0 313 resumeRecording: method(function() {
michael@0 314 this._recording = true;
michael@0 315 }),
michael@0 316
michael@0 317 /**
michael@0 318 * Stops recording function calls.
michael@0 319 */
michael@0 320 pauseRecording: method(function() {
michael@0 321 this._recording = false;
michael@0 322 return this._functionCalls;
michael@0 323 }, {
michael@0 324 response: { calls: RetVal("array:function-call") }
michael@0 325 }),
michael@0 326
michael@0 327 /**
michael@0 328 * Erases all the recorded function calls.
michael@0 329 * Calling `resumeRecording` or `pauseRecording` does not erase history.
michael@0 330 */
michael@0 331 eraseRecording: method(function() {
michael@0 332 this._functionCalls = [];
michael@0 333 }),
michael@0 334
michael@0 335 /**
michael@0 336 * Lightweight listener invoked whenever an instrumented function is called
michael@0 337 * while recording. We're doing this to avoid the event emitter overhead,
michael@0 338 * since this is expected to be a very hot function.
michael@0 339 */
michael@0 340 onCall: function() {},
michael@0 341
michael@0 342 /**
michael@0 343 * Invoked whenever the current tab actor's document global is created.
michael@0 344 */
michael@0 345 _onGlobalCreated: function(window) {
michael@0 346 let self = this;
michael@0 347
michael@0 348 this._tracedWindowId = ContentObserver.GetInnerWindowID(window);
michael@0 349 let unwrappedWindow = XPCNativeWrapper.unwrap(window);
michael@0 350 let callback = this._onContentFunctionCall;
michael@0 351
michael@0 352 for (let global of this._tracedGlobals) {
michael@0 353 let prototype = unwrappedWindow[global].prototype;
michael@0 354 let properties = Object.keys(prototype);
michael@0 355 properties.forEach(name => overrideSymbol(global, prototype, name, callback));
michael@0 356 }
michael@0 357
michael@0 358 for (let name of this._tracedFunctions) {
michael@0 359 overrideSymbol("window", unwrappedWindow, name, callback);
michael@0 360 }
michael@0 361
michael@0 362 /**
michael@0 363 * Instruments a method, getter or setter on the specified target object to
michael@0 364 * invoke a callback whenever it is called.
michael@0 365 */
michael@0 366 function overrideSymbol(global, target, name, callback) {
michael@0 367 let propertyDescriptor = Object.getOwnPropertyDescriptor(target, name);
michael@0 368
michael@0 369 if (propertyDescriptor.get || propertyDescriptor.set) {
michael@0 370 overrideAccessor(global, target, name, propertyDescriptor, callback);
michael@0 371 return;
michael@0 372 }
michael@0 373 if (propertyDescriptor.writable && typeof propertyDescriptor.value == "function") {
michael@0 374 overrideFunction(global, target, name, propertyDescriptor, callback);
michael@0 375 return;
michael@0 376 }
michael@0 377 }
michael@0 378
michael@0 379 /**
michael@0 380 * Instruments a function on the specified target object.
michael@0 381 */
michael@0 382 function overrideFunction(global, target, name, descriptor, callback) {
michael@0 383 let originalFunc = target[name];
michael@0 384
michael@0 385 Object.defineProperty(target, name, {
michael@0 386 value: function(...args) {
michael@0 387 let result = originalFunc.apply(this, args);
michael@0 388
michael@0 389 if (self._recording) {
michael@0 390 let stack = getStack(name);
michael@0 391 let type = CallWatcherFront.METHOD_FUNCTION;
michael@0 392 callback(unwrappedWindow, global, this, type, name, stack, args, result);
michael@0 393 }
michael@0 394 return result;
michael@0 395 },
michael@0 396 configurable: descriptor.configurable,
michael@0 397 enumerable: descriptor.enumerable,
michael@0 398 writable: true
michael@0 399 });
michael@0 400 }
michael@0 401
michael@0 402 /**
michael@0 403 * Instruments a getter or setter on the specified target object.
michael@0 404 */
michael@0 405 function overrideAccessor(global, target, name, descriptor, callback) {
michael@0 406 let originalGetter = target.__lookupGetter__(name);
michael@0 407 let originalSetter = target.__lookupSetter__(name);
michael@0 408
michael@0 409 Object.defineProperty(target, name, {
michael@0 410 get: function(...args) {
michael@0 411 if (!originalGetter) return undefined;
michael@0 412 let result = originalGetter.apply(this, args);
michael@0 413
michael@0 414 if (self._recording) {
michael@0 415 let stack = getStack(name);
michael@0 416 let type = CallWatcherFront.GETTER_FUNCTION;
michael@0 417 callback(unwrappedWindow, global, this, type, name, stack, args, result);
michael@0 418 }
michael@0 419 return result;
michael@0 420 },
michael@0 421 set: function(...args) {
michael@0 422 if (!originalSetter) return;
michael@0 423 originalSetter.apply(this, args);
michael@0 424
michael@0 425 if (self._recording) {
michael@0 426 let stack = getStack(name);
michael@0 427 let type = CallWatcherFront.SETTER_FUNCTION;
michael@0 428 callback(unwrappedWindow, global, this, type, name, stack, args, undefined);
michael@0 429 }
michael@0 430 },
michael@0 431 configurable: descriptor.configurable,
michael@0 432 enumerable: descriptor.enumerable
michael@0 433 });
michael@0 434 }
michael@0 435
michael@0 436 /**
michael@0 437 * Stores the relevant information about calls on the stack when
michael@0 438 * a function is called.
michael@0 439 */
michael@0 440 function getStack(caller) {
michael@0 441 try {
michael@0 442 // Using Components.stack wouldn't be a better idea, since it's
michael@0 443 // much slower because it attempts to retrieve the C++ stack as well.
michael@0 444 throw new Error();
michael@0 445 } catch (e) {
michael@0 446 var stack = e.stack;
michael@0 447 }
michael@0 448
michael@0 449 // Of course, using a simple regex like /(.*?)@(.*):(\d*):\d*/ would be
michael@0 450 // much prettier, but this is a very hot function, so let's sqeeze
michael@0 451 // every drop of performance out of it.
michael@0 452 let calls = [];
michael@0 453 let callIndex = 0;
michael@0 454 let currNewLinePivot = stack.indexOf("\n") + 1;
michael@0 455 let nextNewLinePivot = stack.indexOf("\n", currNewLinePivot);
michael@0 456
michael@0 457 while (nextNewLinePivot > 0) {
michael@0 458 let nameDelimiterIndex = stack.indexOf("@", currNewLinePivot);
michael@0 459 let columnDelimiterIndex = stack.lastIndexOf(":", nextNewLinePivot - 1);
michael@0 460 let lineDelimiterIndex = stack.lastIndexOf(":", columnDelimiterIndex - 1);
michael@0 461
michael@0 462 if (!calls[callIndex]) {
michael@0 463 calls[callIndex] = { name: "", file: "", line: 0 };
michael@0 464 }
michael@0 465 if (!calls[callIndex + 1]) {
michael@0 466 calls[callIndex + 1] = { name: "", file: "", line: 0 };
michael@0 467 }
michael@0 468
michael@0 469 if (callIndex > 0) {
michael@0 470 let file = stack.substring(nameDelimiterIndex + 1, lineDelimiterIndex);
michael@0 471 let line = stack.substring(lineDelimiterIndex + 1, columnDelimiterIndex);
michael@0 472 let name = stack.substring(currNewLinePivot, nameDelimiterIndex);
michael@0 473 calls[callIndex].name = name;
michael@0 474 calls[callIndex - 1].file = file;
michael@0 475 calls[callIndex - 1].line = line;
michael@0 476 } else {
michael@0 477 // Since the topmost stack frame is actually our overwritten function,
michael@0 478 // it will not have the expected name.
michael@0 479 calls[0].name = caller;
michael@0 480 }
michael@0 481
michael@0 482 currNewLinePivot = nextNewLinePivot + 1;
michael@0 483 nextNewLinePivot = stack.indexOf("\n", currNewLinePivot);
michael@0 484 callIndex++;
michael@0 485 }
michael@0 486
michael@0 487 return calls;
michael@0 488 }
michael@0 489 },
michael@0 490
michael@0 491 /**
michael@0 492 * Invoked whenever the current tab actor's inner window is destroyed.
michael@0 493 */
michael@0 494 _onGlobalDestroyed: function(id) {
michael@0 495 if (this._tracedWindowId == id) {
michael@0 496 this.pauseRecording();
michael@0 497 this.eraseRecording();
michael@0 498 }
michael@0 499 },
michael@0 500
michael@0 501 /**
michael@0 502 * Invoked whenever an instrumented function is called.
michael@0 503 */
michael@0 504 _onContentFunctionCall: function(...details) {
michael@0 505 let functionCall = new FunctionCallActor(this.conn, details);
michael@0 506 this._functionCalls.push(functionCall);
michael@0 507 this.onCall(functionCall);
michael@0 508 }
michael@0 509 });
michael@0 510
michael@0 511 /**
michael@0 512 * The corresponding Front object for the CallWatcherActor.
michael@0 513 */
michael@0 514 let CallWatcherFront = exports.CallWatcherFront = protocol.FrontClass(CallWatcherActor, {
michael@0 515 initialize: function(client, { callWatcherActor }) {
michael@0 516 protocol.Front.prototype.initialize.call(this, client, { actor: callWatcherActor });
michael@0 517 client.addActorPool(this);
michael@0 518 this.manage(this);
michael@0 519 }
michael@0 520 });
michael@0 521
michael@0 522 /**
michael@0 523 * Constants.
michael@0 524 */
michael@0 525 CallWatcherFront.METHOD_FUNCTION = 0;
michael@0 526 CallWatcherFront.GETTER_FUNCTION = 1;
michael@0 527 CallWatcherFront.SETTER_FUNCTION = 2;
michael@0 528
michael@0 529 CallWatcherFront.GLOBAL_SCOPE = 0;
michael@0 530 CallWatcherFront.UNKNOWN_SCOPE = 1;
michael@0 531 CallWatcherFront.CANVAS_WEBGL_CONTEXT = 2;
michael@0 532 CallWatcherFront.CANVAS_2D_CONTEXT = 3;
michael@0 533
michael@0 534 /**
michael@0 535 * A lookup table for cross-referencing flags or properties with their name
michael@0 536 * assuming they look LIKE_THIS most of the time.
michael@0 537 *
michael@0 538 * For example, when gl.clear(gl.COLOR_BUFFER_BIT) is called, the actual passed
michael@0 539 * argument's value is 16384, which we want identified as "COLOR_BUFFER_BIT".
michael@0 540 */
michael@0 541 var gEnumRegex = /^[A-Z_]+$/;
michael@0 542 var gEnumsLookupTable = {};
michael@0 543
michael@0 544 function getEnumsLookupTable(type, object) {
michael@0 545 let cachedEnum = gEnumsLookupTable[type];
michael@0 546 if (cachedEnum) {
michael@0 547 return cachedEnum;
michael@0 548 }
michael@0 549
michael@0 550 let table = gEnumsLookupTable[type] = {};
michael@0 551
michael@0 552 for (let key in object) {
michael@0 553 if (key.match(gEnumRegex)) {
michael@0 554 table[object[key]] = key;
michael@0 555 }
michael@0 556 }
michael@0 557
michael@0 558 return table;
michael@0 559 }

mercurial