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