Skip to content

Commit 99a97a6

Browse files
Dispose language services in tests to prevent resource leaks (#660)
1 parent 2699a80 commit 99a97a6

10 files changed

Lines changed: 535 additions & 461 deletions
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@effect/language-service": patch
3+
---
4+
5+
Dispose TypeScript language services in tests to prevent resource leaks
6+
7+
Added `languageService.dispose()` calls via `try/finally` patterns to all test files that create language services through `createServicesWithMockedVFS()`. This ensures proper cleanup of TypeScript compiler resources after each test completes, preventing memory leaks during test runs.

packages/language-service/test/completions.test.ts

Lines changed: 45 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -24,56 +24,60 @@ function testCompletionOnExample(
2424
sourceText: string,
2525
textRangeString: string
2626
) {
27-
const { program, sourceFile } = createServicesWithMockedVFS(
27+
const { languageService, program, sourceFile } = createServicesWithMockedVFS(
2828
getHarnessDir(),
2929
getExamplesDir(),
3030
fileName,
3131
sourceText
3232
)
3333

34-
// gets the position to test
35-
let startPos = 0
36-
for (const lineAndCol of textRangeString.split("-")) {
37-
const [line, character] = lineAndCol.split(":")
38-
startPos = ts.getPositionOfLineAndCharacter(sourceFile, +line! - 1, +character! - 1)
39-
}
34+
try {
35+
// gets the position to test
36+
let startPos = 0
37+
for (const lineAndCol of textRangeString.split("-")) {
38+
const [line, character] = lineAndCol.split(":")
39+
startPos = ts.getPositionOfLineAndCharacter(sourceFile, +line! - 1, +character! - 1)
40+
}
4041

41-
// check and assert the completions is executable
42-
const maybeEntries = pipe(
43-
LSP.getCompletionsAtPosition(
44-
[completion],
45-
sourceFile,
46-
startPos,
47-
undefined,
48-
ts.getDefaultFormatCodeSettings("\n")
49-
),
50-
TypeParser.nanoLayer,
51-
TypeCheckerUtils.nanoLayer,
52-
TypeScriptUtils.nanoLayer,
53-
Nano.provideService(TypeCheckerApi.TypeCheckerApi, program.getTypeChecker()),
54-
Nano.provideService(TypeScriptApi.TypeScriptProgram, program),
55-
Nano.provideService(TypeScriptApi.TypeScriptApi, ts),
56-
Nano.provideService(
57-
LanguageServicePluginOptions.LanguageServicePluginOptions,
58-
LanguageServicePluginOptions.parse({
59-
...LanguageServicePluginOptions.defaults,
60-
completions: true,
61-
refactors: false,
62-
diagnostics: false,
63-
quickinfo: false,
64-
goto: false,
65-
...configFromSourceComment(sourceText)
66-
})
67-
),
68-
Nano.unsafeRun
69-
)
42+
// check and assert the completions is executable
43+
const maybeEntries = pipe(
44+
LSP.getCompletionsAtPosition(
45+
[completion],
46+
sourceFile,
47+
startPos,
48+
undefined,
49+
ts.getDefaultFormatCodeSettings("\n")
50+
),
51+
TypeParser.nanoLayer,
52+
TypeCheckerUtils.nanoLayer,
53+
TypeScriptUtils.nanoLayer,
54+
Nano.provideService(TypeCheckerApi.TypeCheckerApi, program.getTypeChecker()),
55+
Nano.provideService(TypeScriptApi.TypeScriptProgram, program),
56+
Nano.provideService(TypeScriptApi.TypeScriptApi, ts),
57+
Nano.provideService(
58+
LanguageServicePluginOptions.LanguageServicePluginOptions,
59+
LanguageServicePluginOptions.parse({
60+
...LanguageServicePluginOptions.defaults,
61+
completions: true,
62+
refactors: false,
63+
diagnostics: false,
64+
quickinfo: false,
65+
goto: false,
66+
...configFromSourceComment(sourceText)
67+
})
68+
),
69+
Nano.unsafeRun
70+
)
7071

71-
if (Result.isFailure(maybeEntries)) {
72-
expect(sourceText).toMatchSnapshot()
73-
return
74-
}
72+
if (Result.isFailure(maybeEntries)) {
73+
expect(sourceText).toMatchSnapshot()
74+
return
75+
}
7576

76-
expect(maybeEntries.success).toMatchSnapshot()
77+
expect(maybeEntries.success).toMatchSnapshot()
78+
} finally {
79+
languageService.dispose()
80+
}
7781
}
7882

7983
function testAllCompletions() {

packages/language-service/test/diagnostics.test.ts

Lines changed: 87 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -52,63 +52,72 @@ function testDiagnosticOnExample(
5252
sourceText: string
5353
) {
5454
// create the language service with mocked services over a VFS
55-
const { program, sourceFile } = createServicesWithMockedVFS(getHarnessDir(), getExamplesDir(), fileName, sourceText)
56-
57-
// create snapshot path
58-
const snapshotFilePath = path.join(
59-
getSnapshotsSubdir("diagnostics"),
60-
fileName + ".output"
55+
const { languageService, program, sourceFile } = createServicesWithMockedVFS(
56+
getHarnessDir(),
57+
getExamplesDir(),
58+
fileName,
59+
sourceText
6160
)
6261

63-
if (getHarnessVersion() === "v4") {
64-
// expect valid initial code
65-
const typeDiags = program.getSemanticDiagnostics().filter((_) => _.source === sourceFile.fileName)
66-
const syntaxDiags = program.getSyntacticDiagnostics().filter((_) => _.source === sourceFile.fileName)
67-
const tsDiagsText = [...syntaxDiags, ...typeDiags].map((diag) =>
68-
diagnosticToLogFormat(sourceFile, sourceText, diag)
69-
).join("\n\n")
70-
expect(tsDiagsText).toBe("")
71-
}
62+
try {
63+
// create snapshot path
64+
const snapshotFilePath = path.join(
65+
getSnapshotsSubdir("diagnostics"),
66+
fileName + ".output"
67+
)
7268

73-
// attempt to run the diagnostic and get the output
74-
return pipe(
75-
LSP.getSemanticDiagnosticsWithCodeFixes([diagnostic], sourceFile),
76-
TypeParser.nanoLayer,
77-
TypeCheckerUtils.nanoLayer,
78-
TypeScriptUtils.nanoLayer,
79-
Nano.provideService(TypeCheckerApi.TypeCheckerApi, program.getTypeChecker()),
80-
Nano.provideService(TypeScriptApi.TypeScriptProgram, program),
81-
Nano.provideService(TypeScriptApi.TypeScriptApi, ts),
82-
Nano.provideService(
83-
LanguageServicePluginOptions.LanguageServicePluginOptions,
84-
LanguageServicePluginOptions.parse({
85-
...LanguageServicePluginOptions.defaults,
86-
diagnostics: true,
87-
refactors: false,
88-
quickinfo: false,
89-
completions: false,
90-
goto: false,
91-
namespaceImportPackages: ["effect"],
92-
...configFromSourceComment(sourceText)
93-
})
94-
),
95-
Nano.map(({ diagnostics }) => {
96-
// sort by start position
97-
diagnostics.sort((a, b) => (a.start || 0) - (b.start || 0))
98-
// create human readable messages
99-
return diagnostics.length === 0 ?
100-
"// no diagnostics" :
101-
diagnostics.map((error) => diagnosticToLogFormat(sourceFile, sourceText, error))
102-
.join("\n\n")
103-
}),
104-
Nano.unsafeRun,
105-
async (result) => {
106-
expect(Result.isSuccess(result), "should run with no error " + result).toEqual(true)
107-
await expect(Result.getOrElse(result, () => "// no codefixes available")).toMatchFileSnapshot(
108-
snapshotFilePath
109-
)
69+
if (getHarnessVersion() === "v4") {
70+
// expect valid initial code
71+
const typeDiags = program.getSemanticDiagnostics().filter((_) => _.source === sourceFile.fileName)
72+
const syntaxDiags = program.getSyntacticDiagnostics().filter((_) => _.source === sourceFile.fileName)
73+
const tsDiagsText = [...syntaxDiags, ...typeDiags].map((diag) =>
74+
diagnosticToLogFormat(sourceFile, sourceText, diag)
75+
).join("\n\n")
76+
expect(tsDiagsText).toBe("")
11077
}
111-
)
78+
79+
// attempt to run the diagnostic and get the output
80+
return pipe(
81+
LSP.getSemanticDiagnosticsWithCodeFixes([diagnostic], sourceFile),
82+
TypeParser.nanoLayer,
83+
TypeCheckerUtils.nanoLayer,
84+
TypeScriptUtils.nanoLayer,
85+
Nano.provideService(TypeCheckerApi.TypeCheckerApi, program.getTypeChecker()),
86+
Nano.provideService(TypeScriptApi.TypeScriptProgram, program),
87+
Nano.provideService(TypeScriptApi.TypeScriptApi, ts),
88+
Nano.provideService(
89+
LanguageServicePluginOptions.LanguageServicePluginOptions,
90+
LanguageServicePluginOptions.parse({
91+
...LanguageServicePluginOptions.defaults,
92+
diagnostics: true,
93+
refactors: false,
94+
quickinfo: false,
95+
completions: false,
96+
goto: false,
97+
namespaceImportPackages: ["effect"],
98+
...configFromSourceComment(sourceText)
99+
})
100+
),
101+
Nano.map(({ diagnostics }) => {
102+
// sort by start position
103+
diagnostics.sort((a, b) => (a.start || 0) - (b.start || 0))
104+
// create human readable messages
105+
return diagnostics.length === 0 ?
106+
"// no diagnostics" :
107+
diagnostics.map((error) => diagnosticToLogFormat(sourceFile, sourceText, error))
108+
.join("\n\n")
109+
}),
110+
Nano.unsafeRun,
111+
async (result) => {
112+
expect(Result.isSuccess(result), "should run with no error " + result).toEqual(true)
113+
await expect(Result.getOrElse(result, () => "// no codefixes available")).toMatchFileSnapshot(
114+
snapshotFilePath
115+
)
116+
}
117+
)
118+
} finally {
119+
languageService.dispose()
120+
}
112121
}
113122

114123
function testDiagnosticQuickfixesOnExample(
@@ -117,14 +126,16 @@ function testDiagnosticQuickfixesOnExample(
117126
sourceText: string
118127
) {
119128
const promises: Array<Promise<void>> = []
129+
const languageServicesToDispose: Array<ts.LanguageService> = []
120130

121131
// create the language service with mocked services over a VFS
122-
const { languageServiceHost, program, sourceFile } = createServicesWithMockedVFS(
132+
const { languageService, languageServiceHost, program, sourceFile } = createServicesWithMockedVFS(
123133
getHarnessDir(),
124134
getExamplesDir(),
125135
fileName,
126136
sourceText
127137
)
138+
languageServicesToDispose.push(languageService)
128139

129140
// create snapshot path
130141
const snapshotFilePathList = path.join(
@@ -176,17 +187,24 @@ function testDiagnosticQuickfixesOnExample(
176187
codeFix.end + "\n" + applyEdits(edits, fileName, sourceText)
177188

178189
if (getHarnessVersion() === "v4") {
179-
const { program, sourceFile: newSourceFile } = createServicesWithMockedVFS(
190+
const result = createServicesWithMockedVFS(
180191
getHarnessDir(),
181192
getExamplesDir(),
182193
fileName,
183194
finalSource
184195
)
185-
const typeDiags = program.getSemanticDiagnostics().filter((_) => _.source === newSourceFile.fileName)
186-
const syntaxDiags = program.getSyntacticDiagnostics().filter((_) => _.source === newSourceFile.fileName)
196+
languageServicesToDispose.push(result.languageService)
197+
const typeDiags = result.program.getSemanticDiagnostics().filter((_) =>
198+
_.source === result.sourceFile.fileName
199+
)
200+
const syntaxDiags = result.program.getSyntacticDiagnostics().filter((_) =>
201+
_.source === result.sourceFile.fileName
202+
)
187203
const snapshotText = [
188204
finalSource,
189-
...[...syntaxDiags, ...typeDiags].map((diag) => diagnosticToLogFormat(newSourceFile, finalSource, diag))
205+
...[...syntaxDiags, ...typeDiags].map((diag) =>
206+
diagnosticToLogFormat(result.sourceFile, finalSource, diag)
207+
)
190208
].join("\n\n")
191209
promises.push(
192210
expect(snapshotText).toMatchFileSnapshot(snapshotFilePath)
@@ -224,11 +242,17 @@ function testDiagnosticQuickfixesOnExample(
224242
),
225243
Nano.unsafeRun,
226244
async (result) => {
227-
expect(Result.isSuccess(result), "should run with no error " + result).toEqual(true)
228-
await Promise.all(promises)
229-
await expect(Result.getOrElse(result, () => "// no codefixes available")).toMatchFileSnapshot(
230-
snapshotFilePathList
231-
)
245+
try {
246+
expect(Result.isSuccess(result), "should run with no error " + result).toEqual(true)
247+
await Promise.all(promises)
248+
await expect(Result.getOrElse(result, () => "// no codefixes available")).toMatchFileSnapshot(
249+
snapshotFilePathList
250+
)
251+
} finally {
252+
for (const ls of languageServicesToDispose) {
253+
ls.dispose()
254+
}
255+
}
232256
}
233257
)
234258
}

0 commit comments

Comments
 (0)