@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/parse.js ADDED
@@ -0,0 +1,388 @@
1
+ /**
2
+ * The Strategy:
3
+ *
4
+ * 1. parse out the <template>...</template> regions (content-tag)
5
+ * 2. create placeholder JS for the template regions (backtick/static-block, same char length)
6
+ * 3. parse as js/ts — default: oxc-parser, or a custom parser via options
7
+ * 4. splice in processed Glimmer ASTs, invoking visitors during traversal
8
+ * 5. Merge Glimmer visitor keys into the result
9
+ * 6. Done
10
+ */
11
+
12
+ import { parseSync, visitorKeys as oxcVisitorKeys } from "@ohos-ports/oxc-parser";
13
+ import { Preprocessor } from "content-tag";
14
+
15
+ import { processTemplate, DocumentLines, glimmerVisitorKeys, setParent } from "./transforms.js";
16
+
17
+ // Base visitor-keys map for the outer-AST walk: oxc-parser's own keys (covers
18
+ // standard ESTree + TS), plus the `File` wrapper we add on the default path,
19
+ // plus Glimmer's keys. Used to iterate only declared child slots instead of
20
+ // every enumerable property on every node.
21
+ //
22
+ // When `options.parser` returns `visitorKeys`, callers merge on top — but if
23
+ // their parser's AST is oxc-compatible, this base is already sufficient.
24
+ const DEFAULT_VISITOR_KEYS = {
25
+ ...oxcVisitorKeys,
26
+ File: ["program"],
27
+ ...glimmerVisitorKeys,
28
+ };
29
+
30
+ // Swap `oldNode` for `newNode` in whichever slot of `parent` currently holds it.
31
+ // Used to splice a GlimmerTemplate directly into the outer AST without
32
+ // allocating new ancestor objects — keeps WeakMap-keyed data (scope manager,
33
+ // esTreeNodeToTSNodeMap) attached to the existing nodes.
34
+ function replaceInParent(parent, oldNode, newNode) {
35
+ for (const key of Object.keys(parent)) {
36
+ const v = parent[key];
37
+ if (v === oldNode) {
38
+ parent[key] = newNode;
39
+ return true;
40
+ }
41
+ if (Array.isArray(v)) {
42
+ const idx = v.indexOf(oldNode);
43
+ if (idx !== -1) {
44
+ v[idx] = newNode;
45
+ return true;
46
+ }
47
+ }
48
+ }
49
+ return false;
50
+ }
51
+
52
+ const preprocessor = new Preprocessor();
53
+
54
+ // Node types that placeholders parse into (backtick/static-block format)
55
+ const PLACEHOLDER_TYPES = new Set([
56
+ "ExpressionStatement",
57
+ "StaticBlock",
58
+ "TemplateLiteral",
59
+ "ExportDefaultDeclaration",
60
+ ]);
61
+
62
+ /**
63
+ * Parse Ember source and return an ESTree-compatible AST.
64
+ *
65
+ * @param {string} source
66
+ * @param {object} [options]
67
+ * @param {string} [options.filePath] - File path for language detection
68
+ * @param {boolean} [options.tokens] - Generate a flat token stream on the AST (needed by ESLint; skipped by default)
69
+ * @param {boolean} [options.templateOnly] - Parse as raw Glimmer template content (for .hbs)
70
+ * @param {function} [options.parser] - Custom JS/TS parser: (placeholderJS) => { ast, scopeManager?, visitorKeys?, services?, ... }.
71
+ * Recommended to return `visitorKeys` describing the parser's AST; when omitted, oxc-parser's
72
+ * keys are used (fine for oxc-compatible ASTs, incomplete for parsers that emit bespoke node types).
73
+ * @param {object|function} [options.visitors] - Either a map of `{ [Type]: (node, path) => void }`
74
+ * handlers, or a factory `(outerAst) => handlers` invoked once after parsing (before any
75
+ * template splicing) to give callers a view of the raw JS/TS tree. Handlers fire on every
76
+ * node during traversal — outer JS/TS nodes AND spliced Glimmer subtrees — in a single pass.
77
+ * The pseudo-type `GlimmerBlockParams` fires on any node that carries `blockParams`.
78
+ * @return {object}
79
+ */
80
+ export function toTree(source, options = {}) {
81
+ const generateTokens = !!options.tokens;
82
+
83
+ if (options.templateOnly) {
84
+ return processTemplate(source, new DocumentLines(source), {
85
+ templateRange: [0, source.length],
86
+ tokens: generateTokens,
87
+ });
88
+ }
89
+
90
+ let parseResults = preprocessor.parse(source);
91
+ let js = toPlaceholderJS(source, parseResults);
92
+
93
+ const useCustomParser = !!options.parser;
94
+
95
+ // Parse the placeholder JS — use custom parser or default oxc
96
+ let result;
97
+ if (useCustomParser) {
98
+ result = options.parser(js);
99
+ if (!result.ast) {
100
+ result = { ast: result };
101
+ }
102
+ } else {
103
+ let filename = options.filePath || "input.ts";
104
+ if (filename.includes(".gts")) {
105
+ filename = filename.replace(/\.gts$/, ".ts");
106
+ }
107
+ let oxcResult = parseSync(filename, js);
108
+ result = {
109
+ ast: {
110
+ type: "File",
111
+ program: oxcResult.program,
112
+ comments: oxcResult.comments || [],
113
+ start: oxcResult.program.start,
114
+ end: oxcResult.program.end,
115
+ },
116
+ };
117
+ }
118
+
119
+ // Resolve user visitors against the outer AST. A plain object is used
120
+ // as-is; a factory is called once so callers can introspect the raw
121
+ // JS/TS tree before any template splicing. Default to `{}` so downstream
122
+ // dispatch can be a bare `visitors[type]` lookup without null-guards.
123
+ const visitors =
124
+ typeof options.visitors === "function"
125
+ ? (options.visitors(result.ast) ?? {})
126
+ : (options.visitors ?? {});
127
+ const hasVisitors = Object.keys(visitors).length > 0;
128
+ // Guard against dispatching a handler twice on the same node.
129
+ // Visitors that relocate nodes (e.g. moving Glimmer comments into
130
+ // `program.comments`) would otherwise fire a second time when the walk
131
+ // reaches the new location.
132
+ const seen = new WeakSet();
133
+ const hasTemplates = parseResults.length > 0;
134
+
135
+ // Nothing to walk — attach visitor keys and return.
136
+ if (!hasTemplates && !hasVisitors) {
137
+ if (useCustomParser) {
138
+ result.visitorKeys = { ...result.visitorKeys, ...glimmerVisitorKeys };
139
+ return result;
140
+ }
141
+ result.ast.visitorKeys = glimmerVisitorKeys;
142
+ return result.ast;
143
+ }
144
+
145
+ const codeLines = hasTemplates ? new DocumentLines(source) : null;
146
+ const templateInfos = [];
147
+ const templateRangeByStart = hasTemplates
148
+ ? new Map(parseResults.map((r) => [r.range.startUtf16Codepoint, r]))
149
+ : null;
150
+
151
+ // Process a matched placeholder node: create Glimmer AST and tokens.
152
+ // `placeholderNode` is the original JS/TS node being swapped out; we stash
153
+ // it on templateInfos so consumers can forward its parser-services mapping
154
+ // (e.g. esTreeNodeToTSNodeMap) onto the GlimmerTemplate that replaces it.
155
+ function processPlaceholder(parseResult, placeholderNode) {
156
+ let templateContent = parseResult.contents;
157
+ let contentRange = [
158
+ parseResult.contentRange.startUtf16Codepoint,
159
+ parseResult.contentRange.endUtf16Codepoint,
160
+ ];
161
+ let fullRange = [parseResult.range.startUtf16Codepoint, parseResult.range.endUtf16Codepoint];
162
+
163
+ const { ast } = processTemplate(templateContent, codeLines, {
164
+ templateRange: contentRange,
165
+ tokens: generateTokens,
166
+ });
167
+
168
+ // Fix the Template root to cover the full <template>...</template> range
169
+ ast.range = fullRange;
170
+ ast.start = fullRange[0];
171
+ ast.end = fullRange[1];
172
+ ast.loc = {
173
+ start: codeLines.offsetToPosition(fullRange[0]),
174
+ end: codeLines.offsetToPosition(fullRange[1]),
175
+ };
176
+
177
+ if (generateTokens) {
178
+ // Add tokens for the <template> and </template> tags
179
+ const openEnd = contentRange[0];
180
+ const closeStart = contentRange[1];
181
+ const openTag = source.slice(fullRange[0], openEnd);
182
+ const closeTag = source.slice(closeStart, fullRange[1]);
183
+ const makeToken = (value, range) => ({
184
+ type: "Punctuator",
185
+ value,
186
+ range,
187
+ start: range[0],
188
+ end: range[1],
189
+ loc: {
190
+ start: codeLines.offsetToPosition(range[0]),
191
+ end: codeLines.offsetToPosition(range[1]),
192
+ },
193
+ });
194
+ ast.tokens = [
195
+ makeToken(openTag, [fullRange[0], openEnd]),
196
+ ...(ast.tokens || []),
197
+ makeToken(closeTag, [closeStart, fullRange[1]]),
198
+ ];
199
+ }
200
+
201
+ templateInfos.push({ utf16Range: fullRange, ast, placeholder: placeholderNode });
202
+ return ast;
203
+ }
204
+
205
+ // Check if a node matches a template range
206
+ function matchPlaceholder(node) {
207
+ let range = node.range || [node.start, node.end];
208
+ if (node.type === "ExportDefaultDeclaration" && node.declaration) {
209
+ const decl = node.declaration;
210
+ range = decl.range || [decl.start, decl.end];
211
+ }
212
+ const parseResult = templateRangeByStart.get(range[0]);
213
+ if (
214
+ !parseResult ||
215
+ (parseResult.range.endUtf16Codepoint !== range[1] &&
216
+ parseResult.range.endUtf16Codepoint !== range[1] + 1)
217
+ ) {
218
+ return null;
219
+ }
220
+ return parseResult;
221
+ }
222
+
223
+ // Walk the outer AST keyed on visitorKeys — iterating only declared child
224
+ // slots instead of every enumerable property on every node. Custom parsers
225
+ // may supply their own keys; those override the defaults for types they
226
+ // recognise, and Glimmer keys stay on top for the spliced subtrees.
227
+ const allVisitorKeys =
228
+ useCustomParser && result.visitorKeys
229
+ ? { ...DEFAULT_VISITOR_KEYS, ...result.visitorKeys, ...glimmerVisitorKeys }
230
+ : DEFAULT_VISITOR_KEYS;
231
+
232
+ function walkWithKeys(node, parentPath) {
233
+ if (!node || !node.type) return;
234
+
235
+ if (hasTemplates && PLACEHOLDER_TYPES.has(node.type)) {
236
+ const parseResult = matchPlaceholder(node);
237
+ if (parseResult) {
238
+ // Splice in place: write the GlimmerTemplate directly into the parent's
239
+ // slot instead of allocating new ancestor objects. This preserves node
240
+ // identity for every ancestor, which matters for WeakMap-keyed data
241
+ // held by custom parsers (scope manager, esTreeNodeToTSNodeMap).
242
+ const ast = processPlaceholder(parseResult, node);
243
+ const parent = parentPath?.node ?? null;
244
+ if (parent) replaceInParent(parent, node, ast);
245
+ setParent(ast, parent);
246
+ // Recurse into the Glimmer subtree so visitors fire on its nodes too.
247
+ // The Glimmer root's parentPath reflects its true JS parent — the
248
+ // placeholder (TemplateLiteral / StaticBlock) is an internal artifact.
249
+ if (hasVisitors) walkWithKeys(ast, parentPath);
250
+ return;
251
+ }
252
+ }
253
+
254
+ const path = { node, parent: parentPath?.node ?? null, parentPath };
255
+
256
+ if (hasVisitors && !seen.has(node)) {
257
+ seen.add(node);
258
+ const handler = visitors[node.type];
259
+ if (handler) handler(node, path);
260
+ if ("blockParams" in node && visitors.GlimmerBlockParams) {
261
+ visitors.GlimmerBlockParams(node, path);
262
+ }
263
+ }
264
+
265
+ const keys = allVisitorKeys[node.type];
266
+ if (!keys) return;
267
+ for (const key of keys) {
268
+ const child = node[key];
269
+ if (!child) continue;
270
+ if (Array.isArray(child)) {
271
+ for (const item of child) {
272
+ if (item && typeof item === "object" && item.type) {
273
+ walkWithKeys(item, path);
274
+ }
275
+ }
276
+ } else if (typeof child === "object" && child.type) {
277
+ walkWithKeys(child, path);
278
+ }
279
+ }
280
+ }
281
+
282
+ walkWithKeys(result.ast, null);
283
+
284
+ // Splice template tokens into the AST token stream.
285
+ //
286
+ // `tokens` is the flat lexed stream (keywords, punctuators, identifiers,
287
+ // literals) that ESLint, formatters, and source-map tooling consume —
288
+ // `SourceCode.getTokens()` reads it directly.
289
+ //
290
+ // We replaced each <template>...</template> region with a backtick
291
+ // placeholder before handing the source to the JS/TS parser, so the
292
+ // parser's tokens for those ranges describe the placeholder, not the
293
+ // real source. Here we swap them out for the real lexemes:
294
+ // 1. a fabricated `<template>` Punctuator (added in processPlaceholder)
295
+ // 2. the Glimmer AST's own tokens (from transforms.js)
296
+ // 3. a fabricated `</template>` Punctuator
297
+ // so consumers see a position-accurate token stream matching the
298
+ // original source byte-for-byte across JS and Glimmer regions.
299
+ //
300
+ // Tokens are sorted by range, so use binary search for O(log n) lookup.
301
+ // Only splice if the caller asked for tokens — otherwise `ti.ast.tokens`
302
+ // wasn't populated by processPlaceholder, and a custom parser may still
303
+ // have returned its own token stream we shouldn't touch.
304
+ const astRoot = result.ast.program || result.ast;
305
+ if (generateTokens && astRoot.tokens) {
306
+ for (const ti of templateInfos) {
307
+ const [tStart, tEnd] = ti.utf16Range;
308
+ const tokens = astRoot.tokens;
309
+ // Binary search for first token with range[0] >= tStart
310
+ let lo = 0;
311
+ let hi = tokens.length;
312
+ while (lo < hi) {
313
+ const mid = (lo + hi) >>> 1;
314
+ if (tokens[mid].range[0] < tStart) lo = mid + 1;
315
+ else hi = mid;
316
+ }
317
+ const firstIdx = lo;
318
+ if (firstIdx >= tokens.length || tokens[firstIdx].range[0] >= tEnd) continue;
319
+ let lastIdx = firstIdx;
320
+ while (lastIdx < tokens.length && tokens[lastIdx].range[1] <= tEnd) {
321
+ lastIdx++;
322
+ }
323
+ tokens.splice(firstIdx, lastIdx - firstIdx, ...ti.ast.tokens);
324
+ }
325
+ }
326
+
327
+ if (useCustomParser) {
328
+ result.visitorKeys = { ...result.visitorKeys, ...glimmerVisitorKeys };
329
+ result.templateInfos = templateInfos;
330
+ return result;
331
+ }
332
+
333
+ // Default path: return bare AST with visitorKeys attached
334
+ result.ast.visitorKeys = glimmerVisitorKeys;
335
+ return result.ast;
336
+ }
337
+
338
+ export const parse = toTree;
339
+
340
+ // ── Placeholder JS ────────────────────────────────────────────────────
341
+
342
+ /**
343
+ * Replaces <template>...</template> regions with placeholder expressions
344
+ * of the same character length that are valid JS/TS.
345
+ *
346
+ * Expression templates become: `content ` (backtick, space-padded)
347
+ * Class member templates become: static{`content `} (static block, space-padded)
348
+ *
349
+ * This format is compatible with all JS/TS parsers including
350
+ * oxc-parser, @typescript-eslint/parser, and @babel/eslint-parser.
351
+ */
352
+ function toPlaceholderJS(source, parseResults) {
353
+ // Build result in forward order using parts array (avoids intermediate string allocations)
354
+ const parts = [];
355
+ let cursor = 0;
356
+
357
+ for (const pr of parseResults) {
358
+ const start = pr.range.startUtf16Codepoint;
359
+ const end = pr.range.endUtf16Codepoint;
360
+ const tplLength = end - start;
361
+
362
+ parts.push(source.slice(cursor, start));
363
+
364
+ // Blank out backticks and dollar signs instead of backslash-escaping
365
+ // them: escaping grows the content, and once the growth exceeds the
366
+ // padding slack the placeholder no longer lines up with the original
367
+ // region — matchPlaceholder then rejects it and the raw placeholder
368
+ // leaks into the AST (ember-tooling/ember-eslint-parser#230). The
369
+ // content is discarded when the Glimmer AST is spliced in, so only
370
+ // its length and line structure matter.
371
+ const content = source
372
+ .slice(pr.contentRange.startUtf16Codepoint, pr.contentRange.endUtf16Codepoint)
373
+ .replace(/[`$]/g, " ");
374
+
375
+ if (pr.type === "class-member") {
376
+ const spaces = tplLength - content.length - 10; // "static{`" + "`}" = 10
377
+ parts.push(`static{\`${content}${" ".repeat(Math.max(0, spaces))}\`}`);
378
+ } else {
379
+ const spaces = tplLength - content.length - 2; // "`" + "`" = 2
380
+ parts.push(`\`${content}${" ".repeat(Math.max(0, spaces))}\``);
381
+ }
382
+
383
+ cursor = end;
384
+ }
385
+
386
+ parts.push(source.slice(cursor));
387
+ return parts.join("");
388
+ }