@ohos-ports/ember-estree 0.6.11-beta.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/src/tokens.js ADDED
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Token stream generation for Glimmer templates.
3
+ *
4
+ * Only needed by ESLint consumers — codemods, type-checkers, and formatters
5
+ * do not use the flat token stream and skip this entirely by omitting
6
+ * `{ tokens: true }` from processTemplate options.
7
+ *
8
+ * ESLint-specific note: comment tokens are placed at range[0]+1 rather than
9
+ * range[0] because ESLint's createIndexMap infinite-loops when a token and an
10
+ * ast.comments entry share range[0] — both inner loops fail the strict-less-than
11
+ * guard and the outer loop never advances. Tracked upstream as eslint/eslint#20492.
12
+ */
13
+
14
+ function isAlphaNumeric(code) {
15
+ return (code >= 48 && code <= 57) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
16
+ }
17
+
18
+ /**
19
+ * Lex a Glimmer template into a flat token array.
20
+ * Tracks line/column incrementally from a single seed call to
21
+ * doc.offsetToPosition — O(n + log L) instead of O(n log L).
22
+ */
23
+ export function tokenize(template, doc, startOffset) {
24
+ const tokens = [];
25
+ let wordStart = -1;
26
+
27
+ // Seed position from the start offset — one binary search total.
28
+ // Then track line/column incrementally (DocumentLines counts only \n
29
+ // as line separators, so this matches offsetToPosition exactly).
30
+ let { line: curLine, column: curCol } = doc.offsetToPosition(startOffset);
31
+ let wordLine = 0,
32
+ wordCol = 0;
33
+
34
+ for (let i = 0; i < template.length; i++) {
35
+ const code = template.charCodeAt(i);
36
+ if (isAlphaNumeric(code)) {
37
+ if (wordStart < 0) {
38
+ wordStart = i;
39
+ wordLine = curLine;
40
+ wordCol = curCol;
41
+ }
42
+ curCol++;
43
+ } else {
44
+ if (wordStart >= 0) {
45
+ const absStart = startOffset + wordStart;
46
+ const absEnd = startOffset + i;
47
+ tokens.push({
48
+ type: "word",
49
+ value: template.slice(wordStart, i),
50
+ range: [absStart, absEnd],
51
+ start: absStart,
52
+ end: absEnd,
53
+ loc: {
54
+ start: { line: wordLine, column: wordCol, index: absStart },
55
+ end: { line: curLine, column: curCol, index: absEnd },
56
+ },
57
+ });
58
+ wordStart = -1;
59
+ }
60
+ if (code === 10 /* \n */) {
61
+ curLine++;
62
+ curCol = 0;
63
+ } else {
64
+ if (code !== 32 && code !== 9 && code !== 13 && code !== 11 /* non-whitespace */) {
65
+ const absPos = startOffset + i;
66
+ tokens.push({
67
+ type: "Punctuator",
68
+ value: template[i],
69
+ range: [absPos, absPos + 1],
70
+ start: absPos,
71
+ end: absPos + 1,
72
+ loc: {
73
+ start: { line: curLine, column: curCol, index: absPos },
74
+ end: { line: curLine, column: curCol + 1, index: absPos + 1 },
75
+ },
76
+ });
77
+ }
78
+ curCol++;
79
+ }
80
+ }
81
+ }
82
+
83
+ if (wordStart >= 0) {
84
+ const absStart = startOffset + wordStart;
85
+ const absEnd = startOffset + template.length;
86
+ tokens.push({
87
+ type: "word",
88
+ value: template.slice(wordStart),
89
+ range: [absStart, absEnd],
90
+ start: absStart,
91
+ end: absEnd,
92
+ loc: {
93
+ start: { line: wordLine, column: wordCol, index: absStart },
94
+ end: { line: curLine, column: curCol, index: absEnd },
95
+ },
96
+ });
97
+ }
98
+
99
+ return tokens;
100
+ }
101
+
102
+ /**
103
+ * Merge the raw Glimmer token stream with text nodes and comment tokens,
104
+ * dropping raw tokens that fall inside comment or text-node intervals.
105
+ *
106
+ * All inputs are sorted by range[0] (sequential document scan), so the
107
+ * entire operation is a single O(n+m+k) pass with advancing pointers
108
+ * rather than O(n log m) per-token binary searches.
109
+ */
110
+ export function buildTokenStream(rawTokens, comments, textNodes, templateContent, offset) {
111
+ // Comment tokens shifted by 1 to avoid the ESLint createIndexMap conflict
112
+ const commentTokens = comments.map((c) => {
113
+ const start = c.range[0] + 1;
114
+ const end = c.range[1];
115
+ return {
116
+ type: "Block",
117
+ value: templateContent.slice(start - offset, end - offset),
118
+ range: [start, end],
119
+ start,
120
+ end,
121
+ loc: c.loc,
122
+ };
123
+ });
124
+
125
+ const spliceables = linearMerge(textNodes, commentTokens);
126
+
127
+ const result = [];
128
+ let ri = 0; // rawTokens
129
+ let si = 0; // spliceables
130
+ let ci = 0; // comment intervals (for skip detection)
131
+ let ni = 0; // text-node intervals (for skip detection)
132
+
133
+ while (ri < rawTokens.length || si < spliceables.length) {
134
+ if (ri >= rawTokens.length) {
135
+ result.push(spliceables[si++]);
136
+ continue;
137
+ }
138
+
139
+ const tok = rawTokens[ri];
140
+
141
+ // Advance interval pointers past intervals that end before this token
142
+ while (ci < comments.length && comments[ci].range[1] <= tok.range[0]) ci++;
143
+ while (ni < textNodes.length && textNodes[ni].range[1] <= tok.range[0]) ni++;
144
+
145
+ // Skip raw token if it falls inside a comment or text-node interval
146
+ if (
147
+ (ci < comments.length && comments[ci].range[0] <= tok.range[0]) ||
148
+ (ni < textNodes.length && textNodes[ni].range[0] <= tok.range[0])
149
+ ) {
150
+ ri++;
151
+ continue;
152
+ }
153
+
154
+ // Emit the earlier of the next spliceable or this raw token
155
+ if (si < spliceables.length && spliceables[si].range[0] < tok.range[0]) {
156
+ result.push(spliceables[si++]);
157
+ } else {
158
+ result.push(tok);
159
+ ri++;
160
+ }
161
+ }
162
+
163
+ return result;
164
+ }
165
+
166
+ function linearMerge(a, b) {
167
+ const result = Array.from({ length: a.length + b.length });
168
+ let ai = 0,
169
+ bi = 0,
170
+ ri = 0;
171
+ while (ai < a.length && bi < b.length) {
172
+ result[ri++] = a[ai].range[0] <= b[bi].range[0] ? a[ai++] : b[bi++];
173
+ }
174
+ while (ai < a.length) result[ri++] = a[ai++];
175
+ while (bi < b.length) result[ri++] = b[bi++];
176
+ return result;
177
+ }
@@ -0,0 +1,279 @@
1
+ /**
2
+ * Glimmer AST → ESTree transform utilities.
3
+ */
4
+
5
+ import {
6
+ visitorKeys as rawGlimmerVisitorKeys,
7
+ preprocess as glimmerPreprocess,
8
+ } from "@glimmer/syntax";
9
+
10
+ import { tokenize, buildTokenStream } from "./tokens.js";
11
+
12
+ /**
13
+ * Converts between character offsets and line/column positions.
14
+ * Lines are 1-based, columns are 0-based (matching ESTree & Glimmer conventions).
15
+ */
16
+ export class DocumentLines {
17
+ constructor(source) {
18
+ this.lineStarts = [0];
19
+ for (let i = 0; i < source.length; i++) {
20
+ if (source[i] === "\n") {
21
+ this.lineStarts.push(i + 1);
22
+ }
23
+ }
24
+ }
25
+
26
+ positionToOffset(pos) {
27
+ return this.lineStarts[pos.line - 1] + pos.column;
28
+ }
29
+
30
+ offsetToPosition(offset) {
31
+ let lo = 0;
32
+ let hi = this.lineStarts.length - 1;
33
+ while (lo < hi) {
34
+ const mid = (lo + hi + 1) >> 1;
35
+ if (this.lineStarts[mid] <= offset) lo = mid;
36
+ else hi = mid - 1;
37
+ }
38
+ return { line: lo + 1, column: offset - this.lineStarts[lo] };
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Glimmer visitor keys map with "Glimmer" prefix.
44
+ * Computed once at module load.
45
+ */
46
+ export const glimmerVisitorKeys = (() => {
47
+ const keys = {};
48
+ for (const [k, v] of Object.entries(rawGlimmerVisitorKeys)) {
49
+ keys[`Glimmer${k}`] = v;
50
+ }
51
+ keys.GlimmerElementNode = [...keys.GlimmerElementNode, "blockParamNodes", "parts"];
52
+ keys.GlimmerProgram = ["body", "blockParamNodes"];
53
+ keys.GlimmerTemplate = ["body"];
54
+ return keys;
55
+ })();
56
+
57
+ // ── Internal helpers ──────────────────────────────────────────────────
58
+
59
+ // @glimmer/syntax nodes use prototype getters that form circular chains,
60
+ // crashing traversers like esrecurse. We snapshot configurable getters:
61
+ // ElementNode: tag, blockParams, selfClosing
62
+ // PathExpression: original
63
+ // VarHead: name, original
64
+ // Block: blockParams
65
+ const _desc = { value: undefined, configurable: true, enumerable: true, writable: true };
66
+ const _parentDesc = { value: null, configurable: true, enumerable: false, writable: true };
67
+ export function setParent(node, parent) {
68
+ _parentDesc.value = parent;
69
+ Object.defineProperty(node, "parent", _parentDesc);
70
+ }
71
+ function defOwn(obj, key) {
72
+ _desc.value = obj[key];
73
+ Object.defineProperty(obj, key, _desc);
74
+ }
75
+
76
+ function removeFromParent(nodes) {
77
+ for (const node of nodes) {
78
+ const children =
79
+ (node.parent && (node.parent.children || node.parent.body || node.parent.parts)) || [];
80
+ const idx = children.indexOf(node);
81
+ if (idx >= 0) {
82
+ children.splice(idx, 1);
83
+ }
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Parse and transform a Glimmer template into an ESTree-compatible AST.
89
+ * Internal — consumed by toTree.
90
+ *
91
+ * Single recursive pass: collect, categorize, snapshot getters, fix
92
+ * positions, create parts/blockParamNodes, nullify empty hashes, and
93
+ * prefix types. No separate collect-then-transform loop.
94
+ */
95
+ export function processTemplate(templateContent, codeLines, options = {}) {
96
+ const { templateRange, tokens: generateTokens = false } = options;
97
+ const offset = templateRange[0];
98
+ const docLines = offset === 0 ? codeLines : new DocumentLines(templateContent);
99
+
100
+ const toFileRange = (loc) => [
101
+ offset + docLines.positionToOffset(loc.start),
102
+ offset + docLines.positionToOffset(loc.end),
103
+ ];
104
+ const toFileLoc = (range) => ({
105
+ start: codeLines.offsetToPosition(range[0]),
106
+ end: codeLines.offsetToPosition(range[1]),
107
+ });
108
+
109
+ const ast = glimmerPreprocess(templateContent, { mode: "codemod" });
110
+ const comments = [];
111
+ const textNodes = [];
112
+ const emptyTextNodes = [];
113
+
114
+ // Single recursive pass over the glimmer AST. Processes each node
115
+ // fully (getters, positions, parts, blockParams) then recurses into
116
+ // children using raw visitor keys. Type prefixing happens inline
117
+ // AFTER recursing (so children see the original type during lookup).
118
+ function visit(n, parent) {
119
+ setParent(n, parent);
120
+
121
+ // Categorize
122
+ if (n.type === "CommentStatement" || n.type === "MustacheCommentStatement") {
123
+ comments.push(n);
124
+ }
125
+ if (n.type === "TextNode") {
126
+ n.value = n.chars;
127
+ if (n.value.trim().length !== 0 || (parent && parent.type === "AttrNode")) {
128
+ textNodes.push(n);
129
+ } else {
130
+ emptyTextNodes.push(n);
131
+ }
132
+ }
133
+
134
+ // Snapshot configurable prototype getters
135
+ switch (n.type) {
136
+ case "ElementNode":
137
+ defOwn(n, "tag");
138
+ defOwn(n, "blockParams");
139
+ defOwn(n, "selfClosing");
140
+ if (n.path?.head) {
141
+ defOwn(n.path.head, "name");
142
+ defOwn(n.path.head, "original");
143
+ }
144
+ break;
145
+ case "PathExpression":
146
+ defOwn(n, "original");
147
+ if (n.head) {
148
+ defOwn(n.head, "name");
149
+ defOwn(n.head, "original");
150
+ }
151
+ break;
152
+ case "Block":
153
+ defOwn(n, "blockParams");
154
+ break;
155
+ }
156
+
157
+ // Fix positions
158
+ if (n.type === "PathExpression") {
159
+ n.head.range = toFileRange(n.head.loc);
160
+ n.head.start = n.head.range[0];
161
+ n.head.end = n.head.range[1];
162
+ n.head.loc = toFileLoc(n.head.range);
163
+ }
164
+ n.range = n.type === "Template" ? [...templateRange] : toFileRange(n.loc);
165
+ n.start = n.range[0];
166
+ n.end = n.range[1];
167
+ n.loc = toFileLoc(n.range);
168
+
169
+ if (n.type === "MustacheCommentStatement") {
170
+ n.longForm = templateContent.slice(n.start - offset, n.start - offset + 4) === "{{!-";
171
+ }
172
+
173
+ // Create parts for ElementNode
174
+ if (n.type === "ElementNode") {
175
+ n.name = n.tag;
176
+ const p = n.path.head;
177
+ const partRange = toFileRange(p.loc);
178
+ const part = {
179
+ type: "GlimmerElementNodePart",
180
+ original: p.original,
181
+ name: p.original,
182
+ range: partRange,
183
+ start: partRange[0],
184
+ end: partRange[1],
185
+ loc: toFileLoc(partRange),
186
+ };
187
+ setParent(part, n);
188
+ n.parts = [part];
189
+ }
190
+
191
+ // Create blockParamNodes
192
+ if ("blockParams" in n && Array.isArray(n.blockParams)) {
193
+ if (n.params && n.params.length === n.blockParams.length) {
194
+ n.blockParamNodes = n.params.map((p) => {
195
+ const range = toFileRange(p.loc);
196
+ const bp = {
197
+ type: "GlimmerBlockParam",
198
+ name: p.original || p.name,
199
+ original: p.original,
200
+ range,
201
+ start: range[0],
202
+ end: range[1],
203
+ loc: toFileLoc(range),
204
+ };
205
+ setParent(bp, n);
206
+ return bp;
207
+ });
208
+ } else {
209
+ n.blockParamNodes = n.blockParams.map((bpName) => {
210
+ const bp = {
211
+ type: "GlimmerBlockParam",
212
+ name: bpName,
213
+ range: [n.range[0], n.range[1]],
214
+ start: n.range[0],
215
+ end: n.range[1],
216
+ loc: toFileLoc(n.range),
217
+ };
218
+ setParent(bp, n);
219
+ return bp;
220
+ });
221
+ }
222
+ }
223
+
224
+ // Nullify empty hashes
225
+ if (
226
+ (n.type === "MustacheStatement" ||
227
+ n.type === "BlockStatement" ||
228
+ n.type === "SubExpression") &&
229
+ n.hash?.pairs?.length === 0
230
+ ) {
231
+ n.hash = null;
232
+ }
233
+
234
+ // Recurse into children BEFORE prefixing type (visitor keys use original type)
235
+ const keys = rawGlimmerVisitorKeys[n.type];
236
+ if (keys) {
237
+ for (const key of keys) {
238
+ const child = n[key];
239
+ if (!child) continue;
240
+ if (Array.isArray(child)) {
241
+ for (const item of child) {
242
+ if (item && typeof item === "object" && item.type) visit(item, n);
243
+ }
244
+ } else if (typeof child === "object" && child.type) {
245
+ visit(child, n);
246
+ }
247
+ }
248
+ }
249
+
250
+ // Prefix type after children are visited
251
+ n.type = `Glimmer${n.type}`;
252
+ }
253
+
254
+ visit(ast, null);
255
+
256
+ removeFromParent(emptyTextNodes);
257
+
258
+ if (generateTokens) {
259
+ // buildTokenStream walks comments and textNodes as sorted intervals via
260
+ // pointer advancement. AST-traversal order doesn't match source order
261
+ // when an element has both attribute-position and body-position
262
+ // MustacheCommentStatements (e.g. `<li {{! a }}>{{! b }}</li>`), so
263
+ // sort here. Without the sort, raw tokens that fall inside an earlier-
264
+ // source-position comment fail the skip check, the same span ends up
265
+ // tokenized twice (raw punctuators *and* a Block), and downstream
266
+ // ESLint token walks can infinite-loop on the non-monotonic stream.
267
+ comments.sort((a, b) => a.range[0] - b.range[0]);
268
+ ast.tokens = buildTokenStream(
269
+ tokenize(templateContent, codeLines, offset),
270
+ comments,
271
+ textNodes,
272
+ templateContent,
273
+ offset,
274
+ );
275
+ }
276
+ ast.contents = templateContent;
277
+
278
+ return { ast, comments };
279
+ }