From 319a8912fb8caf1d362cbf4de2aca08abda3dc2b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 18:52:32 +0000 Subject: [PATCH 1/8] Fix html program viewer search icon crash Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- .../src/react/tree-navigation.test.tsx | 21 +++++++++++++++++++ .../src/react/tree-navigation.tsx | 6 ++++-- 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 packages/html-program-viewer/src/react/tree-navigation.test.tsx diff --git a/packages/html-program-viewer/src/react/tree-navigation.test.tsx b/packages/html-program-viewer/src/react/tree-navigation.test.tsx new file mode 100644 index 00000000000..463a66531b0 --- /dev/null +++ b/packages/html-program-viewer/src/react/tree-navigation.test.tsx @@ -0,0 +1,21 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { NodeIcon } from "./tree-navigation.js"; + +describe("NodeIcon", () => { + it("falls back when type kind is missing", () => { + render( + , + ); + + expect(screen.getByText("?")).toBeDefined(); + }); +}); diff --git a/packages/html-program-viewer/src/react/tree-navigation.tsx b/packages/html-program-viewer/src/react/tree-navigation.tsx index e0c0eefa9f7..2934d86b60a 100644 --- a/packages/html-program-viewer/src/react/tree-navigation.tsx +++ b/packages/html-program-viewer/src/react/tree-navigation.tsx @@ -21,8 +21,10 @@ export const TreeNavigation = (_: TreeNavigationProps) => { export const NodeIcon = ({ node }: { node: TypeGraphNode }) => { switch (node.kind) { - case "type": - return {node.type.kind[0]}; + case "type": { + const kindPrefix = node.type?.kind?.[0] ?? "?"; + return {kindPrefix}; + } case "list": return ; } From 3bce136c316f97779c8857988af95f906d46e2b2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 18:53:27 +0000 Subject: [PATCH 2/8] Refine regression test type assertion Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- .../html-program-viewer/src/react/tree-navigation.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/html-program-viewer/src/react/tree-navigation.test.tsx b/packages/html-program-viewer/src/react/tree-navigation.test.tsx index 463a66531b0..afdd402c088 100644 --- a/packages/html-program-viewer/src/react/tree-navigation.test.tsx +++ b/packages/html-program-viewer/src/react/tree-navigation.test.tsx @@ -1,4 +1,5 @@ import { render, screen } from "@testing-library/react"; +import type { Type } from "@typespec/compiler"; import { describe, expect, it } from "vitest"; import { NodeIcon } from "./tree-navigation.js"; @@ -10,7 +11,7 @@ describe("NodeIcon", () => { kind: "type", id: "$.broken", name: "broken", - type: { kind: undefined } as never, + type: { kind: undefined } as unknown as Type, children: [], }} />, From b199e6ef252b1e1e3a784a2c9d9dd8f6215585ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 19:29:06 +0000 Subject: [PATCH 3/8] Add changelog for html viewer crash fix Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- ...ilot-fix-html-program-viewer-crash-2026-5-8-19-28-50.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/copilot-fix-html-program-viewer-crash-2026-5-8-19-28-50.md diff --git a/.chronus/changes/copilot-fix-html-program-viewer-crash-2026-5-8-19-28-50.md b/.chronus/changes/copilot-fix-html-program-viewer-crash-2026-5-8-19-28-50.md new file mode 100644 index 00000000000..4197ab2f74f --- /dev/null +++ b/.chronus/changes/copilot-fix-html-program-viewer-crash-2026-5-8-19-28-50.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/html-program-viewer" +--- + +Fix html program viewer crash when rendering search results with missing type kind. \ No newline at end of file From 4bf2e19c5d7d0e075131d829001939abd583731a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 19:58:57 +0000 Subject: [PATCH 4/8] Handle value entities in diagnostic node resolution Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/compiler/src/core/diagnostics.ts | 15 ++++++++-- .../compiler/test/core/diagnostics.test.ts | 29 +++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/packages/compiler/src/core/diagnostics.ts b/packages/compiler/src/core/diagnostics.ts index 180661c70d7..dfb906253e1 100644 --- a/packages/compiler/src/core/diagnostics.ts +++ b/packages/compiler/src/core/diagnostics.ts @@ -51,12 +51,12 @@ export function getRelatedLocations(diagnostic: Diagnostic): RelatedSourceLocati * - For template instance targets: returns the node of the template declaration * - For symbols: returns the first declaration node (or symbol source for using symbols) * - For AST nodes: returns the node itself - * - For types: returns the node associated with the type + * - For entities: returns the most relevant node associated with the entity * * @param target The diagnostic target to extract a node from. Can be a template instance, * symbol, AST node, or type. - * @returns The AST node associated with the target, or undefined if the target is a type - * or symbol that doesn't have an associated node. + * @returns The AST node associated with the target, or undefined if the target + * doesn't have an associated node. */ export function getNodeForTarget(target: TypeSpecDiagnosticTarget): Node | undefined { if (!("kind" in target) && !("entityKind" in target)) { @@ -71,6 +71,15 @@ export function getNodeForTarget(target: TypeSpecDiagnosticTarget): Node | undef } return target.declarations[0]; + } else if ("entityKind" in target) { + switch (target.entityKind) { + case "Value": + return target.node ?? target.type.node; + case "MixedParameterConstraint": + return target.node ?? target.type?.node ?? target.valueType?.node; + case "Indeterminate": + return target.type.node; + } } else if ("kind" in target && typeof target.kind === "number") { // node return target as Node; diff --git a/packages/compiler/test/core/diagnostics.test.ts b/packages/compiler/test/core/diagnostics.test.ts index 179b04b8f3d..614a8a18b36 100644 --- a/packages/compiler/test/core/diagnostics.test.ts +++ b/packages/compiler/test/core/diagnostics.test.ts @@ -3,6 +3,7 @@ import { describe, it } from "vitest"; import { SourceLocationOptions, getSourceLocation } from "../../src/index.js"; import { extractSquiggles } from "../../src/testing/source-utils.js"; import { Tester } from "../tester.js"; +import { getNodeForTarget } from "../../src/core/diagnostics.js"; describe("compiler: diagnostics", () => { async function expectLocationMatch(code: string, options: SourceLocationOptions = {}) { @@ -34,4 +35,32 @@ describe("compiler: diagnostics", () => { { locateId: true }, )); }); + + describe("getNodeForTarget", () => { + it("returns function value node when available", () => { + const valueNode = { kind: 999 } as any; + const typeNode = { kind: 998 } as any; + + const target = { + entityKind: "Value", + valueKind: "Function", + node: valueNode, + type: { kind: "FunctionType", node: typeNode }, + } as any; + + strictEqual(getNodeForTarget(target), valueNode); + }); + + it("falls back to value type node when value has no node", () => { + const typeNode = { kind: 997 } as any; + + const target = { + entityKind: "Value", + valueKind: "StringValue", + type: { kind: "String", node: typeNode }, + } as any; + + strictEqual(getNodeForTarget(target), typeNode); + }); + }); }); From 2731b209c658721e38883cda7179fe851666aeb0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:02:26 +0000 Subject: [PATCH 5/8] Fix diagnostic node resolution for value entities Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/compiler/src/core/diagnostics.ts | 6 +++++- packages/compiler/test/core/diagnostics.test.ts | 10 +++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/core/diagnostics.ts b/packages/compiler/src/core/diagnostics.ts index dfb906253e1..d5ce99addf4 100644 --- a/packages/compiler/src/core/diagnostics.ts +++ b/packages/compiler/src/core/diagnostics.ts @@ -73,12 +73,16 @@ export function getNodeForTarget(target: TypeSpecDiagnosticTarget): Node | undef return target.declarations[0]; } else if ("entityKind" in target) { switch (target.entityKind) { + case "Type": + return target.node; case "Value": - return target.node ?? target.type.node; + return ("node" in target ? target.node : undefined) ?? target.type.node; case "MixedParameterConstraint": return target.node ?? target.type?.node ?? target.valueType?.node; case "Indeterminate": return target.type.node; + default: + return undefined; } } else if ("kind" in target && typeof target.kind === "number") { // node diff --git a/packages/compiler/test/core/diagnostics.test.ts b/packages/compiler/test/core/diagnostics.test.ts index 614a8a18b36..cc1f58f16aa 100644 --- a/packages/compiler/test/core/diagnostics.test.ts +++ b/packages/compiler/test/core/diagnostics.test.ts @@ -37,9 +37,13 @@ describe("compiler: diagnostics", () => { }); describe("getNodeForTarget", () => { + const mockValueNodeKind = 999; + const mockTypeNodeKind = 998; + const mockStringTypeNodeKind = 997; + it("returns function value node when available", () => { - const valueNode = { kind: 999 } as any; - const typeNode = { kind: 998 } as any; + const valueNode = { kind: mockValueNodeKind } as any; + const typeNode = { kind: mockTypeNodeKind } as any; const target = { entityKind: "Value", @@ -52,7 +56,7 @@ describe("compiler: diagnostics", () => { }); it("falls back to value type node when value has no node", () => { - const typeNode = { kind: 997 } as any; + const typeNode = { kind: mockStringTypeNodeKind } as any; const target = { entityKind: "Value", From eeb44bd4f6b987d8815a256de30e827e173e0639 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:03:37 +0000 Subject: [PATCH 6/8] Improve value diagnostic node resolution Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/compiler/src/core/diagnostics.ts | 16 +++++++++++++++- packages/compiler/test/core/diagnostics.test.ts | 7 ++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/core/diagnostics.ts b/packages/compiler/src/core/diagnostics.ts index d5ce99addf4..6d4c6d8428e 100644 --- a/packages/compiler/src/core/diagnostics.ts +++ b/packages/compiler/src/core/diagnostics.ts @@ -16,6 +16,7 @@ import { SyntaxKind, Type, TypeSpecDiagnosticTarget, + Value, } from "./types.js"; export type WriteLine = (text?: string) => void; @@ -76,8 +77,10 @@ export function getNodeForTarget(target: TypeSpecDiagnosticTarget): Node | undef case "Type": return target.node; case "Value": - return ("node" in target ? target.node : undefined) ?? target.type.node; + return getValueNode(target) ?? target.type.node; case "MixedParameterConstraint": + // Prefer the explicit union expression node when present, otherwise fall back + // to whichever side of the constraint has a source node. return target.node ?? target.type?.node ?? target.valueType?.node; case "Indeterminate": return target.type.node; @@ -93,6 +96,17 @@ export function getNodeForTarget(target: TypeSpecDiagnosticTarget): Node | undef } } +function getValueNode(value: Value): Node | undefined { + switch (value.valueKind) { + case "ObjectValue": + case "ArrayValue": + case "Function": + return value.node; + default: + return undefined; + } +} + export interface SourceLocationOptions { /** * If trying to resolve the location of a type with an ID, show the location of the ID node instead of the entire type. diff --git a/packages/compiler/test/core/diagnostics.test.ts b/packages/compiler/test/core/diagnostics.test.ts index cc1f58f16aa..99628cbef49 100644 --- a/packages/compiler/test/core/diagnostics.test.ts +++ b/packages/compiler/test/core/diagnostics.test.ts @@ -4,6 +4,7 @@ import { SourceLocationOptions, getSourceLocation } from "../../src/index.js"; import { extractSquiggles } from "../../src/testing/source-utils.js"; import { Tester } from "../tester.js"; import { getNodeForTarget } from "../../src/core/diagnostics.js"; +import { SyntaxKind } from "../../src/core/types.js"; describe("compiler: diagnostics", () => { async function expectLocationMatch(code: string, options: SourceLocationOptions = {}) { @@ -37,9 +38,9 @@ describe("compiler: diagnostics", () => { }); describe("getNodeForTarget", () => { - const mockValueNodeKind = 999; - const mockTypeNodeKind = 998; - const mockStringTypeNodeKind = 997; + const mockValueNodeKind = SyntaxKind.ModelStatement; + const mockTypeNodeKind = SyntaxKind.ScalarStatement; + const mockStringTypeNodeKind = SyntaxKind.NamespaceStatement; it("returns function value node when available", () => { const valueNode = { kind: mockValueNodeKind } as any; From 68914cc0597538d07aa912f8d5a3a928d09f3005 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:04:48 +0000 Subject: [PATCH 7/8] Add robust entity-node diagnostics handling Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/compiler/src/core/diagnostics.ts | 3 + .../compiler/test/core/diagnostics.test.ts | 58 +++++++++++++++++-- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/packages/compiler/src/core/diagnostics.ts b/packages/compiler/src/core/diagnostics.ts index 6d4c6d8428e..7a77e431eae 100644 --- a/packages/compiler/src/core/diagnostics.ts +++ b/packages/compiler/src/core/diagnostics.ts @@ -97,6 +97,9 @@ export function getNodeForTarget(target: TypeSpecDiagnosticTarget): Node | undef } function getValueNode(value: Value): Node | undefined { + // Only compound values and function values carry their own syntax node. + // Primitive values (string/number/boolean/null/enum/scalar literal values) + // are represented by their resolved value/type and don't retain a direct node. switch (value.valueKind) { case "ObjectValue": case "ArrayValue": diff --git a/packages/compiler/test/core/diagnostics.test.ts b/packages/compiler/test/core/diagnostics.test.ts index 99628cbef49..c55b96d6e29 100644 --- a/packages/compiler/test/core/diagnostics.test.ts +++ b/packages/compiler/test/core/diagnostics.test.ts @@ -38,13 +38,13 @@ describe("compiler: diagnostics", () => { }); describe("getNodeForTarget", () => { - const mockValueNodeKind = SyntaxKind.ModelStatement; - const mockTypeNodeKind = SyntaxKind.ScalarStatement; - const mockStringTypeNodeKind = SyntaxKind.NamespaceStatement; + const mockSyntaxKindA = SyntaxKind.ModelStatement; + const mockSyntaxKindB = SyntaxKind.ScalarStatement; + const mockSyntaxKindC = SyntaxKind.NamespaceStatement; it("returns function value node when available", () => { - const valueNode = { kind: mockValueNodeKind } as any; - const typeNode = { kind: mockTypeNodeKind } as any; + const valueNode = { kind: mockSyntaxKindA } as any; + const typeNode = { kind: mockSyntaxKindB } as any; const target = { entityKind: "Value", @@ -57,7 +57,7 @@ describe("compiler: diagnostics", () => { }); it("falls back to value type node when value has no node", () => { - const typeNode = { kind: mockStringTypeNodeKind } as any; + const typeNode = { kind: mockSyntaxKindC } as any; const target = { entityKind: "Value", @@ -67,5 +67,51 @@ describe("compiler: diagnostics", () => { strictEqual(getNodeForTarget(target), typeNode); }); + + it("returns object value node when available", () => { + const valueNode = { kind: mockSyntaxKindB } as any; + + const target = { + entityKind: "Value", + valueKind: "ObjectValue", + node: valueNode, + type: { kind: "Model", node: undefined }, + } as any; + + strictEqual(getNodeForTarget(target), valueNode); + }); + + it("resolves mixed parameter constraint target in priority order", () => { + const explicitNode = { kind: mockSyntaxKindA } as any; + const typeNode = { kind: mockSyntaxKindB } as any; + + strictEqual( + getNodeForTarget({ + entityKind: "MixedParameterConstraint", + node: explicitNode, + type: { kind: "Model", node: typeNode }, + } as any), + explicitNode, + ); + + strictEqual( + getNodeForTarget({ + entityKind: "MixedParameterConstraint", + type: { kind: "Model", node: typeNode }, + } as any), + typeNode, + ); + }); + + it("resolves indeterminate target to underlying type node", () => { + const typeNode = { kind: mockSyntaxKindC } as any; + + const target = { + entityKind: "Indeterminate", + type: { kind: "String", node: typeNode }, + } as any; + + strictEqual(getNodeForTarget(target), typeNode); + }); }); }); From 6ff6ca5451ffc143c11b2114de47c0ff06117312 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:05:43 +0000 Subject: [PATCH 8/8] Clarify mixed constraint node precedence Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/compiler/src/core/diagnostics.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/core/diagnostics.ts b/packages/compiler/src/core/diagnostics.ts index 7a77e431eae..e7a2c441f87 100644 --- a/packages/compiler/src/core/diagnostics.ts +++ b/packages/compiler/src/core/diagnostics.ts @@ -80,7 +80,9 @@ export function getNodeForTarget(target: TypeSpecDiagnosticTarget): Node | undef return getValueNode(target) ?? target.type.node; case "MixedParameterConstraint": // Prefer the explicit union expression node when present, otherwise fall back - // to whichever side of the constraint has a source node. + // to a side of the constraint that has a source node. Type is preferred + // over valueType to keep location behavior stable for mixed constraints + // that include both branches. return target.node ?? target.type?.node ?? target.valueType?.node; case "Indeterminate": return target.type.node;