@tabnas/proto 0.2.2 → 0.3.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 CHANGED
@@ -54,14 +54,30 @@ reconcile?: boolean }`.
54
54
  ## What it produces
55
55
 
56
56
  A `FileDescriptorProto`-shaped object (the `descriptor.proto` JSON shape):
57
- `package`, `dependency` (+ `publicDependency` / `weakDependency`),
58
- `messageType` (recursive `DescriptorProto` with `field`, `nestedType`,
59
- `enumType`, `oneofDecl`, `extensionRange`, `reservedRange`), `enumType`,
60
- `service`, `extension`, `options`, and `syntax` / `edition`. `map<K,V>`
61
- fields are expanded to a repeated message field plus a synthesised
62
- `…Entry` nested message with `options.mapEntry = true`, exactly as `protoc`
63
- does. Type names are stored as written; cross-file resolution is a separate
64
- concern.
57
+ `package`, `dependency` (+ `publicDependency` / `weakDependency` /
58
+ `optionDependency`), `messageType` (recursive `DescriptorProto` with
59
+ `field`, `nestedType`, `enumType`, `oneofDecl`, `extensionRange`,
60
+ `reservedRange`), `enumType`, `service`, `extension`, `options`, and
61
+ `syntax` / `edition`. `map<K,V>` fields are expanded to a repeated message
62
+ field plus a synthesised `…Entry` nested message with
63
+ `options.mapEntry = true`, groups to a `TYPE_GROUP` field plus a nested
64
+ message, and a proto3 explicit `optional` to a synthetic `_<field>` oneof —
65
+ exactly as `protoc` does. Type names are stored as written and `type` is
66
+ left unset for them; cross-file resolution is a separate concern.
67
+
68
+ Two things are deliberately shaped for readability rather than byte-for-byte
69
+ `protoc` parity: options are a plain `{ name: value }` map (not an
70
+ `uninterpretedOption` list), and `defaultValue` keeps the literal as
71
+ written. See [doc/reference.md](doc/reference.md).
72
+
73
+ ## Conformance
74
+
75
+ Checked against protoc 35.1's own parser test corpus (extracted from
76
+ `parser_unittest.cc`): every one of the 71 in-scope `valid` cases and all
77
+ 50 `accept-only` cases, run by `test/protobuf-conformance.test.ts`. The 11
78
+ excluded cases declare protoc-internal editions (`UNSTABLE`,
79
+ `99998_TEST_ONLY`) outside the proto2 / proto3 / 2023 / 2024 support this
80
+ package claims.
65
81
 
66
82
  See [doc/tutorial.md](doc/tutorial.md), [doc/guide.md](doc/guide.md),
67
83
  [doc/reference.md](doc/reference.md), and [doc/concepts.md](doc/concepts.md).
@@ -10,9 +10,11 @@ exports.buildFile = buildFile;
10
10
  // kind from the keyword that precedes the node's first child (`kw`).
11
11
  const descriptor_1 = require("./descriptor");
12
12
  const detect_version_1 = require("./detect-version");
13
- // Children that are real rule nodes (terminals fold into `src`).
13
+ // Children that are real rule nodes (terminals fold into `src`). An empty
14
+ // `.proto` is a legal (if useless) file and parses to no node at all, so
15
+ // tolerate a missing node here rather than crash at the root.
14
16
  function R(n) {
15
- return (n.kids || []).filter((k) => k && k.rule);
17
+ return ((n && n.kids) || []).filter((k) => k && k.rule);
16
18
  }
17
19
  // The keyword(s) consumed before this node's first child — the part of
18
20
  // `src` ahead of the first child's `src`. For `message Foo {…}` the first
@@ -45,31 +47,66 @@ function constantValue(n) {
45
47
  if (!Number.isNaN(num))
46
48
  return num;
47
49
  }
48
- return s; // identifier (enum value name, inf, nan, …) kept verbatim
50
+ return s; // identifier (enum value name, inf, -nan, …) kept verbatim
49
51
  }
50
- // The option name in an `optionName "=" constant` statement. Because the
51
- // name's leading `optionNamePart` is inlined and the parts are split across
52
- // the node's `src` and later children, read it as everything before the
53
- // `constant`, with the trailing `=` removed.
52
+ // The option name in an `optionName "=" constant` statement.
53
+ //
54
+ // `optionStmt` keeps `optionName` as a child node, so use it when present.
55
+ // Inside `fieldOption` abnf inlines the leading `optionNamePart`, so there
56
+ // the name has to be read out of `src` — as everything BEFORE the trailing
57
+ // `"=" constant`. Anchoring at the end matters: `option (file_opt1) = 1;`
58
+ // would otherwise find the value `1` inside the name `file_opt1`.
54
59
  function optionNameOf(stmt, valueNode) {
60
+ const named = child(stmt, 'optionName');
61
+ if (named)
62
+ return named.src;
55
63
  if (!valueNode)
56
64
  return '';
57
- const i = stmt.src.indexOf(valueNode.src);
58
- return (i <= 0 ? '' : stmt.src.slice(0, i)).replace(/=$/, '');
65
+ const s = stmt.src.replace(/;$/, '');
66
+ const tail = '=' + valueNode.src;
67
+ if (s.endsWith(tail))
68
+ return s.slice(0, s.length - tail.length);
69
+ const i = s.indexOf(valueNode.src);
70
+ return i <= 0 ? '' : s.slice(0, i).replace(/=$/, '');
59
71
  }
60
72
  // fieldOptions = "[" fieldOption *( "," fieldOption ) "]"
61
73
  // fieldOption = optionName "=" constant
62
74
  function readFieldOptions(opts) {
63
- if (!opts)
64
- return undefined;
65
75
  const out = {};
76
+ if (!opts)
77
+ return out;
78
+ const map = {};
66
79
  for (const fo of R(opts)) {
67
80
  const cst = child(fo, 'constant');
68
81
  const name = optionNameOf(fo, cst);
69
- if (cst && name)
70
- out[name] = constantValue(cst);
82
+ if (!cst || !name)
83
+ continue;
84
+ if ('json_name' === name) {
85
+ out.jsonName = String(unquote(cst.src));
86
+ continue;
87
+ }
88
+ if ('default' === name) {
89
+ out.defaultValue = unquote(cst.src);
90
+ continue;
91
+ }
92
+ map[name] = constantValue(cst);
71
93
  }
72
- return Object.keys(out).length ? out : undefined;
94
+ if (Object.keys(map).length)
95
+ out.options = map;
96
+ return out;
97
+ }
98
+ // The plain (non-pseudo) option map, for the places that cannot carry
99
+ // `json_name` / `default` — extension ranges, enum values.
100
+ function plainOptions(opts) {
101
+ return readFieldOptions(opts).options;
102
+ }
103
+ // optionStmt = "option" optionName "=" constant ";"
104
+ function optionFrom(el) {
105
+ const cst = child(el, 'constant');
106
+ if (!cst)
107
+ return {};
108
+ const name = optionNameOf(el, cst);
109
+ return name ? { [name]: constantValue(cst) } : {};
73
110
  }
74
111
  // ---- fields --------------------------------------------------------------
75
112
  function fieldLabel(labelNode, version) {
@@ -91,8 +128,11 @@ function fieldTypeName(typeText) {
91
128
  const scalar = descriptor_1.SCALAR_TYPES[bare];
92
129
  if (scalar)
93
130
  return { type: scalar };
94
- // Message or enum reference; resolution deferred, store as written.
95
- return { type: 'TYPE_MESSAGE', typeName: typeText };
131
+ // A named reference: could be a message OR an enum, and telling them
132
+ // apart needs symbol resolution this parser deliberately does not do.
133
+ // protoc leaves `type` unset here too, and only fills it in once the
134
+ // name resolves — so record `typeName` as written and nothing else.
135
+ return { typeName: typeText };
96
136
  }
97
137
  // A normal field: [label] fieldType ident "=" fieldNumber [fieldOptions].
98
138
  // The surrounding dispatch node (`messageElement` / `oneofElement`) has these
@@ -103,23 +143,79 @@ function fieldTypeName(typeText) {
103
143
  function typeNodeOf(n) {
104
144
  return child(n, 'fieldType') ?? child(n, 'messageType') ?? child(n, 'fullIdent');
105
145
  }
146
+ function applyFieldOptions(f, opts) {
147
+ const po = readFieldOptions(opts);
148
+ if (undefined !== po.jsonName)
149
+ f.jsonName = po.jsonName;
150
+ if (undefined !== po.defaultValue)
151
+ f.defaultValue = po.defaultValue;
152
+ if (po.options)
153
+ f.options = po.options;
154
+ }
106
155
  function buildField(n, version) {
107
156
  const label = child(n, 'label');
108
157
  const type = typeNodeOf(n);
109
158
  const name = child(n, 'ident');
110
159
  const number = child(n, 'fieldNumber');
111
- const opts = child(n, 'fieldOptions');
112
160
  const f = {
113
161
  name: name ? name.src : '',
114
162
  number: number ? Number(number.src) : 0,
115
163
  ...fieldLabel(label, version),
116
164
  ...fieldTypeName(type ? type.src : ''),
117
165
  };
118
- const fo = readFieldOptions(opts);
119
- if (fo)
120
- f.options = fo;
166
+ applyFieldOptions(f, child(n, 'fieldOptions'));
167
+ return f;
168
+ }
169
+ // A group is a field plus an implicit nested message. protoc lowercases the
170
+ // field name and keeps the declared name for the message and for `typeName`.
171
+ // `optional group TestGroup = 1 { … }`
172
+ // -> field { name: 'testgroup', type: TYPE_GROUP, typeName: 'TestGroup' }
173
+ // -> nestedType { name: 'TestGroup', … }
174
+ function buildGroup(n, version, into) {
175
+ const groupName = child(n, 'ident')?.src ?? '';
176
+ const number = child(n, 'fieldNumber');
177
+ into.nestedType.push(messageFromBody(groupName, child(n, 'messageBody'), version));
178
+ const f = {
179
+ name: groupName.toLowerCase(),
180
+ number: number ? Number(number.src) : 0,
181
+ ...fieldLabel(child(n, 'label'), version),
182
+ type: 'TYPE_GROUP',
183
+ typeName: groupName,
184
+ };
185
+ applyFieldOptions(f, child(n, 'fieldOptions'));
121
186
  return f;
122
187
  }
188
+ // A group is the one construct with BOTH a field number and a message body;
189
+ // `message` has a body but no number, `map`/`field` a number but no body.
190
+ function isGroup(n) {
191
+ return !!child(n, 'fieldNumber') && !!child(n, 'messageBody');
192
+ }
193
+ // The subset of an option map whose names are rooted at `features`.
194
+ function features(opts) {
195
+ if (!opts)
196
+ return undefined;
197
+ const out = {};
198
+ for (const [k, v] of Object.entries(opts)) {
199
+ if ('features' === k || k.startsWith('features.'))
200
+ out[k] = v;
201
+ }
202
+ return Object.keys(out).length ? out : undefined;
203
+ }
204
+ // protoc's map-entry name: strip `_`, upper-case the letter that follows
205
+ // (and the first letter), then append `Entry`. `map_field` -> `MapFieldEntry`.
206
+ function mapEntryName(fieldName) {
207
+ let out = '';
208
+ let capNext = true;
209
+ for (const ch of fieldName) {
210
+ if ('_' === ch) {
211
+ capNext = true;
212
+ continue;
213
+ }
214
+ out += capNext && 'a' <= ch && ch <= 'z' ? ch.toUpperCase() : ch;
215
+ capNext = false;
216
+ }
217
+ return out + 'Entry';
218
+ }
123
219
  // map<K,V> name = N; -> a repeated message field whose type is a
124
220
  // synthesised nested `<Name>Entry` message with mapEntry=true.
125
221
  function buildMapField(n, version, into) {
@@ -129,14 +225,12 @@ function buildMapField(n, version, into) {
129
225
  const keyText = types[0] ? types[0].src : '';
130
226
  const valText = types[1] ? types[1].src : '';
131
227
  const fname = name ? name.src : '';
132
- const entryName = fname.replace(/(^|_)([a-z])/g, (_, a, b) => a + b.toUpperCase())
133
- + 'Entry';
228
+ const entryName = mapEntryName(fname);
229
+ const key = { name: 'key', number: 1, label: 'LABEL_OPTIONAL', ...fieldTypeName(keyText) };
230
+ const value = { name: 'value', number: 2, label: 'LABEL_OPTIONAL', ...fieldTypeName(valText) };
134
231
  const entry = {
135
232
  name: entryName,
136
- field: [
137
- { name: 'key', number: 1, label: 'LABEL_OPTIONAL', ...fieldTypeName(keyText) },
138
- { name: 'value', number: 2, label: 'LABEL_OPTIONAL', ...fieldTypeName(valText) },
139
- ],
233
+ field: [key, value],
140
234
  nestedType: [], enumType: [], oneofDecl: [], extension: [],
141
235
  options: { mapEntry: true },
142
236
  };
@@ -145,12 +239,16 @@ function buildMapField(n, version, into) {
145
239
  name: fname,
146
240
  number: number ? Number(number.src) : 0,
147
241
  label: 'LABEL_REPEATED',
148
- type: 'TYPE_MESSAGE',
149
242
  typeName: entryName,
150
243
  };
151
- const fo = readFieldOptions(child(n, 'fieldOptions'));
152
- if (fo)
153
- f.options = fo;
244
+ applyFieldOptions(f, child(n, 'fieldOptions'));
245
+ // `features` on a map field govern the synthesised entry's key and value
246
+ // fields too, so protoc copies them down. Nothing else is propagated.
247
+ const feat = features(f.options);
248
+ if (feat) {
249
+ key.options = { ...feat };
250
+ value.options = { ...feat };
251
+ }
154
252
  return f;
155
253
  }
156
254
  // ---- enums ---------------------------------------------------------------
@@ -159,26 +257,29 @@ function buildEnum(n) {
159
257
  for (const el of R(n).filter((k) => k.rule === 'enumElement')) {
160
258
  const k = kw(el);
161
259
  if (k.startsWith('reserved')) {
162
- addReserved(el, e);
260
+ // Enum reserved ranges are INCLUSIVE and span the whole int32 space.
261
+ addReserved(el, e, { exclusive: false, max: descriptor_1.MAX_ENUM_NUMBER });
163
262
  continue;
164
263
  }
165
264
  if (k.startsWith('option')) {
166
- continue; // enum-level option; folded into options below if needed
265
+ e.options = { ...(e.options || {}), ...optionFrom(el) };
266
+ continue;
167
267
  }
168
268
  // enumField: ident "=" ["-"] fieldNumber -> name is the kw before "="
169
- const name = kw(el).replace(/=.*$/, '');
269
+ const name = k.replace(/=.*$/, '');
170
270
  const num = child(el, 'fieldNumber');
171
271
  if (name && num) {
172
272
  const neg = /=-/.test(el.src.replace(/\s+/g, ''));
173
- e.value.push({ name, number: (neg ? -1 : 1) * Number(num.src) });
273
+ const v = { name, number: (neg ? -1 : 1) * Number(num.src) };
274
+ const vo = plainOptions(child(el, 'fieldOptions'));
275
+ e.value.push(vo ? { ...v, options: vo } : v);
174
276
  }
175
277
  }
176
278
  return e;
177
279
  }
178
- // ---- reserved / extensions ranges ----------------------------------------
179
280
  // `ranges = range *( "," range )`. The leading `range` is inlined into
180
281
  // `ranges.src`, so parse the (whitespace-stripped) text rather than kids.
181
- function ranges(rangesNode) {
282
+ function ranges(rangesNode, ro) {
182
283
  if (!rangesNode)
183
284
  return [];
184
285
  const out = [];
@@ -187,38 +288,97 @@ function ranges(rangesNode) {
187
288
  if (!m)
188
289
  continue;
189
290
  const start = Number(m[1]);
190
- const end = !m[2] ? start : (m[2] === 'max' ? 536870911 : Number(m[2]));
291
+ let end;
292
+ if (!m[2])
293
+ end = ro.exclusive ? start + 1 : start;
294
+ else if ('max' === m[2])
295
+ end = ro.max;
296
+ else
297
+ end = Number(m[2]) + (ro.exclusive ? 1 : 0);
191
298
  out.push({ start, end });
192
299
  }
193
300
  return out;
194
301
  }
195
- function addReserved(n, target) {
302
+ // The reserved-name list. Both the leading `strLit`/`ident` and (for a
303
+ // single-item list) the whole list can be inlined into `src`, so read the
304
+ // names out of the statement text; whole-word tokens make that unambiguous.
305
+ function reservedNames(n) {
306
+ const body = n.src.replace(/^reserved/, '').replace(/;$/, '');
307
+ const re = /"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'|([A-Za-z_][A-Za-z0-9_]*)/g;
308
+ const out = [];
309
+ let m;
310
+ while (null !== (m = re.exec(body)))
311
+ out.push(m[1] ?? m[2] ?? m[3]);
312
+ return out;
313
+ }
314
+ function addReserved(n, target, ro) {
196
315
  const rn = child(n, 'ranges');
197
316
  if (rn) {
198
- (target.reservedRange = target.reservedRange || []).push(...ranges(rn));
317
+ (target.reservedRange = target.reservedRange || []).push(...ranges(rn, ro));
199
318
  return;
200
319
  }
201
- const names = child(n, 'fieldNames');
202
- if (names) {
203
- const list = R(names).filter((k) => k.rule === 'strLit').map((k) => unquote(k.src));
204
- (target.reservedName = target.reservedName || []).push(...list);
320
+ const names = reservedNames(n);
321
+ if (names.length) {
322
+ (target.reservedName = target.reservedName || []).push(...names);
205
323
  }
206
324
  }
207
325
  // ---- messages ------------------------------------------------------------
208
326
  function buildMessage(n, version) {
209
327
  // `n` is a dispatch node whose `message` alt was inlined: kids are
210
328
  // [ident, messageBody-children…] or [ident] then messageBody.
329
+ return messageFromBody(child(n, 'ident')?.src ?? '', child(n, 'messageBody'), version);
330
+ }
331
+ function messageFromBody(name, body, version) {
211
332
  const msg = {
212
- name: child(n, 'ident')?.src ?? '',
333
+ name,
213
334
  field: [], nestedType: [], enumType: [], oneofDecl: [], extension: [],
214
335
  };
215
- const body = child(n, 'messageBody');
216
336
  const elements = body ? R(body).filter((k) => k.rule === 'messageElement') : [];
217
- for (const el of elements)
218
- addMessageElement(el, version, msg);
337
+ // Options first: `message_set_wire_format` changes what `to max` means in
338
+ // an extension/reserved range, and protoc applies it wherever the option
339
+ // sits in the body.
340
+ for (const el of elements) {
341
+ if (isOptionStmt(el))
342
+ msg.options = { ...(msg.options || {}), ...optionFrom(el) };
343
+ }
344
+ const ro = {
345
+ exclusive: true,
346
+ max: true === msg.options?.message_set_wire_format
347
+ ? descriptor_1.MAX_MESSAGE_SET_END : descriptor_1.MAX_FIELD_NUMBER_END,
348
+ };
349
+ for (const el of elements) {
350
+ if (!isOptionStmt(el))
351
+ addMessageElement(el, version, msg, ro);
352
+ }
353
+ generateSyntheticOneofs(msg);
219
354
  return msg;
220
355
  }
221
- function addMessageElement(el, version, msg) {
356
+ // An `option` statement, as opposed to an `optional`-labelled field (whose
357
+ // `kw` is empty because the `label` node starts the src).
358
+ function isOptionStmt(el) {
359
+ return kw(el).startsWith('option');
360
+ }
361
+ // protoc synthesises a single-field oneof for every proto3 explicit
362
+ // `optional` field, appended after the declared oneofs. The name is the
363
+ // field name prefixed with `_`, then prefixed with `X` until unique.
364
+ function generateSyntheticOneofs(msg) {
365
+ const names = new Set();
366
+ for (const f of msg.field)
367
+ names.add(f.name);
368
+ for (const o of msg.oneofDecl)
369
+ names.add(o.name);
370
+ for (const f of msg.field) {
371
+ if (true !== f.proto3Optional || undefined !== f.oneofIndex)
372
+ continue;
373
+ let oneofName = '_' + f.name;
374
+ while (names.has(oneofName))
375
+ oneofName = 'X' + oneofName;
376
+ names.add(oneofName);
377
+ f.oneofIndex = msg.oneofDecl.length;
378
+ msg.oneofDecl.push({ name: oneofName });
379
+ }
380
+ }
381
+ function addMessageElement(el, version, msg, ro) {
222
382
  const k = kw(el);
223
383
  const first = R(el)[0];
224
384
  if (k.startsWith('map<')) {
@@ -231,12 +391,11 @@ function addMessageElement(el, version, msg) {
231
391
  }
232
392
  if (k.startsWith('export') || k.startsWith('local')) {
233
393
  // edition 2024 symbol visibility wraps the message/enum as a child node.
234
- const m = child(el, 'message');
235
- const e = child(el, 'enumDef');
236
- if (m)
237
- msg.nestedType.push(buildMessage(m, version));
238
- else if (e)
239
- msg.enumType.push(buildEnum(e));
394
+ addVisible(el, k, version, msg.nestedType, msg.enumType);
395
+ return;
396
+ }
397
+ if (isGroup(el)) {
398
+ msg.field.push(buildGroup(el, version, msg));
240
399
  return;
241
400
  }
242
401
  if (k.startsWith('message')) {
@@ -248,21 +407,24 @@ function addMessageElement(el, version, msg) {
248
407
  return;
249
408
  }
250
409
  if (k.startsWith('reserved')) {
251
- addReserved(el, msg);
410
+ addReserved(el, msg, ro);
252
411
  return;
253
412
  }
254
413
  if (k.startsWith('extensions')) {
255
- (msg.extensionRange = msg.extensionRange || []).push(...ranges(child(el, 'ranges')));
414
+ const opts = plainOptions(child(el, 'fieldOptions'));
415
+ const rs = ranges(child(el, 'ranges'), ro);
416
+ // A compound `extensions 2, 9 to 11 [(i) = 5];` puts the options on
417
+ // every range, as protoc does.
418
+ for (const r of rs)
419
+ if (opts)
420
+ r.options = { ...opts };
421
+ (msg.extensionRange = msg.extensionRange || []).push(...rs);
256
422
  return;
257
423
  }
258
424
  if (k.startsWith('extend')) {
259
425
  addExtend(el, version, msg.extension);
260
426
  return;
261
427
  }
262
- if (k.startsWith('option')) {
263
- msg.options = { ...(msg.options || {}), ...optionFrom(el) };
264
- return;
265
- }
266
428
  if (';' === el.src)
267
429
  return; // emptyStmt
268
430
  // No keyword and a fieldType/label lead => a field.
@@ -270,6 +432,17 @@ function addMessageElement(el, version, msg) {
270
432
  msg.field.push(buildField(el, version));
271
433
  }
272
434
  }
435
+ // edition 2024 `export` / `local` on a message or enum declaration. The
436
+ // wrapped `message` / `enumDef` stays a child node instead of inlining.
437
+ function addVisible(el, k, version, messages, enums) {
438
+ const vis = k.startsWith('export') ? 'VISIBILITY_EXPORT' : 'VISIBILITY_LOCAL';
439
+ const m = child(el, 'message');
440
+ const e = child(el, 'enumDef');
441
+ if (m)
442
+ messages.push({ ...buildMessage(m, version), visibility: vis });
443
+ else if (e)
444
+ enums.push({ ...buildEnum(e), visibility: vis });
445
+ }
273
446
  function addOneof(el, version, msg) {
274
447
  const name = child(el, 'ident')?.src ?? '';
275
448
  const index = msg.oneofDecl.length;
@@ -279,7 +452,7 @@ function addOneof(el, version, msg) {
279
452
  continue;
280
453
  if (';' === of.src)
281
454
  continue;
282
- const f = buildField(of, version);
455
+ const f = isGroup(of) ? buildGroup(of, version, msg) : buildField(of, version);
283
456
  f.oneofIndex = index;
284
457
  delete f.proto3Optional; // explicit oneof members aren't proto3-optional
285
458
  msg.field.push(f);
@@ -287,25 +460,22 @@ function addOneof(el, version, msg) {
287
460
  }
288
461
  function addExtend(el, version, into) {
289
462
  // extend messageType "{" *field "}" — fields inline as messageElement-like.
463
+ const extendee = child(el, 'messageType')?.src;
290
464
  for (const f of R(el).filter((k) => k.rule === 'field' || k.rule === 'messageElement')) {
291
- into.push(buildField(f, version));
465
+ const fd = buildField(f, version);
466
+ if (extendee)
467
+ fd.extendee = extendee;
468
+ into.push(fd);
292
469
  }
293
470
  }
294
- // ---- options -------------------------------------------------------------
295
- // optionStmt = "option" optionName "=" constant ";"
296
- function optionFrom(el) {
297
- const cst = child(el, 'constant');
298
- if (!cst)
299
- return {};
300
- const name = optionNameOf(el, cst).replace(/^option/, '');
301
- return { [name]: constantValue(cst) };
302
- }
303
471
  // ---- services ------------------------------------------------------------
304
472
  function buildService(n) {
305
473
  const svc = { name: child(n, 'ident')?.src ?? '', method: [] };
306
474
  for (const el of R(n).filter((k) => k.rule === 'serviceElement')) {
307
475
  if (kw(el).startsWith('rpc'))
308
476
  svc.method.push(buildRpc(el));
477
+ else if (isOptionStmt(el))
478
+ svc.options = { ...(svc.options || {}), ...optionFrom(el) };
309
479
  }
310
480
  return svc;
311
481
  }
@@ -328,6 +498,9 @@ function buildRpc(el) {
328
498
  m.clientStreaming = true;
329
499
  if (/\(stream/.test(response))
330
500
  m.serverStreaming = true;
501
+ for (const o of R(el).filter((k) => k.rule === 'optionStmt')) {
502
+ m.options = { ...(m.options || {}), ...optionFrom(o) };
503
+ }
331
504
  return m;
332
505
  }
333
506
  // ---- file ----------------------------------------------------------------
@@ -336,10 +509,13 @@ function buildFile(proto, version) {
336
509
  dependency: [], publicDependency: [], weakDependency: [],
337
510
  messageType: [], enumType: [], service: [], extension: [],
338
511
  };
339
- if ((0, detect_version_1.isEdition)(version))
512
+ if ((0, detect_version_1.isEdition)(version)) {
340
513
  file.edition = 'EDITION_' + version;
341
- else
514
+ file.syntax = 'editions';
515
+ }
516
+ else {
342
517
  file.syntax = version;
518
+ }
343
519
  for (const def of R(proto).filter((k) => k.rule === 'topLevelDef')) {
344
520
  const k = kw(def);
345
521
  if (k.startsWith('package')) {
@@ -348,24 +524,25 @@ function buildFile(proto, version) {
348
524
  else if (k.startsWith('import')) {
349
525
  const s = child(def, 'strLit');
350
526
  if (s) {
351
- const idx = file.dependency.length;
352
- file.dependency.push(unquote(s.src));
353
- if (k.includes('public'))
354
- file.publicDependency.push(idx);
355
- if (k.includes('weak'))
356
- file.weakDependency.push(idx);
527
+ // `import option "x";` (edition 2024) is a separate dependency list.
528
+ if (k.includes('option')) {
529
+ (file.optionDependency = file.optionDependency || []).push(unquote(s.src));
530
+ }
531
+ else {
532
+ const idx = file.dependency.length;
533
+ file.dependency.push(unquote(s.src));
534
+ if (k.includes('public'))
535
+ file.publicDependency.push(idx);
536
+ if (k.includes('weak'))
537
+ file.weakDependency.push(idx);
538
+ }
357
539
  }
358
540
  }
359
- else if (k.startsWith('option')) {
541
+ else if (isOptionStmt(def)) {
360
542
  file.options = { ...(file.options || {}), ...optionFrom(def) };
361
543
  }
362
544
  else if (k.startsWith('export') || k.startsWith('local')) {
363
- const m = child(def, 'message');
364
- const e = child(def, 'enumDef');
365
- if (m)
366
- file.messageType.push(buildMessage(m, version));
367
- else if (e)
368
- file.enumType.push(buildEnum(e));
545
+ addVisible(def, k, version, file.messageType, file.enumType);
369
546
  }
370
547
  else if (k.startsWith('message')) {
371
548
  file.messageType.push(buildMessage(def, version));