@litsx/typescript 0.6.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 +85 -0
- package/dist/authored-semantics.cjs +18567 -0
- package/dist/authored-semantics.cjs.map +1 -0
- package/dist/editor-session.cjs +1345 -0
- package/dist/editor-session.cjs.map +1 -0
- package/dist/index.cjs +841 -0
- package/dist/index.cjs.map +1 -0
- package/dist/litsx-tsc.js +18025 -0
- package/dist/typecheck.cjs +931 -0
- package/dist/typecheck.cjs.map +1 -0
- package/dist/virtualization.cjs +113 -0
- package/dist/virtualization.cjs.map +1 -0
- package/package.json +76 -0
- package/src/authored-semantics.js +1543 -0
- package/src/editor-session.js +1360 -0
- package/src/index.js +844 -0
- package/src/litsx-tsc.js +4 -0
- package/src/typecheck.js +535 -0
- package/src/virtualization.js +125 -0
- package/tsserver-plugin.cjs +11 -0
|
@@ -0,0 +1,1543 @@
|
|
|
1
|
+
import * as babelParser from "@babel/parser";
|
|
2
|
+
import {
|
|
3
|
+
createVirtualLitsxJsxSource,
|
|
4
|
+
decodeVirtualAttributeName,
|
|
5
|
+
decodeVirtualStaticHoistName,
|
|
6
|
+
looksLikeLitsxJsx,
|
|
7
|
+
mapOriginalPositionToVirtual,
|
|
8
|
+
remapTextSpanToOriginal,
|
|
9
|
+
remapVirtualText,
|
|
10
|
+
} from "@litsx/authoring";
|
|
11
|
+
|
|
12
|
+
const EVENT_COMPLETIONS = [
|
|
13
|
+
"click",
|
|
14
|
+
"input",
|
|
15
|
+
"change",
|
|
16
|
+
"focus",
|
|
17
|
+
"blur",
|
|
18
|
+
"keydown",
|
|
19
|
+
"keyup",
|
|
20
|
+
"submit",
|
|
21
|
+
"pointerdown",
|
|
22
|
+
"pointerup",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const EVENT_COMPLETIONS_BY_TAG = {
|
|
26
|
+
input: ["click", "input", "change", "focus", "blur", "keydown", "keyup"],
|
|
27
|
+
textarea: ["click", "input", "change", "focus", "blur", "keydown", "keyup"],
|
|
28
|
+
button: ["click", "focus", "blur", "keydown", "keyup"],
|
|
29
|
+
form: ["submit", "change", "input", "click"],
|
|
30
|
+
video: ["click", "play", "pause", "timeupdate", "loadedmetadata", "volumechange"],
|
|
31
|
+
audio: ["click", "play", "pause", "timeupdate", "loadedmetadata", "volumechange"],
|
|
32
|
+
"suspense-boundary": ["click", "transitionend", "animationend"],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const BOOL_COMPLETIONS = [
|
|
36
|
+
"disabled",
|
|
37
|
+
"hidden",
|
|
38
|
+
"checked",
|
|
39
|
+
"selected",
|
|
40
|
+
"open",
|
|
41
|
+
"required",
|
|
42
|
+
"readonly",
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const BOOL_COMPLETIONS_BY_TAG = {
|
|
46
|
+
input: ["disabled", "checked", "required", "readonly"],
|
|
47
|
+
textarea: ["disabled", "required", "readonly"],
|
|
48
|
+
button: ["disabled"],
|
|
49
|
+
option: ["disabled", "selected"],
|
|
50
|
+
details: ["open"],
|
|
51
|
+
dialog: ["open"],
|
|
52
|
+
"suspense-boundary": ["pending", "resolved"],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const PROP_COMPLETIONS_BY_TAG = {
|
|
56
|
+
input: ["value", "checked", "files", "valueAsNumber", "selectionStart"],
|
|
57
|
+
textarea: ["value", "selectionStart", "selectionEnd"],
|
|
58
|
+
select: ["value", "selectedIndex"],
|
|
59
|
+
option: ["selected", "value"],
|
|
60
|
+
video: ["currentTime", "muted", "volume", "playbackRate"],
|
|
61
|
+
audio: ["currentTime", "muted", "volume", "playbackRate"],
|
|
62
|
+
"suspense-boundary": ["fallbackRenderer", "contentRenderer", "pending", "resolved", "showing", "phase"],
|
|
63
|
+
"suspense-list": ["revealOrder", "tail"],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const GLOBAL_ATTRIBUTE_COMPLETIONS = [
|
|
67
|
+
"class",
|
|
68
|
+
"id",
|
|
69
|
+
"title",
|
|
70
|
+
"style",
|
|
71
|
+
"role",
|
|
72
|
+
"slot",
|
|
73
|
+
"part",
|
|
74
|
+
"tabIndex",
|
|
75
|
+
"lang",
|
|
76
|
+
"dir",
|
|
77
|
+
"hidden",
|
|
78
|
+
"inert",
|
|
79
|
+
"draggable",
|
|
80
|
+
"spellcheck",
|
|
81
|
+
"translate",
|
|
82
|
+
"accessKey",
|
|
83
|
+
"contentEditable",
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const GLOBAL_ARIA_ATTRIBUTE_COMPLETIONS = [
|
|
87
|
+
"aria-label",
|
|
88
|
+
"aria-hidden",
|
|
89
|
+
"aria-describedby",
|
|
90
|
+
"aria-labelledby",
|
|
91
|
+
"aria-controls",
|
|
92
|
+
"aria-expanded",
|
|
93
|
+
"aria-pressed",
|
|
94
|
+
"aria-current",
|
|
95
|
+
"aria-live",
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
const ATTRIBUTE_COMPLETIONS_BY_TAG = {
|
|
99
|
+
a: ["href", "target", "rel", "download", "hreflang"],
|
|
100
|
+
audio: ["src", "controls", "autoplay", "muted", "loop", "preload"],
|
|
101
|
+
button: ["type", "name", "value", "disabled", "form", "autofocus"],
|
|
102
|
+
details: ["name", "open"],
|
|
103
|
+
dialog: ["open"],
|
|
104
|
+
form: ["action", "method", "autocomplete", "name", "novalidate"],
|
|
105
|
+
iframe: ["src", "name", "title", "loading", "allow"],
|
|
106
|
+
img: ["src", "alt", "width", "height", "loading", "decoding"],
|
|
107
|
+
input: ["type", "name", "value", "placeholder", "checked", "disabled", "required", "readonly", "autocomplete", "min", "max", "step"],
|
|
108
|
+
option: ["value", "label", "selected", "disabled"],
|
|
109
|
+
select: ["name", "value", "disabled", "required", "multiple"],
|
|
110
|
+
textarea: ["name", "value", "placeholder", "disabled", "required", "readonly", "rows", "cols"],
|
|
111
|
+
video: ["src", "controls", "autoplay", "muted", "loop", "playsInline", "poster", "preload"],
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const STATIC_HOIST_CALL_RE = /\b(__litsx_static_[A-Za-z_$][\w$]*)\s*\(/g;
|
|
115
|
+
const NATIVE_STATIC_HOISTS = new Set([
|
|
116
|
+
"styles",
|
|
117
|
+
"properties",
|
|
118
|
+
"shadowRootOptions",
|
|
119
|
+
"lightDom",
|
|
120
|
+
]);
|
|
121
|
+
const SINGLETON_STATIC_HOISTS = new Set([
|
|
122
|
+
"styles",
|
|
123
|
+
"properties",
|
|
124
|
+
"shadowRootOptions",
|
|
125
|
+
"lightDom",
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
const STATIC_HOIST_DOCUMENTATION_BY_NAME = {
|
|
129
|
+
"static styles": "LitSX static style hoist. Declare component-scoped styles before render-time statements.",
|
|
130
|
+
"static properties": "LitSX static properties hoist. Declare reactive property metadata before render-time statements.",
|
|
131
|
+
"static shadowRootOptions": "LitSX static shadow root options hoist. Declare shadow root configuration before render-time statements.",
|
|
132
|
+
"static lightDom": "LitSX static light DOM hoist. Declare light DOM rendering before render-time statements.",
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
function formatStaticHoistAuthoredName(macroName) {
|
|
136
|
+
return `static ${macroName}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function levenshteinDistance(left, right) {
|
|
140
|
+
if (left === right) return 0;
|
|
141
|
+
if (left.length === 0) return right.length;
|
|
142
|
+
if (right.length === 0) return left.length;
|
|
143
|
+
|
|
144
|
+
const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
145
|
+
const current = new Array(right.length + 1);
|
|
146
|
+
|
|
147
|
+
for (let i = 1; i <= left.length; i += 1) {
|
|
148
|
+
current[0] = i;
|
|
149
|
+
|
|
150
|
+
for (let j = 1; j <= right.length; j += 1) {
|
|
151
|
+
const substitutionCost = left[i - 1] === right[j - 1] ? 0 : 1;
|
|
152
|
+
current[j] = Math.min(
|
|
153
|
+
current[j - 1] + 1,
|
|
154
|
+
previous[j] + 1,
|
|
155
|
+
previous[j - 1] + substitutionCost,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (let j = 0; j <= right.length; j += 1) {
|
|
160
|
+
previous[j] = current[j];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return previous[right.length];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function findClosestAttributeSuggestion(prefix, localName, candidates = []) {
|
|
168
|
+
let bestCandidate = null;
|
|
169
|
+
let bestDistance = Infinity;
|
|
170
|
+
|
|
171
|
+
for (const candidate of candidates) {
|
|
172
|
+
if (typeof candidate !== "string" || !candidate.startsWith(prefix)) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const candidateLocalName = candidate.slice(prefix.length);
|
|
177
|
+
const distance = levenshteinDistance(localName, candidateLocalName);
|
|
178
|
+
|
|
179
|
+
if (distance < bestDistance) {
|
|
180
|
+
bestDistance = distance;
|
|
181
|
+
bestCandidate = candidate;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!bestCandidate) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const maxDistance = Math.max(2, Math.floor(localName.length / 3));
|
|
190
|
+
return bestDistance <= maxDistance ? bestCandidate : null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function splitAttributeCompletionWords(name) {
|
|
194
|
+
return name
|
|
195
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
196
|
+
.toLowerCase()
|
|
197
|
+
.split(/[^a-z0-9]+/)
|
|
198
|
+
.filter(Boolean);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function rankAttributeCompletion(candidate, partialName) {
|
|
202
|
+
if (typeof candidate !== "string") {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (partialName.length === 0) {
|
|
207
|
+
return {
|
|
208
|
+
score: 0,
|
|
209
|
+
wordIndex: -1,
|
|
210
|
+
lengthDelta: candidate.length,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const normalizedCandidate = candidate.toLowerCase();
|
|
215
|
+
const normalizedPartial = partialName.toLowerCase();
|
|
216
|
+
|
|
217
|
+
if (normalizedCandidate === normalizedPartial) {
|
|
218
|
+
return {
|
|
219
|
+
score: 0,
|
|
220
|
+
wordIndex: -1,
|
|
221
|
+
lengthDelta: 0,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (normalizedCandidate.startsWith(normalizedPartial)) {
|
|
226
|
+
return {
|
|
227
|
+
score: 1,
|
|
228
|
+
wordIndex: -1,
|
|
229
|
+
lengthDelta: candidate.length - partialName.length,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const words = splitAttributeCompletionWords(candidate);
|
|
234
|
+
const exactWordIndex = words.findIndex((word) => word === normalizedPartial);
|
|
235
|
+
|
|
236
|
+
if (exactWordIndex !== -1) {
|
|
237
|
+
return {
|
|
238
|
+
score: 2,
|
|
239
|
+
wordIndex: exactWordIndex,
|
|
240
|
+
lengthDelta: candidate.length - partialName.length,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const prefixWordIndex = words.findIndex((word) => word.startsWith(normalizedPartial));
|
|
245
|
+
|
|
246
|
+
if (prefixWordIndex !== -1) {
|
|
247
|
+
return {
|
|
248
|
+
score: 3,
|
|
249
|
+
wordIndex: prefixWordIndex,
|
|
250
|
+
lengthDelta: candidate.length - partialName.length,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const substringIndex = normalizedCandidate.indexOf(normalizedPartial);
|
|
255
|
+
|
|
256
|
+
if (substringIndex !== -1) {
|
|
257
|
+
return {
|
|
258
|
+
score: 4,
|
|
259
|
+
wordIndex: substringIndex,
|
|
260
|
+
lengthDelta: candidate.length - partialName.length,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function findEnclosingJsxOpeningTagStart(sourceText, position) {
|
|
268
|
+
let tagStart = -1;
|
|
269
|
+
let inTag = false;
|
|
270
|
+
let braceDepth = 0;
|
|
271
|
+
let quote = null;
|
|
272
|
+
|
|
273
|
+
for (let index = 0; index < position; index += 1) {
|
|
274
|
+
const char = sourceText[index];
|
|
275
|
+
|
|
276
|
+
if (!inTag) {
|
|
277
|
+
if (char === "<" && /[A-Za-z]/.test(sourceText[index + 1] ?? "")) {
|
|
278
|
+
inTag = true;
|
|
279
|
+
tagStart = index;
|
|
280
|
+
}
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (quote) {
|
|
285
|
+
if (char === "\\" && index + 1 < position) {
|
|
286
|
+
index += 1;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (char === quote) {
|
|
291
|
+
quote = null;
|
|
292
|
+
}
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
297
|
+
quote = char;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (char === "{") {
|
|
302
|
+
braceDepth += 1;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (char === "}" && braceDepth > 0) {
|
|
307
|
+
braceDepth -= 1;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (char === ">" && braceDepth === 0) {
|
|
312
|
+
inTag = false;
|
|
313
|
+
tagStart = -1;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return inTag ? tagStart : -1;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function findJsxOpeningTagEnd(sourceText, tagStart) {
|
|
321
|
+
let braceDepth = 0;
|
|
322
|
+
let quote = null;
|
|
323
|
+
|
|
324
|
+
for (let index = tagStart + 1; index < sourceText.length; index += 1) {
|
|
325
|
+
const char = sourceText[index];
|
|
326
|
+
|
|
327
|
+
if (quote) {
|
|
328
|
+
if (char === "\\" && index + 1 < sourceText.length) {
|
|
329
|
+
index += 1;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (char === quote) {
|
|
334
|
+
quote = null;
|
|
335
|
+
}
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
340
|
+
quote = char;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (char === "{") {
|
|
345
|
+
braceDepth += 1;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (char === "}" && braceDepth > 0) {
|
|
350
|
+
braceDepth -= 1;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (char === ">" && braceDepth === 0) {
|
|
355
|
+
return index;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return sourceText.length;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function inferLitsxStaticHoistInfoAtPosition(sourceText, position) {
|
|
363
|
+
if (typeof sourceText !== "string" || typeof position !== "number") {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const staticMatch = Array.from(
|
|
368
|
+
sourceText.matchAll(/(?:^|[;{}]\s*)(static\s+([A-Za-z$_][A-Za-z0-9$_]*)\s*=)/gm),
|
|
369
|
+
).find((match) => {
|
|
370
|
+
const start = match.index + match[0].lastIndexOf(match[1]);
|
|
371
|
+
const end = start + match[1].length;
|
|
372
|
+
return position >= start && position <= end;
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
if (staticMatch) {
|
|
376
|
+
const start = staticMatch.index + staticMatch[0].lastIndexOf(staticMatch[1]);
|
|
377
|
+
const name = `static ${staticMatch[2]}`;
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
name,
|
|
381
|
+
start,
|
|
382
|
+
length: name.length,
|
|
383
|
+
documentation: STATIC_HOIST_DOCUMENTATION_BY_NAME[name]
|
|
384
|
+
?? `LitSX static hoist ${name} = .... Declare it before render-time statements in the component body.`,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function walk(node, visitor) {
|
|
392
|
+
if (!node || typeof node !== "object") {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
visitor(node);
|
|
397
|
+
|
|
398
|
+
for (const value of Object.values(node)) {
|
|
399
|
+
if (Array.isArray(value)) {
|
|
400
|
+
for (const item of value) {
|
|
401
|
+
walk(item, visitor);
|
|
402
|
+
}
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
walk(value, visitor);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function collectJsxAttributes(ast) {
|
|
411
|
+
const attributes = [];
|
|
412
|
+
|
|
413
|
+
walk(ast, (node) => {
|
|
414
|
+
if (
|
|
415
|
+
node?.type === "JSXAttribute" &&
|
|
416
|
+
node.name?.type === "JSXIdentifier" &&
|
|
417
|
+
typeof node.name.name === "string"
|
|
418
|
+
) {
|
|
419
|
+
const openingElement = node.__openingElement;
|
|
420
|
+
let tagName = null;
|
|
421
|
+
|
|
422
|
+
if (openingElement?.name?.type === "JSXIdentifier") {
|
|
423
|
+
tagName = openingElement.name.name;
|
|
424
|
+
} else if (openingElement?.name?.type === "JSXNamespacedName") {
|
|
425
|
+
tagName = `${openingElement.name.namespace.name}:${openingElement.name.name.name}`;
|
|
426
|
+
} else if (openingElement?.name?.type === "JSXMemberExpression") {
|
|
427
|
+
tagName = openingElement.name.property?.name ?? null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
attributes.push(node);
|
|
431
|
+
Object.defineProperty(node, "__litsxTagName", {
|
|
432
|
+
value: tagName,
|
|
433
|
+
configurable: true,
|
|
434
|
+
enumerable: false,
|
|
435
|
+
writable: true,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (node?.type === "JSXOpeningElement" && Array.isArray(node.attributes)) {
|
|
440
|
+
for (const attribute of node.attributes) {
|
|
441
|
+
if (attribute && typeof attribute === "object") {
|
|
442
|
+
Object.defineProperty(attribute, "__openingElement", {
|
|
443
|
+
value: node,
|
|
444
|
+
configurable: true,
|
|
445
|
+
enumerable: false,
|
|
446
|
+
writable: true,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
return attributes;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function createOriginalIssue(virtualization, config) {
|
|
457
|
+
const virtualStart = typeof config.start === "number" ? config.start : 0;
|
|
458
|
+
const virtualLength = typeof config.length === "number" ? config.length : 0;
|
|
459
|
+
const span = remapTextSpanToOriginal(
|
|
460
|
+
{
|
|
461
|
+
start: virtualStart,
|
|
462
|
+
length: virtualLength,
|
|
463
|
+
},
|
|
464
|
+
virtualization?.replacements ?? [],
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
kind: config.kind,
|
|
469
|
+
code: config.code,
|
|
470
|
+
severity: config.severity,
|
|
471
|
+
message: config.message,
|
|
472
|
+
start: span?.start ?? 0,
|
|
473
|
+
length: span?.length ?? 0,
|
|
474
|
+
fix: config.fix ?? null,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function collectStaticHoistIssues(ast, virtualization) {
|
|
479
|
+
const issues = [];
|
|
480
|
+
const seenSingletonHoists = new Map();
|
|
481
|
+
|
|
482
|
+
function visit(node, parent = null, functionBody = null) {
|
|
483
|
+
if (!node || typeof node !== "object") {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
let nextFunctionBody = functionBody;
|
|
488
|
+
if (
|
|
489
|
+
(node.type === "FunctionDeclaration" ||
|
|
490
|
+
node.type === "FunctionExpression" ||
|
|
491
|
+
node.type === "ArrowFunctionExpression") &&
|
|
492
|
+
node.body?.type === "BlockStatement"
|
|
493
|
+
) {
|
|
494
|
+
nextFunctionBody = node.body;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (
|
|
498
|
+
node.type === "CallExpression" &&
|
|
499
|
+
node.callee?.type === "Identifier" &&
|
|
500
|
+
node.callee.name.startsWith("__litsx_static_") &&
|
|
501
|
+
nextFunctionBody
|
|
502
|
+
) {
|
|
503
|
+
const macroName = node.callee.name.slice("__litsx_static_".length);
|
|
504
|
+
const authoredName = decodeVirtualStaticHoistName(node.callee.name) ?? formatStaticHoistAuthoredName(macroName);
|
|
505
|
+
const statement = parent?.type === "ExpressionStatement" ? parent : null;
|
|
506
|
+
const enclosingBlock = parent?.type === "ExpressionStatement" ? functionBody : null;
|
|
507
|
+
const isTopLevelStatement = statement && enclosingBlock?.body?.includes(statement);
|
|
508
|
+
|
|
509
|
+
if (!isTopLevelStatement) {
|
|
510
|
+
issues.push(
|
|
511
|
+
createOriginalIssue(virtualization, {
|
|
512
|
+
kind: "static-hoist-top-level",
|
|
513
|
+
severity: "error",
|
|
514
|
+
code: 91007,
|
|
515
|
+
start: node.start ?? 0,
|
|
516
|
+
length: Math.max(0, (node.end ?? node.start ?? 0) - (node.start ?? 0)),
|
|
517
|
+
message: `LitSX static hoists such as ${authoredName} = ... must appear as a top-level statement in the component body.`,
|
|
518
|
+
})
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (SINGLETON_STATIC_HOISTS.has(macroName)) {
|
|
523
|
+
if (seenSingletonHoists.has(macroName)) {
|
|
524
|
+
issues.push(
|
|
525
|
+
createOriginalIssue(virtualization, {
|
|
526
|
+
kind: "duplicate-static-hoist",
|
|
527
|
+
severity: "error",
|
|
528
|
+
code: 91009,
|
|
529
|
+
start: node.start ?? 0,
|
|
530
|
+
length: Math.max(0, (node.end ?? node.start ?? 0) - (node.start ?? 0)),
|
|
531
|
+
message: `Duplicate static hoist "${authoredName} = ..." found. Native LitSX hoists such as ${authoredName} = ... should only be declared once per component.`,
|
|
532
|
+
})
|
|
533
|
+
);
|
|
534
|
+
} else {
|
|
535
|
+
seenSingletonHoists.set(macroName, node);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
for (const [key, value] of Object.entries(node)) {
|
|
541
|
+
if (key === "loc" || key === "leadingComments" || key === "innerComments" || key === "trailingComments") {
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (Array.isArray(value)) {
|
|
546
|
+
for (const child of value) {
|
|
547
|
+
if (child && typeof child.type === "string") {
|
|
548
|
+
visit(child, node, nextFunctionBody);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
} else if (value && typeof value.type === "string") {
|
|
552
|
+
visit(value, node, nextFunctionBody);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
visit(ast.program ?? ast, null, null);
|
|
558
|
+
|
|
559
|
+
if (
|
|
560
|
+
seenSingletonHoists.has("lightDom") &&
|
|
561
|
+
seenSingletonHoists.has("shadowRootOptions")
|
|
562
|
+
) {
|
|
563
|
+
const shadowRootOptionsHoist = seenSingletonHoists.get("shadowRootOptions");
|
|
564
|
+
issues.push(
|
|
565
|
+
createOriginalIssue(virtualization, {
|
|
566
|
+
kind: "ignored-static-hoist",
|
|
567
|
+
severity: "warning",
|
|
568
|
+
code: 91019,
|
|
569
|
+
start: shadowRootOptionsHoist.start ?? 0,
|
|
570
|
+
length: Math.max(
|
|
571
|
+
0,
|
|
572
|
+
(shadowRootOptionsHoist.end ?? shadowRootOptionsHoist.start ?? 0) -
|
|
573
|
+
(shadowRootOptionsHoist.start ?? 0),
|
|
574
|
+
),
|
|
575
|
+
message: 'static shadowRootOptions = ... is ignored when static lightDom = true.',
|
|
576
|
+
}),
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return issues;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function collectReactMemoIssues(ast, virtualization) {
|
|
584
|
+
const issues = [];
|
|
585
|
+
const reactMemoLocalNames = new Set();
|
|
586
|
+
const reactNamespaceNames = new Set();
|
|
587
|
+
const body = ast?.program?.body ?? ast?.body ?? [];
|
|
588
|
+
|
|
589
|
+
for (const node of body) {
|
|
590
|
+
if (node?.type !== "ImportDeclaration" || node.source?.value !== "react") {
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
for (const specifier of node.specifiers || []) {
|
|
595
|
+
if (
|
|
596
|
+
specifier?.type === "ImportSpecifier" &&
|
|
597
|
+
specifier.imported?.type === "Identifier" &&
|
|
598
|
+
specifier.imported.name === "memo" &&
|
|
599
|
+
specifier.local?.type === "Identifier"
|
|
600
|
+
) {
|
|
601
|
+
reactMemoLocalNames.add(specifier.local.name);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (
|
|
605
|
+
(specifier?.type === "ImportDefaultSpecifier" ||
|
|
606
|
+
specifier?.type === "ImportNamespaceSpecifier") &&
|
|
607
|
+
specifier.local?.type === "Identifier"
|
|
608
|
+
) {
|
|
609
|
+
reactNamespaceNames.add(specifier.local.name);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
walk(ast.program ?? ast, (node) => {
|
|
615
|
+
if (node?.type !== "CallExpression") {
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const callee = node.callee;
|
|
620
|
+
const isImportedMemo =
|
|
621
|
+
callee?.type === "Identifier" && reactMemoLocalNames.has(callee.name);
|
|
622
|
+
const isNamespacedMemo =
|
|
623
|
+
callee?.type === "MemberExpression" &&
|
|
624
|
+
callee.computed === false &&
|
|
625
|
+
callee.object?.type === "Identifier" &&
|
|
626
|
+
reactNamespaceNames.has(callee.object.name) &&
|
|
627
|
+
callee.property?.type === "Identifier" &&
|
|
628
|
+
callee.property.name === "memo";
|
|
629
|
+
|
|
630
|
+
if (!isImportedMemo && !isNamespacedMemo) {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
issues.push(
|
|
635
|
+
createOriginalIssue(virtualization, {
|
|
636
|
+
kind: "react-memo",
|
|
637
|
+
severity: "warning",
|
|
638
|
+
code: 91016,
|
|
639
|
+
start: node.start ?? 0,
|
|
640
|
+
length: Math.max(0, (node.end ?? node.start ?? 0) - (node.start ?? 0)),
|
|
641
|
+
message:
|
|
642
|
+
"`memo(...)` is removed during LitSX lowering. LitSX does not use React-style parent re-render bailout semantics, so `memo` is treated as a migration wrapper only.",
|
|
643
|
+
})
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
if ((node.arguments || []).length > 1) {
|
|
647
|
+
issues.push(
|
|
648
|
+
createOriginalIssue(virtualization, {
|
|
649
|
+
kind: "react-memo",
|
|
650
|
+
severity: "warning",
|
|
651
|
+
code: 91017,
|
|
652
|
+
start: node.start ?? 0,
|
|
653
|
+
length: Math.max(0, (node.end ?? node.start ?? 0) - (node.start ?? 0)),
|
|
654
|
+
message:
|
|
655
|
+
"`memo(Component, areEqual)` ignores the comparator during LitSX lowering because LitSX does not use React-style parent re-render bailout semantics.",
|
|
656
|
+
})
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
return issues;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function collectReactCompatSurfaceIssues(ast, virtualization) {
|
|
665
|
+
const issues = [];
|
|
666
|
+
const attributes = collectJsxAttributes(ast);
|
|
667
|
+
|
|
668
|
+
for (const attribute of attributes) {
|
|
669
|
+
const tagName = attribute.__litsxTagName;
|
|
670
|
+
const attrName = attribute.name?.name;
|
|
671
|
+
|
|
672
|
+
if (typeof attrName !== "string" || typeof tagName !== "string" || !/^[a-z]/.test(tagName)) {
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const virtualSpan = {
|
|
677
|
+
start: attribute.name.start ?? attribute.start ?? 0,
|
|
678
|
+
length: (attribute.name.end ?? attribute.end ?? 0) - (attribute.name.start ?? attribute.start ?? 0),
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
if (attrName === "htmlFor") {
|
|
682
|
+
issues.push(createOriginalIssue(virtualization, {
|
|
683
|
+
kind: "react-compat-surface",
|
|
684
|
+
severity: "warning",
|
|
685
|
+
code: 91010,
|
|
686
|
+
start: virtualSpan.start,
|
|
687
|
+
length: virtualSpan.length,
|
|
688
|
+
message: '`htmlFor` is React compatibility syntax. Prefer the native DOM attribute `for` in LitSX-authored intrinsic elements.',
|
|
689
|
+
}));
|
|
690
|
+
} else if (attrName === "dangerouslySetInnerHTML") {
|
|
691
|
+
issues.push(createOriginalIssue(virtualization, {
|
|
692
|
+
kind: "react-compat-surface",
|
|
693
|
+
severity: "warning",
|
|
694
|
+
code: 91011,
|
|
695
|
+
start: virtualSpan.start,
|
|
696
|
+
length: virtualSpan.length,
|
|
697
|
+
message: "`dangerouslySetInnerHTML` is React compatibility surface. Prefer native Lit rendering patterns or explicit DOM escape hatches instead of React-authored HTML injection APIs.",
|
|
698
|
+
}));
|
|
699
|
+
} else if (attrName === "defaultValue") {
|
|
700
|
+
issues.push(createOriginalIssue(virtualization, {
|
|
701
|
+
kind: "react-compat-surface",
|
|
702
|
+
severity: "warning",
|
|
703
|
+
code: 91012,
|
|
704
|
+
start: virtualSpan.start,
|
|
705
|
+
length: virtualSpan.length,
|
|
706
|
+
message: '`defaultValue` is React compatibility syntax. Prefer `value`, `.value`, or native initial DOM state patterns in LitSX.',
|
|
707
|
+
}));
|
|
708
|
+
} else if (attrName === "defaultChecked") {
|
|
709
|
+
issues.push(createOriginalIssue(virtualization, {
|
|
710
|
+
kind: "react-compat-surface",
|
|
711
|
+
severity: "warning",
|
|
712
|
+
code: 91013,
|
|
713
|
+
start: virtualSpan.start,
|
|
714
|
+
length: virtualSpan.length,
|
|
715
|
+
message: '`defaultChecked` is React compatibility syntax. Prefer `checked`, `?checked`, or native initial DOM state patterns in LitSX.',
|
|
716
|
+
}));
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return issues;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function collectComponentLikeFunctions(ast) {
|
|
724
|
+
const functions = [];
|
|
725
|
+
|
|
726
|
+
function isPascalCaseName(name) {
|
|
727
|
+
return typeof name === "string" && /^[A-Z]/.test(name);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function isComponentLikeFunction(node, parent) {
|
|
731
|
+
if (!node || typeof node !== "object") {
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (
|
|
736
|
+
node.type === "FunctionDeclaration" ||
|
|
737
|
+
node.type === "FunctionExpression"
|
|
738
|
+
) {
|
|
739
|
+
return isPascalCaseName(node.id?.name ?? "");
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (node.type === "ArrowFunctionExpression") {
|
|
743
|
+
if (parent?.type === "VariableDeclarator" && parent.id?.type === "Identifier") {
|
|
744
|
+
return isPascalCaseName(parent.id.name);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (parent?.type === "AssignmentExpression" && parent.left?.type === "Identifier") {
|
|
748
|
+
return isPascalCaseName(parent.left.name);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return false;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function collect(node, parent = null) {
|
|
758
|
+
if (!node || typeof node !== "object") {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (isComponentLikeFunction(node, parent)) {
|
|
763
|
+
functions.push({ node, parent });
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
for (const [key, value] of Object.entries(node)) {
|
|
767
|
+
if (key === "loc" || key === "leadingComments" || key === "innerComments" || key === "trailingComments") {
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
if (Array.isArray(value)) {
|
|
771
|
+
for (const child of value) {
|
|
772
|
+
if (child && typeof child.type === "string") {
|
|
773
|
+
collect(child, node);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
} else if (value && typeof value.type === "string") {
|
|
777
|
+
collect(value, node);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
collect(ast.program ?? ast, null);
|
|
783
|
+
return functions;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function getFunctionLikeBody(node) {
|
|
787
|
+
if (!node || typeof node !== "object") {
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (
|
|
792
|
+
node.type === "FunctionDeclaration" ||
|
|
793
|
+
node.type === "FunctionExpression" ||
|
|
794
|
+
node.type === "ArrowFunctionExpression"
|
|
795
|
+
) {
|
|
796
|
+
return node.body ?? null;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function inferEmitAliases(functionNode) {
|
|
803
|
+
const aliases = new Set();
|
|
804
|
+
const body = getFunctionLikeBody(functionNode);
|
|
805
|
+
|
|
806
|
+
if (!body) {
|
|
807
|
+
return aliases;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
walk(body, (child) => {
|
|
811
|
+
if (
|
|
812
|
+
child?.type === "VariableDeclarator" &&
|
|
813
|
+
child.id?.type === "Identifier" &&
|
|
814
|
+
child.init?.type === "CallExpression" &&
|
|
815
|
+
child.init.callee?.type === "Identifier" &&
|
|
816
|
+
child.init.callee.name === "useEmit"
|
|
817
|
+
) {
|
|
818
|
+
aliases.add(child.id.name);
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
return aliases;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function inferEmittedEventNames(functionNode) {
|
|
826
|
+
const aliases = inferEmitAliases(functionNode);
|
|
827
|
+
if (aliases.size === 0) {
|
|
828
|
+
return [];
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const emittedEventNames = new Set();
|
|
832
|
+
const body = getFunctionLikeBody(functionNode);
|
|
833
|
+
if (!body) {
|
|
834
|
+
return [];
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
walk(body, (child) => {
|
|
838
|
+
if (
|
|
839
|
+
child?.type === "CallExpression" &&
|
|
840
|
+
child.callee?.type === "Identifier" &&
|
|
841
|
+
aliases.has(child.callee.name) &&
|
|
842
|
+
child.arguments?.[0]?.type === "StringLiteral"
|
|
843
|
+
) {
|
|
844
|
+
emittedEventNames.add(child.arguments[0].value);
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
return Array.from(emittedEventNames).sort();
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
export function inferLitsxComponentEventNames(sourceText, options = {}) {
|
|
852
|
+
const plugins = Array.from(new Set(["jsx", ...(options.plugins ?? [])]));
|
|
853
|
+
const virtualization = createVirtualLitsxJsxSource(sourceText);
|
|
854
|
+
let ast;
|
|
855
|
+
|
|
856
|
+
try {
|
|
857
|
+
ast = babelParser.parse(virtualization.code, {
|
|
858
|
+
sourceType: "module",
|
|
859
|
+
plugins,
|
|
860
|
+
});
|
|
861
|
+
} catch {
|
|
862
|
+
return {};
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const componentEventNames = {};
|
|
866
|
+
|
|
867
|
+
for (const { node, parent } of collectComponentLikeFunctions(ast)) {
|
|
868
|
+
let componentName = null;
|
|
869
|
+
|
|
870
|
+
if (
|
|
871
|
+
(node.type === "FunctionDeclaration" || node.type === "FunctionExpression") &&
|
|
872
|
+
node.id?.type === "Identifier"
|
|
873
|
+
) {
|
|
874
|
+
componentName = node.id.name;
|
|
875
|
+
} else if (
|
|
876
|
+
node.type === "ArrowFunctionExpression" &&
|
|
877
|
+
parent?.type === "VariableDeclarator" &&
|
|
878
|
+
parent.id?.type === "Identifier"
|
|
879
|
+
) {
|
|
880
|
+
componentName = parent.id.name;
|
|
881
|
+
} else if (
|
|
882
|
+
node.type === "ArrowFunctionExpression" &&
|
|
883
|
+
parent?.type === "AssignmentExpression" &&
|
|
884
|
+
parent.left?.type === "Identifier"
|
|
885
|
+
) {
|
|
886
|
+
componentName = parent.left.name;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (!componentName) {
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const emittedEventNames = inferEmittedEventNames(node);
|
|
894
|
+
if (emittedEventNames.length > 0) {
|
|
895
|
+
componentEventNames[componentName] = emittedEventNames;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
return componentEventNames;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function inferStaticPropertyNames(functionNode) {
|
|
903
|
+
const propertyNames = new Set();
|
|
904
|
+
const body = getFunctionLikeBody(functionNode);
|
|
905
|
+
|
|
906
|
+
if (!body) {
|
|
907
|
+
return [];
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
walk(body, (child) => {
|
|
911
|
+
let propertiesObject = null;
|
|
912
|
+
|
|
913
|
+
if (
|
|
914
|
+
child?.type === "AssignmentExpression" &&
|
|
915
|
+
child.operator === "=" &&
|
|
916
|
+
child.left?.type === "MemberExpression" &&
|
|
917
|
+
child.left.computed === false &&
|
|
918
|
+
child.left.object?.type === "Identifier" &&
|
|
919
|
+
child.left.object.name === "static" &&
|
|
920
|
+
child.left.property?.type === "Identifier" &&
|
|
921
|
+
child.left.property.name === "properties" &&
|
|
922
|
+
child.right?.type === "ObjectExpression"
|
|
923
|
+
) {
|
|
924
|
+
propertiesObject = child.right;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (
|
|
928
|
+
child?.type === "CallExpression" &&
|
|
929
|
+
child.callee?.type === "Identifier" &&
|
|
930
|
+
child.callee.name === "__litsx_static_properties" &&
|
|
931
|
+
child.arguments?.[0]?.type === "ObjectExpression"
|
|
932
|
+
) {
|
|
933
|
+
propertiesObject = child.arguments[0];
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
if (!propertiesObject) {
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
for (const property of propertiesObject.properties ?? []) {
|
|
941
|
+
if (property?.type !== "ObjectProperty") {
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if (property.key?.type === "Identifier") {
|
|
946
|
+
propertyNames.add(property.key.name);
|
|
947
|
+
} else if (property.key?.type === "StringLiteral") {
|
|
948
|
+
propertyNames.add(property.key.value);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
return Array.from(propertyNames).sort();
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
export function inferLitsxComponentPropNames(sourceText, options = {}) {
|
|
957
|
+
const plugins = Array.from(new Set(["jsx", ...(options.plugins ?? [])]));
|
|
958
|
+
const virtualization = createVirtualLitsxJsxSource(sourceText);
|
|
959
|
+
let ast;
|
|
960
|
+
|
|
961
|
+
try {
|
|
962
|
+
ast = babelParser.parse(virtualization.code, {
|
|
963
|
+
sourceType: "module",
|
|
964
|
+
plugins,
|
|
965
|
+
});
|
|
966
|
+
} catch {
|
|
967
|
+
return {};
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const componentPropNames = {};
|
|
971
|
+
|
|
972
|
+
for (const { node, parent } of collectComponentLikeFunctions(ast)) {
|
|
973
|
+
let componentName = null;
|
|
974
|
+
|
|
975
|
+
if (
|
|
976
|
+
(node.type === "FunctionDeclaration" || node.type === "FunctionExpression") &&
|
|
977
|
+
node.id?.type === "Identifier"
|
|
978
|
+
) {
|
|
979
|
+
componentName = node.id.name;
|
|
980
|
+
} else if (
|
|
981
|
+
node.type === "ArrowFunctionExpression" &&
|
|
982
|
+
parent?.type === "VariableDeclarator" &&
|
|
983
|
+
parent.id?.type === "Identifier"
|
|
984
|
+
) {
|
|
985
|
+
componentName = parent.id.name;
|
|
986
|
+
} else if (
|
|
987
|
+
node.type === "ArrowFunctionExpression" &&
|
|
988
|
+
parent?.type === "AssignmentExpression" &&
|
|
989
|
+
parent.left?.type === "Identifier"
|
|
990
|
+
) {
|
|
991
|
+
componentName = parent.left.name;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
if (!componentName) {
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const propNames = inferStaticPropertyNames(node);
|
|
999
|
+
if (propNames.length > 0) {
|
|
1000
|
+
componentPropNames[componentName] = propNames;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
return componentPropNames;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function collectPropsAccessIssues(ast, virtualization) {
|
|
1008
|
+
const issues = [];
|
|
1009
|
+
|
|
1010
|
+
for (const { node } of collectComponentLikeFunctions(ast)) {
|
|
1011
|
+
const firstParam = node.params?.[0];
|
|
1012
|
+
if (!firstParam || firstParam.type !== "Identifier" || typeof firstParam.name !== "string") {
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const propsParamName = firstParam.name;
|
|
1017
|
+
const seenProps = new Set();
|
|
1018
|
+
let foundAnyOpaqueAccess = false;
|
|
1019
|
+
|
|
1020
|
+
walk(node.body, (child) => {
|
|
1021
|
+
if (
|
|
1022
|
+
child?.type === "MemberExpression" &&
|
|
1023
|
+
child.computed === false &&
|
|
1024
|
+
child.object?.type === "Identifier" &&
|
|
1025
|
+
child.object.name === propsParamName &&
|
|
1026
|
+
child.property?.type === "Identifier"
|
|
1027
|
+
) {
|
|
1028
|
+
foundAnyOpaqueAccess = true;
|
|
1029
|
+
const propName = child.property.name;
|
|
1030
|
+
if (!seenProps.has(propName)) {
|
|
1031
|
+
seenProps.add(propName);
|
|
1032
|
+
issues.push(createOriginalIssue(virtualization, {
|
|
1033
|
+
kind: "opaque-prop-metadata-inference",
|
|
1034
|
+
severity: "warning",
|
|
1035
|
+
code: 91018,
|
|
1036
|
+
start: child.start ?? 0,
|
|
1037
|
+
length: Math.max(0, (child.end ?? child.start ?? 0) - (child.start ?? 0)),
|
|
1038
|
+
message: `Falling back to String for prop "${propName}" inferred from opaque props access. Prefer destructuring, TypeScript types, or static properties = ... for stronger property metadata.`,
|
|
1039
|
+
}));
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
if (foundAnyOpaqueAccess) {
|
|
1045
|
+
issues.push(createOriginalIssue(virtualization, {
|
|
1046
|
+
kind: "prefer-destructured-props",
|
|
1047
|
+
severity: "warning",
|
|
1048
|
+
code: 91014,
|
|
1049
|
+
start: firstParam.start ?? 0,
|
|
1050
|
+
length: Math.max(0, (firstParam.end ?? firstParam.start ?? 0) - (firstParam.start ?? 0)),
|
|
1051
|
+
message: `Prefer destructuring component props instead of reading opaque "${propsParamName}.foo" member access directly in LitSX components.`,
|
|
1052
|
+
}));
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
return issues;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function collectHoistsFirstIssues(ast, virtualization) {
|
|
1060
|
+
const issues = [];
|
|
1061
|
+
|
|
1062
|
+
for (const { node } of collectComponentLikeFunctions(ast)) {
|
|
1063
|
+
if (node.body?.type !== "BlockStatement") {
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
let sawNonHoistStatement = false;
|
|
1068
|
+
for (const statement of node.body.body ?? []) {
|
|
1069
|
+
const isHoistStatement =
|
|
1070
|
+
statement?.type === "ExpressionStatement" &&
|
|
1071
|
+
statement.expression?.type === "CallExpression" &&
|
|
1072
|
+
statement.expression.callee?.type === "Identifier" &&
|
|
1073
|
+
statement.expression.callee.name.startsWith("__litsx_static_");
|
|
1074
|
+
|
|
1075
|
+
if (isHoistStatement) {
|
|
1076
|
+
if (sawNonHoistStatement) {
|
|
1077
|
+
const macroName = statement.expression.callee.name.slice("__litsx_static_".length);
|
|
1078
|
+
const authoredName = decodeVirtualStaticHoistName(statement.expression.callee.name) ?? formatStaticHoistAuthoredName(macroName);
|
|
1079
|
+
issues.push(createOriginalIssue(virtualization, {
|
|
1080
|
+
kind: "require-top-level-hoists-first",
|
|
1081
|
+
severity: "warning",
|
|
1082
|
+
code: 91015,
|
|
1083
|
+
start: statement.expression.start ?? statement.start ?? 0,
|
|
1084
|
+
length: Math.max(0, ((statement.expression.end ?? statement.end ?? statement.expression.start ?? statement.start ?? 0) - (statement.expression.start ?? statement.start ?? 0))),
|
|
1085
|
+
message: `Place static hoists such as ${authoredName} = ... before render-time statements in the component body for clearer LitSX structure.`,
|
|
1086
|
+
}));
|
|
1087
|
+
}
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
sawNonHoistStatement = true;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
return issues;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
export function getLitsxAttributeCompletionNames(context) {
|
|
1099
|
+
if (!context) {
|
|
1100
|
+
return [];
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
let candidates = [];
|
|
1104
|
+
|
|
1105
|
+
switch (context.prefix) {
|
|
1106
|
+
case "@":
|
|
1107
|
+
candidates = EVENT_COMPLETIONS_BY_TAG[context.tagName] ?? EVENT_COMPLETIONS;
|
|
1108
|
+
break;
|
|
1109
|
+
case "?":
|
|
1110
|
+
candidates = BOOL_COMPLETIONS_BY_TAG[context.tagName] ?? BOOL_COMPLETIONS;
|
|
1111
|
+
break;
|
|
1112
|
+
case ".":
|
|
1113
|
+
candidates = PROP_COMPLETIONS_BY_TAG[context.tagName] ?? ["value"];
|
|
1114
|
+
break;
|
|
1115
|
+
default:
|
|
1116
|
+
return [];
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
return candidates
|
|
1120
|
+
.map((name, index) => ({
|
|
1121
|
+
name,
|
|
1122
|
+
index,
|
|
1123
|
+
rank: rankAttributeCompletion(name, context.partialName),
|
|
1124
|
+
}))
|
|
1125
|
+
.filter((entry) => entry.rank)
|
|
1126
|
+
.sort((left, right) => {
|
|
1127
|
+
if (context.partialName.length === 0) {
|
|
1128
|
+
return left.index - right.index;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
if (left.rank.score !== right.rank.score) {
|
|
1132
|
+
return left.rank.score - right.rank.score;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
if (left.rank.wordIndex !== right.rank.wordIndex) {
|
|
1136
|
+
return left.rank.wordIndex - right.rank.wordIndex;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
if (left.index !== right.index) {
|
|
1140
|
+
return left.index - right.index;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
if (left.rank.lengthDelta !== right.rank.lengthDelta) {
|
|
1144
|
+
return left.rank.lengthDelta - right.rank.lengthDelta;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
return 0;
|
|
1148
|
+
})
|
|
1149
|
+
.map((entry) => `${context.prefix}${entry.name}`);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
export function inferLitsxAttributeCompletionContext(sourceText, position) {
|
|
1153
|
+
const tagStart = findEnclosingJsxOpeningTagStart(sourceText, position);
|
|
1154
|
+
|
|
1155
|
+
if (tagStart === -1) {
|
|
1156
|
+
return null;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
const prefixText = sourceText.slice(0, position);
|
|
1160
|
+
const lastOpen = tagStart;
|
|
1161
|
+
const openingSegment = prefixText.slice(lastOpen + 1);
|
|
1162
|
+
const tagMatch = /^([A-Za-z][\w:-]*)/.exec(openingSegment.trimStart());
|
|
1163
|
+
|
|
1164
|
+
if (!tagMatch) {
|
|
1165
|
+
return null;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
const attrMatch = /(?:^|\s)([@.?])([\w:-]*)$/.exec(openingSegment);
|
|
1169
|
+
|
|
1170
|
+
if (!attrMatch) {
|
|
1171
|
+
return null;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const [, prefix, partialName] = attrMatch;
|
|
1175
|
+
const matchText = attrMatch[0];
|
|
1176
|
+
const attrStart = lastOpen + 1 + openingSegment.length - matchText.length + matchText.lastIndexOf(prefix);
|
|
1177
|
+
return {
|
|
1178
|
+
tagName: tagMatch[1],
|
|
1179
|
+
prefix,
|
|
1180
|
+
partialName,
|
|
1181
|
+
start: attrStart,
|
|
1182
|
+
length: prefix.length + partialName.length,
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
export function inferLitsxMarkupCompletionContext(sourceText, position) {
|
|
1187
|
+
const tagStart = findEnclosingJsxOpeningTagStart(sourceText, position);
|
|
1188
|
+
|
|
1189
|
+
if (tagStart === -1) {
|
|
1190
|
+
return null;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
const prefixText = sourceText.slice(0, position);
|
|
1194
|
+
const lastOpen = tagStart;
|
|
1195
|
+
const openingSegment = prefixText.slice(lastOpen + 1);
|
|
1196
|
+
const tagMatch = /^([A-Za-z][\w:-]*)/.exec(openingSegment.trimStart());
|
|
1197
|
+
|
|
1198
|
+
if (!tagMatch) {
|
|
1199
|
+
return null;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
if (inferLitsxAttributeCompletionContext(sourceText, position)) {
|
|
1203
|
+
return null;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
if (/\s$/.test(openingSegment)) {
|
|
1207
|
+
return {
|
|
1208
|
+
tagName: tagMatch[1],
|
|
1209
|
+
partialName: "",
|
|
1210
|
+
start: position,
|
|
1211
|
+
length: 0,
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const tailMatch = /(?:^|\s)([A-Za-z_:][\w:.-]*)?$/.exec(openingSegment);
|
|
1216
|
+
if (!tailMatch) {
|
|
1217
|
+
return null;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
const tailText = tailMatch[0];
|
|
1221
|
+
if (/[={'"`]/.test(tailText) || tailText.trim() === "/" || tailText.includes("...")) {
|
|
1222
|
+
return null;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
const partialName = tailMatch[1] ?? "";
|
|
1226
|
+
const trimmedOpening = openingSegment.trimStart();
|
|
1227
|
+
const afterTag = trimmedOpening.slice(tagMatch[0].length);
|
|
1228
|
+
|
|
1229
|
+
if (partialName.length === 0 && !/\s$/.test(openingSegment) && afterTag.length > 0) {
|
|
1230
|
+
return null;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
return {
|
|
1234
|
+
tagName: tagMatch[1],
|
|
1235
|
+
partialName,
|
|
1236
|
+
start: position - partialName.length,
|
|
1237
|
+
length: partialName.length,
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
export function getLitsxMarkupCompletionNames(context) {
|
|
1242
|
+
if (!context) {
|
|
1243
|
+
return [];
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
const seen = new Set();
|
|
1247
|
+
const attributes = [
|
|
1248
|
+
...GLOBAL_ATTRIBUTE_COMPLETIONS,
|
|
1249
|
+
...(ATTRIBUTE_COMPLETIONS_BY_TAG[context.tagName] ?? []),
|
|
1250
|
+
...GLOBAL_ARIA_ATTRIBUTE_COMPLETIONS,
|
|
1251
|
+
]
|
|
1252
|
+
.filter((candidate) => {
|
|
1253
|
+
if (seen.has(candidate)) {
|
|
1254
|
+
return false;
|
|
1255
|
+
}
|
|
1256
|
+
seen.add(candidate);
|
|
1257
|
+
return true;
|
|
1258
|
+
})
|
|
1259
|
+
.map((name, index) => ({
|
|
1260
|
+
name,
|
|
1261
|
+
index,
|
|
1262
|
+
rank: rankAttributeCompletion(name, context.partialName),
|
|
1263
|
+
}))
|
|
1264
|
+
.filter((entry) => entry.rank)
|
|
1265
|
+
.sort((left, right) => {
|
|
1266
|
+
if (context.partialName.length === 0) {
|
|
1267
|
+
return left.index - right.index;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
if (left.rank.score !== right.rank.score) {
|
|
1271
|
+
return left.rank.score - right.rank.score;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
if (left.rank.wordIndex !== right.rank.wordIndex) {
|
|
1275
|
+
return left.rank.wordIndex - right.rank.wordIndex;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
if (left.rank.lengthDelta !== right.rank.lengthDelta) {
|
|
1279
|
+
return left.rank.lengthDelta - right.rank.lengthDelta;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
return left.index - right.index;
|
|
1283
|
+
})
|
|
1284
|
+
.map((entry) => entry.name);
|
|
1285
|
+
|
|
1286
|
+
if (context.partialName.length > 0) {
|
|
1287
|
+
return attributes;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
return [
|
|
1291
|
+
...attributes,
|
|
1292
|
+
...getLitsxAttributeCompletionNames({ ...context, prefix: "@" }),
|
|
1293
|
+
...getLitsxAttributeCompletionNames({ ...context, prefix: "." }),
|
|
1294
|
+
...getLitsxAttributeCompletionNames({ ...context, prefix: "?" }),
|
|
1295
|
+
];
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
export function inferLitsxAttributeInfoAtPosition(sourceText, position) {
|
|
1299
|
+
const tagStart = findEnclosingJsxOpeningTagStart(sourceText, position);
|
|
1300
|
+
|
|
1301
|
+
if (tagStart === -1) {
|
|
1302
|
+
return null;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
const tagEnd = findJsxOpeningTagEnd(sourceText, tagStart);
|
|
1306
|
+
const segment = sourceText.slice(tagStart + 1, tagEnd);
|
|
1307
|
+
const tagMatch = /^([A-Za-z][\w:-]*)/.exec(segment.trimStart());
|
|
1308
|
+
if (!tagMatch) {
|
|
1309
|
+
return null;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
const absoluteSegmentStart = tagStart + 1;
|
|
1313
|
+
const attributePattern = /(?:^|\s)([@.?])([\w:-]+)/g;
|
|
1314
|
+
let match;
|
|
1315
|
+
|
|
1316
|
+
while ((match = attributePattern.exec(segment)) !== null) {
|
|
1317
|
+
const prefix = match[1];
|
|
1318
|
+
const localName = match[2];
|
|
1319
|
+
const start = absoluteSegmentStart + match.index + match[0].length - (prefix.length + localName.length);
|
|
1320
|
+
const end = start + prefix.length + localName.length;
|
|
1321
|
+
|
|
1322
|
+
if (position >= start && position <= end) {
|
|
1323
|
+
return {
|
|
1324
|
+
tagName: tagMatch[1],
|
|
1325
|
+
prefix,
|
|
1326
|
+
localName,
|
|
1327
|
+
name: `${prefix}${localName}`,
|
|
1328
|
+
start,
|
|
1329
|
+
length: end - start,
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
return null;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
export function collectLitsxAuthoredIssues(sourceText, options = {}) {
|
|
1338
|
+
const channel = options.channel === "eslint" ? "eslint" : options.channel === "all" ? "all" : "typescript";
|
|
1339
|
+
const plugins = Array.from(new Set(["jsx", ...(options.plugins ?? [])]));
|
|
1340
|
+
const virtualization = createVirtualLitsxJsxSource(sourceText);
|
|
1341
|
+
let ast;
|
|
1342
|
+
|
|
1343
|
+
try {
|
|
1344
|
+
ast = babelParser.parse(virtualization.code, {
|
|
1345
|
+
sourceType: "module",
|
|
1346
|
+
plugins,
|
|
1347
|
+
});
|
|
1348
|
+
} catch (error) {
|
|
1349
|
+
return [
|
|
1350
|
+
createOriginalIssue(virtualization, {
|
|
1351
|
+
kind: "parse-error",
|
|
1352
|
+
severity: "error",
|
|
1353
|
+
code: 91000,
|
|
1354
|
+
start: typeof error?.pos === "number" ? error.pos : 0,
|
|
1355
|
+
length: 1,
|
|
1356
|
+
message: `LitSX syntax could not be parsed: ${error?.message || "Unexpected syntax."}`,
|
|
1357
|
+
}),
|
|
1358
|
+
];
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const issues = [];
|
|
1362
|
+
const attributes = collectJsxAttributes(ast);
|
|
1363
|
+
issues.push(...collectStaticHoistIssues(ast, virtualization));
|
|
1364
|
+
issues.push(...collectReactMemoIssues(ast, virtualization));
|
|
1365
|
+
issues.push(...collectReactCompatSurfaceIssues(ast, virtualization));
|
|
1366
|
+
issues.push(...collectPropsAccessIssues(ast, virtualization));
|
|
1367
|
+
issues.push(...collectHoistsFirstIssues(ast, virtualization));
|
|
1368
|
+
|
|
1369
|
+
for (const attribute of attributes) {
|
|
1370
|
+
const tagName = attribute.__litsxTagName;
|
|
1371
|
+
const attributeValue = attribute.value;
|
|
1372
|
+
const virtualSpan = {
|
|
1373
|
+
start: attribute.name.start ?? attribute.start ?? 0,
|
|
1374
|
+
length: (attribute.name.end ?? attribute.end ?? 0) - (attribute.name.start ?? attribute.start ?? 0),
|
|
1375
|
+
};
|
|
1376
|
+
const rawAttributeName = attribute.name.name;
|
|
1377
|
+
const attributeName = decodeVirtualAttributeName(rawAttributeName);
|
|
1378
|
+
|
|
1379
|
+
if (!attributeName) {
|
|
1380
|
+
if (rawAttributeName === "className" && typeof tagName === "string" && /^[a-z]/.test(tagName)) {
|
|
1381
|
+
issues.push(
|
|
1382
|
+
createOriginalIssue(virtualization, {
|
|
1383
|
+
kind: "native-classname",
|
|
1384
|
+
severity: "warning",
|
|
1385
|
+
code: 91008,
|
|
1386
|
+
start: virtualSpan.start,
|
|
1387
|
+
length: virtualSpan.length,
|
|
1388
|
+
message:
|
|
1389
|
+
'`className` is not native LitSX syntax. Use `class` in native LitSX, or add the React compatibility layer to rewrite `className`.',
|
|
1390
|
+
fix: {
|
|
1391
|
+
text: "class",
|
|
1392
|
+
},
|
|
1393
|
+
})
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
const prefix = attributeName[0];
|
|
1400
|
+
const localName = attributeName.slice(1);
|
|
1401
|
+
|
|
1402
|
+
if ((prefix === "@" || prefix === ".") && attributeValue?.type !== "JSXExpressionContainer") {
|
|
1403
|
+
issues.push(
|
|
1404
|
+
createOriginalIssue(virtualization, {
|
|
1405
|
+
kind: "invalid-binding-value",
|
|
1406
|
+
severity: "error",
|
|
1407
|
+
code: 91001,
|
|
1408
|
+
start: virtualSpan.start,
|
|
1409
|
+
length: virtualSpan.length,
|
|
1410
|
+
message:
|
|
1411
|
+
prefix === "@"
|
|
1412
|
+
? `Lit listener binding "${attributeName}" must use an expression, for example ${attributeName}={handler}.`
|
|
1413
|
+
: `Lit property binding "${attributeName}" must use an expression, for example ${attributeName}={value}.`,
|
|
1414
|
+
})
|
|
1415
|
+
);
|
|
1416
|
+
continue;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
if (prefix === "?" && attributeValue?.type && attributeValue.type !== "JSXExpressionContainer") {
|
|
1420
|
+
issues.push(
|
|
1421
|
+
createOriginalIssue(virtualization, {
|
|
1422
|
+
kind: "invalid-binding-value",
|
|
1423
|
+
severity: "error",
|
|
1424
|
+
code: 91002,
|
|
1425
|
+
start: virtualSpan.start,
|
|
1426
|
+
length: virtualSpan.length,
|
|
1427
|
+
message:
|
|
1428
|
+
`Lit boolean binding "${attributeName}" must be bare or use an expression, for example ${attributeName} or ${attributeName}={condition}.`,
|
|
1429
|
+
})
|
|
1430
|
+
);
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
if (
|
|
1435
|
+
attributeValue?.type === "JSXExpressionContainer" &&
|
|
1436
|
+
attributeValue.expression?.type === "JSXEmptyExpression"
|
|
1437
|
+
) {
|
|
1438
|
+
issues.push(
|
|
1439
|
+
createOriginalIssue(virtualization, {
|
|
1440
|
+
kind: "invalid-binding-value",
|
|
1441
|
+
severity: "error",
|
|
1442
|
+
code: 91003,
|
|
1443
|
+
start: virtualSpan.start,
|
|
1444
|
+
length: virtualSpan.length,
|
|
1445
|
+
message: `Lit binding "${attributeName}" cannot use an empty expression.`,
|
|
1446
|
+
})
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
if (
|
|
1451
|
+
prefix === "@" &&
|
|
1452
|
+
tagName &&
|
|
1453
|
+
Object.hasOwn(EVENT_COMPLETIONS_BY_TAG, tagName) &&
|
|
1454
|
+
!EVENT_COMPLETIONS_BY_TAG[tagName].includes(localName)
|
|
1455
|
+
) {
|
|
1456
|
+
const suggestion = findClosestAttributeSuggestion(
|
|
1457
|
+
prefix,
|
|
1458
|
+
localName,
|
|
1459
|
+
EVENT_COMPLETIONS_BY_TAG[tagName].map((name) => `@${name}`),
|
|
1460
|
+
);
|
|
1461
|
+
issues.push(
|
|
1462
|
+
createOriginalIssue(virtualization, {
|
|
1463
|
+
kind: "unknown-binding",
|
|
1464
|
+
severity: "warning",
|
|
1465
|
+
code: 91006,
|
|
1466
|
+
start: virtualSpan.start,
|
|
1467
|
+
length: virtualSpan.length,
|
|
1468
|
+
message:
|
|
1469
|
+
`Listener binding "${attributeName}" is not in the known LitSX event set for <${tagName}>.${suggestion ? ` Did you mean "${suggestion}"?` : ""}`,
|
|
1470
|
+
})
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
if (
|
|
1475
|
+
prefix === "." &&
|
|
1476
|
+
tagName &&
|
|
1477
|
+
Object.hasOwn(PROP_COMPLETIONS_BY_TAG, tagName) &&
|
|
1478
|
+
!PROP_COMPLETIONS_BY_TAG[tagName].includes(localName)
|
|
1479
|
+
) {
|
|
1480
|
+
const suggestion = findClosestAttributeSuggestion(
|
|
1481
|
+
prefix,
|
|
1482
|
+
localName,
|
|
1483
|
+
PROP_COMPLETIONS_BY_TAG[tagName].map((name) => `.${name}`),
|
|
1484
|
+
);
|
|
1485
|
+
issues.push(
|
|
1486
|
+
createOriginalIssue(virtualization, {
|
|
1487
|
+
kind: "unknown-binding",
|
|
1488
|
+
severity: "warning",
|
|
1489
|
+
code: 91004,
|
|
1490
|
+
start: virtualSpan.start,
|
|
1491
|
+
length: virtualSpan.length,
|
|
1492
|
+
message:
|
|
1493
|
+
`Property binding "${attributeName}" is not in the known LitSX property set for <${tagName}>.${suggestion ? ` Did you mean "${suggestion}"?` : ""}`,
|
|
1494
|
+
})
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
if (
|
|
1499
|
+
prefix === "?" &&
|
|
1500
|
+
tagName &&
|
|
1501
|
+
Object.hasOwn(BOOL_COMPLETIONS_BY_TAG, tagName) &&
|
|
1502
|
+
!BOOL_COMPLETIONS_BY_TAG[tagName].includes(localName)
|
|
1503
|
+
) {
|
|
1504
|
+
const suggestion = findClosestAttributeSuggestion(
|
|
1505
|
+
prefix,
|
|
1506
|
+
localName,
|
|
1507
|
+
BOOL_COMPLETIONS_BY_TAG[tagName].map((name) => `?${name}`),
|
|
1508
|
+
);
|
|
1509
|
+
issues.push(
|
|
1510
|
+
createOriginalIssue(virtualization, {
|
|
1511
|
+
kind: "unknown-binding",
|
|
1512
|
+
severity: "warning",
|
|
1513
|
+
code: 91005,
|
|
1514
|
+
start: virtualSpan.start,
|
|
1515
|
+
length: virtualSpan.length,
|
|
1516
|
+
message:
|
|
1517
|
+
`Boolean binding "${attributeName}" is not in the known LitSX boolean attribute set for <${tagName}>.${suggestion ? ` Did you mean "${suggestion}"?` : ""}`,
|
|
1518
|
+
})
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
if (channel === "all") {
|
|
1524
|
+
return issues;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
const eslintOnlyCodes = new Set([91015, 91016, 91017]);
|
|
1528
|
+
return issues.filter((issue) => (
|
|
1529
|
+
channel === "eslint" ? true : !eslintOnlyCodes.has(issue.code)
|
|
1530
|
+
));
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
export {
|
|
1534
|
+
createVirtualLitsxJsxSource,
|
|
1535
|
+
decodeVirtualAttributeName,
|
|
1536
|
+
decodeVirtualStaticHoistName,
|
|
1537
|
+
NATIVE_STATIC_HOISTS,
|
|
1538
|
+
looksLikeLitsxJsx,
|
|
1539
|
+
mapOriginalPositionToVirtual,
|
|
1540
|
+
remapTextSpanToOriginal,
|
|
1541
|
+
remapVirtualText,
|
|
1542
|
+
STATIC_HOIST_CALL_RE,
|
|
1543
|
+
};
|