Skip to content

Commit 92b4fdf

Browse files
committed
fix(lsp): improve error handling and input validation for rename
- Add debug logging to prepareRename() and rename() catch handlers - Filter out expected "method not found" errors from logging - Add input validation for empty/whitespace newName parameter - Update JSDoc to clarify null return semantics - Add unit tests for empty newName validation - Add documentation test for documentChanges normalization Addresses PR #19 review feedback: - Silent error handling now logs unexpected failures - Ambiguous null returns are now documented
1 parent 23fc7f5 commit 92b4fdf

2 files changed

Lines changed: 83 additions & 2 deletions

File tree

packages/lsp/src/index.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,11 @@ export class LSPManager {
763763
* Prepare rename at the given position
764764
* Validates if the symbol at the position can be renamed
765765
*
766+
* @returns PrepareRenameResult if symbol can be renamed, null if:
767+
* - Symbol cannot be renamed (LSP server returned null)
768+
* - Position is not on a renameable symbol
769+
* - All LSP servers failed (errors are logged)
770+
*
766771
* @see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_prepareRename
767772
*/
768773
async prepareRename(input: {
@@ -785,7 +790,14 @@ export class LSPManager {
785790
},
786791
})
787792
.then((result: unknown) => this.normalizePrepareRename(result))
788-
.catch(() => null),
793+
.catch((err: unknown) => {
794+
// Log unexpected errors (not "method not found" which is expected for some servers)
795+
const message = err instanceof Error ? err.message : String(err)
796+
if (!message.includes('-32601') && !message.includes('Method not found')) {
797+
console.error(`[lsp:${client.serverID}] prepareRename failed:`, message)
798+
}
799+
return null
800+
}),
789801
),
790802
)
791803

@@ -797,6 +809,12 @@ export class LSPManager {
797809
* Rename the symbol at the given position
798810
* Returns a WorkspaceEdit with all changes needed
799811
*
812+
* @param input.newName - The new name for the symbol (must be non-empty)
813+
* @returns WorkspaceEdit if rename succeeded, null if:
814+
* - Symbol cannot be renamed
815+
* - newName is empty or whitespace-only
816+
* - All LSP servers failed (errors are logged)
817+
*
800818
* @see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_rename
801819
*/
802820
async rename(input: {
@@ -805,6 +823,12 @@ export class LSPManager {
805823
character: number
806824
newName: string
807825
}): Promise<WorkspaceEdit | null> {
826+
// Validate newName
827+
if (!input.newName || input.newName.trim() === '') {
828+
console.warn('[lsp] rename called with empty newName')
829+
return null
830+
}
831+
808832
const clients = await this.getClients(input.file)
809833

810834
const results = await Promise.all(
@@ -821,7 +845,12 @@ export class LSPManager {
821845
newName: input.newName,
822846
})
823847
.then((result: unknown) => this.normalizeWorkspaceEdit(result))
824-
.catch(() => null),
848+
.catch((err: unknown) => {
849+
// Log rename errors - these are more serious since rename is a mutating operation
850+
const message = err instanceof Error ? err.message : String(err)
851+
console.error(`[lsp:${client.serverID}] rename failed:`, message)
852+
return null
853+
}),
825854
),
826855
)
827856

packages/lsp/test/unit/index.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,28 @@ describe('LSPManager', () => {
3939
await manager.shutdown()
4040
// Should not throw
4141
})
42+
43+
test('rename returns null for empty newName', async () => {
44+
const manager = new LSPManager('/test/project')
45+
const result = await manager.rename({
46+
file: '/test/project/test.ts',
47+
line: 0,
48+
character: 0,
49+
newName: '',
50+
})
51+
expect(result).toBeNull()
52+
})
53+
54+
test('rename returns null for whitespace-only newName', async () => {
55+
const manager = new LSPManager('/test/project')
56+
const result = await manager.rename({
57+
file: '/test/project/test.ts',
58+
line: 0,
59+
character: 0,
60+
newName: ' ',
61+
})
62+
expect(result).toBeNull()
63+
})
4264
})
4365

4466
describe('formatDiagnostic', () => {
@@ -254,6 +276,36 @@ describe('WorkspaceEditSchema', () => {
254276
const result = WorkspaceEditSchema.safeParse(multiFileEdit)
255277
expect(result.success).toBe(true)
256278
})
279+
280+
/**
281+
* Note: LSP servers can return WorkspaceEdit in two formats:
282+
* 1. 'changes' format: { changes: { [uri]: TextEdit[] } }
283+
* 2. 'documentChanges' format: { documentChanges: TextDocumentEdit[] }
284+
*
285+
* The schema only validates the normalized 'changes' format output.
286+
* LSPManager.normalizeWorkspaceEdit() converts 'documentChanges' to 'changes' at runtime.
287+
*/
288+
test('schema validates normalized changes format (documentChanges is normalized at runtime)', () => {
289+
// This test documents that the schema validates the normalized output format
290+
// documentChanges format from LSP servers:
291+
// { documentChanges: [{ textDocument: { uri: 'file:///test.ts' }, edits: [...] }] }
292+
// gets normalized to:
293+
// { changes: { 'file:///test.ts': [...] } }
294+
295+
const normalizedFromDocumentChanges = {
296+
changes: {
297+
'file:///test.ts': [
298+
{ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 5 } }, newText: 'renamed' },
299+
],
300+
'file:///other.ts': [
301+
{ range: { start: { line: 10, character: 0 }, end: { line: 10, character: 5 } }, newText: 'renamed' },
302+
],
303+
},
304+
}
305+
306+
const result = WorkspaceEditSchema.safeParse(normalizedFromDocumentChanges)
307+
expect(result.success).toBe(true)
308+
})
257309
})
258310

259311
describe('PrepareRenameResultSchema', () => {

0 commit comments

Comments
 (0)