@malloydata/malloy 0.0.385 → 0.0.387
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/internal.d.ts +2 -0
- package/dist/internal.js +3 -1
- package/dist/lang/prettify/binary-chain.d.ts +3 -0
- package/dist/lang/prettify/binary-chain.js +65 -0
- package/dist/lang/prettify/block-body.d.ts +4 -0
- package/dist/lang/prettify/block-body.js +87 -0
- package/dist/lang/prettify/error-listener.d.ts +6 -0
- package/dist/lang/prettify/error-listener.js +19 -0
- package/dist/lang/prettify/field-properties.d.ts +3 -0
- package/dist/lang/prettify/field-properties.js +57 -0
- package/dist/lang/prettify/formatter.d.ts +17 -0
- package/dist/lang/prettify/formatter.js +150 -0
- package/dist/lang/prettify/import-select.d.ts +3 -0
- package/dist/lang/prettify/import-select.js +88 -0
- package/dist/lang/prettify/index.d.ts +19 -0
- package/dist/lang/prettify/index.js +163 -0
- package/dist/lang/prettify/inline-renderer.d.ts +3 -0
- package/dist/lang/prettify/inline-renderer.js +101 -0
- package/dist/lang/prettify/leaf.d.ts +9 -0
- package/dist/lang/prettify/leaf.js +340 -0
- package/dist/lang/prettify/out.d.ts +12 -0
- package/dist/lang/prettify/out.js +74 -0
- package/dist/lang/prettify/pick-case.d.ts +5 -0
- package/dist/lang/prettify/pick-case.js +222 -0
- package/dist/lang/prettify/rules.d.ts +13 -0
- package/dist/lang/prettify/rules.js +111 -0
- package/dist/lang/prettify/sections.d.ts +4 -0
- package/dist/lang/prettify/sections.js +380 -0
- package/dist/lang/prettify/tokens.d.ts +13 -0
- package/dist/lang/prettify/tokens.js +185 -0
- package/dist/lang/prettify/types.d.ts +9 -0
- package/dist/lang/prettify/types.js +7 -0
- package/dist/lang/run-malloy-parser.d.ts +23 -0
- package/dist/lang/run-malloy-parser.js +32 -8
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright Contributors to the Malloy project
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*
|
|
6
|
+
* RULE: SECTION-STATEMENT — formatSectionStatement / formatSectionList
|
|
7
|
+
*
|
|
8
|
+
* A `keyword: items` block. The statement context wraps:
|
|
9
|
+
* <tags?> <ACCESS_LABEL?> <KEYWORD> <listCtx>
|
|
10
|
+
* We walk children: tags + access-label go through the normal dispatcher
|
|
11
|
+
* (so annotations are preserved); the KEYWORD is emitted manually; the
|
|
12
|
+
* list context dispatches to formatSectionList.
|
|
13
|
+
*
|
|
14
|
+
* formatSectionList rule (locked in with the user):
|
|
15
|
+
* - All bare items + total fits ≤ LINE_BUDGET → inline `kw: a, b, c`.
|
|
16
|
+
* - Single item that fits (even if it has `is`) → inline. Items containing
|
|
17
|
+
* a `{...}` body with more than one section statement are excluded —
|
|
18
|
+
* view bodies don't read on one line regardless of length.
|
|
19
|
+
* - Single is-item that doesn't fit inline → keep keyword and item on the
|
|
20
|
+
* same line (`nest: name is { …wrapped body… }`); the body's `{...}`
|
|
21
|
+
* wraps internally. Annotated items still take the keyword-on-own-line
|
|
22
|
+
* form so the annotation lands above its item.
|
|
23
|
+
* - Otherwise → wrapped: keyword on its own line; items at +1 indent;
|
|
24
|
+
* bare items flow-fill ≤ LINE_BUDGET, comma-separated intra-line,
|
|
25
|
+
* no trailing commas; `is` items each on own line; annotated items
|
|
26
|
+
* each on own line, annotation on the line above.
|
|
27
|
+
*
|
|
28
|
+
* After the last item, trailing comments that sit between the section and
|
|
29
|
+
* the enclosing `}` are also emitted at the inner indent — they belong to
|
|
30
|
+
* the section the user just wrote, not the parent block.
|
|
31
|
+
*/
|
|
32
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
33
|
+
if (k2 === undefined) k2 = k;
|
|
34
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
35
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
36
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
37
|
+
}
|
|
38
|
+
Object.defineProperty(o, k2, desc);
|
|
39
|
+
}) : (function(o, m, k, k2) {
|
|
40
|
+
if (k2 === undefined) k2 = k;
|
|
41
|
+
o[k2] = m[k];
|
|
42
|
+
}));
|
|
43
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
44
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
45
|
+
}) : function(o, v) {
|
|
46
|
+
o["default"] = v;
|
|
47
|
+
});
|
|
48
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
49
|
+
var ownKeys = function(o) {
|
|
50
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
51
|
+
var ar = [];
|
|
52
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
53
|
+
return ar;
|
|
54
|
+
};
|
|
55
|
+
return ownKeys(o);
|
|
56
|
+
};
|
|
57
|
+
return function (mod) {
|
|
58
|
+
if (mod && mod.__esModule) return mod;
|
|
59
|
+
var result = {};
|
|
60
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
61
|
+
__setModuleDefault(result, mod);
|
|
62
|
+
return result;
|
|
63
|
+
};
|
|
64
|
+
})();
|
|
65
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
66
|
+
exports.formatSectionStatement = formatSectionStatement;
|
|
67
|
+
const antlr4ts_1 = require("antlr4ts");
|
|
68
|
+
const tree_1 = require("antlr4ts/tree");
|
|
69
|
+
const parser = __importStar(require("../lib/Malloy/MalloyParser"));
|
|
70
|
+
const tokens_1 = require("./tokens");
|
|
71
|
+
const leaf_1 = require("./leaf");
|
|
72
|
+
const inline_renderer_1 = require("./inline-renderer");
|
|
73
|
+
function formatSectionStatement(f, stmt, rule) {
|
|
74
|
+
var _a;
|
|
75
|
+
const keywordTok = findKeyword(stmt, rule.keywordTypes);
|
|
76
|
+
const listCtx = rule.list(stmt);
|
|
77
|
+
if (!keywordTok || !listCtx) {
|
|
78
|
+
(0, leaf_1.formatTokenRange)(f, stmt._start.tokenIndex, stmt._stop.tokenIndex);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
for (let i = 0; i < stmt.childCount; i++) {
|
|
82
|
+
const c = stmt.getChild(i);
|
|
83
|
+
const isKeywordChild = (c instanceof tree_1.TerminalNode && c.symbol === keywordTok) ||
|
|
84
|
+
(c instanceof antlr4ts_1.ParserRuleContext && childContainsToken(c, keywordTok));
|
|
85
|
+
if (isKeywordChild) {
|
|
86
|
+
(0, leaf_1.flushHiddenBefore)(f, keywordTok.tokenIndex);
|
|
87
|
+
if (f.o.indent > 0)
|
|
88
|
+
f.o.nl();
|
|
89
|
+
else
|
|
90
|
+
(0, leaf_1.startStatementLine)(f);
|
|
91
|
+
f.o.text(((_a = keywordTok.text) !== null && _a !== void 0 ? _a : '').replace(/\s+/g, ''));
|
|
92
|
+
(0, leaf_1.note)(f, keywordTok.type, keywordTok.tokenIndex, keywordTok);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (c === listCtx) {
|
|
96
|
+
const items = listItems(listCtx, rule.itemKind);
|
|
97
|
+
if (items.length > 0)
|
|
98
|
+
formatSectionList(f, items, rule.itemKind);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
f.format(c);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function childContainsToken(node, tok) {
|
|
105
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
106
|
+
const c = node.getChild(i);
|
|
107
|
+
if (c instanceof tree_1.TerminalNode && c.symbol === tok)
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
function findKeyword(node, types) {
|
|
113
|
+
// Direct terminal children — fast path, the common case.
|
|
114
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
115
|
+
const c = node.getChild(i);
|
|
116
|
+
if (c instanceof tree_1.TerminalNode && types.includes(c.symbol.type))
|
|
117
|
+
return c.symbol;
|
|
118
|
+
}
|
|
119
|
+
// Fallback: descend one level. Some rules wrap the keyword in a small
|
|
120
|
+
// nested rule (e.g. includeItem → accessLabelProp → INTERNAL).
|
|
121
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
122
|
+
const c = node.getChild(i);
|
|
123
|
+
if (c instanceof antlr4ts_1.ParserRuleContext) {
|
|
124
|
+
for (let j = 0; j < c.childCount; j++) {
|
|
125
|
+
const g = c.getChild(j);
|
|
126
|
+
if (g instanceof tree_1.TerminalNode && types.includes(g.symbol.type))
|
|
127
|
+
return g.symbol;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
function listItems(listCtx, itemKind) {
|
|
134
|
+
// Type predicate (`c is ParserRuleContext`) so callers don't need a cast.
|
|
135
|
+
const matches = (c) => {
|
|
136
|
+
if (!(c instanceof antlr4ts_1.ParserRuleContext))
|
|
137
|
+
return false;
|
|
138
|
+
switch (itemKind) {
|
|
139
|
+
case 'fieldEntry':
|
|
140
|
+
return c instanceof parser.QueryFieldEntryContext;
|
|
141
|
+
case 'nestEntry':
|
|
142
|
+
return c instanceof parser.NestEntryContext;
|
|
143
|
+
case 'fieldDef':
|
|
144
|
+
return c instanceof parser.FieldDefContext;
|
|
145
|
+
case 'fieldName':
|
|
146
|
+
return c instanceof parser.FieldNameContext;
|
|
147
|
+
case 'collectionMember':
|
|
148
|
+
return c instanceof parser.CollectionMemberContext;
|
|
149
|
+
case 'orderBySpec':
|
|
150
|
+
return c instanceof parser.OrderBySpecContext;
|
|
151
|
+
case 'fieldExpr':
|
|
152
|
+
return c instanceof parser.FieldExprContext;
|
|
153
|
+
case 'joinDef':
|
|
154
|
+
return c instanceof parser.JoinDefContext;
|
|
155
|
+
case 'includeField':
|
|
156
|
+
return c instanceof parser.IncludeFieldContext;
|
|
157
|
+
case 'indexElement':
|
|
158
|
+
return c instanceof parser.IndexElementContext;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const out = [];
|
|
162
|
+
for (let i = 0; i < listCtx.childCount; i++) {
|
|
163
|
+
const c = listCtx.getChild(i);
|
|
164
|
+
if (matches(c))
|
|
165
|
+
out.push(c);
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
function classifyItem(f, ctx, itemKind) {
|
|
170
|
+
// joinDef items always wrap onto their own line. They use `with` / `on`
|
|
171
|
+
// instead of `is`, but they're structurally one-per-line like is-items.
|
|
172
|
+
let hasIs = itemKind === 'joinDef';
|
|
173
|
+
let hasAnnotation = false;
|
|
174
|
+
for (let i = ctx._start.tokenIndex; i <= ctx._stop.tokenIndex; i++) {
|
|
175
|
+
const t = f.tokens[i];
|
|
176
|
+
if (t.type === tokens_1.L.IS)
|
|
177
|
+
hasIs = true;
|
|
178
|
+
if (t.type === tokens_1.L.ANNOTATION ||
|
|
179
|
+
t.type === tokens_1.L.DOC_ANNOTATION ||
|
|
180
|
+
t.type === tokens_1.L.BLOCK_ANNOTATION_BEGIN ||
|
|
181
|
+
t.type === tokens_1.L.DOC_BLOCK_ANNOTATION_BEGIN)
|
|
182
|
+
hasAnnotation = true;
|
|
183
|
+
}
|
|
184
|
+
return { ctx, hasIs, hasAnnotation };
|
|
185
|
+
}
|
|
186
|
+
function formatSectionList(f, items, itemKind) {
|
|
187
|
+
const itemInfos = items.map(it => classifyItem(f, it, itemKind));
|
|
188
|
+
const noAnnotations = itemInfos.every(info => !info.hasAnnotation);
|
|
189
|
+
const allBare = itemInfos.every(info => !info.hasIs && !info.hasAnnotation);
|
|
190
|
+
const firstItem = items[0];
|
|
191
|
+
const lastItem = items[items.length - 1];
|
|
192
|
+
// Inline candidate: no annotations, no hidden-channel comments anywhere in
|
|
193
|
+
// the items' span (renderItemInline drops them), AND either all bare or
|
|
194
|
+
// exactly one item. Items containing a `{...}` body with multiple inner
|
|
195
|
+
// statements are also excluded — collapsing a view body onto one line is
|
|
196
|
+
// hostile to read regardless of length.
|
|
197
|
+
const itemsHaveComments = (0, leaf_1.hasCommentsInRange)(f, firstItem._start.tokenIndex, lastItem._stop.tokenIndex);
|
|
198
|
+
const itemsHaveMultiStatementBody = itemInfos.some(info => hasMultiStatementCurlyBody(f, info.ctx));
|
|
199
|
+
const inlineEligible = noAnnotations &&
|
|
200
|
+
!itemsHaveComments &&
|
|
201
|
+
!itemsHaveMultiStatementBody &&
|
|
202
|
+
(allBare || items.length === 1);
|
|
203
|
+
if (inlineEligible) {
|
|
204
|
+
const renderedItems = itemInfos.map(info => (0, inline_renderer_1.renderItemInline)(f, info.ctx));
|
|
205
|
+
const inlineBody = renderedItems.join(', ');
|
|
206
|
+
const candidateLen = f.o.lineLengthSoFar() + 1 /* space */ + inlineBody.length;
|
|
207
|
+
if (candidateLen <= tokens_1.LINE_BUDGET) {
|
|
208
|
+
f.o.text(' ');
|
|
209
|
+
f.o.text(inlineBody);
|
|
210
|
+
(0, leaf_1.note)(f, lastItem._stop.type, lastItem._stop.tokenIndex, lastItem._stop);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Single is-item that doesn't fit inline: keep the keyword on the same
|
|
215
|
+
// line as the item (`nest: name is { …wrapped body… }`) instead of
|
|
216
|
+
// breaking before the name. The body's `{...}` will wrap on its own.
|
|
217
|
+
// Annotated items still need the keyword-on-own-line form so the
|
|
218
|
+
// annotation can land between them.
|
|
219
|
+
if (items.length === 1 &&
|
|
220
|
+
itemInfos[0].hasIs &&
|
|
221
|
+
!itemInfos[0].hasAnnotation &&
|
|
222
|
+
!itemsHaveComments) {
|
|
223
|
+
f.o.text(' ');
|
|
224
|
+
f.format(itemInfos[0].ctx);
|
|
225
|
+
f.lastEmittedType = lastItem._stop.type;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
// Wrapped form. Two paths:
|
|
229
|
+
// - No comments anywhere: original flow-fill — bare items pack into lines
|
|
230
|
+
// at LINE_BUDGET, `is`/annotated items each get their own line.
|
|
231
|
+
// - Comments anywhere in the items' span: every item emits on its own
|
|
232
|
+
// line via f.format(), which goes through emitVisibleToken /
|
|
233
|
+
// flushHiddenBefore and handles trailing-vs-leading attachment for
|
|
234
|
+
// hidden-channel tokens correctly. Flow-fill density is sacrificed for
|
|
235
|
+
// comment correctness.
|
|
236
|
+
f.o.indent++;
|
|
237
|
+
f.o.nl();
|
|
238
|
+
if (itemsHaveComments) {
|
|
239
|
+
f.lastEmittedIdx = firstItem._start.tokenIndex - 1;
|
|
240
|
+
for (let k = 0; k < itemInfos.length; k++) {
|
|
241
|
+
const info = itemInfos[k];
|
|
242
|
+
// flushHiddenBefore handles between-item comments and advances
|
|
243
|
+
// lastEmittedIdx so they aren't re-emitted by f.format(item)'s own first
|
|
244
|
+
// emit.
|
|
245
|
+
(0, leaf_1.flushHiddenBefore)(f, info.ctx._start.tokenIndex);
|
|
246
|
+
f.o.nl();
|
|
247
|
+
f.format(info.ctx);
|
|
248
|
+
}
|
|
249
|
+
flushSameLineTail(f, lastItem._stop);
|
|
250
|
+
f.o.indent--;
|
|
251
|
+
// Update lastEmittedType for leading-action decisions, but DO NOT reset
|
|
252
|
+
// lastEmittedIdx — flushSameLineTail may have advanced it past tail
|
|
253
|
+
// comments, and rolling it back would let the parent re-emit them.
|
|
254
|
+
f.lastEmittedType = lastItem._stop.type;
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
// No-comments fast path: flow-fill packing as before.
|
|
258
|
+
let curBare = '';
|
|
259
|
+
const flushBare = () => {
|
|
260
|
+
if (curBare.length > 0) {
|
|
261
|
+
f.o.text(curBare);
|
|
262
|
+
f.o.nl();
|
|
263
|
+
curBare = '';
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
for (const info of itemInfos) {
|
|
267
|
+
if (info.hasIs || info.hasAnnotation) {
|
|
268
|
+
flushBare();
|
|
269
|
+
(0, leaf_1.flushHiddenBefore)(f, info.ctx._start.tokenIndex);
|
|
270
|
+
f.format(info.ctx);
|
|
271
|
+
f.o.nl();
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
const itemText = (0, inline_renderer_1.renderItemInline)(f, info.ctx);
|
|
275
|
+
const indentChars = f.o.indent * tokens_1.INDENT_STR.length;
|
|
276
|
+
const sepLen = curBare.length > 0 ? 2 : 0; // ", "
|
|
277
|
+
const projected = indentChars + curBare.length + sepLen + itemText.length;
|
|
278
|
+
if (curBare.length > 0 && projected > tokens_1.LINE_BUDGET) {
|
|
279
|
+
flushBare();
|
|
280
|
+
curBare = itemText;
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
curBare = curBare.length > 0 ? curBare + ', ' + itemText : itemText;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
flushBare();
|
|
288
|
+
flushSameLineTail(f, lastItem._stop);
|
|
289
|
+
f.o.indent--;
|
|
290
|
+
// See comment in the with-comments branch: keep the advanced
|
|
291
|
+
// lastEmittedIdx so tail comments aren't re-emitted by the parent.
|
|
292
|
+
f.lastEmittedType = lastItem._stop.type;
|
|
293
|
+
}
|
|
294
|
+
// Does any `{...}` block inside this item contain more than one section
|
|
295
|
+
// keyword (group_by:, aggregate:, where:, …) at its top level? Used to gate
|
|
296
|
+
// the section-list inline form: a view body with multiple statements never
|
|
297
|
+
// reads well on a single line, even when it fits. Single-statement bodies
|
|
298
|
+
// like `{ where: x = 1 }` may still inline.
|
|
299
|
+
function hasMultiStatementCurlyBody(f, ctx) {
|
|
300
|
+
const fromIdx = ctx._start.tokenIndex;
|
|
301
|
+
const toIdx = ctx._stop.tokenIndex;
|
|
302
|
+
for (let i = fromIdx; i <= toIdx; i++) {
|
|
303
|
+
if (f.tokens[i].type !== tokens_1.L.OCURLY)
|
|
304
|
+
continue;
|
|
305
|
+
const close = (0, tokens_1.findMatching)(f.tokens, i, tokens_1.L.OCURLY, tokens_1.L.CCURLY);
|
|
306
|
+
let count = 0;
|
|
307
|
+
let depth = 0;
|
|
308
|
+
for (let j = i + 1; j < close; j++) {
|
|
309
|
+
const t = f.tokens[j];
|
|
310
|
+
if (t.type === tokens_1.L.OCURLY || t.type === tokens_1.L.OPAREN || t.type === tokens_1.L.OBRACK) {
|
|
311
|
+
depth++;
|
|
312
|
+
}
|
|
313
|
+
else if (t.type === tokens_1.L.CCURLY ||
|
|
314
|
+
t.type === tokens_1.L.CPAREN ||
|
|
315
|
+
t.type === tokens_1.L.CBRACK) {
|
|
316
|
+
depth--;
|
|
317
|
+
}
|
|
318
|
+
else if (depth === 0 && tokens_1.SECTION_TOKENS.has(t.type)) {
|
|
319
|
+
count++;
|
|
320
|
+
if (count > 1)
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
i = close;
|
|
325
|
+
}
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
// After the last item of a wrapped section list, flush trailing comments
|
|
329
|
+
// belonging to the section. There are two cases:
|
|
330
|
+
//
|
|
331
|
+
// 1. Same-line tail: a comment on the SAME source line as the last item.
|
|
332
|
+
// Always belongs to the last item; emit at the section's inner indent.
|
|
333
|
+
//
|
|
334
|
+
// 2. Different-line trailing comments that sit between the last item and
|
|
335
|
+
// the closing `}` of the enclosing block (no other statement follows).
|
|
336
|
+
// These visually belong to the section the user just wrote, not the
|
|
337
|
+
// block. Emit them at the inner indent so they stay associated with
|
|
338
|
+
// the section. If a real statement follows the comments, leave them
|
|
339
|
+
// for the parent — they're leading comments for that statement.
|
|
340
|
+
function flushSameLineTail(f, lastTok) {
|
|
341
|
+
const lastEndLine = (0, tokens_1.endLineOf)(lastTok);
|
|
342
|
+
let j = lastTok.tokenIndex + 1;
|
|
343
|
+
// Phase 1: same-line tail comments.
|
|
344
|
+
while (j < f.tokens.length) {
|
|
345
|
+
const t = f.tokens[j];
|
|
346
|
+
if (t.channel !== antlr4ts_1.Token.HIDDEN_CHANNEL)
|
|
347
|
+
break;
|
|
348
|
+
if (t.line !== lastEndLine)
|
|
349
|
+
break;
|
|
350
|
+
j++;
|
|
351
|
+
}
|
|
352
|
+
if (j > lastTok.tokenIndex + 1) {
|
|
353
|
+
// Same-line comments: drop the wrapping loop's per-item newline so
|
|
354
|
+
// emitHiddenToken's same-line branch reattaches the comment correctly
|
|
355
|
+
// (and adds a trailing newline back for EOL comments). Otherwise a
|
|
356
|
+
// re-parse sees a different-line comment, breaking idempotence.
|
|
357
|
+
f.o.trimTrailingNewlines();
|
|
358
|
+
(0, leaf_1.flushHiddenBefore)(f, j);
|
|
359
|
+
}
|
|
360
|
+
// Phase 2: own-line comments before the next visible token. If the next
|
|
361
|
+
// visible token is a closing `}`, the comments visually belong to this
|
|
362
|
+
// section — emit them at the inner indent. Otherwise leave them for the
|
|
363
|
+
// parent (they're leading comments for whatever follows).
|
|
364
|
+
let k = j;
|
|
365
|
+
let trailingHidden = 0;
|
|
366
|
+
while (k < f.tokens.length) {
|
|
367
|
+
const t = f.tokens[k];
|
|
368
|
+
if (t.channel !== antlr4ts_1.Token.HIDDEN_CHANNEL)
|
|
369
|
+
break;
|
|
370
|
+
trailingHidden++;
|
|
371
|
+
k++;
|
|
372
|
+
}
|
|
373
|
+
if (trailingHidden > 0 && k < f.tokens.length) {
|
|
374
|
+
const next = f.tokens[k];
|
|
375
|
+
if (next.type === tokens_1.L.CCURLY) {
|
|
376
|
+
(0, leaf_1.flushHiddenBefore)(f, k);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
//# sourceMappingURL=sections.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Token } from 'antlr4ts';
|
|
2
|
+
import { MalloyLexer } from '../lib/Malloy/MalloyLexer';
|
|
3
|
+
export declare const L: typeof MalloyLexer;
|
|
4
|
+
export declare const LINE_BUDGET = 100;
|
|
5
|
+
export declare const INDENT_STR = " ";
|
|
6
|
+
export declare const SECTION_TOKENS: Set<number>;
|
|
7
|
+
export declare const TOP_LEVEL_STARTERS: Set<number>;
|
|
8
|
+
export declare const CALL_HUG_AFTER: Set<number>;
|
|
9
|
+
export declare const BINARY_OPS: Set<number>;
|
|
10
|
+
export type LeadingAction = 'glue' | 'hug' | 'space';
|
|
11
|
+
export declare function leadingAction(prevType: number | null, nextType: number): LeadingAction;
|
|
12
|
+
export declare function endLineOf(t: Token): number;
|
|
13
|
+
export declare function findMatching(tokens: Token[], startIdx: number, beginType: number, endType: number): number;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright Contributors to the Malloy project
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*
|
|
6
|
+
* Token classification, layout config, and small token utilities used by the
|
|
7
|
+
* prettifier's leaf walker and rule formatters.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.BINARY_OPS = exports.CALL_HUG_AFTER = exports.TOP_LEVEL_STARTERS = exports.SECTION_TOKENS = exports.INDENT_STR = exports.LINE_BUDGET = exports.L = void 0;
|
|
11
|
+
exports.leadingAction = leadingAction;
|
|
12
|
+
exports.endLineOf = endLineOf;
|
|
13
|
+
exports.findMatching = findMatching;
|
|
14
|
+
const MalloyLexer_1 = require("../lib/Malloy/MalloyLexer");
|
|
15
|
+
exports.L = MalloyLexer_1.MalloyLexer;
|
|
16
|
+
// ---------- Global formatting config ----------
|
|
17
|
+
exports.LINE_BUDGET = 100;
|
|
18
|
+
exports.INDENT_STR = ' '; // two spaces per indent level
|
|
19
|
+
// ---------- Token classification ----------
|
|
20
|
+
// Section keywords that introduce a `keyword: items` block (or a single value
|
|
21
|
+
// for some). Used by the leaf walker as a fallback newline rule when no
|
|
22
|
+
// explicit section-statement handler caught us.
|
|
23
|
+
exports.SECTION_TOKENS = new Set([
|
|
24
|
+
exports.L.ACCEPT,
|
|
25
|
+
exports.L.AGGREGATE,
|
|
26
|
+
exports.L.CALCULATE,
|
|
27
|
+
exports.L.CALCULATION,
|
|
28
|
+
exports.L.CONNECTION,
|
|
29
|
+
exports.L.DECLARE,
|
|
30
|
+
exports.L.DIMENSION,
|
|
31
|
+
exports.L.DRILL,
|
|
32
|
+
exports.L.EXCEPT,
|
|
33
|
+
exports.L.EXTENDQ,
|
|
34
|
+
exports.L.GROUP_BY,
|
|
35
|
+
exports.L.GROUPED_BY,
|
|
36
|
+
exports.L.HAVING,
|
|
37
|
+
exports.L.INDEX,
|
|
38
|
+
exports.L.INTERNAL,
|
|
39
|
+
exports.L.JOIN_CROSS,
|
|
40
|
+
exports.L.JOIN_ONE,
|
|
41
|
+
exports.L.JOIN_MANY,
|
|
42
|
+
exports.L.LIMIT,
|
|
43
|
+
exports.L.MEASURE,
|
|
44
|
+
exports.L.NEST,
|
|
45
|
+
exports.L.ORDER_BY,
|
|
46
|
+
exports.L.PARTITION_BY,
|
|
47
|
+
exports.L.PRIMARY_KEY,
|
|
48
|
+
exports.L.PRIVATE,
|
|
49
|
+
exports.L.PROJECT,
|
|
50
|
+
exports.L.PUBLIC,
|
|
51
|
+
exports.L.QUERY,
|
|
52
|
+
exports.L.RENAME,
|
|
53
|
+
exports.L.RUN,
|
|
54
|
+
exports.L.SAMPLE,
|
|
55
|
+
exports.L.SELECT,
|
|
56
|
+
exports.L.SOURCE,
|
|
57
|
+
exports.L.TYPE,
|
|
58
|
+
exports.L.TOP,
|
|
59
|
+
exports.L.WHERE,
|
|
60
|
+
exports.L.VIEW,
|
|
61
|
+
exports.L.TIMEZONE,
|
|
62
|
+
]);
|
|
63
|
+
// Top-level statement starters. At column 0 each gets a blank line before it
|
|
64
|
+
// when introducing a new statement (subject to the same-kind-no-blank rule).
|
|
65
|
+
exports.TOP_LEVEL_STARTERS = new Set([
|
|
66
|
+
exports.L.SOURCE,
|
|
67
|
+
exports.L.QUERY,
|
|
68
|
+
exports.L.RUN,
|
|
69
|
+
exports.L.IMPORT,
|
|
70
|
+
]);
|
|
71
|
+
// Token types after which an immediately following `(` or `[` is a call /
|
|
72
|
+
// subscript and should hug (no leading space). Anything else (binary ops,
|
|
73
|
+
// IS/AS/EXTEND/ON/WHEN/PICK/etc.) gets a space — the `(` is grouping.
|
|
74
|
+
exports.CALL_HUG_AFTER = new Set([
|
|
75
|
+
exports.L.IDENTIFIER,
|
|
76
|
+
exports.L.CPAREN,
|
|
77
|
+
exports.L.CBRACK,
|
|
78
|
+
// Aggregate / built-in callable keywords commonly used as function names.
|
|
79
|
+
exports.L.COUNT,
|
|
80
|
+
exports.L.SUM,
|
|
81
|
+
exports.L.AVG,
|
|
82
|
+
exports.L.MIN,
|
|
83
|
+
exports.L.MAX,
|
|
84
|
+
exports.L.TABLE,
|
|
85
|
+
exports.L.SQL,
|
|
86
|
+
exports.L.COMPOSE,
|
|
87
|
+
exports.L.CAST,
|
|
88
|
+
exports.L.NOW,
|
|
89
|
+
exports.L.LAST,
|
|
90
|
+
// Ungrouped / level-modifier function-style calls.
|
|
91
|
+
exports.L.ALL,
|
|
92
|
+
exports.L.EXCLUDE,
|
|
93
|
+
// Timeframe truncation keywords used as functions: year(x), month(x), …
|
|
94
|
+
exports.L.YEAR,
|
|
95
|
+
exports.L.QUARTER,
|
|
96
|
+
exports.L.MONTH,
|
|
97
|
+
exports.L.WEEK,
|
|
98
|
+
exports.L.DAY,
|
|
99
|
+
exports.L.HOUR,
|
|
100
|
+
exports.L.MINUTE,
|
|
101
|
+
exports.L.SECOND,
|
|
102
|
+
// Type-cast keyword names: timestamp(x), date(x), number(x), string(x), …
|
|
103
|
+
exports.L.TIMESTAMP,
|
|
104
|
+
exports.L.TIMESTAMPTZ,
|
|
105
|
+
exports.L.DATE,
|
|
106
|
+
exports.L.NUMBER,
|
|
107
|
+
exports.L.STRING,
|
|
108
|
+
exports.L.BOOLEAN,
|
|
109
|
+
exports.L.JSON,
|
|
110
|
+
]);
|
|
111
|
+
// Binary operators that get spaces on both sides at the leaf level.
|
|
112
|
+
// (Chain wrapping for and/or/??/+/- happens at parse-tree level — see
|
|
113
|
+
// formatBinaryChain.)
|
|
114
|
+
exports.BINARY_OPS = new Set([
|
|
115
|
+
exports.L.PLUS,
|
|
116
|
+
exports.L.MINUS,
|
|
117
|
+
exports.L.STAR,
|
|
118
|
+
exports.L.SLASH,
|
|
119
|
+
exports.L.PERCENT,
|
|
120
|
+
exports.L.STARSTAR,
|
|
121
|
+
exports.L.EQ,
|
|
122
|
+
exports.L.NE,
|
|
123
|
+
exports.L.LT,
|
|
124
|
+
exports.L.GT,
|
|
125
|
+
exports.L.LTE,
|
|
126
|
+
exports.L.GTE,
|
|
127
|
+
exports.L.AND,
|
|
128
|
+
exports.L.OR,
|
|
129
|
+
exports.L.MATCH,
|
|
130
|
+
exports.L.NOT_MATCH,
|
|
131
|
+
exports.L.ARROW,
|
|
132
|
+
exports.L.FAT_ARROW,
|
|
133
|
+
exports.L.BAR,
|
|
134
|
+
exports.L.AMPER,
|
|
135
|
+
]);
|
|
136
|
+
function leadingAction(prevType, nextType) {
|
|
137
|
+
if (nextType === exports.L.DOT ||
|
|
138
|
+
nextType === exports.L.COMMA ||
|
|
139
|
+
nextType === exports.L.SEMI ||
|
|
140
|
+
nextType === exports.L.COLON ||
|
|
141
|
+
nextType === exports.L.TRIPLECOLON ||
|
|
142
|
+
nextType === exports.L.EXCLAM ||
|
|
143
|
+
nextType === exports.L.CPAREN ||
|
|
144
|
+
nextType === exports.L.CBRACK) {
|
|
145
|
+
return 'glue';
|
|
146
|
+
}
|
|
147
|
+
// After the `!` cast operator (`epoch_ms!timestamp(x)`), the next token
|
|
148
|
+
// glues to it like the `.` operator does.
|
|
149
|
+
if (prevType === exports.L.EXCLAM)
|
|
150
|
+
return 'glue';
|
|
151
|
+
if ((nextType === exports.L.OPAREN || nextType === exports.L.OBRACK) &&
|
|
152
|
+
prevType !== null &&
|
|
153
|
+
exports.CALL_HUG_AFTER.has(prevType)) {
|
|
154
|
+
return 'hug';
|
|
155
|
+
}
|
|
156
|
+
return 'space';
|
|
157
|
+
}
|
|
158
|
+
// ---------- Token utilities ----------
|
|
159
|
+
function endLineOf(t) {
|
|
160
|
+
var _a, _b;
|
|
161
|
+
const text = (_a = t.text) !== null && _a !== void 0 ? _a : '';
|
|
162
|
+
const newlines = ((_b = text.match(/\n/g)) !== null && _b !== void 0 ? _b : []).length;
|
|
163
|
+
return t.line + newlines;
|
|
164
|
+
}
|
|
165
|
+
// Find the index of the closing token that matches the opener at startIdx.
|
|
166
|
+
// Counts nested begin/end pairs of the same types.
|
|
167
|
+
//
|
|
168
|
+
// Precondition: must not be called inside an SQL string region — depth
|
|
169
|
+
// tracking only knows about the begin/end token types passed in, not about
|
|
170
|
+
// embedded SQL. In practice we never call this inside an SQL block (we skip
|
|
171
|
+
// past them).
|
|
172
|
+
function findMatching(tokens, startIdx, beginType, endType) {
|
|
173
|
+
let depth = 1;
|
|
174
|
+
for (let j = startIdx + 1; j < tokens.length; j++) {
|
|
175
|
+
if (tokens[j].type === beginType)
|
|
176
|
+
depth++;
|
|
177
|
+
else if (tokens[j].type === endType) {
|
|
178
|
+
depth--;
|
|
179
|
+
if (depth === 0)
|
|
180
|
+
return j;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return tokens.length - 1;
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=tokens.js.map
|
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
import { CommonTokenStream } from 'antlr4ts';
|
|
2
|
+
import type { ANTLRErrorListener, Token, CodePointCharStream } from 'antlr4ts';
|
|
3
|
+
import { MalloyParser } from './lib/Malloy/MalloyParser';
|
|
1
4
|
import type { MessageLogger } from './parse-log';
|
|
2
5
|
import type { ParseInfo, SourceInfo } from './utils';
|
|
6
|
+
export interface MalloyParserSetupOptions {
|
|
7
|
+
lexerErrorListener?: ANTLRErrorListener<any>;
|
|
8
|
+
parserErrorListener?: ANTLRErrorListener<Token>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Build a Malloy lexer/token-stream/parser triplet ready to invoke a grammar
|
|
12
|
+
* rule on `code`. Centralises the lexer fix (HandlesOverpoppingLexer for
|
|
13
|
+
* unmatched `}%`) and the project's MalloyErrorStrategy so callers can't
|
|
14
|
+
* accidentally diverge on those.
|
|
15
|
+
*
|
|
16
|
+
* The token stream is NOT pre-filled. Callers that need all tokens up front
|
|
17
|
+
* (e.g. the prettifier's leaf walker) should call `tokenStream.fill()` before
|
|
18
|
+
* iterating; callers that only need parser-driven access can leave it alone
|
|
19
|
+
* — the parser pulls tokens as it consumes the rule.
|
|
20
|
+
*/
|
|
21
|
+
export declare function makeMalloyParser(code: string, options?: MalloyParserSetupOptions): {
|
|
22
|
+
inputStream: CodePointCharStream;
|
|
23
|
+
tokenStream: CommonTokenStream;
|
|
24
|
+
parser: MalloyParser;
|
|
25
|
+
};
|
|
3
26
|
export declare function runMalloyParser(code: string, sourceURL: string, sourceInfo: SourceInfo, logger: MessageLogger, grammarRule?: string): ParseInfo;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.makeMalloyParser = makeMalloyParser;
|
|
9
10
|
exports.runMalloyParser = runMalloyParser;
|
|
10
11
|
const antlr4ts_1 = require("antlr4ts");
|
|
11
12
|
const MalloyLexer_1 = require("./lib/Malloy/MalloyLexer");
|
|
@@ -24,24 +25,47 @@ class HandlesOverpoppingLexer extends MalloyLexer_1.MalloyLexer {
|
|
|
24
25
|
return super.popMode();
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Build a Malloy lexer/token-stream/parser triplet ready to invoke a grammar
|
|
30
|
+
* rule on `code`. Centralises the lexer fix (HandlesOverpoppingLexer for
|
|
31
|
+
* unmatched `}%`) and the project's MalloyErrorStrategy so callers can't
|
|
32
|
+
* accidentally diverge on those.
|
|
33
|
+
*
|
|
34
|
+
* The token stream is NOT pre-filled. Callers that need all tokens up front
|
|
35
|
+
* (e.g. the prettifier's leaf walker) should call `tokenStream.fill()` before
|
|
36
|
+
* iterating; callers that only need parser-driven access can leave it alone
|
|
37
|
+
* — the parser pulls tokens as it consumes the rule.
|
|
38
|
+
*/
|
|
39
|
+
function makeMalloyParser(code, options = {}) {
|
|
28
40
|
const inputStream = antlr4ts_1.CharStreams.fromString(code);
|
|
29
41
|
const lexer = new HandlesOverpoppingLexer(inputStream);
|
|
42
|
+
if (options.lexerErrorListener) {
|
|
43
|
+
lexer.removeErrorListeners();
|
|
44
|
+
lexer.addErrorListener(options.lexerErrorListener);
|
|
45
|
+
}
|
|
30
46
|
const tokenStream = new antlr4ts_1.CommonTokenStream(lexer);
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
47
|
+
const parser = new MalloyParser_1.MalloyParser(tokenStream);
|
|
48
|
+
if (options.parserErrorListener) {
|
|
49
|
+
parser.removeErrorListeners();
|
|
50
|
+
parser.addErrorListener(options.parserErrorListener);
|
|
51
|
+
}
|
|
52
|
+
parser.errorHandler = new malloy_error_strategy_1.MalloyErrorStrategy();
|
|
53
|
+
return { inputStream, tokenStream, parser };
|
|
54
|
+
}
|
|
55
|
+
function runMalloyParser(code, sourceURL, sourceInfo, logger, grammarRule = 'malloyDocument') {
|
|
56
|
+
const { inputStream, tokenStream, parser } = makeMalloyParser(code, {
|
|
57
|
+
parserErrorListener: new malloy_parser_error_listener_1.MalloyParserErrorListener(logger, sourceURL, sourceInfo),
|
|
58
|
+
});
|
|
35
59
|
// Admitted code smell here, testing likes to parse from an arbitrary
|
|
36
60
|
// node and this is the simplest way to allow that.
|
|
37
61
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
|
-
const parseFunc =
|
|
62
|
+
const parseFunc = parser[grammarRule];
|
|
39
63
|
if (!parseFunc) {
|
|
40
64
|
throw new Error(`No such parse rule as ${grammarRule}`);
|
|
41
65
|
}
|
|
42
66
|
return {
|
|
43
|
-
root: parseFunc.call(
|
|
44
|
-
tokenStream
|
|
67
|
+
root: parseFunc.call(parser),
|
|
68
|
+
tokenStream,
|
|
45
69
|
sourceStream: inputStream,
|
|
46
70
|
sourceInfo,
|
|
47
71
|
sourceURL,
|