docs: function formatNodes(merged, notSplit) {
      // grab the class name out (if it exists)
      remainingNodes = _.reject(merged, function(i) {
        if (i["tagname"] === "class") {
          classPrefix = i["name"];
          nodes[classPrefix] = createBasicTranslation(classPrefix, "class", i, options);
  
          if (i["inherits"] !== undefined && i["inherits"].length > 0) 
            nodes[classPrefix]["inherits"] = i["inherits"];
  
          if (i["allowchild"] !== undefined) 
            nodes[classPrefix]["allowchild"] = i["allowchild"].doc;
  
          return true;
        }
      });
  
      // flag nodes that don't have doc
      // or that just contain "@todo" in the beginning
      remainingNodes = _.map(remainingNodes, function(n) {
        if (n["doc"] === undefined || /^@todo/i.test(n["doc"]))
          n.undocumented = true;
  
        return n;
      });
  
      remainingNodes = _.reject(remainingNodes, function(i) {
        var isEvent = i["tagname"] == "event";
        var isConstructor = i["tagname"] == "constructor";
        var joinChar = isEvent ? "@" : ".";
  
        if (classPrefix === undefined && options.globalNS) 
          classPrefix = options.globalNS;
  
        if (new RegExp("^" + options.globalNS + joinChar + i["name"]).test(i["original_name"]))
          className = options.globalNS;
        else
          className = classPrefix;
  
        memberName = [className, i["name"]].join(joinChar);
  
        if (memberName.lastIndexOf(".") === memberName.length - 1 && i["inheritdoc"] === undefined) {
          console.error(i);    
          console.error("FATAL".red + ": this object doesn't have a proper name. Check that your comment is written correctly. Typically, his happens when: ");
          console.error("   * A statement above is missing a semicolon--including closing function tags--see this bug: " + "http://code.google.com/p/esprima/issues/detail?id=347&thanks=347&ts=1348621099".cyan);
          console.error("   * The line above a comment is a single line comment--see this bug: " + "https://github.com/senchalabs/jsduck/issues/247".cyan);
          process.exit(1)
        }
  
        if (isConstructor)
          memberName = classPrefix + ".new";
        
        if (memberName.charAt(0) == ".")
          memberName = memberName.slice(1);
  
        if (i["tagname"] === "method" || i["tagname"] === "constructor" || isEvent) {
          nodes[memberName] = createBasicTranslation(memberName, i["tagname"], i, options);
          nodes[memberName]["signatures"] = [];
          var ret = {};
  
          // these next blocks don't yet handle alternate signatures...
          // but are written as if they will :)
          if (i["params"] !== undefined) {
              var sig = { "arguments": [] };
              nodes[memberName]["arguments"] = [];
  
              _.each(i["params"], function(p) {
                // construct the args object
                var args = {};
                args.name = p["name"];
                args.description = p["doc"];
                args.default_value = p["default"];
  
                args.types = [];
  
                // types could be multiple
                if (p["type"].indexOf("|") >= 0) {
                  p["type"].split("|").forEach(function(t) {
                    args.types.push(trim(t));
                  });
                }
                else if (p["type"].indexOf(",") >= 0) {
                  console.warn("Warning".yellow + ": you're using ',' to separate types in " + memberName + ", use '|' instead.");
                }
                else {
                  args.types.push(p["type"]);
                }
  
                args.optional = p["optional"];
  
                sig.arguments.push(args);
  
                // there are args on "signatures" and a separate arguments property
                nodes[memberName]["arguments"].push(args);
              });
          }
  
          if (i["return"] !== undefined && i["return"].type !== undefined) {
            if (i["return"].type.indexOf("[") == 0) { // TODO: probably not best to do this here
              ret.type = i["return"].type.substr(1, i["return"].type.length - 2);
              ret.isArray = true;
            }
            else {
              ret.type = i["return"].type;
            }
  
            ret.description = i["return"].doc;
  
            // there are returns on "signatures" and a separate returns property
            //nodes[memberName]["returns"].push(ret);
          }
  
          var pushedObj = { };
  
          if (_.size(nodes[memberName]["arguments"]) > 0)    
            pushedObj["arguments"] = nodes[memberName]["arguments"];
  
          if (_.size(ret) > 0 && ret.type !== "undefined")
            pushedObj["returns"] = [ret];
  
          nodes[memberName]["signatures"].push(pushedObj);
  
          if (isEvent) {
            if (i["cancelable"] !== undefined)
              nodes[memberName]["cancelable"] = i["cancelable"];
            if (i["bubbles"] !== undefined)
              nodes[memberName]["bubbles"] = i["bubbles"];
          }
  
          if (i["tagname"] === "method") {
            // avoid reinitializing on each node loop
            if (reportObject[className] === undefined) {
                reportObject[className] = {};
                reportObject[className].missingMembers = [];
            }
            
            // go through the list of what esprima said were methods
            // and make sure they have real documentation
            var funcLength = availableFunctions.length;
  
            for (var l = 0; l < funcLength; l++) {
              var func = availableFunctions[l];
  
              // we found a matching function! 
              if (func.name === i["name"]) { 
                // if it's tagged as incomplete or private, just skip it
                if (nodes[memberName]["private"] || nodes[memberName]["ignore"]) {
                  // no op, we'll slice below
                }
                // now make sure the parameters match, too
                else if (func.params !== undefined) {
                  _.any(func.params, function (funcArg, idx) {
                    // catch instance where args are missing documentation
                    if (idx >= nodes[memberName]["arguments"].length) {
                      reportObject[className].missingMembers.push({name: i["name"], member: nodes[memberName], reason: "Missing documented arguments"});
                      return true;
                    }
                    // catch instance where arg and func names don't match
                    else if (funcArg.name !== nodes[memberName]["arguments"][idx].name) {
                      reportObject[className].missingMembers.push({name: i["name"], member: nodes[memberName], reason: "Documented arguments missing proper name"});
                      return true;
                    }
                  });
                }
  
                // all the doc "tests" passed, remove this method from the reporting object
                availableFunctions.splice(l, 1);
                break;
              }
            }
          }
  
          methodCount++;
          return true;
        }
  
        else if (i["tagname"] === "property") {
          nodes[memberName] = createBasicTranslation(memberName, "property", i, options);
          nodes[memberName]["signatures"] = [ { arguments: undefined, returns: [ { type: i["type"], default_value: i["default"] } ] } ];
          return true;
        }
  
        else if (i["tagname"] === "attribute") {
          nodes[memberName] = createBasicTranslation(memberName, "attribute", i, options);
  
          var returnsArray = [];
  
          if (i["default"] !== undefined && i["default"] !== null)
            nodes[memberName]["defaults"] = i["default"];
  
          if (i["type"]) {
            var types = i["type"].split("|");
  
            _.each(types, function(t) {
              returnsArray.push({type: trim(t)});
            });
          }
  
          nodes[memberName]["signatures"] = [ { arguments: undefined, returns: returnsArray } ];
          return true;
        }
  
        else if (i["tagname"] === "binding") {
          nodes[memberName] = createBasicTranslation(memberName, "binding", i, options);
          nodes[memberName]["signatures"] = [ { arguments: undefined, returns: [ { type: i["type"] } ] } ];
          return true;
        }
  
        else {
          console.warn("Warning".yellow + ": I don't know what " + i["tagname"] + " is supposed to do in " + file);
          return true;
        }
      });
  
      if (notSplit) {
        checkMissingFunctions(notSplit);      
        reportObject[className].documented = methodCount;
      }
  
      // do pre-distribute early work
      _.each(nodes, function (node, id) {
        var clone;
  
        // assign hierarchy helpers
        node.aliases  = [];
        node.children = [];
  
        // set source file of the node
        node.file = file;
  
        if ('class' === node.type) {
          node.subclasses = [];
        }
  
        // collect sections
        if ('section' === node.type) {
          list[node.id] = node;
          return;
        }
  
        // elements with undefined section get '' section,
        // and will be resolved later, when we'll have full
        // element list
        list[(node.section || '') + '.' + node.id] = node;
  
        // bound methods produce two methods with the same description but different signatures
        // E.g. Element.foo(@element, a, b) becomes
        // Element.foo(element, a, b) and Element#foo(a, b)
        if ('method' === node.type && node.bound) {
          clone = _.clone(node);
          clone.id = node.id.replace(/(.+)\.(.+)$/, '$1#$2');
  
          // link to methods
          node.bound = clone.id;
          clone.bound = node.id;
  
          // insert bound method clone
          list[(node.section || '') + '.' + clone.id] = clone;
        }
      });
    }