@tabnas/proto 0.2.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -8
- package/dist/build-descriptor.js +263 -86
- package/dist/build-descriptor.js.map +1 -1
- package/dist/descriptor.d.ts +18 -12
- package/dist/descriptor.js +8 -1
- package/dist/descriptor.js.map +1 -1
- package/dist/grammar.d.ts +1 -1
- package/dist/grammar.js +19 -4
- package/dist/grammar.js.map +1 -1
- package/dist/proto.d.ts +2 -1
- package/dist/proto.js +6 -1
- package/dist/proto.js.map +1 -1
- package/package.json +6 -2
- package/src/build-descriptor.ts +270 -77
- package/src/descriptor.ts +34 -4
- package/src/grammar.ts +19 -4
- package/src/proto.ts +6 -1
package/src/build-descriptor.ts
CHANGED
|
@@ -10,15 +10,18 @@
|
|
|
10
10
|
import {
|
|
11
11
|
FileDescriptorProto, DescriptorProto, FieldDescriptorProto,
|
|
12
12
|
EnumDescriptorProto, ServiceDescriptorProto, MethodDescriptorProto,
|
|
13
|
-
|
|
13
|
+
DescriptorRange, SymbolVisibility, OptionValue, FieldLabel, SCALAR_TYPES,
|
|
14
|
+
MAX_FIELD_NUMBER_END, MAX_MESSAGE_SET_END, MAX_ENUM_NUMBER,
|
|
14
15
|
} from './descriptor'
|
|
15
16
|
import { ProtoVersion, isEdition } from './detect-version'
|
|
16
17
|
|
|
17
18
|
type Node = { rule?: string; src: string; kids?: Node[] }
|
|
18
19
|
|
|
19
|
-
// Children that are real rule nodes (terminals fold into `src`).
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// Children that are real rule nodes (terminals fold into `src`). An empty
|
|
21
|
+
// `.proto` is a legal (if useless) file and parses to no node at all, so
|
|
22
|
+
// tolerate a missing node here rather than crash at the root.
|
|
23
|
+
function R(n: Node | undefined | null): Node[] {
|
|
24
|
+
return ((n && n.kids) || []).filter((k) => k && k.rule)
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
// The keyword(s) consumed before this node's first child — the part of
|
|
@@ -51,30 +54,65 @@ function constantValue(n: Node): OptionValue {
|
|
|
51
54
|
const num = Number(s.replace(/^\+/, ''))
|
|
52
55
|
if (!Number.isNaN(num)) return num
|
|
53
56
|
}
|
|
54
|
-
return s // identifier (enum value name, inf, nan, …) kept verbatim
|
|
57
|
+
return s // identifier (enum value name, inf, -nan, …) kept verbatim
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
// The option name in an `optionName "=" constant` statement.
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
// `
|
|
60
|
+
// The option name in an `optionName "=" constant` statement.
|
|
61
|
+
//
|
|
62
|
+
// `optionStmt` keeps `optionName` as a child node, so use it when present.
|
|
63
|
+
// Inside `fieldOption` abnf inlines the leading `optionNamePart`, so there
|
|
64
|
+
// the name has to be read out of `src` — as everything BEFORE the trailing
|
|
65
|
+
// `"=" constant`. Anchoring at the end matters: `option (file_opt1) = 1;`
|
|
66
|
+
// would otherwise find the value `1` inside the name `file_opt1`.
|
|
61
67
|
function optionNameOf(stmt: Node, valueNode: Node | undefined): string {
|
|
68
|
+
const named = child(stmt, 'optionName')
|
|
69
|
+
if (named) return named.src
|
|
62
70
|
if (!valueNode) return ''
|
|
63
|
-
const
|
|
64
|
-
|
|
71
|
+
const s = stmt.src.replace(/;$/, '')
|
|
72
|
+
const tail = '=' + valueNode.src
|
|
73
|
+
if (s.endsWith(tail)) return s.slice(0, s.length - tail.length)
|
|
74
|
+
const i = s.indexOf(valueNode.src)
|
|
75
|
+
return i <= 0 ? '' : s.slice(0, i).replace(/=$/, '')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// The `json_name` and `default` field options are pseudo-options: protoc
|
|
79
|
+
// lifts them out of the option set into FieldDescriptorProto fields.
|
|
80
|
+
type PseudoOptions = {
|
|
81
|
+
options?: Record<string, OptionValue>
|
|
82
|
+
jsonName?: string
|
|
83
|
+
defaultValue?: string
|
|
65
84
|
}
|
|
66
85
|
|
|
67
86
|
// fieldOptions = "[" fieldOption *( "," fieldOption ) "]"
|
|
68
87
|
// fieldOption = optionName "=" constant
|
|
69
|
-
function readFieldOptions(opts: Node | undefined):
|
|
70
|
-
|
|
71
|
-
|
|
88
|
+
function readFieldOptions(opts: Node | undefined): PseudoOptions {
|
|
89
|
+
const out: PseudoOptions = {}
|
|
90
|
+
if (!opts) return out
|
|
91
|
+
const map: Record<string, OptionValue> = {}
|
|
72
92
|
for (const fo of R(opts)) {
|
|
73
93
|
const cst = child(fo, 'constant')
|
|
74
94
|
const name = optionNameOf(fo, cst)
|
|
75
|
-
if (cst
|
|
95
|
+
if (!cst || !name) continue
|
|
96
|
+
if ('json_name' === name) { out.jsonName = String(unquote(cst.src)); continue }
|
|
97
|
+
if ('default' === name) { out.defaultValue = unquote(cst.src); continue }
|
|
98
|
+
map[name] = constantValue(cst)
|
|
76
99
|
}
|
|
77
|
-
|
|
100
|
+
if (Object.keys(map).length) out.options = map
|
|
101
|
+
return out
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// The plain (non-pseudo) option map, for the places that cannot carry
|
|
105
|
+
// `json_name` / `default` — extension ranges, enum values.
|
|
106
|
+
function plainOptions(opts: Node | undefined): Record<string, OptionValue> | undefined {
|
|
107
|
+
return readFieldOptions(opts).options
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// optionStmt = "option" optionName "=" constant ";"
|
|
111
|
+
function optionFrom(el: Node): Record<string, OptionValue> {
|
|
112
|
+
const cst = child(el, 'constant')
|
|
113
|
+
if (!cst) return {}
|
|
114
|
+
const name = optionNameOf(el, cst)
|
|
115
|
+
return name ? { [name]: constantValue(cst) } : {}
|
|
78
116
|
}
|
|
79
117
|
|
|
80
118
|
// ---- fields --------------------------------------------------------------
|
|
@@ -99,8 +137,11 @@ function fieldTypeName(typeText: string): Pick<FieldDescriptorProto, 'type' | 't
|
|
|
99
137
|
const bare = typeText.replace(/^\./, '')
|
|
100
138
|
const scalar = SCALAR_TYPES[bare]
|
|
101
139
|
if (scalar) return { type: scalar }
|
|
102
|
-
//
|
|
103
|
-
|
|
140
|
+
// A named reference: could be a message OR an enum, and telling them
|
|
141
|
+
// apart needs symbol resolution this parser deliberately does not do.
|
|
142
|
+
// protoc leaves `type` unset here too, and only fills it in once the
|
|
143
|
+
// name resolves — so record `typeName` as written and nothing else.
|
|
144
|
+
return { typeName: typeText }
|
|
104
145
|
}
|
|
105
146
|
|
|
106
147
|
// A normal field: [label] fieldType ident "=" fieldNumber [fieldOptions].
|
|
@@ -113,12 +154,18 @@ function typeNodeOf(n: Node): Node | undefined {
|
|
|
113
154
|
return child(n, 'fieldType') ?? child(n, 'messageType') ?? child(n, 'fullIdent')
|
|
114
155
|
}
|
|
115
156
|
|
|
157
|
+
function applyFieldOptions(f: FieldDescriptorProto, opts: Node | undefined): void {
|
|
158
|
+
const po = readFieldOptions(opts)
|
|
159
|
+
if (undefined !== po.jsonName) f.jsonName = po.jsonName
|
|
160
|
+
if (undefined !== po.defaultValue) f.defaultValue = po.defaultValue
|
|
161
|
+
if (po.options) f.options = po.options
|
|
162
|
+
}
|
|
163
|
+
|
|
116
164
|
function buildField(n: Node, version: ProtoVersion): FieldDescriptorProto {
|
|
117
165
|
const label = child(n, 'label')
|
|
118
166
|
const type = typeNodeOf(n)
|
|
119
167
|
const name = child(n, 'ident')
|
|
120
168
|
const number = child(n, 'fieldNumber')
|
|
121
|
-
const opts = child(n, 'fieldOptions')
|
|
122
169
|
|
|
123
170
|
const f: FieldDescriptorProto = {
|
|
124
171
|
name: name ? name.src : '',
|
|
@@ -126,11 +173,64 @@ function buildField(n: Node, version: ProtoVersion): FieldDescriptorProto {
|
|
|
126
173
|
...fieldLabel(label, version),
|
|
127
174
|
...fieldTypeName(type ? type.src : ''),
|
|
128
175
|
}
|
|
129
|
-
|
|
130
|
-
|
|
176
|
+
applyFieldOptions(f, child(n, 'fieldOptions'))
|
|
177
|
+
return f
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// A group is a field plus an implicit nested message. protoc lowercases the
|
|
181
|
+
// field name and keeps the declared name for the message and for `typeName`.
|
|
182
|
+
// `optional group TestGroup = 1 { … }`
|
|
183
|
+
// -> field { name: 'testgroup', type: TYPE_GROUP, typeName: 'TestGroup' }
|
|
184
|
+
// -> nestedType { name: 'TestGroup', … }
|
|
185
|
+
function buildGroup(
|
|
186
|
+
n: Node, version: ProtoVersion, into: DescriptorProto,
|
|
187
|
+
): FieldDescriptorProto {
|
|
188
|
+
const groupName = child(n, 'ident')?.src ?? ''
|
|
189
|
+
const number = child(n, 'fieldNumber')
|
|
190
|
+
into.nestedType.push(messageFromBody(groupName, child(n, 'messageBody'), version))
|
|
191
|
+
|
|
192
|
+
const f: FieldDescriptorProto = {
|
|
193
|
+
name: groupName.toLowerCase(),
|
|
194
|
+
number: number ? Number(number.src) : 0,
|
|
195
|
+
...fieldLabel(child(n, 'label'), version),
|
|
196
|
+
type: 'TYPE_GROUP',
|
|
197
|
+
typeName: groupName,
|
|
198
|
+
}
|
|
199
|
+
applyFieldOptions(f, child(n, 'fieldOptions'))
|
|
131
200
|
return f
|
|
132
201
|
}
|
|
133
202
|
|
|
203
|
+
// A group is the one construct with BOTH a field number and a message body;
|
|
204
|
+
// `message` has a body but no number, `map`/`field` a number but no body.
|
|
205
|
+
function isGroup(n: Node): boolean {
|
|
206
|
+
return !!child(n, 'fieldNumber') && !!child(n, 'messageBody')
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// The subset of an option map whose names are rooted at `features`.
|
|
210
|
+
function features(
|
|
211
|
+
opts: Record<string, OptionValue> | undefined,
|
|
212
|
+
): Record<string, OptionValue> | undefined {
|
|
213
|
+
if (!opts) return undefined
|
|
214
|
+
const out: Record<string, OptionValue> = {}
|
|
215
|
+
for (const [k, v] of Object.entries(opts)) {
|
|
216
|
+
if ('features' === k || k.startsWith('features.')) out[k] = v
|
|
217
|
+
}
|
|
218
|
+
return Object.keys(out).length ? out : undefined
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// protoc's map-entry name: strip `_`, upper-case the letter that follows
|
|
222
|
+
// (and the first letter), then append `Entry`. `map_field` -> `MapFieldEntry`.
|
|
223
|
+
function mapEntryName(fieldName: string): string {
|
|
224
|
+
let out = ''
|
|
225
|
+
let capNext = true
|
|
226
|
+
for (const ch of fieldName) {
|
|
227
|
+
if ('_' === ch) { capNext = true; continue }
|
|
228
|
+
out += capNext && 'a' <= ch && ch <= 'z' ? ch.toUpperCase() : ch
|
|
229
|
+
capNext = false
|
|
230
|
+
}
|
|
231
|
+
return out + 'Entry'
|
|
232
|
+
}
|
|
233
|
+
|
|
134
234
|
// map<K,V> name = N; -> a repeated message field whose type is a
|
|
135
235
|
// synthesised nested `<Name>Entry` message with mapEntry=true.
|
|
136
236
|
function buildMapField(
|
|
@@ -142,15 +242,15 @@ function buildMapField(
|
|
|
142
242
|
const keyText = types[0] ? types[0].src : ''
|
|
143
243
|
const valText = types[1] ? types[1].src : ''
|
|
144
244
|
const fname = name ? name.src : ''
|
|
145
|
-
const entryName = fname
|
|
146
|
-
+ 'Entry'
|
|
245
|
+
const entryName = mapEntryName(fname)
|
|
147
246
|
|
|
247
|
+
const key: FieldDescriptorProto =
|
|
248
|
+
{ name: 'key', number: 1, label: 'LABEL_OPTIONAL', ...fieldTypeName(keyText) }
|
|
249
|
+
const value: FieldDescriptorProto =
|
|
250
|
+
{ name: 'value', number: 2, label: 'LABEL_OPTIONAL', ...fieldTypeName(valText) }
|
|
148
251
|
const entry: DescriptorProto = {
|
|
149
252
|
name: entryName,
|
|
150
|
-
field: [
|
|
151
|
-
{ name: 'key', number: 1, label: 'LABEL_OPTIONAL', ...fieldTypeName(keyText) },
|
|
152
|
-
{ name: 'value', number: 2, label: 'LABEL_OPTIONAL', ...fieldTypeName(valText) },
|
|
153
|
-
],
|
|
253
|
+
field: [key, value],
|
|
154
254
|
nestedType: [], enumType: [], oneofDecl: [], extension: [],
|
|
155
255
|
options: { mapEntry: true },
|
|
156
256
|
}
|
|
@@ -160,11 +260,14 @@ function buildMapField(
|
|
|
160
260
|
name: fname,
|
|
161
261
|
number: number ? Number(number.src) : 0,
|
|
162
262
|
label: 'LABEL_REPEATED',
|
|
163
|
-
type: 'TYPE_MESSAGE',
|
|
164
263
|
typeName: entryName,
|
|
165
264
|
}
|
|
166
|
-
|
|
167
|
-
|
|
265
|
+
applyFieldOptions(f, child(n, 'fieldOptions'))
|
|
266
|
+
|
|
267
|
+
// `features` on a map field govern the synthesised entry's key and value
|
|
268
|
+
// fields too, so protoc copies them down. Nothing else is propagated.
|
|
269
|
+
const feat = features(f.options)
|
|
270
|
+
if (feat) { key.options = { ...feat }; value.options = { ...feat } }
|
|
168
271
|
return f
|
|
169
272
|
}
|
|
170
273
|
|
|
@@ -175,18 +278,22 @@ function buildEnum(n: Node): EnumDescriptorProto {
|
|
|
175
278
|
for (const el of R(n).filter((k) => k.rule === 'enumElement')) {
|
|
176
279
|
const k = kw(el)
|
|
177
280
|
if (k.startsWith('reserved')) {
|
|
178
|
-
|
|
281
|
+
// Enum reserved ranges are INCLUSIVE and span the whole int32 space.
|
|
282
|
+
addReserved(el, e, { exclusive: false, max: MAX_ENUM_NUMBER })
|
|
179
283
|
continue
|
|
180
284
|
}
|
|
181
285
|
if (k.startsWith('option')) {
|
|
182
|
-
|
|
286
|
+
e.options = { ...(e.options || {}), ...optionFrom(el) }
|
|
287
|
+
continue
|
|
183
288
|
}
|
|
184
289
|
// enumField: ident "=" ["-"] fieldNumber -> name is the kw before "="
|
|
185
|
-
const name =
|
|
290
|
+
const name = k.replace(/=.*$/, '')
|
|
186
291
|
const num = child(el, 'fieldNumber')
|
|
187
292
|
if (name && num) {
|
|
188
293
|
const neg = /=-/.test(el.src.replace(/\s+/g, ''))
|
|
189
|
-
|
|
294
|
+
const v = { name, number: (neg ? -1 : 1) * Number(num.src) }
|
|
295
|
+
const vo = plainOptions(child(el, 'fieldOptions'))
|
|
296
|
+
e.value.push(vo ? { ...v, options: vo } : v)
|
|
190
297
|
}
|
|
191
298
|
}
|
|
192
299
|
return e
|
|
@@ -194,31 +301,51 @@ function buildEnum(n: Node): EnumDescriptorProto {
|
|
|
194
301
|
|
|
195
302
|
// ---- reserved / extensions ranges ----------------------------------------
|
|
196
303
|
|
|
304
|
+
type RangeOpts = { exclusive: boolean; max: number }
|
|
305
|
+
|
|
197
306
|
// `ranges = range *( "," range )`. The leading `range` is inlined into
|
|
198
307
|
// `ranges.src`, so parse the (whitespace-stripped) text rather than kids.
|
|
199
|
-
function ranges(rangesNode: Node | undefined
|
|
308
|
+
function ranges(rangesNode: Node | undefined, ro: RangeOpts): DescriptorRange[] {
|
|
200
309
|
if (!rangesNode) return []
|
|
201
|
-
const out:
|
|
310
|
+
const out: DescriptorRange[] = []
|
|
202
311
|
for (const part of rangesNode.src.replace(/\s+/g, '').split(',')) {
|
|
203
312
|
const m = part.match(/^(-?\d+)(?:to(-?\d+|max))?$/)
|
|
204
313
|
if (!m) continue
|
|
205
314
|
const start = Number(m[1])
|
|
206
|
-
|
|
315
|
+
let end: number
|
|
316
|
+
if (!m[2]) end = ro.exclusive ? start + 1 : start
|
|
317
|
+
else if ('max' === m[2]) end = ro.max
|
|
318
|
+
else end = Number(m[2]) + (ro.exclusive ? 1 : 0)
|
|
207
319
|
out.push({ start, end })
|
|
208
320
|
}
|
|
209
321
|
return out
|
|
210
322
|
}
|
|
211
323
|
|
|
212
|
-
|
|
324
|
+
// The reserved-name list. Both the leading `strLit`/`ident` and (for a
|
|
325
|
+
// single-item list) the whole list can be inlined into `src`, so read the
|
|
326
|
+
// names out of the statement text; whole-word tokens make that unambiguous.
|
|
327
|
+
function reservedNames(n: Node): string[] {
|
|
328
|
+
const body = n.src.replace(/^reserved/, '').replace(/;$/, '')
|
|
329
|
+
const re = /"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'|([A-Za-z_][A-Za-z0-9_]*)/g
|
|
330
|
+
const out: string[] = []
|
|
331
|
+
let m: RegExpExecArray | null
|
|
332
|
+
while (null !== (m = re.exec(body))) out.push(m[1] ?? m[2] ?? m[3])
|
|
333
|
+
return out
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function addReserved(
|
|
337
|
+
n: Node,
|
|
338
|
+
target: { reservedRange?: DescriptorRange[]; reservedName?: string[] },
|
|
339
|
+
ro: RangeOpts,
|
|
340
|
+
): void {
|
|
213
341
|
const rn = child(n, 'ranges')
|
|
214
342
|
if (rn) {
|
|
215
|
-
(target.reservedRange = target.reservedRange || []).push(...ranges(rn))
|
|
343
|
+
(target.reservedRange = target.reservedRange || []).push(...ranges(rn, ro))
|
|
216
344
|
return
|
|
217
345
|
}
|
|
218
|
-
const names =
|
|
219
|
-
if (names) {
|
|
220
|
-
|
|
221
|
-
;(target.reservedName = target.reservedName || []).push(...list)
|
|
346
|
+
const names = reservedNames(n)
|
|
347
|
+
if (names.length) {
|
|
348
|
+
(target.reservedName = target.reservedName || []).push(...names)
|
|
222
349
|
}
|
|
223
350
|
}
|
|
224
351
|
|
|
@@ -227,39 +354,87 @@ function addReserved(n: Node, target: { reservedRange?: any[]; reservedName?: st
|
|
|
227
354
|
function buildMessage(n: Node, version: ProtoVersion): DescriptorProto {
|
|
228
355
|
// `n` is a dispatch node whose `message` alt was inlined: kids are
|
|
229
356
|
// [ident, messageBody-children…] or [ident] then messageBody.
|
|
357
|
+
return messageFromBody(
|
|
358
|
+
child(n, 'ident')?.src ?? '', child(n, 'messageBody'), version)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function messageFromBody(
|
|
362
|
+
name: string, body: Node | undefined, version: ProtoVersion,
|
|
363
|
+
): DescriptorProto {
|
|
230
364
|
const msg: DescriptorProto = {
|
|
231
|
-
name
|
|
365
|
+
name,
|
|
232
366
|
field: [], nestedType: [], enumType: [], oneofDecl: [], extension: [],
|
|
233
367
|
}
|
|
234
|
-
const body = child(n, 'messageBody')
|
|
235
368
|
const elements = body ? R(body).filter((k) => k.rule === 'messageElement') : []
|
|
236
|
-
|
|
369
|
+
|
|
370
|
+
// Options first: `message_set_wire_format` changes what `to max` means in
|
|
371
|
+
// an extension/reserved range, and protoc applies it wherever the option
|
|
372
|
+
// sits in the body.
|
|
373
|
+
for (const el of elements) {
|
|
374
|
+
if (isOptionStmt(el)) msg.options = { ...(msg.options || {}), ...optionFrom(el) }
|
|
375
|
+
}
|
|
376
|
+
const ro: RangeOpts = {
|
|
377
|
+
exclusive: true,
|
|
378
|
+
max: true === msg.options?.message_set_wire_format
|
|
379
|
+
? MAX_MESSAGE_SET_END : MAX_FIELD_NUMBER_END,
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
for (const el of elements) {
|
|
383
|
+
if (!isOptionStmt(el)) addMessageElement(el, version, msg, ro)
|
|
384
|
+
}
|
|
385
|
+
generateSyntheticOneofs(msg)
|
|
237
386
|
return msg
|
|
238
387
|
}
|
|
239
388
|
|
|
240
|
-
|
|
389
|
+
// An `option` statement, as opposed to an `optional`-labelled field (whose
|
|
390
|
+
// `kw` is empty because the `label` node starts the src).
|
|
391
|
+
function isOptionStmt(el: Node): boolean {
|
|
392
|
+
return kw(el).startsWith('option')
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// protoc synthesises a single-field oneof for every proto3 explicit
|
|
396
|
+
// `optional` field, appended after the declared oneofs. The name is the
|
|
397
|
+
// field name prefixed with `_`, then prefixed with `X` until unique.
|
|
398
|
+
function generateSyntheticOneofs(msg: DescriptorProto): void {
|
|
399
|
+
const names = new Set<string>()
|
|
400
|
+
for (const f of msg.field) names.add(f.name)
|
|
401
|
+
for (const o of msg.oneofDecl) names.add(o.name)
|
|
402
|
+
for (const f of msg.field) {
|
|
403
|
+
if (true !== f.proto3Optional || undefined !== f.oneofIndex) continue
|
|
404
|
+
let oneofName = '_' + f.name
|
|
405
|
+
while (names.has(oneofName)) oneofName = 'X' + oneofName
|
|
406
|
+
names.add(oneofName)
|
|
407
|
+
f.oneofIndex = msg.oneofDecl.length
|
|
408
|
+
msg.oneofDecl.push({ name: oneofName })
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function addMessageElement(
|
|
413
|
+
el: Node, version: ProtoVersion, msg: DescriptorProto, ro: RangeOpts,
|
|
414
|
+
): void {
|
|
241
415
|
const k = kw(el)
|
|
242
416
|
const first = R(el)[0]
|
|
243
417
|
if (k.startsWith('map<')) { msg.field.push(buildMapField(el, version, msg)); return }
|
|
244
418
|
if (k.startsWith('oneof')) { addOneof(el, version, msg); return }
|
|
245
419
|
if (k.startsWith('export') || k.startsWith('local')) {
|
|
246
420
|
// edition 2024 symbol visibility wraps the message/enum as a child node.
|
|
247
|
-
|
|
248
|
-
if (m) msg.nestedType.push(buildMessage(m, version))
|
|
249
|
-
else if (e) msg.enumType.push(buildEnum(e))
|
|
421
|
+
addVisible(el, k, version, msg.nestedType, msg.enumType)
|
|
250
422
|
return
|
|
251
423
|
}
|
|
424
|
+
if (isGroup(el)) { msg.field.push(buildGroup(el, version, msg)); return }
|
|
252
425
|
if (k.startsWith('message')) { msg.nestedType.push(buildMessage(el, version)); return }
|
|
253
426
|
if (k.startsWith('enum')) { msg.enumType.push(buildEnum(el)); return }
|
|
254
|
-
if (k.startsWith('reserved')) { addReserved(el, msg); return }
|
|
427
|
+
if (k.startsWith('reserved')) { addReserved(el, msg, ro); return }
|
|
255
428
|
if (k.startsWith('extensions')) {
|
|
256
|
-
|
|
429
|
+
const opts = plainOptions(child(el, 'fieldOptions'))
|
|
430
|
+
const rs = ranges(child(el, 'ranges'), ro)
|
|
431
|
+
// A compound `extensions 2, 9 to 11 [(i) = 5];` puts the options on
|
|
432
|
+
// every range, as protoc does.
|
|
433
|
+
for (const r of rs) if (opts) r.options = { ...opts }
|
|
434
|
+
;(msg.extensionRange = msg.extensionRange || []).push(...rs)
|
|
257
435
|
return
|
|
258
436
|
}
|
|
259
437
|
if (k.startsWith('extend')) { addExtend(el, version, msg.extension); return }
|
|
260
|
-
if (k.startsWith('option')) {
|
|
261
|
-
msg.options = { ...(msg.options || {}), ...optionFrom(el) }; return
|
|
262
|
-
}
|
|
263
438
|
if (';' === el.src) return // emptyStmt
|
|
264
439
|
// No keyword and a fieldType/label lead => a field.
|
|
265
440
|
if (first && (first.rule === 'fieldType' || first.rule === 'label')) {
|
|
@@ -267,6 +442,20 @@ function addMessageElement(el: Node, version: ProtoVersion, msg: DescriptorProto
|
|
|
267
442
|
}
|
|
268
443
|
}
|
|
269
444
|
|
|
445
|
+
// edition 2024 `export` / `local` on a message or enum declaration. The
|
|
446
|
+
// wrapped `message` / `enumDef` stays a child node instead of inlining.
|
|
447
|
+
function addVisible(
|
|
448
|
+
el: Node, k: string, version: ProtoVersion,
|
|
449
|
+
messages: DescriptorProto[], enums: EnumDescriptorProto[],
|
|
450
|
+
): void {
|
|
451
|
+
const vis: SymbolVisibility =
|
|
452
|
+
k.startsWith('export') ? 'VISIBILITY_EXPORT' : 'VISIBILITY_LOCAL'
|
|
453
|
+
const m = child(el, 'message')
|
|
454
|
+
const e = child(el, 'enumDef')
|
|
455
|
+
if (m) messages.push({ ...buildMessage(m, version), visibility: vis })
|
|
456
|
+
else if (e) enums.push({ ...buildEnum(e), visibility: vis })
|
|
457
|
+
}
|
|
458
|
+
|
|
270
459
|
function addOneof(el: Node, version: ProtoVersion, msg: DescriptorProto): void {
|
|
271
460
|
const name = child(el, 'ident')?.src ?? ''
|
|
272
461
|
const index = msg.oneofDecl.length
|
|
@@ -274,7 +463,7 @@ function addOneof(el: Node, version: ProtoVersion, msg: DescriptorProto): void {
|
|
|
274
463
|
for (const of of R(el).filter((k) => k.rule === 'oneofElement')) {
|
|
275
464
|
if (kw(of).startsWith('option')) continue
|
|
276
465
|
if (';' === of.src) continue
|
|
277
|
-
const f = buildField(of, version)
|
|
466
|
+
const f = isGroup(of) ? buildGroup(of, version, msg) : buildField(of, version)
|
|
278
467
|
f.oneofIndex = index
|
|
279
468
|
delete f.proto3Optional // explicit oneof members aren't proto3-optional
|
|
280
469
|
msg.field.push(f)
|
|
@@ -283,27 +472,21 @@ function addOneof(el: Node, version: ProtoVersion, msg: DescriptorProto): void {
|
|
|
283
472
|
|
|
284
473
|
function addExtend(el: Node, version: ProtoVersion, into: FieldDescriptorProto[]): void {
|
|
285
474
|
// extend messageType "{" *field "}" — fields inline as messageElement-like.
|
|
475
|
+
const extendee = child(el, 'messageType')?.src
|
|
286
476
|
for (const f of R(el).filter((k) => k.rule === 'field' || k.rule === 'messageElement')) {
|
|
287
|
-
|
|
477
|
+
const fd = buildField(f, version)
|
|
478
|
+
if (extendee) fd.extendee = extendee
|
|
479
|
+
into.push(fd)
|
|
288
480
|
}
|
|
289
481
|
}
|
|
290
482
|
|
|
291
|
-
// ---- options -------------------------------------------------------------
|
|
292
|
-
|
|
293
|
-
// optionStmt = "option" optionName "=" constant ";"
|
|
294
|
-
function optionFrom(el: Node): Record<string, OptionValue> {
|
|
295
|
-
const cst = child(el, 'constant')
|
|
296
|
-
if (!cst) return {}
|
|
297
|
-
const name = optionNameOf(el, cst).replace(/^option/, '')
|
|
298
|
-
return { [name]: constantValue(cst) }
|
|
299
|
-
}
|
|
300
|
-
|
|
301
483
|
// ---- services ------------------------------------------------------------
|
|
302
484
|
|
|
303
485
|
function buildService(n: Node): ServiceDescriptorProto {
|
|
304
486
|
const svc: ServiceDescriptorProto = { name: child(n, 'ident')?.src ?? '', method: [] }
|
|
305
487
|
for (const el of R(n).filter((k) => k.rule === 'serviceElement')) {
|
|
306
488
|
if (kw(el).startsWith('rpc')) svc.method.push(buildRpc(el))
|
|
489
|
+
else if (isOptionStmt(el)) svc.options = { ...(svc.options || {}), ...optionFrom(el) }
|
|
307
490
|
}
|
|
308
491
|
return svc
|
|
309
492
|
}
|
|
@@ -325,6 +508,9 @@ function buildRpc(el: Node): MethodDescriptorProto {
|
|
|
325
508
|
const response = ri >= 0 ? flat.slice(ri) : ''
|
|
326
509
|
if (/\(stream/.test(request)) m.clientStreaming = true
|
|
327
510
|
if (/\(stream/.test(response)) m.serverStreaming = true
|
|
511
|
+
for (const o of R(el).filter((k) => k.rule === 'optionStmt')) {
|
|
512
|
+
m.options = { ...(m.options || {}), ...optionFrom(o) }
|
|
513
|
+
}
|
|
328
514
|
return m
|
|
329
515
|
}
|
|
330
516
|
|
|
@@ -335,8 +521,12 @@ export function buildFile(proto: Node, version: ProtoVersion): FileDescriptorPro
|
|
|
335
521
|
dependency: [], publicDependency: [], weakDependency: [],
|
|
336
522
|
messageType: [], enumType: [], service: [], extension: [],
|
|
337
523
|
}
|
|
338
|
-
if (isEdition(version))
|
|
339
|
-
|
|
524
|
+
if (isEdition(version)) {
|
|
525
|
+
file.edition = 'EDITION_' + version
|
|
526
|
+
file.syntax = 'editions'
|
|
527
|
+
} else {
|
|
528
|
+
file.syntax = version
|
|
529
|
+
}
|
|
340
530
|
|
|
341
531
|
for (const def of R(proto).filter((k) => k.rule === 'topLevelDef')) {
|
|
342
532
|
const k = kw(def)
|
|
@@ -345,17 +535,20 @@ export function buildFile(proto: Node, version: ProtoVersion): FileDescriptorPro
|
|
|
345
535
|
} else if (k.startsWith('import')) {
|
|
346
536
|
const s = child(def, 'strLit')
|
|
347
537
|
if (s) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
538
|
+
// `import option "x";` (edition 2024) is a separate dependency list.
|
|
539
|
+
if (k.includes('option')) {
|
|
540
|
+
(file.optionDependency = file.optionDependency || []).push(unquote(s.src))
|
|
541
|
+
} else {
|
|
542
|
+
const idx = file.dependency.length
|
|
543
|
+
file.dependency.push(unquote(s.src))
|
|
544
|
+
if (k.includes('public')) file.publicDependency.push(idx)
|
|
545
|
+
if (k.includes('weak')) file.weakDependency.push(idx)
|
|
546
|
+
}
|
|
352
547
|
}
|
|
353
|
-
} else if (
|
|
548
|
+
} else if (isOptionStmt(def)) {
|
|
354
549
|
file.options = { ...(file.options || {}), ...optionFrom(def) }
|
|
355
550
|
} else if (k.startsWith('export') || k.startsWith('local')) {
|
|
356
|
-
|
|
357
|
-
if (m) file.messageType.push(buildMessage(m, version))
|
|
358
|
-
else if (e) file.enumType.push(buildEnum(e))
|
|
551
|
+
addVisible(def, k, version, file.messageType, file.enumType)
|
|
359
552
|
} else if (k.startsWith('message')) {
|
|
360
553
|
file.messageType.push(buildMessage(def, version))
|
|
361
554
|
} else if (k.startsWith('enum')) {
|
package/src/descriptor.ts
CHANGED
|
@@ -19,6 +19,17 @@ export type FieldType =
|
|
|
19
19
|
|
|
20
20
|
export type OptionValue = string | number | boolean | { [k: string]: OptionValue }
|
|
21
21
|
|
|
22
|
+
// Symbol visibility, an edition-2024 feature (`export` / `local`).
|
|
23
|
+
export type SymbolVisibility = 'VISIBILITY_EXPORT' | 'VISIBILITY_LOCAL'
|
|
24
|
+
|
|
25
|
+
// A numeric range. `end` is EXCLUSIVE for message extension/reserved ranges
|
|
26
|
+
// and INCLUSIVE for enum reserved ranges — the same asymmetry protoc has.
|
|
27
|
+
export interface DescriptorRange {
|
|
28
|
+
start: number
|
|
29
|
+
end: number
|
|
30
|
+
options?: Record<string, OptionValue>
|
|
31
|
+
}
|
|
32
|
+
|
|
22
33
|
export interface FieldDescriptorProto {
|
|
23
34
|
name: string
|
|
24
35
|
number: number
|
|
@@ -26,6 +37,13 @@ export interface FieldDescriptorProto {
|
|
|
26
37
|
type?: FieldType
|
|
27
38
|
// Set for message/enum/group field types (resolution deferred).
|
|
28
39
|
typeName?: string
|
|
40
|
+
// For an `extend` member: the message being extended, as written.
|
|
41
|
+
extendee?: string
|
|
42
|
+
// The `json_name = "..."` pseudo-option, lifted out of `options`.
|
|
43
|
+
jsonName?: string
|
|
44
|
+
// The `default = ...` pseudo-option, lifted out of `options`. Always a
|
|
45
|
+
// string, as in descriptor.proto; the literal is stored as written.
|
|
46
|
+
defaultValue?: string
|
|
29
47
|
// proto3 explicit `optional` (synthesises a single-field oneof in protoc).
|
|
30
48
|
proto3Optional?: boolean
|
|
31
49
|
// The oneof this field belongs to, as an index into the message's
|
|
@@ -43,8 +61,9 @@ export interface EnumValueDescriptorProto {
|
|
|
43
61
|
export interface EnumDescriptorProto {
|
|
44
62
|
name: string
|
|
45
63
|
value: EnumValueDescriptorProto[]
|
|
46
|
-
reservedRange?:
|
|
64
|
+
reservedRange?: DescriptorRange[]
|
|
47
65
|
reservedName?: string[]
|
|
66
|
+
visibility?: SymbolVisibility
|
|
48
67
|
options?: Record<string, OptionValue>
|
|
49
68
|
}
|
|
50
69
|
|
|
@@ -60,9 +79,10 @@ export interface DescriptorProto {
|
|
|
60
79
|
enumType: EnumDescriptorProto[]
|
|
61
80
|
oneofDecl: OneofDescriptorProto[]
|
|
62
81
|
extension: FieldDescriptorProto[]
|
|
63
|
-
extensionRange?:
|
|
64
|
-
reservedRange?:
|
|
82
|
+
extensionRange?: DescriptorRange[]
|
|
83
|
+
reservedRange?: DescriptorRange[]
|
|
65
84
|
reservedName?: string[]
|
|
85
|
+
visibility?: SymbolVisibility
|
|
66
86
|
options?: Record<string, OptionValue>
|
|
67
87
|
}
|
|
68
88
|
|
|
@@ -88,17 +108,27 @@ export interface FileDescriptorProto {
|
|
|
88
108
|
dependency: string[]
|
|
89
109
|
publicDependency: number[]
|
|
90
110
|
weakDependency: number[]
|
|
111
|
+
// `import option "..."` targets (edition 2024). Present only when used.
|
|
112
|
+
optionDependency?: string[]
|
|
91
113
|
messageType: DescriptorProto[]
|
|
92
114
|
enumType: EnumDescriptorProto[]
|
|
93
115
|
service: ServiceDescriptorProto[]
|
|
94
116
|
extension: FieldDescriptorProto[]
|
|
95
117
|
options?: Record<string, OptionValue>
|
|
96
|
-
// 'proto2' | 'proto3' for syntax files;
|
|
118
|
+
// 'proto2' | 'proto3' for syntax files; 'editions' for edition files.
|
|
97
119
|
syntax?: string
|
|
98
120
|
// 'EDITION_2023' | 'EDITION_2024' for edition files; absent otherwise.
|
|
99
121
|
edition?: string
|
|
100
122
|
}
|
|
101
123
|
|
|
124
|
+
// protoc's sentinels for `to max` in a range.
|
|
125
|
+
// Message field numbers stop at 2^29-1, so an exclusive `end` is 2^29;
|
|
126
|
+
// a message_set_wire_format message may use the full 32-bit space.
|
|
127
|
+
export const MAX_FIELD_NUMBER_END = 536870912
|
|
128
|
+
export const MAX_MESSAGE_SET_END = 2147483647
|
|
129
|
+
// Enum numbers are plain int32 and enum reserved ranges are inclusive.
|
|
130
|
+
export const MAX_ENUM_NUMBER = 2147483647
|
|
131
|
+
|
|
102
132
|
// Scalar protobuf types -> FieldDescriptorProto.type. A field whose type
|
|
103
133
|
// is not in this table is a message/enum/group reference (resolution
|
|
104
134
|
// deferred) and gets a `typeName` instead.
|
package/src/grammar.ts
CHANGED
|
@@ -59,11 +59,19 @@ extendStmt = "extend" messageType "{" *( field / emptyStmt ) "}"
|
|
|
59
59
|
extensions = "extensions" ranges [ fieldOptions ] ";"
|
|
60
60
|
reserved = "reserved" ( ranges / fieldNames ) ";"
|
|
61
61
|
ranges = range *( "," range )
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
; Enum reserved ranges may be negative, so a range bound is signed.
|
|
63
|
+
range = [ "-" ] NR [ "to" ( [ "-" ] NR / "max" ) ]
|
|
64
|
+
; Reserved names are string literals in proto2/proto3 and bare identifiers
|
|
65
|
+
; from edition 2023 on; the union accepts either (and a mix).
|
|
66
|
+
fieldNames = reservedName *( "," reservedName )
|
|
67
|
+
reservedName = strLit / ident
|
|
68
|
+
|
|
69
|
+
constant = boolLit / fullIdent / signedNumber / signedIdent / strLit
|
|
70
|
+
/ messageValue
|
|
66
71
|
signedNumber = [ "-" / "+" ] NR
|
|
72
|
+
; \`-inf\` / \`-nan\`: a negated identifier constant. Tried after signedNumber
|
|
73
|
+
; so \`-33\` still lexes as a number.
|
|
74
|
+
signedIdent = "-" fullIdent
|
|
67
75
|
boolLit = VL
|
|
68
76
|
messageValue = "{" *messageValueEntry "}"
|
|
69
77
|
messageValueEntry = ident [ ":" ] constant [ "," / ";" ]
|
|
@@ -79,6 +87,8 @@ emptyStmt = ";"
|
|
|
79
87
|
; optional/repeated), extend, and extensions live in common (the permissive
|
|
80
88
|
; union); the descriptor walk enforces what each version actually allows.
|
|
81
89
|
messageElement =/ groupField
|
|
90
|
+
; A group is also a legal oneof member (\`oneof foo { group D = 4 {…} }\`).
|
|
91
|
+
oneofElement =/ groupField
|
|
82
92
|
groupField = [ label ] "group" ident "=" fieldNumber messageBody
|
|
83
93
|
|
|
84
94
|
; ===== proto3.abnf =====
|
|
@@ -99,4 +109,9 @@ importStmt =/ "import" "option" strLit ";"
|
|
|
99
109
|
topLevelDef =/ symbolVisibility message / symbolVisibility enumDef
|
|
100
110
|
messageElement =/ symbolVisibility message / symbolVisibility enumDef
|
|
101
111
|
symbolVisibility = "export" / "local"
|
|
112
|
+
; \`export\` and \`local\` are contextual: they are only visibility modifiers
|
|
113
|
+
; ahead of \`message\`/\`enum\`, and stay usable as ordinary name components
|
|
114
|
+
; (\`local.pkg.SomeMessage\`). The union grammar lexes them as keywords, so
|
|
115
|
+
; give fullIdent an alternative that may open with one.
|
|
116
|
+
fullIdent =/ symbolVisibility *( "." ident )
|
|
102
117
|
`
|