Skip to content

Commit c2d8522

Browse files
authored
Merge branch 'main' into optimize-pipeline-for-complete-test-cases
2 parents 686f8fd + 46fbbc0 commit c2d8522

18 files changed

Lines changed: 218 additions & 34 deletions
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
changeKind: fix
3+
packages:
4+
- "@typespec/compiler"
5+
---
6+
7+
Fix server crashes caused by undefined symbol declarations: add null checks for `getSymNode()` in hover, completion, type-details, and type-signature handlers, and use fallback name for empty DocumentSymbol names
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
changeKind: fix
3+
packages:
4+
- typespec-vscode
5+
---
6+
7+
Handle unhandled exceptions in VS Code extension: add custom error handler for server crashes with restart notification, wrap commands with graceful exception handling, and add null guards to prevent extension host errors when LSP client is unavailable

‎.github/workflows/copilot-setup-steps.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@ jobs:
2121
- name: Checkout repository
2222
uses: actions/checkout@v6
2323
- name: Install gh-aw extension
24-
uses: github/gh-aw/actions/setup-cli@v0.71.1
24+
uses: github/gh-aw/actions/setup-cli@v0.71.5
2525
with:
2626
version: v0.50.1

‎packages/compiler/src/server/completion.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -421,20 +421,20 @@ async function addIdentifierCompletion(
421421
let kind: CompletionItemKind;
422422
let deprecated = false;
423423
const symNode = getSymNode(sym);
424-
const type = sym.type ?? program.checker.getTypeForNode(symNode);
424+
const type = sym.type ?? (symNode ? program.checker.getTypeForNode(symNode) : undefined);
425425
if (sym.flags & (SymbolFlags.Function | SymbolFlags.Decorator)) {
426426
kind = CompletionItemKind.Function;
427427
} else if (
428428
sym.flags & SymbolFlags.Namespace &&
429-
symNode.kind !== SyntaxKind.NamespaceStatement
429+
symNode?.kind !== SyntaxKind.NamespaceStatement
430430
) {
431431
kind = CompletionItemKind.Module;
432432
} else if (symNode?.kind === SyntaxKind.AliasStatement) {
433433
kind = CompletionItemKind.Variable;
434434
deprecated = getDeprecationDetails(program, symNode) !== undefined;
435435
} else {
436-
kind = getCompletionItemKind(program, type);
437-
deprecated = getDeprecationDetails(program, type) !== undefined;
436+
kind = type ? getCompletionItemKind(program, type) : CompletionItemKind.Variable;
437+
deprecated = type ? getDeprecationDetails(program, type) !== undefined : false;
438438
}
439439
const documentation = await getSymbolDetails(program, sym);
440440

‎packages/compiler/src/server/serverlib.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -822,7 +822,7 @@ export function createServer(
822822
// Avoid showing full definition in other cases which can be long and not useful
823823
let includeExpandedDefinition = false;
824824
const sn = getSymNode(sym[0]);
825-
if (sn.kind !== SyntaxKind.AliasStatement) {
825+
if (sn && sn.kind !== SyntaxKind.AliasStatement) {
826826
const type = sym[0].type ?? program.checker.getTypeOrValueForNode(sn);
827827
if (type && "kind" in type) {
828828
const modelHasExtendOrIs: boolean =

‎packages/compiler/src/server/symbol-structure.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,10 @@ export function getSymbolStructure(ast: TypeSpecScriptNode): DocumentSymbol[] {
109109
const start = file.getLineAndCharacterOfPosition(node.pos);
110110
const end = file.getLineAndCharacterOfPosition(node.end);
111111
const range = Range.create(start, end);
112-
return DocumentSymbol.create(name, undefined, kind, range, range, symbols);
112+
// VS Code requires name to be non-empty; use a placeholder for empty/invalid names
113+
// that can occur during parse errors or with empty string literal properties.
114+
const safeName = name || "<anonymous>";
115+
return DocumentSymbol.create(safeName, undefined, kind, range, range, symbols);
113116
}
114117

115118
function getName(id: IdentifierNode | StringLiteralNode): string {

‎packages/compiler/src/server/type-details.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,12 @@ function getSymbolDocumentation(program: Program, symbol: Sym) {
8080
// Add @doc(...) API docs
8181
let type = symbol.type;
8282
if (!type) {
83-
const entity = program.checker.getTypeOrValueForNode(getSymNode(symbol));
84-
if (entity && isType(entity)) {
85-
type = entity;
83+
const symNode = getSymNode(symbol);
84+
if (symNode) {
85+
const entity = program.checker.getTypeOrValueForNode(symNode);
86+
if (entity && isType(entity)) {
87+
type = entity;
88+
}
8689
}
8790
}
8891
if (type) {

‎packages/compiler/src/server/type-signature.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export async function getSymbolSignature(
4646
case SyntaxKind.AliasStatement:
4747
return fence(`alias ${await getAliasSignature(decl)}`);
4848
}
49-
const entity = sym.type ?? program.checker.getTypeOrValueForNode(decl);
49+
const entity = sym.type ?? (decl ? program.checker.getTypeOrValueForNode(decl) : null);
5050
return getEntitySignature(sym, entity, options);
5151
}
5252

‎packages/typespec-vscode/src/extension.ts‎

Lines changed: 94 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,13 @@ export async function activate(context: ExtensionContext) {
126126
async (tel): Promise<ResultCode> => {
127127
return await emitCode(context, uri, tel);
128128
},
129+
undefined,
130+
(e) => {
131+
logger.error("Unexpected error when emitting code from TypeSpec.", [e], {
132+
showPopup: true,
133+
});
134+
return ResultCode.Fail;
135+
},
129136
);
130137
},
131138
);
@@ -165,6 +172,14 @@ export async function activate(context: ExtensionContext) {
165172
}
166173
},
167174
args?.activityId,
175+
(e) => {
176+
logger.error(
177+
"Unexpected error when restarting TypeSpec language server.",
178+
[e],
179+
{ showPopup: true },
180+
);
181+
return { code: ResultCode.Fail };
182+
},
168183
);
169184
},
170185
);
@@ -198,7 +213,26 @@ export async function activate(context: ExtensionContext) {
198213
await telemetryClient.doOperationWithTelemetry(
199214
TelemetryEventName.PreviewOpenApi3,
200215
async (tel): Promise<ResultCode> => {
201-
return await showOpenApi3(uri, context, tspLanguageClient!, tel);
216+
if (!tspLanguageClient || tspLanguageClient.state !== State.Running) {
217+
logger.error(
218+
"TypeSpec language server is not running. Please restart the server.",
219+
[],
220+
{ showPopup: true },
221+
);
222+
telemetryClient.logOperationDetailTelemetry(tel.activityId, {
223+
error: "LSP client is not running",
224+
});
225+
tel.lastStep = "Check LSP client";
226+
return ResultCode.Fail;
227+
}
228+
return await showOpenApi3(uri, context, tspLanguageClient, tel);
229+
},
230+
undefined,
231+
(e) => {
232+
logger.error("Unexpected error when previewing OpenAPI3.", [e], {
233+
showPopup: true,
234+
});
235+
return ResultCode.Fail;
202236
},
203237
);
204238
}),
@@ -213,6 +247,13 @@ export async function activate(context: ExtensionContext) {
213247
async (tel) => {
214248
return await recreateLSPClient(context, tel.activityId);
215249
},
250+
undefined,
251+
(e) => {
252+
logger.error("Unexpected error when restarting server after path change.", [e], {
253+
showPopup: true,
254+
});
255+
return { code: ResultCode.Fail };
256+
},
216257
);
217258
}
218259
}),
@@ -318,14 +359,30 @@ export async function activate(context: ExtensionContext) {
318359
return installResult.code;
319360
},
320361
tel.activityId,
362+
(e) => {
363+
logger.error("Unexpected error when starting TypeSpec language server.", [e], {
364+
showPopup: true,
365+
});
366+
return ResultCode.Fail;
367+
},
321368
);
322369
} else {
323370
logger.info("No workspace opened, Skip starting TypeSpec language service.");
371+
telemetryClient.logOperationDetailTelemetry(tel.activityId, {
372+
compilerLocation: "skipped-no-workspace-or-tsp-file",
373+
});
324374
}
325375
showStartUpMessages(stateManager);
326376
telemetryClient.sendDelayedTelemetryEvents();
327377
return ResultCode.Success;
328378
},
379+
undefined,
380+
(e) => {
381+
logger.error("Unexpected error when starting TypeSpec extension.", [e], {
382+
showPopup: true,
383+
});
384+
return ResultCode.Fail;
385+
},
329386
);
330387

