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;