module.exports = grammar({
name: 'protobuf',
extras: ($) => [$.comment, /\s/],
rules: {
source_file: ($) =>
seq($.syntax, repeat(choice($.import, $.package, $.option, $.emptyStatement, $.enum, $.message, $.service))),
comment: ($) => token(seq('//', /.*/)),
syntax: ($) => seq('syntax', '=', /"proto3"/, ';'),
package: ($) => seq('package', $.fullIdent, ';'),
import: ($) => seq('import', $.strLit, ';'),
option: ($) => seq('option', $.optionName, '=', $.constant, ';'),
optionName: ($) => choice(seq('(', $.fullIdent, ')'), $.fullIdent),
enum: ($) => seq('enum', $.enumName, $.enumBody),
enumBody: ($) => seq('{', repeat(choice($.option, $.enumField, $.emptyStatement)), '}'),
enumField: ($) =>
seq(
$.ident,
'=',
optional('-'),
$.intLit,
optional(seq('[', $.enumValueOption, repeat(seq(',', $.enumValueOption)), ']')),
';'
),
enumValueOption: ($) => seq($.optionName, '=', $.constant),
message: ($) => seq('message', $.messageName, $.messageBody),
messageBody: ($) =>
seq(
'{',
repeat(choice($.field, $.enum, $.message, $.option, $.oneof, $.mapField, $.reserved, $.emptyStatement)),
'}'
),
service: ($) => seq('service', $.serviceName, '{', repeat(choice($.option, $.rpc, $.emptyStatement)), '}'),
rpc: ($) =>
seq(
'rpc',
$.rpcName,
'(',
optional('stream'),
$.enumMessageType,
')',
'returns',
'(',
optional('stream'),
$.enumMessageType,
')',
choice(seq('{', repeat(choice($.option, $.emptyStatement)), '}'), ';')
),
field: ($) =>
seq(optional('repeated'), $.type, $.fieldName, '=', $.fieldNumber, optional(seq('[', $.fieldOptions, ']')), ';'),
fieldOptions: ($) => seq($.fieldOption, repeat(seq(',', $.fieldOption))),
fieldOption: ($) => seq($.optionName, '=', $.constant),
oneof: ($) => seq('oneof', $.oneofName, '{', repeat(choice($.option, $.oneofField, $.emptyStatement)), '}'),
oneofField: ($) => seq($.type, $.fieldName, '=', $.fieldNumber, optional(seq('[', $.fieldOptions, ']')), ';'),
mapField: ($) =>
seq(
'map',
'<',
$.keyType,
',',
$.type,
'>',
$.mapName,
'=',
$.fieldNumber,
optional(seq('[', $.fieldOptions, ']')),
';'
),
keyType: ($) =>
choice(
'int32',
'int64',
'uint32',
'uint64',
'sint32',
'sint64',
'fixed32',
'fixed64',
'sfixed32',
'sfixed64',
'bool',
'string'
),
reserved: ($) => seq('reserved', choice($.ranges, $.fieldNames)),
ranges: ($) => seq($.range, repeat(seq(',', $.range))),
range: ($) => seq($.intLit, optional(seq('to', choice($.intLit, 'max')))),
fieldNames: ($) => seq($.fieldName, repeat(seq(',', $.fieldName))),
intLit: ($) => /(\d\d*|0[0-7]*|0[xX][\da-fA-F]*)/,
floatLit: ($) => choice(/\d\.\d*([eE][+-]\d*)?/, /\d*[eE][+-]\d*/, /\.\d*[eE][+-]\d*/, 'inf', 'nan'),
boolLit: ($) => /(true|false)/,
strLit: ($) =>
choice(
seq('"', /([^"\n\\]|\\[xX][\da-fA-F]{2}|\\[0-7]{3}|\\[abfnrtv\\'"])*/, '"'),
seq("'", /([^'\n\\]|\\[xX][\da-fA-F]{2}|\\[0-7]{3}|\\[abfnrtv\\'"])*/, "'")
),
type: ($) =>
choice(
'double',
'float',
'int32',
'int64',
'uint32',
'uint64',
'sint32',
'sint64',
'fixed32',
'fixed64',
'sfixed32',
'sfixed64',
'bool',
'string',
'bytes',
$.enumMessageType
),
fieldNumber: ($) => $.intLit,
emptyStatement: ($) => ';',
constant: ($) =>
choice(
$.fullIdent,
seq(optional(/[+-]/), $.intLit),
seq(optional(/[+-]/), $.floatLit),
$.strLit,
$.boolLit,
$.msgLit
),
msgLit: ($) => seq('{', repeat(seq($.fieldName, ':', $.constant)), '}'),
ident: ($) => /[a-zA-Z_]\w*/,
fullIdent: ($) => seq($.ident, repeat(seq('.', $.ident))),
messageName: ($) => $.ident,
mapName: ($) => $.ident,
enumName: ($) => $.ident,
fieldName: ($) => $.ident,
oneofName: ($) => $.ident,
serviceName: ($) => $.ident,
rpcName: ($) => $.ident,
enumMessageType: ($) => seq(optional('.'), repeat(seq($.ident, '.')), $.messageName),
},
});