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