--- /dev/null
+"use strict";
+module.exports = parse;
+
+parse.filename = null;
+parse.defaults = { keepCase: false };
+
+var tokenize = require("./tokenize"),
+ Root = require("./root"),
+ Type = require("./type"),
+ Field = require("./field"),
+ MapField = require("./mapfield"),
+ OneOf = require("./oneof"),
+ Enum = require("./enum"),
+ Service = require("./service"),
+ Method = require("./method"),
+ types = require("./types"),
+ util = require("./util");
+
+var base10Re = /^[1-9][0-9]*$/,
+ base10NegRe = /^-?[1-9][0-9]*$/,
+ base16Re = /^0[x][0-9a-fA-F]+$/,
+ base16NegRe = /^-?0[x][0-9a-fA-F]+$/,
+ base8Re = /^0[0-7]+$/,
+ base8NegRe = /^-?0[0-7]+$/,
+ numberRe = /^(?![eE])[0-9]*(?:\.[0-9]*)?(?:[eE][+-]?[0-9]+)?$/,
+ nameRe = /^[a-zA-Z_][a-zA-Z_0-9]*$/,
+ typeRefRe = /^(?:\.?[a-zA-Z_][a-zA-Z_0-9]*)(?:\.[a-zA-Z_][a-zA-Z_0-9]*)*$/,
+ fqTypeRefRe = /^(?:\.[a-zA-Z_][a-zA-Z_0-9]*)+$/;
+
+/**
+ * Result object returned from {@link parse}.
+ * @interface IParserResult
+ * @property {string|undefined} package Package name, if declared
+ * @property {string[]|undefined} imports Imports, if any
+ * @property {string[]|undefined} weakImports Weak imports, if any
+ * @property {string|undefined} syntax Syntax, if specified (either `"proto2"` or `"proto3"`)
+ * @property {Root} root Populated root instance
+ */
+
+/**
+ * Options modifying the behavior of {@link parse}.
+ * @interface IParseOptions
+ * @property {boolean} [keepCase=false] Keeps field casing instead of converting to camel case
+ * @property {boolean} [alternateCommentMode=false] Recognize double-slash comments in addition to doc-block comments.
+ * @property {boolean} [preferTrailingComment=false] Use trailing comment when both leading comment and trailing comment exist.
+ */
+
+/**
+ * Options modifying the behavior of JSON serialization.
+ * @interface IToJSONOptions
+ * @property {boolean} [keepComments=false] Serializes comments.
+ */
+
+/**
+ * Parses the given .proto source and returns an object with the parsed contents.
+ * @param {string} source Source contents
+ * @param {Root} root Root to populate
+ * @param {IParseOptions} [options] Parse options. Defaults to {@link parse.defaults} when omitted.
+ * @returns {IParserResult} Parser result
+ * @property {string} filename=null Currently processing file name for error reporting, if known
+ * @property {IParseOptions} defaults Default {@link IParseOptions}
+ */
+function parse(source, root, options) {
+ /* eslint-disable callback-return */
+ if (!(root instanceof Root)) {
+ options = root;
+ root = new Root();
+ }
+ if (!options)
+ options = parse.defaults;
+
+ var preferTrailingComment = options.preferTrailingComment || false;
+ var tn = tokenize(source, options.alternateCommentMode || false),
+ next = tn.next,
+ push = tn.push,
+ peek = tn.peek,
+ skip = tn.skip,
+ cmnt = tn.cmnt;
+
+ var head = true,
+ pkg,
+ imports,
+ weakImports,
+ syntax,
+ isProto3 = false;
+
+ var ptr = root;
+
+ var applyCase = options.keepCase ? function(name) { return name; } : util.camelCase;
+
+ /* istanbul ignore next */
+ function illegal(token, name, insideTryCatch) {
+ var filename = parse.filename;
+ if (!insideTryCatch)
+ parse.filename = null;
+ return Error("illegal " + (name || "token") + " '" + token + "' (" + (filename ? filename + ", " : "") + "line " + tn.line + ")");
+ }
+
+ function readString() {
+ var values = [],
+ token;
+ do {
+ /* istanbul ignore if */
+ if ((token = next()) !== "\"" && token !== "'")
+ throw illegal(token);
+
+ values.push(next());
+ skip(token);
+ token = peek();
+ } while (token === "\"" || token === "'");
+ return values.join("");
+ }
+
+ function readValue(acceptTypeRef) {
+ var token = next();
+ switch (token) {
+ case "'":
+ case "\"":
+ push(token);
+ return readString();
+ case "true": case "TRUE":
+ return true;
+ case "false": case "FALSE":
+ return false;
+ }
+ try {
+ return parseNumber(token, /* insideTryCatch */ true);
+ } catch (e) {
+
+ /* istanbul ignore else */
+ if (acceptTypeRef && typeRefRe.test(token))
+ return token;
+
+ /* istanbul ignore next */
+ throw illegal(token, "value");
+ }
+ }
+
+ function readRanges(target, acceptStrings) {
+ var token, start;
+ do {
+ if (acceptStrings && ((token = peek()) === "\"" || token === "'"))
+ target.push(readString());
+ else
+ target.push([ start = parseId(next()), skip("to", true) ? parseId(next()) : start ]);
+ } while (skip(",", true));
+ skip(";");
+ }
+
+ function parseNumber(token, insideTryCatch) {
+ var sign = 1;
+ if (token.charAt(0) === "-") {
+ sign = -1;
+ token = token.substring(1);
+ }
+ switch (token) {
+ case "inf": case "INF": case "Inf":
+ return sign * Infinity;
+ case "nan": case "NAN": case "Nan": case "NaN":
+ return NaN;
+ case "0":
+ return 0;
+ }
+ if (base10Re.test(token))
+ return sign * parseInt(token, 10);
+ if (base16Re.test(token))
+ return sign * parseInt(token, 16);
+ if (base8Re.test(token))
+ return sign * parseInt(token, 8);
+
+ /* istanbul ignore else */
+ if (numberRe.test(token))
+ return sign * parseFloat(token);
+
+ /* istanbul ignore next */
+ throw illegal(token, "number", insideTryCatch);
+ }
+
+ function parseId(token, acceptNegative) {
+ switch (token) {
+ case "max": case "MAX": case "Max":
+ return 536870911;
+ case "0":
+ return 0;
+ }
+
+ /* istanbul ignore if */
+ if (!acceptNegative && token.charAt(0) === "-")
+ throw illegal(token, "id");
+
+ if (base10NegRe.test(token))
+ return parseInt(token, 10);
+ if (base16NegRe.test(token))
+ return parseInt(token, 16);
+
+ /* istanbul ignore else */
+ if (base8NegRe.test(token))
+ return parseInt(token, 8);
+
+ /* istanbul ignore next */
+ throw illegal(token, "id");
+ }
+
+ function parsePackage() {
+
+ /* istanbul ignore if */
+ if (pkg !== undefined)
+ throw illegal("package");
+
+ pkg = next();
+
+ /* istanbul ignore if */
+ if (!typeRefRe.test(pkg))
+ throw illegal(pkg, "name");
+
+ ptr = ptr.define(pkg);
+ skip(";");
+ }
+
+ function parseImport() {
+ var token = peek();
+ var whichImports;
+ switch (token) {
+ case "weak":
+ whichImports = weakImports || (weakImports = []);
+ next();
+ break;
+ case "public":
+ next();
+ // eslint-disable-line no-fallthrough
+ default:
+ whichImports = imports || (imports = []);
+ break;
+ }
+ token = readString();
+ skip(";");
+ whichImports.push(token);
+ }
+
+ function parseSyntax() {
+ skip("=");
+ syntax = readString();
+ isProto3 = syntax === "proto3";
+
+ /* istanbul ignore if */
+ if (!isProto3 && syntax !== "proto2")
+ throw illegal(syntax, "syntax");
+
+ skip(";");
+ }
+
+ function parseCommon(parent, token) {
+ switch (token) {
+
+ case "option":
+ parseOption(parent, token);
+ skip(";");
+ return true;
+
+ case "message":
+ parseType(parent, token);
+ return true;
+
+ case "enum":
+ parseEnum(parent, token);
+ return true;
+
+ case "service":
+ parseService(parent, token);
+ return true;
+
+ case "extend":
+ parseExtension(parent, token);
+ return true;
+ }
+ return false;
+ }
+
+ function ifBlock(obj, fnIf, fnElse) {
+ var trailingLine = tn.line;
+ if (obj) {
+ if(typeof obj.comment !== "string") {
+ obj.comment = cmnt(); // try block-type comment
+ }
+ obj.filename = parse.filename;
+ }
+ if (skip("{", true)) {
+ var token;
+ while ((token = next()) !== "}")
+ fnIf(token);
+ skip(";", true);
+ } else {
+ if (fnElse)
+ fnElse();
+ skip(";");
+ if (obj && (typeof obj.comment !== "string" || preferTrailingComment))
+ obj.comment = cmnt(trailingLine) || obj.comment; // try line-type comment
+ }
+ }
+
+ function parseType(parent, token) {
+
+ /* istanbul ignore if */
+ if (!nameRe.test(token = next()))
+ throw illegal(token, "type name");
+
+ var type = new Type(token);
+ ifBlock(type, function parseType_block(token) {
+ if (parseCommon(type, token))
+ return;
+
+ switch (token) {
+
+ case "map":
+ parseMapField(type, token);
+ break;
+
+ case "required":
+ case "optional":
+ case "repeated":
+ parseField(type, token);
+ break;
+
+ case "oneof":
+ parseOneOf(type, token);
+ break;
+
+ case "extensions":
+ readRanges(type.extensions || (type.extensions = []));
+ break;
+
+ case "reserved":
+ readRanges(type.reserved || (type.reserved = []), true);
+ break;
+
+ default:
+ /* istanbul ignore if */
+ if (!isProto3 || !typeRefRe.test(token))
+ throw illegal(token);
+
+ push(token);
+ parseField(type, "optional");
+ break;
+ }
+ });
+ parent.add(type);
+ }
+
+ function parseField(parent, rule, extend) {
+ var type = next();
+ if (type === "group") {
+ parseGroup(parent, rule);
+ return;
+ }
+
+ /* istanbul ignore if */
+ if (!typeRefRe.test(type))
+ throw illegal(type, "type");
+
+ var name = next();
+
+ /* istanbul ignore if */
+ if (!nameRe.test(name))
+ throw illegal(name, "name");
+
+ name = applyCase(name);
+ skip("=");
+
+ var field = new Field(name, parseId(next()), type, rule, extend);
+ ifBlock(field, function parseField_block(token) {
+
+ /* istanbul ignore else */
+ if (token === "option") {
+ parseOption(field, token);
+ skip(";");
+ } else
+ throw illegal(token);
+
+ }, function parseField_line() {
+ parseInlineOptions(field);
+ });
+ parent.add(field);
+
+ // JSON defaults to packed=true if not set so we have to set packed=false explicity when
+ // parsing proto2 descriptors without the option, where applicable. This must be done for
+ // all known packable types and anything that could be an enum (= is not a basic type).
+ if (!isProto3 && field.repeated && (types.packed[type] !== undefined || types.basic[type] === undefined))
+ field.setOption("packed", false, /* ifNotSet */ true);
+ }
+
+ function parseGroup(parent, rule) {
+ var name = next();
+
+ /* istanbul ignore if */
+ if (!nameRe.test(name))
+ throw illegal(name, "name");
+
+ var fieldName = util.lcFirst(name);
+ if (name === fieldName)
+ name = util.ucFirst(name);
+ skip("=");
+ var id = parseId(next());
+ var type = new Type(name);
+ type.group = true;
+ var field = new Field(fieldName, id, name, rule);
+ field.filename = parse.filename;
+ ifBlock(type, function parseGroup_block(token) {
+ switch (token) {
+
+ case "option":
+ parseOption(type, token);
+ skip(";");
+ break;
+
+ case "required":
+ case "optional":
+ case "repeated":
+ parseField(type, token);
+ break;
+
+ /* istanbul ignore next */
+ default:
+ throw illegal(token); // there are no groups with proto3 semantics
+ }
+ });
+ parent.add(type)
+ .add(field);
+ }
+
+ function parseMapField(parent) {
+ skip("<");
+ var keyType = next();
+
+ /* istanbul ignore if */
+ if (types.mapKey[keyType] === undefined)
+ throw illegal(keyType, "type");
+
+ skip(",");
+ var valueType = next();
+
+ /* istanbul ignore if */
+ if (!typeRefRe.test(valueType))
+ throw illegal(valueType, "type");
+
+ skip(">");
+ var name = next();
+
+ /* istanbul ignore if */
+ if (!nameRe.test(name))
+ throw illegal(name, "name");
+
+ skip("=");
+ var field = new MapField(applyCase(name), parseId(next()), keyType, valueType);
+ ifBlock(field, function parseMapField_block(token) {
+
+ /* istanbul ignore else */
+ if (token === "option") {
+ parseOption(field, token);
+ skip(";");
+ } else
+ throw illegal(token);
+
+ }, function parseMapField_line() {
+ parseInlineOptions(field);
+ });
+ parent.add(field);
+ }
+
+ function parseOneOf(parent, token) {
+
+ /* istanbul ignore if */
+ if (!nameRe.test(token = next()))
+ throw illegal(token, "name");
+
+ var oneof = new OneOf(applyCase(token));
+ ifBlock(oneof, function parseOneOf_block(token) {
+ if (token === "option") {
+ parseOption(oneof, token);
+ skip(";");
+ } else {
+ push(token);
+ parseField(oneof, "optional");
+ }
+ });
+ parent.add(oneof);
+ }
+
+ function parseEnum(parent, token) {
+
+ /* istanbul ignore if */
+ if (!nameRe.test(token = next()))
+ throw illegal(token, "name");
+
+ var enm = new Enum(token);
+ ifBlock(enm, function parseEnum_block(token) {
+ switch(token) {
+ case "option":
+ parseOption(enm, token);
+ skip(";");
+ break;
+
+ case "reserved":
+ readRanges(enm.reserved || (enm.reserved = []), true);
+ break;
+
+ default:
+ parseEnumValue(enm, token);
+ }
+ });
+ parent.add(enm);
+ }
+
+ function parseEnumValue(parent, token) {
+
+ /* istanbul ignore if */
+ if (!nameRe.test(token))
+ throw illegal(token, "name");
+
+ skip("=");
+ var value = parseId(next(), true),
+ dummy = {};
+ ifBlock(dummy, function parseEnumValue_block(token) {
+
+ /* istanbul ignore else */
+ if (token === "option") {
+ parseOption(dummy, token); // skip
+ skip(";");
+ } else
+ throw illegal(token);
+
+ }, function parseEnumValue_line() {
+ parseInlineOptions(dummy); // skip
+ });
+ parent.add(token, value, dummy.comment);
+ }
+
+ function parseOption(parent, token) {
+ var isCustom = skip("(", true);
+
+ /* istanbul ignore if */
+ if (!typeRefRe.test(token = next()))
+ throw illegal(token, "name");
+
+ var name = token;
+ var option = name;
+ var propName;
+
+ if (isCustom) {
+ skip(")");
+ name = "(" + name + ")";
+ option = name;
+ token = peek();
+ if (fqTypeRefRe.test(token)) {
+ propName = token.substr(1); //remove '.' before property name
+ name += token;
+ next();
+ }
+ }
+ skip("=");
+ var optionValue = parseOptionValue(parent, name);
+ setParsedOption(parent, option, optionValue, propName);
+ }
+
+ function parseOptionValue(parent, name) {
+ if (skip("{", true)) { // { a: "foo" b { c: "bar" } }
+ var result = {};
+ while (!skip("}", true)) {
+ /* istanbul ignore if */
+ if (!nameRe.test(token = next()))
+ throw illegal(token, "name");
+
+ var value;
+ var propName = token;
+ if (peek() === "{")
+ value = parseOptionValue(parent, name + "." + token);
+ else {
+ skip(":");
+ if (peek() === "{")
+ value = parseOptionValue(parent, name + "." + token);
+ else {
+ value = readValue(true);
+ setOption(parent, name + "." + token, value);
+ }
+ }
+ var prevValue = result[propName];
+ if (prevValue)
+ value = [].concat(prevValue).concat(value);
+ result[propName] = value;
+ skip(",", true);
+ }
+ return result;
+ }
+
+ var simpleValue = readValue(true);
+ setOption(parent, name, simpleValue);
+ return simpleValue;
+ // Does not enforce a delimiter to be universal
+ }
+
+ function setOption(parent, name, value) {
+ if (parent.setOption)
+ parent.setOption(name, value);
+ }
+
+ function setParsedOption(parent, name, value, propName) {
+ if (parent.setParsedOption)
+ parent.setParsedOption(name, value, propName);
+ }
+
+ function parseInlineOptions(parent) {
+ if (skip("[", true)) {
+ do {
+ parseOption(parent, "option");
+ } while (skip(",", true));
+ skip("]");
+ }
+ return parent;
+ }
+
+ function parseService(parent, token) {
+
+ /* istanbul ignore if */
+ if (!nameRe.test(token = next()))
+ throw illegal(token, "service name");
+
+ var service = new Service(token);
+ ifBlock(service, function parseService_block(token) {
+ if (parseCommon(service, token))
+ return;
+
+ /* istanbul ignore else */
+ if (token === "rpc")
+ parseMethod(service, token);
+ else
+ throw illegal(token);
+ });
+ parent.add(service);
+ }
+
+ function parseMethod(parent, token) {
+ // Get the comment of the preceding line now (if one exists) in case the
+ // method is defined across multiple lines.
+ var commentText = cmnt();
+
+ var type = token;
+
+ /* istanbul ignore if */
+ if (!nameRe.test(token = next()))
+ throw illegal(token, "name");
+
+ var name = token,
+ requestType, requestStream,
+ responseType, responseStream;
+
+ skip("(");
+ if (skip("stream", true))
+ requestStream = true;
+
+ /* istanbul ignore if */
+ if (!typeRefRe.test(token = next()))
+ throw illegal(token);
+
+ requestType = token;
+ skip(")"); skip("returns"); skip("(");
+ if (skip("stream", true))
+ responseStream = true;
+
+ /* istanbul ignore if */
+ if (!typeRefRe.test(token = next()))
+ throw illegal(token);
+
+ responseType = token;
+ skip(")");
+
+ var method = new Method(name, type, requestType, responseType, requestStream, responseStream);
+ method.comment = commentText;
+ ifBlock(method, function parseMethod_block(token) {
+
+ /* istanbul ignore else */
+ if (token === "option") {
+ parseOption(method, token);
+ skip(";");
+ } else
+ throw illegal(token);
+
+ });
+ parent.add(method);
+ }
+
+ function parseExtension(parent, token) {
+
+ /* istanbul ignore if */
+ if (!typeRefRe.test(token = next()))
+ throw illegal(token, "reference");
+
+ var reference = token;
+ ifBlock(null, function parseExtension_block(token) {
+ switch (token) {
+
+ case "required":
+ case "repeated":
+ case "optional":
+ parseField(parent, token, reference);
+ break;
+
+ default:
+ /* istanbul ignore if */
+ if (!isProto3 || !typeRefRe.test(token))
+ throw illegal(token);
+ push(token);
+ parseField(parent, "optional", reference);
+ break;
+ }
+ });
+ }
+
+ var token;
+ while ((token = next()) !== null) {
+ switch (token) {
+
+ case "package":
+
+ /* istanbul ignore if */
+ if (!head)
+ throw illegal(token);
+
+ parsePackage();
+ break;
+
+ case "import":
+
+ /* istanbul ignore if */
+ if (!head)
+ throw illegal(token);
+
+ parseImport();
+ break;
+
+ case "syntax":
+
+ /* istanbul ignore if */
+ if (!head)
+ throw illegal(token);
+
+ parseSyntax();
+ break;
+
+ case "option":
+
+ parseOption(ptr, token);
+ skip(";");
+ break;
+
+ default:
+
+ /* istanbul ignore else */
+ if (parseCommon(ptr, token)) {
+ head = false;
+ continue;
+ }
+
+ /* istanbul ignore next */
+ throw illegal(token);
+ }
+ }
+
+ parse.filename = null;
+ return {
+ "package" : pkg,
+ "imports" : imports,
+ weakImports : weakImports,
+ syntax : syntax,
+ root : root
+ };
+}
+
+/**
+ * Parses the given .proto source and returns an object with the parsed contents.
+ * @name parse
+ * @function
+ * @param {string} source Source contents
+ * @param {IParseOptions} [options] Parse options. Defaults to {@link parse.defaults} when omitted.
+ * @returns {IParserResult} Parser result
+ * @property {string} filename=null Currently processing file name for error reporting, if known
+ * @property {IParseOptions} defaults Default {@link IParseOptions}
+ * @variation 2
+ */