aero-vscode 0.0.2
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/.vscodeignore +13 -0
- package/LICENSE +21 -0
- package/README.md +69 -0
- package/aero-vscode-0.0.1.vsix +0 -0
- package/dist/extension.js +19 -0
- package/images/logo.png +0 -0
- package/package.json +98 -0
- package/src/__tests__/analyzer.test.ts +202 -0
- package/src/__tests__/diagnostics.test.ts +964 -0
- package/src/__tests__/providers.test.ts +292 -0
- package/src/__tests__/utils.test.ts +120 -0
- package/src/analyzer.ts +914 -0
- package/src/completionProvider.ts +328 -0
- package/src/constants.ts +35 -0
- package/src/definitionProvider.ts +371 -0
- package/src/diagnostics.ts +732 -0
- package/src/extension.ts +74 -0
- package/src/hoverProvider.ts +134 -0
- package/src/pathResolver.ts +171 -0
- package/src/positionAt.ts +509 -0
- package/src/scope.ts +116 -0
- package/src/utils.ts +56 -0
- package/syntaxes/aero-attributes.json +54 -0
- package/syntaxes/aero-expressions.json +26 -0
- package/syntaxes/aero-globals.json +22 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,964 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for AeroDiagnostics (diagnostics.ts): unused/undefined variable reporting,
|
|
3
|
+
* script tag validation (is:build, is:inline, type="module"), conditional chains
|
|
4
|
+
* (data-if/else-if/else), directive brace requirements, duplicate declarations, and component
|
|
5
|
+
* reference checks. Uses mocked vscode APIs and calls updateDiagnostics(doc) to assert reported diagnostics.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
9
|
+
|
|
10
|
+
const mockSet = vi.fn()
|
|
11
|
+
const mockCollection = {
|
|
12
|
+
set: mockSet,
|
|
13
|
+
delete: vi.fn(),
|
|
14
|
+
dispose: vi.fn(),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
vi.mock('vscode', () => {
|
|
18
|
+
return {
|
|
19
|
+
Range: class {
|
|
20
|
+
start: any
|
|
21
|
+
end: any
|
|
22
|
+
constructor(start: any, end: any) {
|
|
23
|
+
this.start = start
|
|
24
|
+
this.end = end
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
Position: class {
|
|
28
|
+
line: any
|
|
29
|
+
character: any
|
|
30
|
+
constructor(line: any, character: any) {
|
|
31
|
+
this.line = line
|
|
32
|
+
this.character = character
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
Diagnostic: class {
|
|
36
|
+
range: any
|
|
37
|
+
message: any
|
|
38
|
+
severity: any
|
|
39
|
+
tags: any[]
|
|
40
|
+
constructor(range: any, message: any, severity: any) {
|
|
41
|
+
this.range = range
|
|
42
|
+
this.message = message
|
|
43
|
+
this.severity = severity
|
|
44
|
+
this.tags = []
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
DiagnosticSeverity: { Error: 0, Warning: 1, Information: 2, Hint: 3 },
|
|
48
|
+
DiagnosticTag: { Unnecessary: 1 },
|
|
49
|
+
workspace: {
|
|
50
|
+
onDidOpenTextDocument: vi.fn(),
|
|
51
|
+
onDidSaveTextDocument: vi.fn(),
|
|
52
|
+
onDidChangeTextDocument: vi.fn(),
|
|
53
|
+
onDidCloseTextDocument: vi.fn(),
|
|
54
|
+
textDocuments: [],
|
|
55
|
+
getWorkspaceFolder: vi.fn(),
|
|
56
|
+
getConfiguration: () => ({ get: () => 'always' }),
|
|
57
|
+
},
|
|
58
|
+
languages: {
|
|
59
|
+
createDiagnosticCollection: () => mockCollection,
|
|
60
|
+
},
|
|
61
|
+
Uri: { parse: (s: string) => ({ toString: () => s, fsPath: s, scheme: 'file' }) },
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
import { AeroDiagnostics } from '../diagnostics'
|
|
66
|
+
|
|
67
|
+
/** Unused imports/vars: build scope vs bundled scope are separate; usage in template/Alpine/HTMX/getStaticPaths must count. */
|
|
68
|
+
describe('AeroDiagnostics Unused Variables', () => {
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
mockSet.mockClear()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should flag unused component import even if name exists in import path', () => {
|
|
74
|
+
const text = `
|
|
75
|
+
<script is:build>
|
|
76
|
+
import header from './header'
|
|
77
|
+
// header is NOT used in template
|
|
78
|
+
</script>
|
|
79
|
+
<div></div>
|
|
80
|
+
`
|
|
81
|
+
const doc = {
|
|
82
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
83
|
+
getText: () => text,
|
|
84
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
85
|
+
languageId: 'html',
|
|
86
|
+
fileName: '/test.html',
|
|
87
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
88
|
+
} as any
|
|
89
|
+
|
|
90
|
+
const context = { subscriptions: [] } as any
|
|
91
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
92
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
93
|
+
|
|
94
|
+
expect(mockSet).toHaveBeenCalled()
|
|
95
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
96
|
+
|
|
97
|
+
const unusedHeaderDiag = reportedDiagnostics.find((d: any) =>
|
|
98
|
+
d.message.includes("'header' is declared but its value is never read"),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
expect(unusedHeaderDiag).toBeDefined()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should NOT flag used component import', () => {
|
|
105
|
+
const text = `
|
|
106
|
+
<script is:build>
|
|
107
|
+
import header from './header'
|
|
108
|
+
</script>
|
|
109
|
+
<header-component />
|
|
110
|
+
`
|
|
111
|
+
const doc = {
|
|
112
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
113
|
+
getText: () => text,
|
|
114
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
115
|
+
languageId: 'html',
|
|
116
|
+
fileName: '/test.html',
|
|
117
|
+
} as any
|
|
118
|
+
|
|
119
|
+
const context = { subscriptions: [] } as any
|
|
120
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
121
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
122
|
+
|
|
123
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
124
|
+
const unusedHeaderDiag = reportedDiagnostics.find((d: any) =>
|
|
125
|
+
d.message.includes("'header' is declared but its value is never read"),
|
|
126
|
+
)
|
|
127
|
+
expect(unusedHeaderDiag).toBeUndefined()
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('should NOT flag imports from is:build as unused when a client script block is also present', () => {
|
|
131
|
+
const text = `
|
|
132
|
+
<script is:build>
|
|
133
|
+
import base from '@layouts/base'
|
|
134
|
+
import { render } from 'aero:content'
|
|
135
|
+
const doc = Aero.props
|
|
136
|
+
const { html } = await render(doc)
|
|
137
|
+
</script>
|
|
138
|
+
<base-layout title="{doc.data.title}">
|
|
139
|
+
<section>{html}</section>
|
|
140
|
+
</base-layout>
|
|
141
|
+
<script>
|
|
142
|
+
console.log('client side')
|
|
143
|
+
</script>
|
|
144
|
+
`
|
|
145
|
+
const doc = {
|
|
146
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
147
|
+
getText: () => text,
|
|
148
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
149
|
+
languageId: 'html',
|
|
150
|
+
fileName: '/test.html',
|
|
151
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
152
|
+
} as any
|
|
153
|
+
|
|
154
|
+
const context = { subscriptions: [] } as any
|
|
155
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
156
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
157
|
+
|
|
158
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
159
|
+
const unusedDiags = reportedDiagnostics.filter((d: any) =>
|
|
160
|
+
d.message.includes('is declared but its value is never read'),
|
|
161
|
+
)
|
|
162
|
+
// render is used inside is:build (render(doc)) — should NOT be flagged
|
|
163
|
+
// base is used as <base-layout> in template — should NOT be flagged
|
|
164
|
+
// doc/html are used in template expressions
|
|
165
|
+
expect(unusedDiags).toHaveLength(0)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('should flag is:build import as unused even if the same name is declared in client script', () => {
|
|
169
|
+
const text = `
|
|
170
|
+
<script is:build>
|
|
171
|
+
import base from '@layouts/base'
|
|
172
|
+
</script>
|
|
173
|
+
<div>no template usage of base</div>
|
|
174
|
+
<script>
|
|
175
|
+
const base = 'test'
|
|
176
|
+
console.log(base)
|
|
177
|
+
</script>
|
|
178
|
+
`
|
|
179
|
+
const doc = {
|
|
180
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
181
|
+
getText: () => text,
|
|
182
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
183
|
+
languageId: 'html',
|
|
184
|
+
fileName: '/test.html',
|
|
185
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
186
|
+
} as any
|
|
187
|
+
|
|
188
|
+
const context = { subscriptions: [] } as any
|
|
189
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
190
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
191
|
+
|
|
192
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
193
|
+
const unusedBaseDiag = reportedDiagnostics.find((d: any) =>
|
|
194
|
+
d.message.includes("'base' is declared but its value is never read"),
|
|
195
|
+
)
|
|
196
|
+
expect(unusedBaseDiag).toBeDefined()
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('should NOT flag variable as unused when used in Alpine x-data attribute', () => {
|
|
200
|
+
const text = `
|
|
201
|
+
<script is:build>
|
|
202
|
+
const dismiss = el => setTimeout(() => el.replaceChildren(), 3000)
|
|
203
|
+
</script>
|
|
204
|
+
<section x-data="{input: '', dismiss: dismiss}">
|
|
205
|
+
<span @click="dismiss($el)">Click me</span>
|
|
206
|
+
</section>
|
|
207
|
+
`
|
|
208
|
+
const doc = {
|
|
209
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
210
|
+
getText: () => text,
|
|
211
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
212
|
+
languageId: 'html',
|
|
213
|
+
fileName: '/test.html',
|
|
214
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
215
|
+
} as any
|
|
216
|
+
|
|
217
|
+
const context = { subscriptions: [] } as any
|
|
218
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
219
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
220
|
+
|
|
221
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
222
|
+
const unusedDismissDiag = reportedDiagnostics.find((d: any) =>
|
|
223
|
+
d.message.includes("'dismiss' is declared but its value is never read"),
|
|
224
|
+
)
|
|
225
|
+
expect(unusedDismissDiag).toBeUndefined()
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('should NOT flag variable as unused when used in HTMX event handler', () => {
|
|
229
|
+
const text = `
|
|
230
|
+
<script is:build>
|
|
231
|
+
const dismiss = el => setTimeout(() => el.replaceChildren(), 3000)
|
|
232
|
+
</script>
|
|
233
|
+
<span @htmx:after-swap="dismiss($el)"></span>
|
|
234
|
+
`
|
|
235
|
+
const doc = {
|
|
236
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
237
|
+
getText: () => text,
|
|
238
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
239
|
+
languageId: 'html',
|
|
240
|
+
fileName: '/test.html',
|
|
241
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
242
|
+
} as any
|
|
243
|
+
|
|
244
|
+
const context = { subscriptions: [] } as any
|
|
245
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
246
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
247
|
+
|
|
248
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
249
|
+
const unusedDismissDiag = reportedDiagnostics.find((d: any) =>
|
|
250
|
+
d.message.includes("'dismiss' is declared but its value is never read"),
|
|
251
|
+
)
|
|
252
|
+
expect(unusedDismissDiag).toBeUndefined()
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('should NOT flag getCollection when used in getStaticPaths', () => {
|
|
256
|
+
const text = `
|
|
257
|
+
<script is:build>
|
|
258
|
+
import { getCollection } from 'aero:content'
|
|
259
|
+
|
|
260
|
+
export async function getStaticPaths() {
|
|
261
|
+
const posts = await getCollection('posts')
|
|
262
|
+
return posts.map(post => ({ params: { slug: post.slug } }))
|
|
263
|
+
}
|
|
264
|
+
</script>
|
|
265
|
+
<div></div>
|
|
266
|
+
`
|
|
267
|
+
const doc = {
|
|
268
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
269
|
+
getText: () => text,
|
|
270
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
271
|
+
languageId: 'html',
|
|
272
|
+
fileName: '/test.html',
|
|
273
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
274
|
+
} as any
|
|
275
|
+
|
|
276
|
+
const context = { subscriptions: [] } as any
|
|
277
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
278
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
279
|
+
|
|
280
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
281
|
+
const unusedDiag = reportedDiagnostics.find((d: any) =>
|
|
282
|
+
d.message.includes("'getCollection' is declared but its value is never read"),
|
|
283
|
+
)
|
|
284
|
+
expect(unusedDiag).toBeUndefined()
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('should NOT flag render when used in getStaticPaths', () => {
|
|
288
|
+
const text = `
|
|
289
|
+
<script is:build>
|
|
290
|
+
import { getCollection, render } from 'aero:content'
|
|
291
|
+
|
|
292
|
+
export async function getStaticPaths() {
|
|
293
|
+
const posts = await getCollection('posts')
|
|
294
|
+
return posts.map(post => {
|
|
295
|
+
const content = render(post)
|
|
296
|
+
return { params: { slug: post.slug }, props: { content } }
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
</script>
|
|
300
|
+
<div></div>
|
|
301
|
+
`
|
|
302
|
+
const doc = {
|
|
303
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
304
|
+
getText: () => text,
|
|
305
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
306
|
+
languageId: 'html',
|
|
307
|
+
fileName: '/test.html',
|
|
308
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
309
|
+
} as any
|
|
310
|
+
|
|
311
|
+
const context = { subscriptions: [] } as any
|
|
312
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
313
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
314
|
+
|
|
315
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
316
|
+
const unusedRenderDiag = reportedDiagnostics.find((d: any) =>
|
|
317
|
+
d.message.includes("'render' is declared but its value is never read"),
|
|
318
|
+
)
|
|
319
|
+
const unusedCollectionDiag = reportedDiagnostics.find((d: any) =>
|
|
320
|
+
d.message.includes("'getCollection' is declared but its value is never read"),
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
expect(unusedRenderDiag).toBeUndefined()
|
|
324
|
+
expect(unusedCollectionDiag).toBeUndefined()
|
|
325
|
+
})
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
/** Undefined refs in template expressions; content globals (e.g. site) and Alpine x-data are excluded. */
|
|
329
|
+
describe('AeroDiagnostics Undefined Variables', () => {
|
|
330
|
+
beforeEach(() => {
|
|
331
|
+
mockSet.mockClear()
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('should flag undefined variable in template expression', () => {
|
|
335
|
+
const text = `
|
|
336
|
+
<div>{undefinedVar}</div>
|
|
337
|
+
`
|
|
338
|
+
const doc = {
|
|
339
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
340
|
+
getText: () => text,
|
|
341
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
342
|
+
languageId: 'html',
|
|
343
|
+
fileName: '/test.html',
|
|
344
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
345
|
+
} as any
|
|
346
|
+
|
|
347
|
+
const context = { subscriptions: [] } as any
|
|
348
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
349
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
350
|
+
|
|
351
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
352
|
+
const undefinedDiag = reportedDiagnostics.find((d: any) =>
|
|
353
|
+
d.message.includes("'undefinedVar' is not defined"),
|
|
354
|
+
)
|
|
355
|
+
expect(undefinedDiag).toBeDefined()
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
it('should NOT flag defined variable in template expression', () => {
|
|
359
|
+
const text = `
|
|
360
|
+
<script is:build>
|
|
361
|
+
const myVar = 'hello'
|
|
362
|
+
</script>
|
|
363
|
+
<div>{myVar}</div>
|
|
364
|
+
`
|
|
365
|
+
const doc = {
|
|
366
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
367
|
+
getText: () => text,
|
|
368
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
369
|
+
languageId: 'html',
|
|
370
|
+
fileName: '/test.html',
|
|
371
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
372
|
+
} as any
|
|
373
|
+
|
|
374
|
+
const context = { subscriptions: [] } as any
|
|
375
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
376
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
377
|
+
|
|
378
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
379
|
+
const undefinedDiag = reportedDiagnostics.find((d: any) =>
|
|
380
|
+
d.message.includes("'myVar' is not defined"),
|
|
381
|
+
)
|
|
382
|
+
expect(undefinedDiag).toBeUndefined()
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it('should NOT flag content globals as undefined', () => {
|
|
386
|
+
const text = `
|
|
387
|
+
<div>{site.title}</div>
|
|
388
|
+
`
|
|
389
|
+
const doc = {
|
|
390
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
391
|
+
getText: () => text,
|
|
392
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
393
|
+
languageId: 'html',
|
|
394
|
+
fileName: '/test.html',
|
|
395
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
396
|
+
} as any
|
|
397
|
+
|
|
398
|
+
const context = { subscriptions: [] } as any
|
|
399
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
400
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
401
|
+
|
|
402
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
403
|
+
const undefinedDiag = reportedDiagnostics.find((d: any) =>
|
|
404
|
+
d.message.includes("'site' is not defined"),
|
|
405
|
+
)
|
|
406
|
+
expect(undefinedDiag).toBeUndefined()
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
it('should NOT flag undefined variable in Alpine x-data', () => {
|
|
410
|
+
const text = `
|
|
411
|
+
<section x-data="{ input: '' }">
|
|
412
|
+
<input x-model="input" />
|
|
413
|
+
</section>
|
|
414
|
+
`
|
|
415
|
+
const doc = {
|
|
416
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
417
|
+
getText: () => text,
|
|
418
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
419
|
+
languageId: 'html',
|
|
420
|
+
fileName: '/test.html',
|
|
421
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
422
|
+
} as any
|
|
423
|
+
|
|
424
|
+
const context = { subscriptions: [] } as any
|
|
425
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
426
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
427
|
+
|
|
428
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
429
|
+
const undefinedDiag = reportedDiagnostics.find((d: any) =>
|
|
430
|
+
d.message.includes("'input' is not defined"),
|
|
431
|
+
)
|
|
432
|
+
expect(undefinedDiag).toBeUndefined()
|
|
433
|
+
})
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
/** Script tag rules: plain/is:build valid; is:inline import requires type="module"; comments ignored. */
|
|
437
|
+
describe('AeroDiagnostics Script Tags', () => {
|
|
438
|
+
beforeEach(() => {
|
|
439
|
+
mockSet.mockClear()
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
it('should NOT warn when plain script tag (no attribute) — bundled as module by default', () => {
|
|
443
|
+
const text = `
|
|
444
|
+
<script>
|
|
445
|
+
const foo = 'bar'
|
|
446
|
+
</script>
|
|
447
|
+
<div></div>
|
|
448
|
+
`
|
|
449
|
+
const doc = {
|
|
450
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
451
|
+
getText: () => text,
|
|
452
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
453
|
+
languageId: 'html',
|
|
454
|
+
fileName: '/test.html',
|
|
455
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
456
|
+
} as any
|
|
457
|
+
|
|
458
|
+
const context = { subscriptions: [] } as any
|
|
459
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
460
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
461
|
+
|
|
462
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
463
|
+
const scriptDiag = reportedDiagnostics.find((d: any) =>
|
|
464
|
+
d.message.includes('<script> without attribute'),
|
|
465
|
+
)
|
|
466
|
+
expect(scriptDiag).toBeUndefined()
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
it('should NOT warn when script has is:build', () => {
|
|
470
|
+
const text = `
|
|
471
|
+
<script is:build>
|
|
472
|
+
const foo = 'bar'
|
|
473
|
+
</script>
|
|
474
|
+
<div></div>
|
|
475
|
+
`
|
|
476
|
+
const doc = {
|
|
477
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
478
|
+
getText: () => text,
|
|
479
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
480
|
+
languageId: 'html',
|
|
481
|
+
fileName: '/test.html',
|
|
482
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
483
|
+
} as any
|
|
484
|
+
|
|
485
|
+
const context = { subscriptions: [] } as any
|
|
486
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
487
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
488
|
+
|
|
489
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
490
|
+
const scriptDiag = reportedDiagnostics.find((d: any) =>
|
|
491
|
+
d.message.includes('<script> without attribute'),
|
|
492
|
+
)
|
|
493
|
+
expect(scriptDiag).toBeUndefined()
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
it('should warn when is:inline script has import without type="module"', () => {
|
|
497
|
+
const text = `
|
|
498
|
+
<script is:inline>
|
|
499
|
+
import { foo } from 'bar'
|
|
500
|
+
console.log(foo)
|
|
501
|
+
</script>
|
|
502
|
+
<div></div>
|
|
503
|
+
`
|
|
504
|
+
const doc = {
|
|
505
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
506
|
+
getText: () => text,
|
|
507
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
508
|
+
languageId: 'html',
|
|
509
|
+
fileName: '/test.html',
|
|
510
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
511
|
+
} as any
|
|
512
|
+
|
|
513
|
+
const context = { subscriptions: [] } as any
|
|
514
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
515
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
516
|
+
|
|
517
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
518
|
+
const importDiag = reportedDiagnostics.find((d: any) =>
|
|
519
|
+
d.message.includes('Imports in <script is:inline> require type="module"'),
|
|
520
|
+
)
|
|
521
|
+
expect(importDiag).toBeDefined()
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
it('should NOT warn when is:inline script has import WITH type="module"', () => {
|
|
525
|
+
const text = `
|
|
526
|
+
<script is:inline type="module">
|
|
527
|
+
import { foo } from 'bar'
|
|
528
|
+
console.log(foo)
|
|
529
|
+
</script>
|
|
530
|
+
<div></div>
|
|
531
|
+
`
|
|
532
|
+
const doc = {
|
|
533
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
534
|
+
getText: () => text,
|
|
535
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
536
|
+
languageId: 'html',
|
|
537
|
+
fileName: '/test.html',
|
|
538
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
539
|
+
} as any
|
|
540
|
+
|
|
541
|
+
const context = { subscriptions: [] } as any
|
|
542
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
543
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
544
|
+
|
|
545
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
546
|
+
const importDiag = reportedDiagnostics.find((d: any) =>
|
|
547
|
+
d.message.includes('Imports in <script is:inline>'),
|
|
548
|
+
)
|
|
549
|
+
expect(importDiag).toBeUndefined()
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
it('should NOT warn when plain script has import — bundled as module by default', () => {
|
|
553
|
+
const text = `
|
|
554
|
+
<script>
|
|
555
|
+
import { foo } from 'bar'
|
|
556
|
+
console.log(foo)
|
|
557
|
+
</script>
|
|
558
|
+
<div></div>
|
|
559
|
+
`
|
|
560
|
+
const doc = {
|
|
561
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
562
|
+
getText: () => text,
|
|
563
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
564
|
+
languageId: 'html',
|
|
565
|
+
fileName: '/test.html',
|
|
566
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
567
|
+
} as any
|
|
568
|
+
|
|
569
|
+
const context = { subscriptions: [] } as any
|
|
570
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
571
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
572
|
+
|
|
573
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
574
|
+
const importDiag = reportedDiagnostics.find((d: any) =>
|
|
575
|
+
d.message.includes('Imports in bundled scripts'),
|
|
576
|
+
)
|
|
577
|
+
expect(importDiag).toBeUndefined()
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
it('should NOT warn when pass:data script has import (Vite handles bundling)', () => {
|
|
581
|
+
const text = `
|
|
582
|
+
<script pass:data="{ foo }">
|
|
583
|
+
import { bar } from 'baz'
|
|
584
|
+
console.log(bar)
|
|
585
|
+
</script>
|
|
586
|
+
<div></div>
|
|
587
|
+
`
|
|
588
|
+
const doc = {
|
|
589
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
590
|
+
getText: () => text,
|
|
591
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
592
|
+
languageId: 'html',
|
|
593
|
+
fileName: '/test.html',
|
|
594
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
595
|
+
} as any
|
|
596
|
+
|
|
597
|
+
const context = { subscriptions: [] } as any
|
|
598
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
599
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
600
|
+
|
|
601
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
602
|
+
const importDiag = reportedDiagnostics.find((d: any) =>
|
|
603
|
+
d.message.includes('Imports in bundled scripts'),
|
|
604
|
+
)
|
|
605
|
+
expect(importDiag).toBeUndefined()
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
it('should NOT warn when bundled script has import WITH type="module"', () => {
|
|
609
|
+
const text = `
|
|
610
|
+
<script type="module">
|
|
611
|
+
import { foo } from 'bar'
|
|
612
|
+
console.log(foo)
|
|
613
|
+
</script>
|
|
614
|
+
<div></div>
|
|
615
|
+
`
|
|
616
|
+
const doc = {
|
|
617
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
618
|
+
getText: () => text,
|
|
619
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
620
|
+
languageId: 'html',
|
|
621
|
+
fileName: '/test.html',
|
|
622
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
623
|
+
} as any
|
|
624
|
+
|
|
625
|
+
const context = { subscriptions: [] } as any
|
|
626
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
627
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
628
|
+
|
|
629
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
630
|
+
const importDiag = reportedDiagnostics.find((d: any) =>
|
|
631
|
+
d.message.includes('Imports in bundled scripts'),
|
|
632
|
+
)
|
|
633
|
+
expect(importDiag).toBeUndefined()
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
it('should NOT flag scripts inside HTML comments', () => {
|
|
637
|
+
const text = `
|
|
638
|
+
<!--<script>
|
|
639
|
+
import { foo } from 'bar'
|
|
640
|
+
</script>-->
|
|
641
|
+
<div></div>
|
|
642
|
+
`
|
|
643
|
+
const doc = {
|
|
644
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
645
|
+
getText: () => text,
|
|
646
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
647
|
+
languageId: 'html',
|
|
648
|
+
fileName: '/test.html',
|
|
649
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
650
|
+
} as any
|
|
651
|
+
|
|
652
|
+
const context = { subscriptions: [] } as any
|
|
653
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
654
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
655
|
+
|
|
656
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
657
|
+
// Should not flag duplicate imports from commented script
|
|
658
|
+
const dupDiag = reportedDiagnostics.find((d: any) =>
|
|
659
|
+
d.message.includes('declared multiple times'),
|
|
660
|
+
)
|
|
661
|
+
expect(dupDiag).toBeUndefined()
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
it('should NOT flag duplicate when commented script has same import as real script', () => {
|
|
665
|
+
const text = `
|
|
666
|
+
<script is:build>
|
|
667
|
+
import { allCaps } from '@scripts/utils'
|
|
668
|
+
</script>
|
|
669
|
+
<!--<script>
|
|
670
|
+
import { allCaps } from '@scripts/utils'
|
|
671
|
+
</script>-->
|
|
672
|
+
<div></div>
|
|
673
|
+
`
|
|
674
|
+
const doc = {
|
|
675
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
676
|
+
getText: () => text,
|
|
677
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
678
|
+
languageId: 'html',
|
|
679
|
+
fileName: '/test.html',
|
|
680
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
681
|
+
} as any
|
|
682
|
+
|
|
683
|
+
const context = { subscriptions: [] } as any
|
|
684
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
685
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
686
|
+
|
|
687
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
688
|
+
const dupDiag = reportedDiagnostics.find((d: any) =>
|
|
689
|
+
d.message.includes('declared multiple times'),
|
|
690
|
+
)
|
|
691
|
+
expect(dupDiag).toBeUndefined()
|
|
692
|
+
})
|
|
693
|
+
|
|
694
|
+
it('should warn when is:inline has import in multi-script file structure', () => {
|
|
695
|
+
const text = `
|
|
696
|
+
<script is:build>
|
|
697
|
+
import base from '@layouts/base'
|
|
698
|
+
import header from '@components/header'
|
|
699
|
+
</script>
|
|
700
|
+
<base-layout>
|
|
701
|
+
<header-component />
|
|
702
|
+
</base-layout>
|
|
703
|
+
<script is:inline>
|
|
704
|
+
console.log('first inline')
|
|
705
|
+
</script>
|
|
706
|
+
<script is:inline>
|
|
707
|
+
import { allCaps } from '@scripts/utils'
|
|
708
|
+
console.log(allCaps('test'))
|
|
709
|
+
</script>
|
|
710
|
+
`
|
|
711
|
+
const doc = {
|
|
712
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
713
|
+
getText: () => text,
|
|
714
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
715
|
+
languageId: 'html',
|
|
716
|
+
fileName: '/test.html',
|
|
717
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
718
|
+
} as any
|
|
719
|
+
|
|
720
|
+
const context = { subscriptions: [] } as any
|
|
721
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
722
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
723
|
+
|
|
724
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
725
|
+
const importDiag = reportedDiagnostics.find((d: any) =>
|
|
726
|
+
d.message.includes('Imports in <script is:inline>'),
|
|
727
|
+
)
|
|
728
|
+
expect(importDiag).toBeDefined()
|
|
729
|
+
})
|
|
730
|
+
})
|
|
731
|
+
|
|
732
|
+
/** data-else-if and data-else must follow an element with data-if or data-else-if. */
|
|
733
|
+
describe('AeroDiagnostics Conditional Chains', () => {
|
|
734
|
+
beforeEach(() => {
|
|
735
|
+
mockSet.mockClear()
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
it('should flag orphaned else-if without preceding if', () => {
|
|
739
|
+
const text = `
|
|
740
|
+
<div>Before</div>
|
|
741
|
+
<div data-else-if="{condition}">Else If</div>
|
|
742
|
+
`
|
|
743
|
+
const doc = {
|
|
744
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
745
|
+
getText: () => text,
|
|
746
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
747
|
+
languageId: 'html',
|
|
748
|
+
fileName: '/test.html',
|
|
749
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
750
|
+
} as any
|
|
751
|
+
|
|
752
|
+
const context = { subscriptions: [] } as any
|
|
753
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
754
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
755
|
+
|
|
756
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
757
|
+
const elseIfDiag = reportedDiagnostics.find((d: any) =>
|
|
758
|
+
d.message.includes('else-if must follow an element with if or else-if'),
|
|
759
|
+
)
|
|
760
|
+
expect(elseIfDiag).toBeDefined()
|
|
761
|
+
})
|
|
762
|
+
|
|
763
|
+
it('should flag orphaned else without preceding if', () => {
|
|
764
|
+
const text = `
|
|
765
|
+
<div>Before</div>
|
|
766
|
+
<div data-else>Else</div>
|
|
767
|
+
`
|
|
768
|
+
const doc = {
|
|
769
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
770
|
+
getText: () => text,
|
|
771
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
772
|
+
languageId: 'html',
|
|
773
|
+
fileName: '/test.html',
|
|
774
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
775
|
+
} as any
|
|
776
|
+
|
|
777
|
+
const context = { subscriptions: [] } as any
|
|
778
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
779
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
780
|
+
|
|
781
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
782
|
+
const elseDiag = reportedDiagnostics.find((d: any) =>
|
|
783
|
+
d.message.includes('else must follow an element with if or else-if'),
|
|
784
|
+
)
|
|
785
|
+
expect(elseDiag).toBeDefined()
|
|
786
|
+
})
|
|
787
|
+
|
|
788
|
+
it('should NOT flag valid if-else-if-else chain', () => {
|
|
789
|
+
const text = `
|
|
790
|
+
<div data-if="{a}">A</div>
|
|
791
|
+
<div data-else-if="{b}">B</div>
|
|
792
|
+
<div data-else>C</div>
|
|
793
|
+
`
|
|
794
|
+
const doc = {
|
|
795
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
796
|
+
getText: () => text,
|
|
797
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
798
|
+
languageId: 'html',
|
|
799
|
+
fileName: '/test.html',
|
|
800
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
801
|
+
} as any
|
|
802
|
+
|
|
803
|
+
const context = { subscriptions: [] } as any
|
|
804
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
805
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
806
|
+
|
|
807
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
808
|
+
const conditionalDiag = reportedDiagnostics.find((d: any) =>
|
|
809
|
+
d.message.includes('must follow an element with if or else-if'),
|
|
810
|
+
)
|
|
811
|
+
expect(conditionalDiag).toBeUndefined()
|
|
812
|
+
})
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
/** Directives (data-if, data-each, etc.) must use braced expressions (e.g. data-if="{ cond }"). */
|
|
816
|
+
describe('AeroDiagnostics Directive Expression Braces', () => {
|
|
817
|
+
beforeEach(() => {
|
|
818
|
+
mockSet.mockClear()
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
it('should flag directive without braced expression', () => {
|
|
822
|
+
const text = `
|
|
823
|
+
<div data-if="condition">Content</div>
|
|
824
|
+
`
|
|
825
|
+
const doc = {
|
|
826
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
827
|
+
getText: () => text,
|
|
828
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
829
|
+
languageId: 'html',
|
|
830
|
+
fileName: '/test.html',
|
|
831
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
832
|
+
} as any
|
|
833
|
+
|
|
834
|
+
const context = { subscriptions: [] } as any
|
|
835
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
836
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
837
|
+
|
|
838
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
839
|
+
const directiveDiag = reportedDiagnostics.find((d: any) =>
|
|
840
|
+
d.message.includes('must use a braced expression'),
|
|
841
|
+
)
|
|
842
|
+
expect(directiveDiag).toBeDefined()
|
|
843
|
+
})
|
|
844
|
+
|
|
845
|
+
it('should NOT flag directive with braced expression', () => {
|
|
846
|
+
const text = `
|
|
847
|
+
<div data-if="{condition}">Content</div>
|
|
848
|
+
`
|
|
849
|
+
const doc = {
|
|
850
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
851
|
+
getText: () => text,
|
|
852
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
853
|
+
languageId: 'html',
|
|
854
|
+
fileName: '/test.html',
|
|
855
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
856
|
+
} as any
|
|
857
|
+
|
|
858
|
+
const context = { subscriptions: [] } as any
|
|
859
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
860
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
861
|
+
|
|
862
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
863
|
+
const directiveDiag = reportedDiagnostics.find((d: any) =>
|
|
864
|
+
d.message.includes('must use a braced expression'),
|
|
865
|
+
)
|
|
866
|
+
expect(directiveDiag).toBeUndefined()
|
|
867
|
+
})
|
|
868
|
+
})
|
|
869
|
+
|
|
870
|
+
/** Same name declared in same scope (e.g. import + const) → "declared multiple times". */
|
|
871
|
+
describe('AeroDiagnostics Duplicate Declarations', () => {
|
|
872
|
+
beforeEach(() => {
|
|
873
|
+
mockSet.mockClear()
|
|
874
|
+
})
|
|
875
|
+
|
|
876
|
+
it('should flag import conflicting with local declaration', () => {
|
|
877
|
+
const text = `
|
|
878
|
+
<script is:build>
|
|
879
|
+
import header from '@components/header'
|
|
880
|
+
const header = { title: 'Test' }
|
|
881
|
+
</script>
|
|
882
|
+
<header-component />
|
|
883
|
+
`
|
|
884
|
+
const doc = {
|
|
885
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
886
|
+
getText: () => text,
|
|
887
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
888
|
+
languageId: 'html',
|
|
889
|
+
fileName: '/test.html',
|
|
890
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
891
|
+
} as any
|
|
892
|
+
|
|
893
|
+
const context = { subscriptions: [] } as any
|
|
894
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
895
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
896
|
+
|
|
897
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
898
|
+
const dupDiag = reportedDiagnostics.find((d: any) =>
|
|
899
|
+
d.message.includes('declared multiple times'),
|
|
900
|
+
)
|
|
901
|
+
expect(dupDiag).toBeDefined()
|
|
902
|
+
})
|
|
903
|
+
|
|
904
|
+
it('should NOT flag when no duplicate', () => {
|
|
905
|
+
const text = `
|
|
906
|
+
<script is:build>
|
|
907
|
+
import header from '@components/header'
|
|
908
|
+
const props = { title: 'Test' }
|
|
909
|
+
</script>
|
|
910
|
+
<header-component />
|
|
911
|
+
`
|
|
912
|
+
const doc = {
|
|
913
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
914
|
+
getText: () => text,
|
|
915
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
916
|
+
languageId: 'html',
|
|
917
|
+
fileName: '/test.html',
|
|
918
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
919
|
+
} as any
|
|
920
|
+
|
|
921
|
+
const context = { subscriptions: [] } as any
|
|
922
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
923
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
924
|
+
|
|
925
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1]
|
|
926
|
+
const dupDiag = reportedDiagnostics.find((d: any) =>
|
|
927
|
+
d.message.includes('declared multiple times'),
|
|
928
|
+
)
|
|
929
|
+
expect(dupDiag).toBeUndefined()
|
|
930
|
+
})
|
|
931
|
+
})
|
|
932
|
+
|
|
933
|
+
/** Component usage in template must have matching import; strings inside client scripts are ignored. */
|
|
934
|
+
describe('AeroDiagnostics Component References', () => {
|
|
935
|
+
beforeEach(() => {
|
|
936
|
+
mockSet.mockClear()
|
|
937
|
+
})
|
|
938
|
+
|
|
939
|
+
it('should ignore components inside client scripts', () => {
|
|
940
|
+
const text = `
|
|
941
|
+
<script>
|
|
942
|
+
const tag = "<header-component>"
|
|
943
|
+
</script>
|
|
944
|
+
`
|
|
945
|
+
const doc = {
|
|
946
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html', scheme: 'file' },
|
|
947
|
+
getText: () => text,
|
|
948
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
949
|
+
languageId: 'html',
|
|
950
|
+
fileName: '/test.html',
|
|
951
|
+
lineAt: (line: number) => ({ text: text.split('\n')[line] }),
|
|
952
|
+
} as any
|
|
953
|
+
|
|
954
|
+
const context = { subscriptions: [] } as any
|
|
955
|
+
const diagnostics = new AeroDiagnostics(context)
|
|
956
|
+
;(diagnostics as any).updateDiagnostics(doc)
|
|
957
|
+
|
|
958
|
+
const reportedDiagnostics = mockSet.mock.calls[0][1] || []
|
|
959
|
+
const componentDiag = reportedDiagnostics.find((d: any) =>
|
|
960
|
+
d.message.includes('is not imported'),
|
|
961
|
+
)
|
|
962
|
+
expect(componentDiag).toBeUndefined()
|
|
963
|
+
})
|
|
964
|
+
})
|