@tabnas/proto 0.6.11 → 0.6.12
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/dist/build-descriptor.d.ts +12 -1
- package/dist/build-descriptor.js +24 -2
- package/dist/build-descriptor.js.map +1 -1
- package/dist/build-tree.d.ts +6 -0
- package/dist/build-tree.js +333 -0
- package/dist/build-tree.js.map +1 -0
- package/dist/proto.d.ts +7 -3
- package/dist/proto.js +28 -1
- package/dist/proto.js.map +1 -1
- package/dist/translate.d.ts +1 -0
- package/dist/translate.js +955 -1096
- package/dist/translate.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/build-descriptor.ts +20 -2
- package/src/build-tree.ts +313 -0
- package/src/proto.ts +34 -3
- package/src/translate.ts +956 -1096
package/dist/translate.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translate.js","sourceRoot":"","sources":["../src/translate.ts"],"names":[],"mappings":";AAAA,2EAA2E;;
|
|
1
|
+
{"version":3,"file":"translate.js","sourceRoot":"","sources":["../src/translate.ts"],"names":[],"mappings":";AAAA,2EAA2E;;QAghCzE,SAAS;AA9/BX,MAAM,WAAW,GAAqB,MAAM,CAAC,MAAM,CAAC;IAClD,QAAQ,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0CX;IACC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkcxD,EAAE,CAAC;IACF,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAugB9D,EAAE,CAAC;CACH,CAAC,CAAA;AAEF,SAAS,SAAS;IAChB,OAAO,WAAW,CAAA;AACpB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"7.0.2","root":["../src/aggregate.ts","../src/build-descriptor.ts","../src/descriptor.ts","../src/detect-version.ts","../src/grammar.ts","../src/preflight.ts","../src/proto.ts","../src/translate.ts"],"packageJsons":["../node_modules/@tabnas/parser/package.json","../package.json"],"missingPackageJsons":["../node_modules/@tabnas/parser/dist/package.json"]}
|
|
1
|
+
{"version":"7.0.2","root":["../src/aggregate.ts","../src/build-descriptor.ts","../src/build-tree.ts","../src/descriptor.ts","../src/detect-version.ts","../src/grammar.ts","../src/preflight.ts","../src/proto.ts","../src/translate.ts"],"packageJsons":["../node_modules/@tabnas/parser/package.json","../package.json"],"missingPackageJsons":["../node_modules/@tabnas/parser/dist/package.json"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tabnas/proto",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.12",
|
|
4
4
|
"description": "Parse Protocol Buffers .proto IDL (proto2, proto3, edition 2023/2024) into FileDescriptorProto-shaped JSON, using the Tabnas parser and an ABNF grammar.",
|
|
5
5
|
"main": "dist/proto.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"LICENSE"
|
|
56
56
|
],
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"@tabnas/parser": ">=0.12.
|
|
58
|
+
"@tabnas/parser": ">=0.12.16"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@tabnas/abnf": "*",
|
package/src/build-descriptor.ts
CHANGED
|
@@ -214,6 +214,18 @@ function typeNodeOf(n: Node): Node | undefined {
|
|
|
214
214
|
return child(n, 'fieldType') ?? child(n, 'messageType') ?? child(n, 'fullIdent')
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
// The field type's text. Where the type surfaces as a bare `fullIdent`,
|
|
218
|
+
// the optional leading dot of `messageType` was inlined with it, into the
|
|
219
|
+
// text ahead of the node, and a fully qualified type keeps it there:
|
|
220
|
+
// `oneof o { .a.B t = 5; }` names `.a.B`, not `a.B`.
|
|
221
|
+
function typeTextOf(n: Node): string {
|
|
222
|
+
const t = typeNodeOf(n)
|
|
223
|
+
if (null == t) return ''
|
|
224
|
+
if ('fullIdent' !== t.rule) return t.src
|
|
225
|
+
const gap = gapsBefore(n, 'fullIdent')[0] ?? ''
|
|
226
|
+
return (gap.endsWith('.') ? '.' : '') + t.src
|
|
227
|
+
}
|
|
228
|
+
|
|
217
229
|
function applyFieldOptions(f: FieldDescriptorProto, opts: Node | undefined): void {
|
|
218
230
|
const po = readFieldOptions(opts)
|
|
219
231
|
if (undefined !== po.jsonName) f.jsonName = po.jsonName
|
|
@@ -223,7 +235,6 @@ function applyFieldOptions(f: FieldDescriptorProto, opts: Node | undefined): voi
|
|
|
223
235
|
|
|
224
236
|
function buildField(n: Node, version: ProtoVersion): FieldDescriptorProto {
|
|
225
237
|
const label = child(n, 'label')
|
|
226
|
-
const type = typeNodeOf(n)
|
|
227
238
|
const name = child(n, 'ident')
|
|
228
239
|
const number = child(n, 'fieldNumber')
|
|
229
240
|
|
|
@@ -231,7 +242,7 @@ function buildField(n: Node, version: ProtoVersion): FieldDescriptorProto {
|
|
|
231
242
|
name: name ? name.src : '',
|
|
232
243
|
number: number ? Number(number.src) : 0,
|
|
233
244
|
...fieldLabel(label, version),
|
|
234
|
-
...fieldTypeName(
|
|
245
|
+
...fieldTypeName(typeTextOf(n)),
|
|
235
246
|
}
|
|
236
247
|
applyFieldOptions(f, child(n, 'fieldOptions'))
|
|
237
248
|
return f
|
|
@@ -659,3 +670,10 @@ export function buildFile(proto: Node, version: ProtoVersion): FileDescriptorPro
|
|
|
659
670
|
}
|
|
660
671
|
return file
|
|
661
672
|
}
|
|
673
|
+
|
|
674
|
+
// The CST readers the source-ordered tree's walk shares (./build-tree.ts).
|
|
675
|
+
export type { Node }
|
|
676
|
+
export {
|
|
677
|
+
R, kw, child, gaps, gapsBefore, optionNameOf, typeTextOf, isGroup,
|
|
678
|
+
isOptionStmt, parenthesised,
|
|
679
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/* Copyright (c) 2026 Richard Rodger and other contributors, MIT License */
|
|
2
|
+
|
|
3
|
+
// Walk the `{rule, src, kids}` CST into the source-ordered tree, the
|
|
4
|
+
// schema `proto-ast` (alchemy's docs/streaming-plan.md, section 13.1):
|
|
5
|
+
// the file's declarations in the order the file writes them, each node an
|
|
6
|
+
// object whose first member, `type`, names it and whose other members
|
|
7
|
+
// follow in the order the statement spells them. A scalar is the text
|
|
8
|
+
// the source spells, its tokens run together as a CST node's `src` keeps
|
|
9
|
+
// them: a name, a type, a number with its sign, an option's name, and a
|
|
10
|
+
// string literal with its quotes and escapes as written. An aggregate
|
|
11
|
+
// option value is its fields, `[{name, value}]`, each value text or an
|
|
12
|
+
// aggregate again. An empty statement declares nothing and is left out.
|
|
13
|
+
//
|
|
14
|
+
// The transducer part (alchemy/transduce.alc) makes the same tree's
|
|
15
|
+
// events from the parse's rule passes; this walk is the tree a host reads
|
|
16
|
+
// whole, and the one the part is held to. It finds each statement the way
|
|
17
|
+
// ./build-descriptor.ts does, by the keyword ahead of the node's first
|
|
18
|
+
// child (`kw`), since the ABNF compiler inlines a leading rule into the
|
|
19
|
+
// dispatch node that holds it.
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
Node, R, kw, child, gaps, gapsBefore, optionNameOf, typeTextOf, isGroup,
|
|
23
|
+
isOptionStmt, parenthesised,
|
|
24
|
+
} from './build-descriptor'
|
|
25
|
+
|
|
26
|
+
// A node of the tree: an object whose members are in the schema's order.
|
|
27
|
+
export type TreeNode = { [key: string]: TreeValue }
|
|
28
|
+
export type TreeValue = string | boolean | TreeValue[] | TreeNode
|
|
29
|
+
|
|
30
|
+
// A constant's value: its text, or an aggregate's fields.
|
|
31
|
+
function constantTree(n: Node | undefined): TreeValue {
|
|
32
|
+
if (null == n) return ''
|
|
33
|
+
if (!n.src.startsWith('{')) return n.src
|
|
34
|
+
return R(n).filter((k) => 'messageValueEntry' === k.rule).map(aggregateField)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// `messageValueEntry = ident [":"] (strLit 1*strLit / constant) [","/";"]`.
|
|
38
|
+
// The leading `ident` is inlined, so the name is the text ahead of the
|
|
39
|
+
// first child, less the colon; adjacent string literals are their texts
|
|
40
|
+
// run together, as the entry's `src` keeps them.
|
|
41
|
+
function aggregateField(e: Node): TreeNode {
|
|
42
|
+
const kids = R(e)
|
|
43
|
+
const name = (gaps(e)[0] ?? '').replace(/:$/, '')
|
|
44
|
+
const value: TreeValue =
|
|
45
|
+
1 === kids.length && 'constant' === kids[0].rule
|
|
46
|
+
? constantTree(kids[0])
|
|
47
|
+
: kids.map((k) => k.src).join('')
|
|
48
|
+
return { name, value }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// `option name = constant;`, as a statement.
|
|
52
|
+
function optionTree(el: Node): TreeNode {
|
|
53
|
+
const cst = child(el, 'constant')
|
|
54
|
+
return { type: 'option', name: optionNameOf(el, cst), value: constantTree(cst) }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// `[name = constant, ...]`: a field's, an enum value's or an extension
|
|
58
|
+
// range's options.
|
|
59
|
+
function optionsTree(opts: Node | undefined): TreeNode[] {
|
|
60
|
+
return R(opts).filter((k) => 'fieldOption' === k.rule).map((fo) => {
|
|
61
|
+
const cst = child(fo, 'constant')
|
|
62
|
+
return { name: optionNameOf(fo, cst), value: constantTree(cst) }
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The members of a statement that carry `[options]`, appended where the
|
|
67
|
+
// statement writes them.
|
|
68
|
+
function withOptions(node: TreeNode, el: Node): TreeNode {
|
|
69
|
+
const opts = child(el, 'fieldOptions')
|
|
70
|
+
if (opts) node.options = optionsTree(opts)
|
|
71
|
+
return node
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// `[label] fieldType ident "=" fieldNumber [fieldOptions] ";"`.
|
|
75
|
+
function fieldTree(n: Node): TreeNode {
|
|
76
|
+
const node: TreeNode = { type: 'field' }
|
|
77
|
+
const label = child(n, 'label')
|
|
78
|
+
if (label) node.label = label.src
|
|
79
|
+
node.fieldType = typeTextOf(n)
|
|
80
|
+
node.name = child(n, 'ident')?.src ?? ''
|
|
81
|
+
node.number = child(n, 'fieldNumber')?.src ?? ''
|
|
82
|
+
return withOptions(node, n)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// `[label] "group" ident "=" fieldNumber messageBody`.
|
|
86
|
+
function groupTree(n: Node): TreeNode {
|
|
87
|
+
const node: TreeNode = { type: 'group' }
|
|
88
|
+
const label = child(n, 'label')
|
|
89
|
+
if (label) node.label = label.src
|
|
90
|
+
node.name = child(n, 'ident')?.src ?? ''
|
|
91
|
+
node.number = child(n, 'fieldNumber')?.src ?? ''
|
|
92
|
+
node.body = messageBodyTree(child(n, 'messageBody'))
|
|
93
|
+
return node
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// `"map" "<" fieldType "," fieldType ">" ident "=" fieldNumber [fieldOptions] ";"`.
|
|
97
|
+
function mapTree(n: Node): TreeNode {
|
|
98
|
+
const types = R(n).filter((k) => 'fieldType' === k.rule)
|
|
99
|
+
return withOptions({
|
|
100
|
+
type: 'map',
|
|
101
|
+
keyType: types[0]?.src ?? '',
|
|
102
|
+
valueType: types[1]?.src ?? '',
|
|
103
|
+
name: child(n, 'ident')?.src ?? '',
|
|
104
|
+
number: child(n, 'fieldNumber')?.src ?? '',
|
|
105
|
+
}, n)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// The ranges of a `reserved` or `extensions` statement, each `from` and,
|
|
109
|
+
// where the range writes one, `to`. The leading `range` is inlined into
|
|
110
|
+
// `ranges`, so they are read from its text, which no number or `max`
|
|
111
|
+
// spells with a `t`.
|
|
112
|
+
function rangesTree(n: Node | undefined): TreeNode[] {
|
|
113
|
+
if (null == n) return []
|
|
114
|
+
return n.src.split(',').map((part): TreeNode => {
|
|
115
|
+
const at = part.indexOf('to')
|
|
116
|
+
return at < 0 ? { from: part } : { from: part.slice(0, at), to: part.slice(at + 2) }
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// The names of a `reserved` statement, each as written: a string literal
|
|
121
|
+
// with its quotes, or an identifier. They are read from the statement's
|
|
122
|
+
// text, cut at the commas outside a literal.
|
|
123
|
+
function reservedNamesTree(n: Node): string[] {
|
|
124
|
+
const names = child(n, 'fieldNames')
|
|
125
|
+
const src = names ? names.src : ''
|
|
126
|
+
const out: string[] = []
|
|
127
|
+
let from = 0
|
|
128
|
+
for (let i = 0; i < src.length; i++) {
|
|
129
|
+
const c = src[i]
|
|
130
|
+
if ('"' === c || "'" === c || '`' === c) {
|
|
131
|
+
for (i++; i < src.length && c !== src[i]; i++) if ('\\' === src[i]) i++
|
|
132
|
+
} else if (',' === c) {
|
|
133
|
+
out.push(src.slice(from, i))
|
|
134
|
+
from = i + 1
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
out.push(src.slice(from))
|
|
138
|
+
return out
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function reservedTree(n: Node): TreeNode {
|
|
142
|
+
const ranges = child(n, 'ranges')
|
|
143
|
+
return ranges
|
|
144
|
+
? { type: 'reserved', ranges: rangesTree(ranges) }
|
|
145
|
+
: { type: 'reserved', names: reservedNamesTree(n) }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// The edition-2024 `export` / `local` modifier keeps the message or enum
|
|
149
|
+
// as a child node, which the walk reads with the modifier ahead of it.
|
|
150
|
+
function visibleTree(el: Node, k: string): TreeNode | undefined {
|
|
151
|
+
const visibility = k.startsWith('export') ? 'export' : 'local'
|
|
152
|
+
const m = child(el, 'message')
|
|
153
|
+
if (m) return messageTree(m, visibility)
|
|
154
|
+
const e = child(el, 'enumDef')
|
|
155
|
+
if (e) return enumTree(e, visibility)
|
|
156
|
+
return undefined
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function messageTree(n: Node, visibility?: string): TreeNode {
|
|
160
|
+
const node: TreeNode = { type: 'message' }
|
|
161
|
+
if (visibility) node.visibility = visibility
|
|
162
|
+
node.name = child(n, 'ident')?.src ?? ''
|
|
163
|
+
node.body = messageBodyTree(child(n, 'messageBody'))
|
|
164
|
+
return node
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function messageBodyTree(body: Node | undefined): TreeNode[] {
|
|
168
|
+
const out: TreeNode[] = []
|
|
169
|
+
for (const el of R(body)) {
|
|
170
|
+
if ('messageElement' !== el.rule) continue
|
|
171
|
+
const node = messageElementTree(el)
|
|
172
|
+
if (node) out.push(node)
|
|
173
|
+
}
|
|
174
|
+
return out
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function messageElementTree(el: Node): TreeNode | undefined {
|
|
178
|
+
const k = kw(el)
|
|
179
|
+
if (isOptionStmt(el)) return optionTree(el)
|
|
180
|
+
if (k.startsWith('map<')) return mapTree(el)
|
|
181
|
+
if (k.startsWith('oneof')) return oneofTree(el)
|
|
182
|
+
if (k.startsWith('export') || k.startsWith('local')) return visibleTree(el, k)
|
|
183
|
+
if (isGroup(el)) return groupTree(el)
|
|
184
|
+
if (k.startsWith('message')) return messageTree(el)
|
|
185
|
+
if (k.startsWith('enum')) return enumTree(el)
|
|
186
|
+
if (k.startsWith('reserved')) return reservedTree(el)
|
|
187
|
+
if (k.startsWith('extensions')) {
|
|
188
|
+
return withOptions({ type: 'extensions', ranges: rangesTree(child(el, 'ranges')) }, el)
|
|
189
|
+
}
|
|
190
|
+
if (k.startsWith('extend')) return extendTree(el)
|
|
191
|
+
if (';' === el.src) return undefined
|
|
192
|
+
const first = R(el)[0]
|
|
193
|
+
if (first && ('fieldType' === first.rule || 'label' === first.rule)) return fieldTree(el)
|
|
194
|
+
return undefined
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function oneofTree(el: Node): TreeNode {
|
|
198
|
+
const body: TreeNode[] = []
|
|
199
|
+
for (const of of R(el)) {
|
|
200
|
+
if ('oneofElement' !== of.rule) continue
|
|
201
|
+
if (isOptionStmt(of)) body.push(optionTree(of))
|
|
202
|
+
else if (';' === of.src) continue
|
|
203
|
+
else body.push(isGroup(of) ? groupTree(of) : fieldTree(of))
|
|
204
|
+
}
|
|
205
|
+
return { type: 'oneof', name: child(el, 'ident')?.src ?? '', body }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function enumTree(n: Node, visibility?: string): TreeNode {
|
|
209
|
+
const node: TreeNode = { type: 'enum' }
|
|
210
|
+
if (visibility) node.visibility = visibility
|
|
211
|
+
node.name = child(n, 'ident')?.src ?? ''
|
|
212
|
+
const body: TreeNode[] = []
|
|
213
|
+
for (const el of R(n)) {
|
|
214
|
+
if ('enumElement' !== el.rule) continue
|
|
215
|
+
// The first child says what the statement is (./build-descriptor.ts
|
|
216
|
+
// has why): `optionName` an option, `ranges` or `fieldNames` a
|
|
217
|
+
// `reserved`, `fieldNumber` a value.
|
|
218
|
+
const first = R(el)[0]
|
|
219
|
+
const rule = first?.rule
|
|
220
|
+
if ('ranges' === rule || 'fieldNames' === rule) body.push(reservedTree(el))
|
|
221
|
+
else if ('optionName' === rule) body.push(optionTree(el))
|
|
222
|
+
else if ('fieldNumber' === rule) {
|
|
223
|
+
// `ident "=" ["-"] fieldNumber`: the name and the sign are in the
|
|
224
|
+
// text ahead of the number node.
|
|
225
|
+
const gap = gapsBefore(el, 'fieldNumber')[0] ?? ''
|
|
226
|
+
const eq = gap.indexOf('=')
|
|
227
|
+
const name = eq < 0 ? gap : gap.slice(0, eq)
|
|
228
|
+
const number = (gap.endsWith('-') ? '-' : '') + first.src
|
|
229
|
+
body.push(withOptions({ type: 'enumValue', name, number }, el))
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
node.body = body
|
|
233
|
+
return node
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// `extend messageType "{" *( field / emptyStmt ) "}"`.
|
|
237
|
+
function extendTree(n: Node): TreeNode {
|
|
238
|
+
const body: TreeNode[] = []
|
|
239
|
+
for (const f of R(n)) {
|
|
240
|
+
if ('field' === f.rule || 'messageElement' === f.rule) body.push(fieldTree(f))
|
|
241
|
+
}
|
|
242
|
+
return { type: 'extend', extendee: child(n, 'messageType')?.src ?? '', body }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function serviceTree(n: Node): TreeNode {
|
|
246
|
+
const body: TreeNode[] = []
|
|
247
|
+
for (const el of R(n)) {
|
|
248
|
+
if ('serviceElement' !== el.rule) continue
|
|
249
|
+
if (kw(el).startsWith('rpc')) body.push(rpcTree(el))
|
|
250
|
+
else if (isOptionStmt(el)) body.push(optionTree(el))
|
|
251
|
+
}
|
|
252
|
+
return { type: 'service', name: child(n, 'ident')?.src ?? '', body }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// `rpc ident "(" ["stream"] messageType ")" "returns" "(" ["stream"]
|
|
256
|
+
// messageType ")" ( ";" / "{" *( optionStmt / emptyStmt ) "}" )`. Whether
|
|
257
|
+
// each side streams is read from its parenthesised span, as the
|
|
258
|
+
// descriptor's walk reads it; the block is there where the statement
|
|
259
|
+
// ends in a brace.
|
|
260
|
+
function rpcTree(el: Node): TreeNode {
|
|
261
|
+
const types = R(el).filter((k) => 'messageType' === k.rule)
|
|
262
|
+
const [input, output] = parenthesised(el.src)
|
|
263
|
+
const inputType = types[0]?.src ?? ''
|
|
264
|
+
const outputType = types[1]?.src ?? ''
|
|
265
|
+
const node: TreeNode = { type: 'rpc', name: child(el, 'ident')?.src ?? '' }
|
|
266
|
+
if ('stream' + inputType === input) node.inputStream = true
|
|
267
|
+
node.inputType = inputType
|
|
268
|
+
if ('stream' + outputType === output) node.outputStream = true
|
|
269
|
+
node.outputType = outputType
|
|
270
|
+
if (el.src.endsWith('}')) {
|
|
271
|
+
node.body = R(el).filter((k) => 'optionStmt' === k.rule).map(optionTree)
|
|
272
|
+
}
|
|
273
|
+
return node
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function topLevelTree(def: Node): TreeNode | undefined {
|
|
277
|
+
const k = kw(def)
|
|
278
|
+
if (k.startsWith('import')) {
|
|
279
|
+
const node: TreeNode = { type: 'import' }
|
|
280
|
+
const modifier = k.slice('import'.length)
|
|
281
|
+
if ('' !== modifier) node.modifier = modifier
|
|
282
|
+
node.path = child(def, 'strLit')?.src ?? ''
|
|
283
|
+
return node
|
|
284
|
+
}
|
|
285
|
+
if (k.startsWith('package')) {
|
|
286
|
+
return { type: 'package', name: child(def, 'fullIdent')?.src ?? '' }
|
|
287
|
+
}
|
|
288
|
+
if (isOptionStmt(def)) return optionTree(def)
|
|
289
|
+
if (k.startsWith('export') || k.startsWith('local')) return visibleTree(def, k)
|
|
290
|
+
if (k.startsWith('message')) return messageTree(def)
|
|
291
|
+
if (k.startsWith('enum')) return enumTree(def)
|
|
292
|
+
if (k.startsWith('service')) return serviceTree(def)
|
|
293
|
+
if (k.startsWith('extend')) return extendTree(def)
|
|
294
|
+
return undefined
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// The file's tree: its declarations in source order. An empty file, which
|
|
298
|
+
// parses to no node at all, is a file with an empty body.
|
|
299
|
+
export function buildTree(proto: Node | undefined | null): TreeNode {
|
|
300
|
+
const body: TreeNode[] = []
|
|
301
|
+
for (const k of R(proto)) {
|
|
302
|
+
if ('syntaxOrEdition' === k.rule) {
|
|
303
|
+
body.push({
|
|
304
|
+
type: kw(k).startsWith('edition') ? 'edition' : 'syntax',
|
|
305
|
+
value: child(k, 'strLit')?.src ?? '',
|
|
306
|
+
})
|
|
307
|
+
} else if ('topLevelDef' === k.rule) {
|
|
308
|
+
const node = topLevelTree(k)
|
|
309
|
+
if (node) body.push(node)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return { type: 'file', body }
|
|
313
|
+
}
|
package/src/proto.ts
CHANGED
|
@@ -12,6 +12,8 @@ import type { GrammarSpec } from '@tabnas/parser'
|
|
|
12
12
|
// run gen-grammar` (gen-grammar.js). Generated: never edit it.
|
|
13
13
|
import compiledGrammar from './proto-grammar.json'
|
|
14
14
|
import { buildFile } from './build-descriptor'
|
|
15
|
+
import { buildTree } from './build-tree'
|
|
16
|
+
import type { TreeNode, TreeValue } from './build-tree'
|
|
15
17
|
import { recordAggregate } from './aggregate'
|
|
16
18
|
import { MAX_NESTING_DEPTH, preflight } from './preflight'
|
|
17
19
|
import {
|
|
@@ -86,13 +88,42 @@ function parse(src: string, options?: Partial<ProtoOptions>): FileDescriptorProt
|
|
|
86
88
|
return toDescriptor(tn.parse(src), options)
|
|
87
89
|
}
|
|
88
90
|
|
|
91
|
+
// The parser a host reads a `.proto` document with: a fresh engine with
|
|
92
|
+
// the grammar installed, as `parse` builds one. Its rule passes are what
|
|
93
|
+
// the transducer part (alchemy/transduce.alc) reads, and its value is the
|
|
94
|
+
// `{rule, src, kids}` CST that `toTree` and `toDescriptor` walk. A host
|
|
95
|
+
// runs `preflight(src)` on a source before it hands it to this parser.
|
|
96
|
+
function make(): Tabnas {
|
|
97
|
+
const tn = new Tabnas({ rewind: { history: 8192 } })
|
|
98
|
+
Proto(tn)
|
|
99
|
+
return tn
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// The source-ordered tree of a parsed proto CST: the file's declarations
|
|
103
|
+
// in the order the file writes them (the schema `proto-ast`; see
|
|
104
|
+
// ./build-tree.ts). It is the tree the translation parts read and write.
|
|
105
|
+
function toTree(cst: any): TreeNode {
|
|
106
|
+
return buildTree(cst)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Convenience: parse a `.proto` source string to its source-ordered tree
|
|
110
|
+
// in one call, refusing a document that nests past MAX_NESTING_DEPTH
|
|
111
|
+
// first, as `parse` does.
|
|
112
|
+
function parseTree(src: string): TreeNode {
|
|
113
|
+
preflight(src)
|
|
114
|
+
return toTree(make().parse(src))
|
|
115
|
+
}
|
|
116
|
+
|
|
89
117
|
// VERSION is this package's version. It MUST equal package.json "version":
|
|
90
118
|
// the release orchestrator rewrites both, and test/version.test.ts fails the
|
|
91
119
|
// build if they drift. Mirrors `const VERSION` in go/proto.go.
|
|
92
|
-
const VERSION = '0.6.
|
|
120
|
+
const VERSION = '0.6.12'
|
|
93
121
|
|
|
94
|
-
export {
|
|
95
|
-
|
|
122
|
+
export {
|
|
123
|
+
Proto, parse, toDescriptor, parseTree, toTree, make, preflight,
|
|
124
|
+
MAX_NESTING_DEPTH, VERSION,
|
|
125
|
+
}
|
|
126
|
+
export type { ProtoVersion, TreeNode, TreeValue }
|
|
96
127
|
export * from './descriptor'
|
|
97
128
|
export { translate } from './translate'
|
|
98
129
|
export type { TranslationPart, TranslationParts } from './translate'
|