1import { expect, afterAll, beforeAll, it, describe } from 'vitest';
2import { TSServer } from './server';
3import path from 'node:path';
4import fs from 'node:fs';
5import url from 'node:url';
6import ts from 'typescript/lib/tsserverlibrary';
7
8const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
9
10const projectPath = path.resolve(__dirname, 'fixture-project-unused-fields');
11describe('unused fields', () => {
12 const outfileDestructuringFromStart = path.join(
13 projectPath,
14 'immediate-destructuring.tsx'
15 );
16 const outfileDestructuring = path.join(projectPath, 'destructuring.tsx');
17 const outfileFragmentDestructuring = path.join(
18 projectPath,
19 'fragment-destructuring.tsx'
20 );
21 const outfileFragment = path.join(projectPath, 'fragment.tsx');
22 const outfilePropAccess = path.join(projectPath, 'property-access.tsx');
23
24 let server: TSServer;
25 beforeAll(async () => {
26 server = new TSServer(projectPath, { debugLog: false });
27
28 server.sendCommand('open', {
29 file: outfileDestructuring,
30 fileContent: '// empty',
31 scriptKindName: 'TS',
32 } satisfies ts.server.protocol.OpenRequestArgs);
33 server.sendCommand('open', {
34 file: outfileFragment,
35 fileContent: '// empty',
36 scriptKindName: 'TS',
37 } satisfies ts.server.protocol.OpenRequestArgs);
38 server.sendCommand('open', {
39 file: outfilePropAccess,
40 fileContent: '// empty',
41 scriptKindName: 'TS',
42 } satisfies ts.server.protocol.OpenRequestArgs);
43 server.sendCommand('open', {
44 file: outfileFragmentDestructuring,
45 fileContent: '// empty',
46 scriptKindName: 'TS',
47 } satisfies ts.server.protocol.OpenRequestArgs);
48 server.sendCommand('open', {
49 file: outfileDestructuringFromStart,
50 fileContent: '// empty',
51 scriptKindName: 'TS',
52 } satisfies ts.server.protocol.OpenRequestArgs);
53
54 server.sendCommand('updateOpen', {
55 openFiles: [
56 {
57 file: outfileDestructuring,
58 fileContent: fs.readFileSync(
59 path.join(projectPath, 'fixtures/destructuring.tsx'),
60 'utf-8'
61 ),
62 },
63 {
64 file: outfileFragment,
65 fileContent: fs.readFileSync(
66 path.join(projectPath, 'fixtures/fragment.tsx'),
67 'utf-8'
68 ),
69 },
70 {
71 file: outfilePropAccess,
72 fileContent: fs.readFileSync(
73 path.join(projectPath, 'fixtures/property-access.tsx'),
74 'utf-8'
75 ),
76 },
77 {
78 file: outfileDestructuringFromStart,
79 fileContent: fs.readFileSync(
80 path.join(projectPath, 'fixtures/immediate-destructuring.tsx'),
81 'utf-8'
82 ),
83 },
84 {
85 file: outfileFragmentDestructuring,
86 fileContent: fs.readFileSync(
87 path.join(projectPath, 'fixtures/fragment-destructuring.tsx'),
88 'utf-8'
89 ),
90 },
91 ],
92 } satisfies ts.server.protocol.UpdateOpenRequestArgs);
93
94 server.sendCommand('saveto', {
95 file: outfileDestructuring,
96 tmpfile: outfileDestructuring,
97 } satisfies ts.server.protocol.SavetoRequestArgs);
98 server.sendCommand('saveto', {
99 file: outfileFragment,
100 tmpfile: outfileFragment,
101 } satisfies ts.server.protocol.SavetoRequestArgs);
102 server.sendCommand('saveto', {
103 file: outfilePropAccess,
104 tmpfile: outfilePropAccess,
105 } satisfies ts.server.protocol.SavetoRequestArgs);
106 server.sendCommand('saveto', {
107 file: outfileFragmentDestructuring,
108 tmpfile: outfileFragmentDestructuring,
109 } satisfies ts.server.protocol.SavetoRequestArgs);
110 server.sendCommand('saveto', {
111 file: outfileDestructuringFromStart,
112 tmpfile: outfileDestructuringFromStart,
113 } satisfies ts.server.protocol.SavetoRequestArgs);
114 });
115
116 afterAll(() => {
117 try {
118 fs.unlinkSync(outfileDestructuring);
119 fs.unlinkSync(outfileFragment);
120 fs.unlinkSync(outfilePropAccess);
121 fs.unlinkSync(outfileFragmentDestructuring);
122 fs.unlinkSync(outfileDestructuringFromStart);
123 } catch {}
124 });
125
126 it('gives unused fields with fragments', async () => {
127 await server.waitForResponse(
128 e =>
129 e.type === 'event' &&
130 e.event === 'semanticDiag' &&
131 e.body?.file === outfileFragment
132 );
133 const res = server.responses.filter(
134 resp =>
135 resp.type === 'event' &&
136 resp.event === 'semanticDiag' &&
137 resp.body?.file === outfileFragment
138 );
139 expect(res[0].body.diagnostics).toMatchInlineSnapshot(`
140 [
141 {
142 "category": "warning",
143 "code": 52005,
144 "end": {
145 "line": 10,
146 "offset": 15,
147 },
148 "start": {
149 "line": 10,
150 "offset": 9,
151 },
152 "text": "Field 'attacks.fast.damage' is not used.",
153 },
154 {
155 "category": "warning",
156 "code": 52005,
157 "end": {
158 "line": 11,
159 "offset": 13,
160 },
161 "start": {
162 "line": 11,
163 "offset": 9,
164 },
165 "text": "Field 'attacks.fast.name' is not used.",
166 },
167 ]
168 `);
169 }, 30000);
170
171 it('gives unused fields with fragments destructuring', async () => {
172 await server.waitForResponse(
173 e =>
174 e.type === 'event' &&
175 e.event === 'semanticDiag' &&
176 e.body?.file === outfileFragmentDestructuring
177 );
178 const res = server.responses.filter(
179 resp =>
180 resp.type === 'event' &&
181 resp.event === 'semanticDiag' &&
182 resp.body?.file === outfileFragmentDestructuring
183 );
184 expect(res[0].body.diagnostics).toMatchInlineSnapshot(`
185 [
186 {
187 "category": "warning",
188 "code": 52005,
189 "end": {
190 "line": 10,
191 "offset": 15,
192 },
193 "start": {
194 "line": 10,
195 "offset": 9,
196 },
197 "text": "Field 'attacks.fast.damage' is not used.",
198 },
199 {
200 "category": "warning",
201 "code": 52005,
202 "end": {
203 "line": 11,
204 "offset": 13,
205 },
206 "start": {
207 "line": 11,
208 "offset": 9,
209 },
210 "text": "Field 'attacks.fast.name' is not used.",
211 },
212 ]
213 `);
214 }, 30000);
215
216 it('gives semantc diagnostics with property access', async () => {
217 await server.waitForResponse(
218 e =>
219 e.type === 'event' &&
220 e.event === 'semanticDiag' &&
221 e.body?.file === outfilePropAccess
222 );
223 const res = server.responses.filter(
224 resp =>
225 resp.type === 'event' &&
226 resp.event === 'semanticDiag' &&
227 resp.body?.file === outfilePropAccess
228 );
229 expect(res[0].body.diagnostics).toMatchInlineSnapshot(`
230 [
231 {
232 "category": "warning",
233 "code": 52005,
234 "end": {
235 "line": 11,
236 "offset": 15,
237 },
238 "start": {
239 "line": 11,
240 "offset": 7,
241 },
242 "text": "Field 'pokemon.fleeRate' is not used.",
243 },
244 {
245 "category": "warning",
246 "code": 52005,
247 "end": {
248 "line": 16,
249 "offset": 17,
250 },
251 "start": {
252 "line": 16,
253 "offset": 11,
254 },
255 "text": "Field 'pokemon.attacks.special.damage' is not used.",
256 },
257 {
258 "category": "warning",
259 "code": 52005,
260 "end": {
261 "line": 20,
262 "offset": 16,
263 },
264 "start": {
265 "line": 20,
266 "offset": 9,
267 },
268 "text": "Field 'pokemon.weight.minimum' is not used.",
269 },
270 {
271 "category": "warning",
272 "code": 52005,
273 "end": {
274 "line": 21,
275 "offset": 16,
276 },
277 "start": {
278 "line": 21,
279 "offset": 9,
280 },
281 "text": "Field 'pokemon.weight.maximum' is not used.",
282 },
283 {
284 "category": "error",
285 "code": 2578,
286 "end": {
287 "line": 3,
288 "offset": 20,
289 },
290 "start": {
291 "line": 3,
292 "offset": 1,
293 },
294 "text": "Unused '@ts-expect-error' directive.",
295 },
296 ]
297 `);
298 }, 30000);
299
300 it('gives unused fields with destructuring', async () => {
301 const res = server.responses.filter(
302 resp =>
303 resp.type === 'event' &&
304 resp.event === 'semanticDiag' &&
305 resp.body?.file === outfileDestructuring
306 );
307 expect(res[0].body.diagnostics).toMatchInlineSnapshot(`
308 [
309 {
310 "category": "warning",
311 "code": 52005,
312 "end": {
313 "line": 15,
314 "offset": 15,
315 },
316 "start": {
317 "line": 15,
318 "offset": 11,
319 },
320 "text": "Field 'pokemon.attacks.special.name' is not used.",
321 },
322 {
323 "category": "warning",
324 "code": 52005,
325 "end": {
326 "line": 16,
327 "offset": 17,
328 },
329 "start": {
330 "line": 16,
331 "offset": 11,
332 },
333 "text": "Field 'pokemon.attacks.special.damage' is not used.",
334 },
335 {
336 "category": "warning",
337 "code": 52005,
338 "end": {
339 "line": 23,
340 "offset": 11,
341 },
342 "start": {
343 "line": 23,
344 "offset": 7,
345 },
346 "text": "Field 'pokemon.name' is not used.",
347 },
348 {
349 "category": "error",
350 "code": 2578,
351 "end": {
352 "line": 3,
353 "offset": 20,
354 },
355 "start": {
356 "line": 3,
357 "offset": 1,
358 },
359 "text": "Unused '@ts-expect-error' directive.",
360 },
361 ]
362 `);
363 }, 30000);
364
365 it('gives unused fields with immedaite destructuring', async () => {
366 const res = server.responses.filter(
367 resp =>
368 resp.type === 'event' &&
369 resp.event === 'semanticDiag' &&
370 resp.body?.file === outfileDestructuringFromStart
371 );
372 expect(res[0].body.diagnostics).toMatchInlineSnapshot(`
373 [
374 {
375 "category": "warning",
376 "code": 52005,
377 "end": {
378 "line": 15,
379 "offset": 15,
380 },
381 "start": {
382 "line": 15,
383 "offset": 11,
384 },
385 "text": "Field 'pokemon.attacks.special.name' is not used.",
386 },
387 {
388 "category": "warning",
389 "code": 52005,
390 "end": {
391 "line": 16,
392 "offset": 17,
393 },
394 "start": {
395 "line": 16,
396 "offset": 11,
397 },
398 "text": "Field 'pokemon.attacks.special.damage' is not used.",
399 },
400 {
401 "category": "warning",
402 "code": 52005,
403 "end": {
404 "line": 23,
405 "offset": 11,
406 },
407 "start": {
408 "line": 23,
409 "offset": 7,
410 },
411 "text": "Field 'pokemon.name' is not used.",
412 },
413 {
414 "category": "error",
415 "code": 2578,
416 "end": {
417 "line": 3,
418 "offset": 20,
419 },
420 "start": {
421 "line": 3,
422 "offset": 1,
423 },
424 "text": "Unused '@ts-expect-error' directive.",
425 },
426 ]
427 `);
428 }, 30000);
429});