@zenithbuild/language-server 0.2.6 → 0.5.0-beta.2.1
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/CHANGELOG.md +6 -0
- package/README.md +14 -0
- package/RELEASE_NOTES.md +33 -0
- package/package.json +5 -2
- package/scripts/release.ts +24 -7
- package/src/code-actions.ts +60 -0
- package/src/contracts.ts +100 -0
- package/src/diagnostics.ts +391 -74
- package/src/project.ts +141 -25
- package/src/server.ts +68 -46
- package/src/settings.ts +15 -0
- package/src/types/zenith-compiler.d.ts +3 -0
- package/test/contracts.spec.ts +37 -0
- package/test/diagnostics.spec.ts +101 -0
- package/test/project-root.spec.ts +44 -0
- package/tsconfig.test.json +25 -0
- package/bun.lock +0 -83
package/src/diagnostics.ts
CHANGED
|
@@ -1,81 +1,392 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Diagnostics
|
|
3
|
-
*
|
|
4
|
-
* Compile-time validation mirroring
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Important: No runtime execution. Pure static analysis only.
|
|
3
|
+
*
|
|
4
|
+
* Compile-time validation mirroring Zenith contracts.
|
|
5
|
+
* No runtime execution. Pure static analysis only.
|
|
8
6
|
*/
|
|
9
7
|
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
import {
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
|
|
10
|
+
import { parseForExpression } from './metadata/directive-metadata';
|
|
13
11
|
import { parseZenithImports, resolveModule, isPluginModule } from './imports';
|
|
14
12
|
import type { ProjectGraph } from './project';
|
|
13
|
+
import {
|
|
14
|
+
classifyZenithFile,
|
|
15
|
+
isCssContractImportSpecifier,
|
|
16
|
+
isLocalCssSpecifier,
|
|
17
|
+
resolveCssImportPath
|
|
18
|
+
} from './contracts';
|
|
19
|
+
import type { ZenithServerSettings } from './settings';
|
|
20
|
+
import { EVENT_BINDING_DIAGNOSTIC_CODE } from './code-actions';
|
|
21
|
+
|
|
22
|
+
const COMPONENT_SCRIPT_CONTRACT_MESSAGE =
|
|
23
|
+
'Zenith Contract Violation: Components are structural; move <script> to the parent route scope.';
|
|
24
|
+
|
|
25
|
+
const CSS_BARE_IMPORT_MESSAGE =
|
|
26
|
+
'CSS import contract violation: bare CSS imports are not supported.';
|
|
27
|
+
|
|
28
|
+
const CSS_ESCAPE_MESSAGE =
|
|
29
|
+
'CSS import contract violation: imported CSS path escapes project root.';
|
|
30
|
+
|
|
31
|
+
const DiagnosticSeverity = {
|
|
32
|
+
Error: 1,
|
|
33
|
+
Warning: 2,
|
|
34
|
+
Information: 3,
|
|
35
|
+
Hint: 4
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
38
|
+
export interface ZenithPosition {
|
|
39
|
+
line: number;
|
|
40
|
+
character: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ZenithRange {
|
|
44
|
+
start: ZenithPosition;
|
|
45
|
+
end: ZenithPosition;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ZenithDiagnostic {
|
|
49
|
+
severity: number;
|
|
50
|
+
range: ZenithRange;
|
|
51
|
+
message: string;
|
|
52
|
+
source: string;
|
|
53
|
+
code?: string;
|
|
54
|
+
data?: unknown;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ZenithTextDocumentLike {
|
|
58
|
+
uri: string;
|
|
59
|
+
getText(): string;
|
|
60
|
+
positionAt(offset: number): ZenithPosition;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function uriToFilePath(uri: string): string {
|
|
64
|
+
try {
|
|
65
|
+
return decodeURIComponent(new URL(uri).pathname);
|
|
66
|
+
} catch {
|
|
67
|
+
return decodeURIComponent(uri.replace('file://', ''));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function stripScriptAndStylePreserveIndices(text: string): string {
|
|
72
|
+
return text.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi, (match) => ' '.repeat(match.length));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface ScriptBlock {
|
|
76
|
+
content: string;
|
|
77
|
+
contentStartOffset: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getScriptBlocks(text: string): ScriptBlock[] {
|
|
81
|
+
const blocks: ScriptBlock[] = [];
|
|
82
|
+
const scriptPattern = /<script\b[^>]*>([\s\S]*?)<\/script>/gi;
|
|
83
|
+
let match: RegExpExecArray | null;
|
|
84
|
+
|
|
85
|
+
while ((match = scriptPattern.exec(text)) !== null) {
|
|
86
|
+
const whole = match[0] || '';
|
|
87
|
+
const content = match[1] || '';
|
|
88
|
+
const localStart = whole.indexOf(content);
|
|
89
|
+
const contentStartOffset = (match.index || 0) + Math.max(localStart, 0);
|
|
90
|
+
blocks.push({ content, contentStartOffset });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return blocks;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface ParsedImportSpecifier {
|
|
97
|
+
specifier: string;
|
|
98
|
+
startOffset: number;
|
|
99
|
+
endOffset: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseImportSpecifiers(scriptContent: string, scriptStartOffset: number): ParsedImportSpecifier[] {
|
|
103
|
+
const imports: ParsedImportSpecifier[] = [];
|
|
104
|
+
const importPattern = /import\s+(?:[^'";]+?\s+from\s+)?['"]([^'"\n]+)['"]/g;
|
|
105
|
+
let match: RegExpExecArray | null;
|
|
106
|
+
|
|
107
|
+
while ((match = importPattern.exec(scriptContent)) !== null) {
|
|
108
|
+
const statement = match[0] || '';
|
|
109
|
+
const specifier = match[1] || '';
|
|
110
|
+
const rel = statement.indexOf(specifier);
|
|
111
|
+
const startOffset = scriptStartOffset + (match.index || 0) + Math.max(rel, 0);
|
|
112
|
+
const endOffset = startOffset + specifier.length;
|
|
113
|
+
imports.push({ specifier, startOffset, endOffset });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return imports;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalizeEventHandlerValue(rawValue: string): string {
|
|
120
|
+
let value = rawValue.trim();
|
|
121
|
+
|
|
122
|
+
if ((value.startsWith('{') && value.endsWith('}')) ||
|
|
123
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
124
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
125
|
+
value = value.slice(1, -1).trim();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*\(\)$/.test(value)) {
|
|
129
|
+
value = value.slice(0, -2);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!value) {
|
|
133
|
+
return 'handler';
|
|
134
|
+
}
|
|
15
135
|
|
|
16
|
-
|
|
17
|
-
document: TextDocument;
|
|
18
|
-
text: string;
|
|
19
|
-
graph: ProjectGraph | null;
|
|
136
|
+
return value;
|
|
20
137
|
}
|
|
21
138
|
|
|
22
139
|
/**
|
|
23
|
-
* Collect all diagnostics for a document
|
|
140
|
+
* Collect all diagnostics for a document.
|
|
24
141
|
*/
|
|
25
|
-
export function collectDiagnostics(
|
|
26
|
-
|
|
142
|
+
export async function collectDiagnostics(
|
|
143
|
+
document: ZenithTextDocumentLike,
|
|
144
|
+
graph: ProjectGraph | null,
|
|
145
|
+
settings: ZenithServerSettings,
|
|
146
|
+
projectRoot: string | null
|
|
147
|
+
): Promise<ZenithDiagnostic[]> {
|
|
148
|
+
const diagnostics: ZenithDiagnostic[] = [];
|
|
27
149
|
const text = document.getText();
|
|
150
|
+
const filePath = uriToFilePath(document.uri);
|
|
28
151
|
|
|
29
|
-
|
|
30
|
-
collectComponentDiagnostics(document, text, graph, diagnostics);
|
|
152
|
+
let hasComponentScriptCompilerDiagnostic = false;
|
|
31
153
|
|
|
32
|
-
//
|
|
33
|
-
|
|
154
|
+
// 1) Compiler validation (source-of-truth), with configurable suppression for component script contract.
|
|
155
|
+
try {
|
|
156
|
+
process.env.ZENITH_CACHE = '1';
|
|
157
|
+
const { compile } = await import('@zenithbuild/compiler');
|
|
158
|
+
await compile(text, filePath);
|
|
159
|
+
} catch (error: any) {
|
|
160
|
+
const message = String(error?.message || 'Unknown compiler error');
|
|
161
|
+
const isContractViolation = message.includes(COMPONENT_SCRIPT_CONTRACT_MESSAGE);
|
|
34
162
|
|
|
35
|
-
|
|
36
|
-
|
|
163
|
+
if (isContractViolation) {
|
|
164
|
+
hasComponentScriptCompilerDiagnostic = true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!(settings.componentScripts === 'allow' && isContractViolation)) {
|
|
168
|
+
diagnostics.push({
|
|
169
|
+
severity: DiagnosticSeverity.Error,
|
|
170
|
+
range: {
|
|
171
|
+
start: { line: (error?.line || 1) - 1, character: (error?.column || 1) - 1 },
|
|
172
|
+
end: { line: (error?.line || 1) - 1, character: (error?.column || 1) + 20 }
|
|
173
|
+
},
|
|
174
|
+
message: `[${error?.code || 'compiler'}] ${message}${error?.hints ? '\n\nHints:\n' + error.hints.join('\n') : ''}`,
|
|
175
|
+
source: 'zenith-compiler'
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
diagnostics.push(
|
|
181
|
+
...collectContractDiagnostics(
|
|
182
|
+
document,
|
|
183
|
+
graph,
|
|
184
|
+
settings,
|
|
185
|
+
projectRoot,
|
|
186
|
+
hasComponentScriptCompilerDiagnostic
|
|
187
|
+
)
|
|
188
|
+
);
|
|
37
189
|
|
|
38
|
-
|
|
190
|
+
return diagnostics;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function collectContractDiagnostics(
|
|
194
|
+
document: ZenithTextDocumentLike,
|
|
195
|
+
graph: ProjectGraph | null,
|
|
196
|
+
settings: ZenithServerSettings,
|
|
197
|
+
projectRoot: string | null,
|
|
198
|
+
hasComponentScriptCompilerDiagnostic = false
|
|
199
|
+
): ZenithDiagnostic[] {
|
|
200
|
+
const diagnostics: ZenithDiagnostic[] = [];
|
|
201
|
+
const text = document.getText();
|
|
202
|
+
const filePath = uriToFilePath(document.uri);
|
|
203
|
+
|
|
204
|
+
collectComponentScriptDiagnostics(document, text, filePath, settings, diagnostics, hasComponentScriptCompilerDiagnostic);
|
|
205
|
+
collectEventBindingDiagnostics(document, text, diagnostics);
|
|
206
|
+
collectDirectiveDiagnostics(document, text, diagnostics);
|
|
207
|
+
collectImportDiagnostics(document, text, diagnostics);
|
|
208
|
+
collectCssImportContractDiagnostics(document, text, filePath, projectRoot, diagnostics);
|
|
39
209
|
collectExpressionDiagnostics(document, text, diagnostics);
|
|
210
|
+
collectComponentDiagnostics(document, text, graph, diagnostics);
|
|
40
211
|
|
|
41
212
|
return diagnostics;
|
|
42
213
|
}
|
|
43
214
|
|
|
215
|
+
function collectComponentScriptDiagnostics(
|
|
216
|
+
document: ZenithTextDocumentLike,
|
|
217
|
+
text: string,
|
|
218
|
+
filePath: string,
|
|
219
|
+
settings: ZenithServerSettings,
|
|
220
|
+
diagnostics: ZenithDiagnostic[],
|
|
221
|
+
hasComponentScriptCompilerDiagnostic: boolean
|
|
222
|
+
): void {
|
|
223
|
+
if (settings.componentScripts !== 'forbid') {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (classifyZenithFile(filePath) !== 'component') {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (hasComponentScriptCompilerDiagnostic) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const scriptTagMatch = /<script\b[^>]*>/i.exec(text);
|
|
236
|
+
if (!scriptTagMatch || scriptTagMatch.index == null) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
diagnostics.push({
|
|
241
|
+
severity: DiagnosticSeverity.Error,
|
|
242
|
+
range: {
|
|
243
|
+
start: document.positionAt(scriptTagMatch.index),
|
|
244
|
+
end: document.positionAt(scriptTagMatch.index + scriptTagMatch[0].length)
|
|
245
|
+
},
|
|
246
|
+
message: COMPONENT_SCRIPT_CONTRACT_MESSAGE,
|
|
247
|
+
source: 'zenith-contract'
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function collectEventBindingDiagnostics(
|
|
252
|
+
document: ZenithTextDocumentLike,
|
|
253
|
+
text: string,
|
|
254
|
+
diagnostics: ZenithDiagnostic[]
|
|
255
|
+
): void {
|
|
256
|
+
const stripped = stripScriptAndStylePreserveIndices(text);
|
|
257
|
+
|
|
258
|
+
// Invalid @click={handler}
|
|
259
|
+
const atEventPattern = /@([a-zA-Z][a-zA-Z0-9_-]*)\s*=\s*(\{[^}]*\}|"[^"]*"|'[^']*')/g;
|
|
260
|
+
let match: RegExpExecArray | null;
|
|
261
|
+
|
|
262
|
+
while ((match = atEventPattern.exec(stripped)) !== null) {
|
|
263
|
+
const fullMatch = match[0] || '';
|
|
264
|
+
const eventName = match[1] || 'click';
|
|
265
|
+
const rawHandler = match[2] || '{handler}';
|
|
266
|
+
const handler = normalizeEventHandlerValue(rawHandler);
|
|
267
|
+
const replacement = `on:${eventName}={${handler}}`;
|
|
268
|
+
|
|
269
|
+
diagnostics.push({
|
|
270
|
+
severity: DiagnosticSeverity.Error,
|
|
271
|
+
range: {
|
|
272
|
+
start: document.positionAt(match.index || 0),
|
|
273
|
+
end: document.positionAt((match.index || 0) + fullMatch.length)
|
|
274
|
+
},
|
|
275
|
+
message: `Invalid event binding syntax. Use on:${eventName}={handler}.`,
|
|
276
|
+
source: 'zenith-contract',
|
|
277
|
+
code: EVENT_BINDING_DIAGNOSTIC_CODE,
|
|
278
|
+
data: {
|
|
279
|
+
replacement,
|
|
280
|
+
title: `Convert to ${replacement}`
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Invalid onclick="handler" / onclick={handler}
|
|
286
|
+
const onEventPattern = /\bon([a-zA-Z][a-zA-Z0-9_-]*)\s*=\s*(\{[^}]*\}|"[^"]*"|'[^']*')/g;
|
|
287
|
+
while ((match = onEventPattern.exec(stripped)) !== null) {
|
|
288
|
+
const fullMatch = match[0] || '';
|
|
289
|
+
const eventName = match[1] || 'click';
|
|
290
|
+
const rawHandler = match[2] || '{handler}';
|
|
291
|
+
const handler = normalizeEventHandlerValue(rawHandler);
|
|
292
|
+
const replacement = `on:${eventName}={${handler}}`;
|
|
293
|
+
|
|
294
|
+
diagnostics.push({
|
|
295
|
+
severity: DiagnosticSeverity.Error,
|
|
296
|
+
range: {
|
|
297
|
+
start: document.positionAt(match.index || 0),
|
|
298
|
+
end: document.positionAt((match.index || 0) + fullMatch.length)
|
|
299
|
+
},
|
|
300
|
+
message: `Invalid event binding syntax. Use on:${eventName}={handler}.`,
|
|
301
|
+
source: 'zenith-contract',
|
|
302
|
+
code: EVENT_BINDING_DIAGNOSTIC_CODE,
|
|
303
|
+
data: {
|
|
304
|
+
replacement,
|
|
305
|
+
title: `Convert to ${replacement}`
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function collectCssImportContractDiagnostics(
|
|
312
|
+
document: ZenithTextDocumentLike,
|
|
313
|
+
text: string,
|
|
314
|
+
filePath: string,
|
|
315
|
+
projectRoot: string | null,
|
|
316
|
+
diagnostics: ZenithDiagnostic[]
|
|
317
|
+
): void {
|
|
318
|
+
const scriptBlocks = getScriptBlocks(text);
|
|
319
|
+
if (scriptBlocks.length === 0) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const effectiveProjectRoot = projectRoot ? path.resolve(projectRoot) : path.dirname(filePath);
|
|
324
|
+
|
|
325
|
+
for (const block of scriptBlocks) {
|
|
326
|
+
const imports = parseImportSpecifiers(block.content, block.contentStartOffset);
|
|
327
|
+
for (const imp of imports) {
|
|
328
|
+
if (!isCssContractImportSpecifier(imp.specifier)) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!isLocalCssSpecifier(imp.specifier)) {
|
|
333
|
+
diagnostics.push({
|
|
334
|
+
severity: DiagnosticSeverity.Error,
|
|
335
|
+
range: {
|
|
336
|
+
start: document.positionAt(imp.startOffset),
|
|
337
|
+
end: document.positionAt(imp.endOffset)
|
|
338
|
+
},
|
|
339
|
+
message: CSS_BARE_IMPORT_MESSAGE,
|
|
340
|
+
source: 'zenith-contract'
|
|
341
|
+
});
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const resolved = resolveCssImportPath(filePath, imp.specifier, effectiveProjectRoot);
|
|
346
|
+
if (resolved.escapesProjectRoot) {
|
|
347
|
+
diagnostics.push({
|
|
348
|
+
severity: DiagnosticSeverity.Error,
|
|
349
|
+
range: {
|
|
350
|
+
start: document.positionAt(imp.startOffset),
|
|
351
|
+
end: document.positionAt(imp.endOffset)
|
|
352
|
+
},
|
|
353
|
+
message: CSS_ESCAPE_MESSAGE,
|
|
354
|
+
source: 'zenith-contract'
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
44
361
|
/**
|
|
45
|
-
* Validate component references
|
|
362
|
+
* Validate component references.
|
|
46
363
|
*/
|
|
47
364
|
function collectComponentDiagnostics(
|
|
48
|
-
document:
|
|
365
|
+
document: ZenithTextDocumentLike,
|
|
49
366
|
text: string,
|
|
50
367
|
graph: ProjectGraph | null,
|
|
51
|
-
diagnostics:
|
|
368
|
+
diagnostics: ZenithDiagnostic[]
|
|
52
369
|
): void {
|
|
53
370
|
if (!graph) return;
|
|
54
371
|
|
|
55
|
-
// Strip script and style blocks to avoid matching PascalCase names in TS generics
|
|
56
|
-
// We use a simplified version that preserves indices by replacing content with spaces
|
|
57
372
|
const strippedText = text
|
|
58
|
-
.replace(/<(script|style)[^>]*>([\s\S]*?)<\/\1>/gi, (match,
|
|
373
|
+
.replace(/<(script|style)[^>]*>([\s\S]*?)<\/\1>/gi, (match, _tag, content) => {
|
|
59
374
|
return match.replace(content, ' '.repeat(content.length));
|
|
60
375
|
});
|
|
61
376
|
|
|
62
|
-
// Match component tags (PascalCase)
|
|
63
377
|
const componentPattern = /<([A-Z][a-zA-Z0-9]*)(?=[\s/>])/g;
|
|
64
|
-
let match;
|
|
378
|
+
let match: RegExpExecArray | null;
|
|
65
379
|
|
|
66
380
|
while ((match = componentPattern.exec(strippedText)) !== null) {
|
|
67
381
|
const componentName = match[1];
|
|
68
|
-
|
|
69
|
-
// Skip known router components
|
|
70
382
|
if (componentName === 'ZenLink') continue;
|
|
71
383
|
|
|
72
|
-
// Check if component exists in project graph
|
|
73
384
|
const inLayouts = graph.layouts.has(componentName);
|
|
74
385
|
const inComponents = graph.components.has(componentName);
|
|
75
386
|
|
|
76
387
|
if (!inLayouts && !inComponents) {
|
|
77
|
-
const startPos = document.positionAt(match.index + 1);
|
|
78
|
-
const endPos = document.positionAt(match.index + 1 + componentName.length);
|
|
388
|
+
const startPos = document.positionAt((match.index || 0) + 1);
|
|
389
|
+
const endPos = document.positionAt((match.index || 0) + 1 + componentName.length);
|
|
79
390
|
|
|
80
391
|
diagnostics.push({
|
|
81
392
|
severity: DiagnosticSeverity.Warning,
|
|
@@ -88,41 +399,38 @@ function collectComponentDiagnostics(
|
|
|
88
399
|
}
|
|
89
400
|
|
|
90
401
|
/**
|
|
91
|
-
* Validate directive usage
|
|
402
|
+
* Validate directive usage.
|
|
92
403
|
*/
|
|
93
404
|
function collectDirectiveDiagnostics(
|
|
94
|
-
document:
|
|
405
|
+
document: ZenithTextDocumentLike,
|
|
95
406
|
text: string,
|
|
96
|
-
diagnostics:
|
|
407
|
+
diagnostics: ZenithDiagnostic[]
|
|
97
408
|
): void {
|
|
98
|
-
// Match zen:* directives
|
|
99
409
|
const directivePattern = /(zen:(?:if|for|effect|show))\s*=\s*["']([^"']*)["']/g;
|
|
100
|
-
let match;
|
|
410
|
+
let match: RegExpExecArray | null;
|
|
101
411
|
|
|
102
412
|
while ((match = directivePattern.exec(text)) !== null) {
|
|
103
413
|
const directiveName = match[1];
|
|
104
414
|
const directiveValue = match[2];
|
|
105
415
|
|
|
106
|
-
// Validate zen:for syntax
|
|
107
416
|
if (directiveName === 'zen:for') {
|
|
108
417
|
const parsed = parseForExpression(directiveValue);
|
|
109
418
|
if (!parsed) {
|
|
110
|
-
const startPos = document.positionAt(match.index);
|
|
111
|
-
const endPos = document.positionAt(match.index + match[0].length);
|
|
419
|
+
const startPos = document.positionAt(match.index || 0);
|
|
420
|
+
const endPos = document.positionAt((match.index || 0) + (match[0] || '').length);
|
|
112
421
|
|
|
113
422
|
diagnostics.push({
|
|
114
423
|
severity: DiagnosticSeverity.Error,
|
|
115
424
|
range: { start: startPos, end: endPos },
|
|
116
|
-
message:
|
|
425
|
+
message: 'Invalid zen:for syntax. Expected: "item in items" or "item, index in items"',
|
|
117
426
|
source: 'zenith'
|
|
118
427
|
});
|
|
119
428
|
}
|
|
120
429
|
}
|
|
121
430
|
|
|
122
|
-
// Check for empty directive values
|
|
123
431
|
if (!directiveValue.trim()) {
|
|
124
|
-
const startPos = document.positionAt(match.index);
|
|
125
|
-
const endPos = document.positionAt(match.index + match[0].length);
|
|
432
|
+
const startPos = document.positionAt(match.index || 0);
|
|
433
|
+
const endPos = document.positionAt((match.index || 0) + (match[0] || '').length);
|
|
126
434
|
|
|
127
435
|
diagnostics.push({
|
|
128
436
|
severity: DiagnosticSeverity.Error,
|
|
@@ -133,45 +441,40 @@ function collectDirectiveDiagnostics(
|
|
|
133
441
|
}
|
|
134
442
|
}
|
|
135
443
|
|
|
136
|
-
// Check for zen:for on slot elements (forbidden)
|
|
137
444
|
const slotForPattern = /<slot[^>]*zen:for/g;
|
|
138
445
|
while ((match = slotForPattern.exec(text)) !== null) {
|
|
139
|
-
const startPos = document.positionAt(match.index);
|
|
140
|
-
const endPos = document.positionAt(match.index + match[0].length);
|
|
446
|
+
const startPos = document.positionAt(match.index || 0);
|
|
447
|
+
const endPos = document.positionAt((match.index || 0) + (match[0] || '').length);
|
|
141
448
|
|
|
142
449
|
diagnostics.push({
|
|
143
450
|
severity: DiagnosticSeverity.Error,
|
|
144
451
|
range: { start: startPos, end: endPos },
|
|
145
|
-
message:
|
|
452
|
+
message: 'zen:for cannot be used on <slot> elements',
|
|
146
453
|
source: 'zenith'
|
|
147
454
|
});
|
|
148
455
|
}
|
|
149
456
|
}
|
|
150
457
|
|
|
151
458
|
/**
|
|
152
|
-
* Validate imports
|
|
459
|
+
* Validate imports.
|
|
153
460
|
*/
|
|
154
461
|
function collectImportDiagnostics(
|
|
155
|
-
document:
|
|
462
|
+
document: ZenithTextDocumentLike,
|
|
156
463
|
text: string,
|
|
157
|
-
diagnostics:
|
|
464
|
+
diagnostics: ZenithDiagnostic[]
|
|
158
465
|
): void {
|
|
159
|
-
// Extract script content
|
|
160
466
|
const scriptMatch = text.match(/<script[^>]*>([\s\S]*?)<\/script>/i);
|
|
161
467
|
if (!scriptMatch) return;
|
|
162
468
|
|
|
163
469
|
const scriptContent = scriptMatch[1];
|
|
164
|
-
const scriptStart = scriptMatch.index
|
|
165
|
-
|
|
470
|
+
const scriptStart = (scriptMatch.index || 0) + scriptMatch[0].indexOf(scriptContent);
|
|
166
471
|
const imports = parseZenithImports(scriptContent);
|
|
167
472
|
|
|
168
473
|
for (const imp of imports) {
|
|
169
474
|
const resolved = resolveModule(imp.module);
|
|
170
475
|
|
|
171
|
-
// Warn about unknown plugin modules (soft diagnostic)
|
|
172
476
|
if (isPluginModule(imp.module) && !resolved.isKnown) {
|
|
173
|
-
|
|
174
|
-
const importPattern = new RegExp(`import[^'"]*['"]${imp.module.replace(':', '\\:')}['"]`);
|
|
477
|
+
const importPattern = new RegExp(`import[^'\"]*['\"]${imp.module.replace(':', '\\:')}['\"]`);
|
|
175
478
|
const importMatch = scriptContent.match(importPattern);
|
|
176
479
|
|
|
177
480
|
if (importMatch) {
|
|
@@ -188,9 +491,8 @@ function collectImportDiagnostics(
|
|
|
188
491
|
}
|
|
189
492
|
}
|
|
190
493
|
|
|
191
|
-
// Check for invalid specifiers in known modules
|
|
192
494
|
if (resolved.isKnown && resolved.metadata) {
|
|
193
|
-
const validExports = resolved.metadata.exports.map(e => e.name);
|
|
495
|
+
const validExports = resolved.metadata.exports.map((e) => e.name);
|
|
194
496
|
|
|
195
497
|
for (const specifier of imp.specifiers) {
|
|
196
498
|
if (!validExports.includes(specifier)) {
|
|
@@ -216,45 +518,60 @@ function collectImportDiagnostics(
|
|
|
216
518
|
}
|
|
217
519
|
|
|
218
520
|
/**
|
|
219
|
-
* Validate expressions for dangerous patterns
|
|
521
|
+
* Validate expressions for dangerous patterns.
|
|
220
522
|
*/
|
|
221
523
|
function collectExpressionDiagnostics(
|
|
222
|
-
document:
|
|
524
|
+
document: ZenithTextDocumentLike,
|
|
223
525
|
text: string,
|
|
224
|
-
diagnostics:
|
|
526
|
+
diagnostics: ZenithDiagnostic[]
|
|
225
527
|
): void {
|
|
226
|
-
// Match expressions in templates
|
|
227
528
|
const expressionPattern = /\{([^}]+)\}/g;
|
|
228
|
-
let match;
|
|
529
|
+
let match: RegExpExecArray | null;
|
|
229
530
|
|
|
230
531
|
while ((match = expressionPattern.exec(text)) !== null) {
|
|
231
532
|
const expression = match[1];
|
|
232
|
-
const offset = match.index;
|
|
533
|
+
const offset = match.index || 0;
|
|
233
534
|
|
|
234
|
-
// Check for dangerous patterns
|
|
235
535
|
if (expression.includes('eval(') || expression.includes('Function(')) {
|
|
236
536
|
const startPos = document.positionAt(offset);
|
|
237
|
-
const endPos = document.positionAt(offset + match[0].length);
|
|
537
|
+
const endPos = document.positionAt(offset + (match[0] || '').length);
|
|
238
538
|
|
|
239
539
|
diagnostics.push({
|
|
240
540
|
severity: DiagnosticSeverity.Error,
|
|
241
541
|
range: { start: startPos, end: endPos },
|
|
242
|
-
message:
|
|
542
|
+
message: 'Dangerous pattern detected: eval() and Function() are not allowed in expressions',
|
|
243
543
|
source: 'zenith'
|
|
244
544
|
});
|
|
245
545
|
}
|
|
246
546
|
|
|
247
|
-
// Check for with statement
|
|
248
547
|
if (/\bwith\s*\(/.test(expression)) {
|
|
249
548
|
const startPos = document.positionAt(offset);
|
|
250
|
-
const endPos = document.positionAt(offset + match[0].length);
|
|
549
|
+
const endPos = document.positionAt(offset + (match[0] || '').length);
|
|
550
|
+
|
|
551
|
+
diagnostics.push({
|
|
552
|
+
severity: DiagnosticSeverity.Error,
|
|
553
|
+
range: { start: startPos, end: endPos },
|
|
554
|
+
message: "'with' statement is not allowed in expressions",
|
|
555
|
+
source: 'zenith'
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (expression.includes(' as ') || (expression.includes('<') && expression.includes('>'))) {
|
|
560
|
+
const startPos = document.positionAt(offset);
|
|
561
|
+
const endPos = document.positionAt(offset + (match[0] || '').length);
|
|
251
562
|
|
|
252
563
|
diagnostics.push({
|
|
253
564
|
severity: DiagnosticSeverity.Error,
|
|
254
565
|
range: { start: startPos, end: endPos },
|
|
255
|
-
message:
|
|
566
|
+
message: 'TypeScript syntax (type casting or generics) detected in runtime expression. Runtime code must be pure JavaScript.',
|
|
256
567
|
source: 'zenith'
|
|
257
568
|
});
|
|
258
569
|
}
|
|
259
570
|
}
|
|
260
571
|
}
|
|
572
|
+
|
|
573
|
+
export const CONTRACT_MESSAGES = {
|
|
574
|
+
componentScript: COMPONENT_SCRIPT_CONTRACT_MESSAGE,
|
|
575
|
+
cssBareImport: CSS_BARE_IMPORT_MESSAGE,
|
|
576
|
+
cssEscape: CSS_ESCAPE_MESSAGE
|
|
577
|
+
} as const;
|