331388
// Expose API for other extensions to consume
@@ -339,36 +396,52 @@ export async function activate(context: ExtensionContext) {
339396
}
340397

341398
export async function deactivate() {
342-
await tspLanguageClient?.stop();
343-
await clearOpenApi3PreviewTempFolders();
399+
try {
400+
await tspLanguageClient?.stop();
401+
await clearOpenApi3PreviewTempFolders();
402+
} catch (e) {
403+
logger.error("Error during extension deactivation", [e]);
404+
}
344405
}
345406

346407
async function recreateLSPClient(
347408
context: ExtensionContext,
348409
activityId: string,
349410
): Promise<Result<TspLanguageClient>> {
350-
logger.info("Recreating TypeSpec LSP server...");
351-
const oldClient = tspLanguageClient;
352-
setTspLanguageClient(await TspLanguageClient.create(activityId, context, outputChannel));
353-
await oldClient?.stop();
354-
if (!tspLanguageClient) {
355-
telemetryClient.logOperationDetailTelemetry(activityId, {
356-
error: "Failed to create TspLanguageClient",
357-
});
358-
return { code: ResultCode.Fail, details: "Failed to create TspLanguageClient." };
359-
} else {
360-
await tspLanguageClient.start(activityId);
361-
if (tspLanguageClient.state === State.Running) {
411+
try {
412+
logger.info("Recreating TypeSpec LSP server...");
413+
const oldClient = tspLanguageClient;
414+
setTspLanguageClient(await TspLanguageClient.create(activityId, context, outputChannel));
415+
await oldClient?.stop();
416+
if (!tspLanguageClient) {
362417
telemetryClient.logOperationDetailTelemetry(activityId, {
363-
compilerVersion: tspLanguageClient.initializeResult?.serverInfo?.version ?? "< 0.64.0",
418+
error: "Failed to create TspLanguageClient. Compiler could not be resolved.",
364419
});
365-
return { code: ResultCode.Success, value: tspLanguageClient };
420+
return { code: ResultCode.Fail, details: "Failed to create TspLanguageClient." };
366421
} else {
367-
telemetryClient.logOperationDetailTelemetry(activityId, {
368-
error: `Failed to start TspLanguageClient.`,
369-
});
370-
return { code: ResultCode.Fail, details: "TspLanguageClient is not running." };
422+
await tspLanguageClient.start(activityId);
423+
if (tspLanguageClient.state === State.Running) {
424+
telemetryClient.logOperationDetailTelemetry(activityId, {
425+
compilerVersion: tspLanguageClient.initializeResult?.serverInfo?.version ?? "< 0.64.0",
426+
});
427+
return { code: ResultCode.Success, value: tspLanguageClient };
428+
} else {
429+
telemetryClient.logOperationDetailTelemetry(activityId, {
430+
error: `Failed to start TspLanguageClient. State: ${tspLanguageClient.state}`,
431+
});
432+
return { code: ResultCode.Fail, details: "TspLanguageClient is not running." };
433+
}
371434
}
435+
} catch (e) {
436+
logger.error(
437+
"TypeSpec language server is unavailable due to an unexpected error. Please restart the server.",
438+
[e],
439+
{ showPopup: true },
440+
);
441+
telemetryClient.logOperationDetailTelemetry(activityId, {
442+
error: `Unexpected error in recreateLSPClient: ${e}`,
443+
});
444+
return { code: ResultCode.Fail, details: `Unexpected error: ${e}` };
372445
}
373446
}
374447

‎packages/typespec-vscode/src/telemetry/telemetry-client.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,13 @@ export class TelemetryClient {
120120
sendTelemetryEvent: (result: ResultCode, delay: boolean) => void,
121121
) => Promise<T>,
122122
activityId?: string,
123+
/**
124+
* Optional callback to handle unhandled exceptions from the operation.
125+
* If provided, the exception will NOT be re-thrown and this callback will be invoked instead,
126+
* and its return value will be used as the result of the operation.
127+
* If not provided, the exception will be re-thrown after telemetry logging (default behavior).
128+
*/
129+
onUnhandledException?: (error: unknown) => T,
123130
): Promise<T> {
124131
const opTelemetryEvent = this.createOperationTelemetryEvent(eventName, activityId);
125132
let eventSent = false;
@@ -149,6 +156,9 @@ export class TelemetryClient {
149156
error: "Unhandled exception from operation to doOperationWithTelemetry: \n" + inspect(e),
150157
});
151158
opTelemetryEvent.result = ResultCode.Fail;
159+
if (onUnhandledException) {
160+
return onUnhandledException(e);
161+
}
152162
// just report the issue in telemetry and re-throw the error
153163
throw e;
154164
} finally {

0 commit comments

Comments
 (0)