|
"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]*)+$/; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function parse(source, root, options) { |
|
|
|
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; |
|
|
|
|
|
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 { |
|
|
|
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, true); |
|
} catch (e) { |
|
|
|
|
|
if (acceptTypeRef && typeRefRe.test(token)) |
|
return token; |
|
|
|
|
|
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); |
|
|
|
|
|
if (numberRe.test(token)) |
|
return sign * parseFloat(token); |
|
|
|
|
|
throw illegal(token, "number", insideTryCatch); |
|
} |
|
|
|
function parseId(token, acceptNegative) { |
|
switch (token) { |
|
case "max": case "MAX": case "Max": |
|
return 536870911; |
|
case "0": |
|
return 0; |
|
} |
|
|
|
|
|
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); |
|
|
|
|
|
if (base8NegRe.test(token)) |
|
return parseInt(token, 8); |
|
|
|
|
|
throw illegal(token, "id"); |
|
} |
|
|
|
function parsePackage() { |
|
|
|
|
|
if (pkg !== undefined) |
|
throw illegal("package"); |
|
|
|
pkg = next(); |
|
|
|
|
|
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(); |
|
|
|
default: |
|
whichImports = imports || (imports = []); |
|
break; |
|
} |
|
token = readString(); |
|
skip(";"); |
|
whichImports.push(token); |
|
} |
|
|
|
function parseSyntax() { |
|
skip("="); |
|
syntax = readString(); |
|
isProto3 = syntax === "proto3"; |
|
|
|
|
|
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(); |
|
} |
|
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; |
|
} |
|
} |
|
|
|
function parseType(parent, token) { |
|
|
|
|
|
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 "repeated": |
|
parseField(type, token); |
|
break; |
|
|
|
case "optional": |
|
|
|
if (isProto3) { |
|
parseField(type, "proto3_optional"); |
|
} else { |
|
parseField(type, "optional"); |
|
} |
|
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: |
|
|
|
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; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
while (type.endsWith(".") || peek().startsWith(".")) { |
|
type += next(); |
|
} |
|
|
|
|
|
if (!typeRefRe.test(type)) |
|
throw illegal(type, "type"); |
|
|
|
var name = next(); |
|
|
|
|
|
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) { |
|
|
|
|
|
if (token === "option") { |
|
parseOption(field, token); |
|
skip(";"); |
|
} else |
|
throw illegal(token); |
|
|
|
}, function parseField_line() { |
|
parseInlineOptions(field); |
|
}); |
|
|
|
if (rule === "proto3_optional") { |
|
|
|
var oneof = new OneOf("_" + name); |
|
field.setOption("proto3_optional", true); |
|
oneof.add(field); |
|
parent.add(oneof); |
|
} else { |
|
parent.add(field); |
|
} |
|
|
|
|
|
|
|
|
|
if (!isProto3 && field.repeated && (types.packed[type] !== undefined || types.basic[type] === undefined)) |
|
field.setOption("packed", false, true); |
|
} |
|
|
|
function parseGroup(parent, rule) { |
|
var name = next(); |
|
|
|
|
|
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 "repeated": |
|
parseField(type, token); |
|
break; |
|
|
|
case "optional": |
|
|
|
if (isProto3) { |
|
parseField(type, "proto3_optional"); |
|
} else { |
|
parseField(type, "optional"); |
|
} |
|
break; |
|
|
|
case "message": |
|
parseType(type, token); |
|
break; |
|
|
|
case "enum": |
|
parseEnum(type, token); |
|
break; |
|
|
|
|
|
default: |
|
throw illegal(token); |
|
} |
|
}); |
|
parent.add(type) |
|
.add(field); |
|
} |
|
|
|
function parseMapField(parent) { |
|
skip("<"); |
|
var keyType = next(); |
|
|
|
|
|
if (types.mapKey[keyType] === undefined) |
|
throw illegal(keyType, "type"); |
|
|
|
skip(","); |
|
var valueType = next(); |
|
|
|
|
|
if (!typeRefRe.test(valueType)) |
|
throw illegal(valueType, "type"); |
|
|
|
skip(">"); |
|
var name = next(); |
|
|
|
|
|
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) { |
|
|
|
|
|
if (token === "option") { |
|
parseOption(field, token); |
|
skip(";"); |
|
} else |
|
throw illegal(token); |
|
|
|
}, function parseMapField_line() { |
|
parseInlineOptions(field); |
|
}); |
|
parent.add(field); |
|
} |
|
|
|
function parseOneOf(parent, token) { |
|
|
|
|
|
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) { |
|
|
|
|
|
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) { |
|
|
|
|
|
if (!nameRe.test(token)) |
|
throw illegal(token, "name"); |
|
|
|
skip("="); |
|
var value = parseId(next(), true), |
|
dummy = { |
|
options: undefined |
|
}; |
|
dummy.setOption = function(name, value) { |
|
if (this.options === undefined) |
|
this.options = {}; |
|
this.options[name] = value; |
|
}; |
|
ifBlock(dummy, function parseEnumValue_block(token) { |
|
|
|
|
|
if (token === "option") { |
|
parseOption(dummy, token); |
|
skip(";"); |
|
} else |
|
throw illegal(token); |
|
|
|
}, function parseEnumValue_line() { |
|
parseInlineOptions(dummy); |
|
}); |
|
parent.add(token, value, dummy.comment, dummy.options); |
|
} |
|
|
|
function parseOption(parent, token) { |
|
var isCustom = skip("(", true); |
|
|
|
|
|
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.slice(1); |
|
name += token; |
|
next(); |
|
} |
|
} |
|
skip("="); |
|
var optionValue = parseOptionValue(parent, name); |
|
setParsedOption(parent, option, optionValue, propName); |
|
} |
|
|
|
function parseOptionValue(parent, name) { |
|
|
|
if (skip("{", true)) { |
|
var objectResult = {}; |
|
|
|
while (!skip("}", true)) { |
|
|
|
if (!nameRe.test(token = next())) { |
|
throw illegal(token, "name"); |
|
} |
|
if (token === null) { |
|
throw illegal(token, "end of input"); |
|
} |
|
|
|
var value; |
|
var propName = token; |
|
|
|
skip(":", true); |
|
|
|
if (peek() === "{") |
|
value = parseOptionValue(parent, name + "." + token); |
|
else if (peek() === "[") { |
|
|
|
|
|
|
|
value = []; |
|
var lastValue; |
|
if (skip("[", true)) { |
|
do { |
|
lastValue = readValue(true); |
|
value.push(lastValue); |
|
} while (skip(",", true)); |
|
skip("]"); |
|
if (typeof lastValue !== "undefined") { |
|
setOption(parent, name + "." + token, lastValue); |
|
} |
|
} |
|
} else { |
|
value = readValue(true); |
|
setOption(parent, name + "." + token, value); |
|
} |
|
|
|
var prevValue = objectResult[propName]; |
|
|
|
if (prevValue) |
|
value = [].concat(prevValue).concat(value); |
|
|
|
objectResult[propName] = value; |
|
|
|
|
|
skip(",", true); |
|
skip(";", true); |
|
} |
|
|
|
return objectResult; |
|
} |
|
|
|
var simpleValue = readValue(true); |
|
setOption(parent, name, simpleValue); |
|
return simpleValue; |
|
|
|
} |
|
|
|
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) { |
|
|
|
|
|
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; |
|
|
|
|
|
if (token === "rpc") |
|
parseMethod(service, token); |
|
else |
|
throw illegal(token); |
|
}); |
|
parent.add(service); |
|
} |
|
|
|
function parseMethod(parent, token) { |
|
|
|
|
|
var commentText = cmnt(); |
|
|
|
var type = token; |
|
|
|
|
|
if (!nameRe.test(token = next())) |
|
throw illegal(token, "name"); |
|
|
|
var name = token, |
|
requestType, requestStream, |
|
responseType, responseStream; |
|
|
|
skip("("); |
|
if (skip("stream", true)) |
|
requestStream = true; |
|
|
|
|
|
if (!typeRefRe.test(token = next())) |
|
throw illegal(token); |
|
|
|
requestType = token; |
|
skip(")"); skip("returns"); skip("("); |
|
if (skip("stream", true)) |
|
responseStream = true; |
|
|
|
|
|
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) { |
|
|
|
|
|
if (token === "option") { |
|
parseOption(method, token); |
|
skip(";"); |
|
} else |
|
throw illegal(token); |
|
|
|
}); |
|
parent.add(method); |
|
} |
|
|
|
function parseExtension(parent, token) { |
|
|
|
|
|
if (!typeRefRe.test(token = next())) |
|
throw illegal(token, "reference"); |
|
|
|
var reference = token; |
|
ifBlock(null, function parseExtension_block(token) { |
|
switch (token) { |
|
|
|
case "required": |
|
case "repeated": |
|
parseField(parent, token, reference); |
|
break; |
|
|
|
case "optional": |
|
|
|
if (isProto3) { |
|
parseField(parent, "proto3_optional", reference); |
|
} else { |
|
parseField(parent, "optional", reference); |
|
} |
|
break; |
|
|
|
default: |
|
|
|
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": |
|
|
|
|
|
if (!head) |
|
throw illegal(token); |
|
|
|
parsePackage(); |
|
break; |
|
|
|
case "import": |
|
|
|
|
|
if (!head) |
|
throw illegal(token); |
|
|
|
parseImport(); |
|
break; |
|
|
|
case "syntax": |
|
|
|
|
|
if (!head) |
|
throw illegal(token); |
|
|
|
parseSyntax(); |
|
break; |
|
|
|
case "option": |
|
|
|
parseOption(ptr, token); |
|
skip(";"); |
|
break; |
|
|
|
default: |
|
|
|
|
|
if (parseCommon(ptr, token)) { |
|
head = false; |
|
continue; |
|
} |
|
|
|
|
|
throw illegal(token); |
|
} |
|
} |
|
|
|
parse.filename = null; |
|
return { |
|
"package" : pkg, |
|
"imports" : imports, |
|
weakImports : weakImports, |
|
syntax : syntax, |
|
root : root |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|