@tabnas/proto 0.2.3 → 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 +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/package.json +6 -2
- package/src/build-descriptor.ts +270 -77
- package/src/descriptor.ts +34 -4
- package/src/grammar.ts +19 -4
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
|
|
59
|
-
`
|
|
60
|
-
`service`, `extension`, `options`, and
|
|
61
|
-
fields are expanded to a repeated message
|
|
62
|
-
`…Entry` nested message with
|
|
63
|
-
|
|
64
|
-
|
|
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).
|
package/dist/build-descriptor.js
CHANGED
|
@@ -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.
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
// `
|
|
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
|
|
58
|
-
|
|
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
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
95
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
133
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
265
|
+
e.options = { ...(e.options || {}), ...optionFrom(el) };
|
|
266
|
+
continue;
|
|
167
267
|
}
|
|
168
268
|
// enumField: ident "=" ["-"] fieldNumber -> name is the kw before "="
|
|
169
|
-
const name =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
202
|
-
if (names) {
|
|
203
|
-
|
|
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
|
|
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
|
-
|
|
218
|
-
|
|
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
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
file.
|
|
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 (
|
|
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
|
-
|
|
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));
|