From e762c5297ee84e28711d89de18126881a9d3b367 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Sat, 1 Aug 2026 09:15:06 -0400 Subject: [PATCH 1/2] fix(http-server-csharp): only emit types that belong to the service Types declared outside the service namespace are now emitted only when the service references them, directly or transitively, instead of emitting every model, enum and union declared by imported libraries. --- ...emit-referenced-types-2026-7-31-18-15-0.md | 7 + .../http-server-csharp/src/diagnostics.ts | 23 +- packages/http-server-csharp/src/emitter.tsx | 7 +- .../src/service-discovery.ts | 50 ++- .../src/service-resolution.test.ts | 108 +++++++ .../src/service-resolution.ts | 296 +++++++++--------- 6 files changed, 328 insertions(+), 163 deletions(-) create mode 100644 .chronus/changes/http-server-csharp-only-emit-referenced-types-2026-7-31-18-15-0.md create mode 100644 packages/http-server-csharp/src/service-resolution.test.ts diff --git a/.chronus/changes/http-server-csharp-only-emit-referenced-types-2026-7-31-18-15-0.md b/.chronus/changes/http-server-csharp-only-emit-referenced-types-2026-7-31-18-15-0.md new file mode 100644 index 00000000000..7c51b4b9dc2 --- /dev/null +++ b/.chronus/changes/http-server-csharp-only-emit-referenced-types-2026-7-31-18-15-0.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http-server-csharp" +--- + +Only emit types that belong to the service. Previously every model, enum and union declared in any imported library (`Azure.Core`, `Azure.ResourceManager`, ...) was emitted, producing large amounts of dead C# code and `anonymous-model` warnings for library types the spec author cannot change. Types declared outside the service namespace are now emitted only when the service references them, directly or transitively, and diagnostics are reported only for types the service itself declares. diff --git a/packages/http-server-csharp/src/diagnostics.ts b/packages/http-server-csharp/src/diagnostics.ts index 10a25fe152c..de46ceb6ab5 100644 --- a/packages/http-server-csharp/src/diagnostics.ts +++ b/packages/http-server-csharp/src/diagnostics.ts @@ -1,9 +1,5 @@ import type { Interface, Model, Program } from "@typespec/compiler"; -import { - isStdNamespace, - isTemplateDeclaration, - type Namespace as TspNamespace, -} from "@typespec/compiler"; +import { isTemplateDeclaration, type Namespace as TspNamespace } from "@typespec/compiler"; import { $ } from "@typespec/compiler/typekit"; import type { OperationHttpCanonicalization } from "@typespec/http-canonicalization"; import { assignAnonymousName } from "./components/models/anonymous-models.js"; @@ -13,18 +9,23 @@ import { isValidCSharpIdentifier } from "./utils/naming.js"; /** * Reports diagnostic warnings for models, scalars, and operations. * This pre-pass mirrors the old emitter behavior so that `tester.diagnose()` tests pass. + * + * Only types declared by the service itself are checked: types coming from imported + * libraries are not actionable for the spec author, and most of them are never emitted. */ export function reportEmitterDiagnostics( program: Program, interfaces: Interface[], canonicalOpsMap: Map, + serviceNamespaces: Set, ): void { const tk = $(program); const visited = new Set(); - // Walk all models in the service namespace(s) to check properties - for (const ns of program.getGlobalNamespaceType().namespaces.values()) { - if (isStdNamespace(ns)) continue; + // Walk the models declared by the service to check properties + const globalNs = program.getGlobalNamespaceType(); + for (const ns of serviceNamespaces) { + if (ns === globalNs) continue; walkNamespaceModels(program, ns, tk, visited); } @@ -130,12 +131,6 @@ function walkNamespaceModels( } } } - - // Recurse into sub-namespaces - for (const childNs of ns.namespaces.values()) { - if (isStdNamespace(childNs)) continue; - walkNamespaceModels(program, childNs, tk, visited); - } } function checkPropertyDiagnostics( diff --git a/packages/http-server-csharp/src/emitter.tsx b/packages/http-server-csharp/src/emitter.tsx index 76f6d035ba1..6c36510bebf 100644 --- a/packages/http-server-csharp/src/emitter.tsx +++ b/packages/http-server-csharp/src/emitter.tsx @@ -42,7 +42,12 @@ export async function $onEmit(context: EmitContext) const projectName = options["project-name"] ?? "ServiceProject"; // Report diagnostic warnings (pre-pass before rendering) - reportEmitterDiagnostics(context.program, resolution.interfaces, resolution.canonicalOpsMap); + reportEmitterDiagnostics( + context.program, + resolution.interfaces, + resolution.canonicalOpsMap, + resolution.declarationNamespaces, + ); // Resolve OpenAPI path for SwaggerUI let openApiPath: string | undefined = options["openapi-path"]; diff --git a/packages/http-server-csharp/src/service-discovery.ts b/packages/http-server-csharp/src/service-discovery.ts index e9e44987dd9..f3c7eb98b02 100644 --- a/packages/http-server-csharp/src/service-discovery.ts +++ b/packages/http-server-csharp/src/service-discovery.ts @@ -2,12 +2,52 @@ import type { Interface } from "@typespec/compiler"; import { isStdNamespace, isTemplateDeclaration, + listServices, type Operation, + type Program, type Namespace as TspNamespace, } from "@typespec/compiler"; import type { useTsp } from "@typespec/emitter-framework"; import { getCSharpIdentifier, NameCasingType } from "./utils/naming.js"; +/** + * Collects the namespaces whose declarations are emitted even when nothing references them. + * + * When the spec declares one or more services, only the service namespaces and their + * sub-namespaces qualify. Types coming from imported libraries (`Azure.Core`, + * `Azure.ResourceManager`, ...) live outside of those namespaces and are therefore only + * emitted when the service actually references them. + * + * When no service is declared there is nothing to anchor the emit on, so every + * non-standard namespace is treated as part of the service. + */ +export function getDeclarationNamespaces(program: Program): Set { + const namespaces = new Set(); + const globalNs = program.getGlobalNamespaceType(); + + function addWithChildren(ns: TspNamespace): void { + if (namespaces.has(ns)) return; + namespaces.add(ns); + for (const child of ns.namespaces.values()) { + if (isStdNamespace(child)) continue; + addWithChildren(child); + } + } + + const services = listServices(program); + if (services.length === 0) { + addWithChildren(globalNs); + return namespaces; + } + + // Declarations at the global namespace level are always authored by the spec itself. + namespaces.add(globalNs); + for (const service of services) { + addWithChildren(service.type); + } + return namespaces; +} + /** * Collects all interfaces from the service namespace(s). * Also creates synthetic interfaces for namespace-level operations @@ -15,6 +55,7 @@ import { getCSharpIdentifier, NameCasingType } from "./utils/naming.js"; */ export function getServiceInterfaces( program: ReturnType["$"]["program"], + serviceNamespaces: Set = getDeclarationNamespaces(program), ): Interface[] { const interfaces: Interface[] = []; const globalNs = program.getGlobalNamespaceType(); @@ -54,15 +95,10 @@ export function getServiceInterfaces( interfaces.push(syntheticIface); } } - - for (const childNs of ns.namespaces?.values() ?? []) { - if (isStdNamespace(childNs)) continue; - collectFromNamespace(childNs); - } } - for (const ns of globalNs.namespaces.values()) { - if (isStdNamespace(ns)) continue; + for (const ns of serviceNamespaces) { + if (ns === globalNs) continue; collectFromNamespace(ns); } return interfaces; diff --git a/packages/http-server-csharp/src/service-resolution.test.ts b/packages/http-server-csharp/src/service-resolution.test.ts new file mode 100644 index 00000000000..04a06a7c523 --- /dev/null +++ b/packages/http-server-csharp/src/service-resolution.test.ts @@ -0,0 +1,108 @@ +import { Tester } from "#test/tester.js"; +import type { TesterInstance } from "@typespec/compiler/testing"; +import { $ } from "@typespec/compiler/typekit"; +import { HttpCanonicalizer } from "@typespec/http-canonicalization"; +import { beforeEach, expect, it } from "vitest"; +import { resolveServiceTypes, type ServiceTypeResolution } from "./service-resolution.js"; + +let runner: TesterInstance; + +beforeEach(async () => { + runner = await Tester.createInstance(); +}); + +async function resolve(code: string): Promise { + await runner.compile(code); + const tk = $(runner.program); + return resolveServiceTypes(runner.program, tk, new HttpCanonicalizer(tk)); +} + +it("excludes models declared outside the service namespace that are not referenced", async () => { + const resolution = await resolve(` + namespace Library { + model Used { name: string; } + model Unused { name: string; } + } + + @service + namespace Contoso { + model Widget { id: string; used: Library.Used; } + op read(): Widget; + } + `); + + expect(resolution.models.map((m) => m.name).sort()).toEqual(["Used", "Widget"]); +}); + +it("excludes enums and union enums declared outside the service namespace that are not referenced", async () => { + const resolution = await resolve(` + namespace Library { + enum UsedEnum { red, blue } + enum UnusedEnum { up, down } + union UsedUnion { "on", "off" } + union UnusedUnion { "left", "right" } + } + + @service + namespace Contoso { + model Widget { color: Library.UsedEnum; state: Library.UsedUnion; } + op read(): Widget; + } + `); + + expect(resolution.enums.map((e) => e.name)).toEqual(["UsedEnum"]); + expect(resolution.unionEnums.map((u) => u.name)).toEqual(["UsedUnion"]); +}); + +it("discovers types referenced transitively through operations only", async () => { + const resolution = await resolve(` + namespace Library { + model Envelope { detail: Detail; } + model Detail { kind: Kind; } + enum Kind { simple, complex } + model Untouched { name: string; } + } + + @service + namespace Contoso { + op read(): Library.Envelope; + } + `); + + expect(resolution.models.map((m) => m.name).sort()).toEqual(["Detail", "Envelope"]); + expect(resolution.enums.map((e) => e.name)).toEqual(["Kind"]); +}); + +it("does not create interfaces for operations declared outside the service namespace", async () => { + const resolution = await resolve(` + namespace Library { + interface LibraryOps { + @route("/lib") libRead(): void; + } + } + + @service + namespace Contoso { + interface Widgets { + @route("/widgets") read(): void; + } + } + `); + + expect(resolution.interfaces.map((i) => i.name)).toEqual(["Widgets"]); +}); + +it("emits every user namespace when no service is declared", async () => { + const resolution = await resolve(` + namespace Library { + model Standalone { name: string; } + } + + namespace Contoso { + model Widget { id: string; } + op read(): Widget; + } + `); + + expect(resolution.models.map((m) => m.name).sort()).toEqual(["Standalone", "Widget"]); +}); diff --git a/packages/http-server-csharp/src/service-resolution.ts b/packages/http-server-csharp/src/service-resolution.ts index 281c6147799..54d39e10344 100644 --- a/packages/http-server-csharp/src/service-resolution.ts +++ b/packages/http-server-csharp/src/service-resolution.ts @@ -21,7 +21,11 @@ import { preAssignAnonymousResponseNames, resetAnonymousModels, } from "./components/models/anonymous-models.js"; -import { getServiceInterfaces, getServiceNamespaceName } from "./service-discovery.js"; +import { + getDeclarationNamespaces, + getServiceInterfaces, + getServiceNamespaceName, +} from "./service-discovery.js"; import { findServiceNamespace } from "./utils/namespace-utils.js"; /** All resolved service types, computed once before rendering. */ @@ -40,19 +44,24 @@ export interface ServiceTypeResolution { unionEnums: Union[]; /** Canonicalized HTTP operations per interface. */ canonicalOpsMap: Map; + /** Namespaces whose declarations are emitted without being referenced. */ + declarationNamespaces: Set; } /** * Resolves all service types in a single pass, eliminating redundant * namespace traversals that previously occurred in individual components. * + * Only types declared in the service namespace(s) are emitted unconditionally. + * Types coming from imported libraries are emitted only when the service + * references them, directly or transitively. + * * Ordering: * 1. Service namespace discovery * 2. Interface collection (including synthetic interfaces for namespace-level ops) * 3. Anonymous response model naming (depends on interfaces) - * 4. Enum and union-enum collection - * 5. Model discovery (walks operations from interfaces + namespace models) - * 6. Canonicalization of HTTP operations + * 4. Type discovery (service declarations + everything they reference) + * 5. Canonicalization of HTTP operations */ export function resolveServiceTypes( program: Program, @@ -66,26 +75,27 @@ export function resolveServiceTypes( // Phase 1: Service namespace const serviceNamespace = findServiceNamespace(globalNs); const serviceNamespaceName = getServiceNamespaceName(program); + const declarationNamespaces = getDeclarationNamespaces(program); // Phase 2: Interfaces (includes synthetic ones for namespace-level operations) - const interfaces = getServiceInterfaces(program); + const interfaces = getServiceInterfaces(program, declarationNamespaces); // Phase 3: Pre-assign contextual names to anonymous response models preAssignAnonymousResponseNames(interfaces); - // Phase 4: Enums and union-enums from all non-std namespaces - const enums: Enum[] = []; - const unionEnums: Union[] = []; - collectEnumsFromNamespaces(globalNs, enums, unionEnums); - - // Phase 5: Model discovery (namespace models + operation-referenced models) + // Phase 4: Models, enums and union-enums. // Auth scheme models (e.g. those referenced by `@useAuth`) are protocol metadata, // not payload data, so they must not be emitted as C# model classes (aligns with // the OpenAPI3 emitter, which emits them under `components.securitySchemes`). const authModels = getAuthSchemeModels(program); - const models = getServiceModels($, globalNs, authModels); + const { models, enums, unionEnums } = collectServiceTypes( + $, + declarationNamespaces, + interfaces, + authModels, + ); - // Phase 6: Canonicalize all HTTP operations + // Phase 5: Canonicalize all HTTP operations const canonicalOpsMap = canonicalizeAllInterfaces(canonicalizer, interfaces); return { @@ -96,42 +106,10 @@ export function resolveServiceTypes( enums, unionEnums, canonicalOpsMap, + declarationNamespaces, }; } -/** - * Collects all enums and union-enums from non-std namespaces in a single walk. - */ -function collectEnumsFromNamespaces( - globalNs: TspNamespace, - enums: Enum[], - unionEnums: Union[], -): void { - const seenEnums = new Set(); - const seenUnions = new Set(); - - function walk(ns: TspNamespace): void { - for (const en of ns.enums?.values() ?? []) { - if (!seenEnums.has(en) && en.name) { - seenEnums.add(en); - enums.push(en); - } - } - for (const union of ns.unions?.values() ?? []) { - if (!seenUnions.has(union) && isUnionEnum(union)) { - seenUnions.add(union); - unionEnums.push(union); - } - } - for (const childNs of ns.namespaces?.values() ?? []) { - if (isStdNamespace(childNs)) continue; - walk(childNs); - } - } - - walk(globalNs); -} - /** * Canonicalize all operations for each interface, skipping any that fail. */ @@ -154,139 +132,175 @@ function canonicalizeAllInterfaces( return result; } -// ── Model discovery ───────────────────────────────────────────────────── +// ── Type discovery ────────────────────────────────────────────────────── + +/** The set of types that qualify for emission. */ +interface CollectedTypes { + models: Model[]; + enums: Enum[]; + unionEnums: Union[]; +} + +/** Sink used while walking the type graph. */ +interface TypeCollector { + addModel(model: Model): void; + addEnum(en: Enum): void; + addUnion(union: Union): void; +} /** - * Retrieves all models from the program that should be emitted. - * Includes namespace-level models AND models referenced by operations. + * Collects every type that should be emitted: the declarations of the service + * namespace(s) plus everything the service references, directly or transitively. + * + * Types declared by imported libraries (`Azure.Core`, `Azure.ResourceManager`, ...) + * are only emitted when the service actually references them; emitting the rest + * would generate dead code for constructs the service never exposes. * * @param authModels Models that back authentication schemes; these are excluded * from emission because they represent protocol metadata rather than payloads. */ -function getServiceModels($: Typekit, globalNs: TspNamespace, authModels: Set): Model[] { +function collectServiceTypes( + $: Typekit, + declarationNamespaces: Set, + interfaces: Interface[], + authModels: Set, +): CollectedTypes { const models: Model[] = []; - const seen = new Set(); + const enums: Enum[] = []; + const unionEnums: Union[] = []; + const seenModels = new Set(); + const seenEnums = new Set(); + const seenUnions = new Set(); + const visited = new Set(); - function addModel(model: Model) { - if (seen.has(model)) return; - seen.add(model); - if (authModels.has(model)) return; - if (shouldEmitModel($, model)) { - models.push(model); - } - } + const collector: TypeCollector = { + addModel(model) { + if (seenModels.has(model)) return; + seenModels.add(model); + if (authModels.has(model)) return; + if (shouldEmitModel($, model)) { + models.push(model); + } + }, + addEnum(en) { + if (seenEnums.has(en) || !en.name) return; + seenEnums.add(en); + if (isBuiltInNamespace(en.namespace)) return; + enums.push(en); + }, + addUnion(union) { + if (seenUnions.has(union)) return; + seenUnions.add(union); + if (isBuiltInNamespace(union.namespace)) return; + if (isUnionEnum(union)) { + unionEnums.push(union); + } + }, + }; - // Collect from namespaces - for (const model of globalNs.models.values()) { - addModel(model); - } - for (const ns of globalNs.namespaces.values()) { - if (isStdNamespace(ns)) continue; - collectModelsFromNamespace($, ns, models, seen, authModels); + // Declarations of the service itself are always emitted. + const declaredUnions: Union[] = []; + for (const ns of declarationNamespaces) { + for (const model of ns.models?.values() ?? []) { + collector.addModel(model); + } + for (const en of ns.enums?.values() ?? []) { + collector.addEnum(en); + } + for (const union of ns.unions?.values() ?? []) { + collector.addUnion(union); + declaredUnions.push(union); + } } - // Walk operations to discover referenced models (template instantiations, etc.) - const visited = new Set(); - for (const ns of globalNs.namespaces.values()) { - if (isStdNamespace(ns)) continue; - discoverReferencedModels($, ns, addModel, visited); + // Walk operations to discover referenced types (template instantiations, etc.) + for (const iface of interfaces) { + for (const [, op] of iface.operations) { + discoverTypes($, op.returnType, collector, visited); + for (const param of op.parameters?.properties?.values() ?? []) { + discoverTypes($, param.type, collector, visited); + } + } } - // Walk all collected model properties to discover anonymous sub-models - const modelsSnapshot = [...models]; - for (const model of modelsSnapshot) { + // Walk the declared types to discover everything they reference. + for (const model of [...models]) { for (const prop of model.properties.values()) { - discoverModelsInType($, prop.type, addModel, visited); + discoverTypes($, prop.type, collector, visited); } if (model.baseModel) { - discoverModelsInType($, model.baseModel, addModel, visited); + discoverTypes($, model.baseModel, collector, visited); } } - - return models; -} - -/** Walks operations in a namespace to discover referenced model types. */ -function discoverReferencedModels( - $: Typekit, - ns: TspNamespace, - addModel: (m: Model) => void, - visited: Set, -): void { - for (const op of ns.operations?.values() ?? []) { - discoverModelsInType($, op.returnType, addModel, visited); - for (const param of op.parameters?.properties?.values() ?? []) { - discoverModelsInType($, param.type, addModel, visited); - } - } - for (const iface of ns.interfaces?.values() ?? []) { - for (const op of iface.operations?.values() ?? []) { - discoverModelsInType($, op.returnType, addModel, visited); - for (const param of op.parameters?.properties?.values() ?? []) { - discoverModelsInType($, param.type, addModel, visited); - } - } - } - for (const childNs of ns.namespaces?.values() ?? []) { - if (isStdNamespace(childNs)) continue; - discoverReferencedModels($, childNs, addModel, visited); + for (const union of declaredUnions) { + discoverTypes($, union, collector, visited); } + + return { models, enums, unionEnums }; } -/** Recursively discovers models referenced by a type. */ -function discoverModelsInType( - $: Typekit, - type: Type, - addModel: (m: Model) => void, - visited: Set, -): void { +/** Recursively discovers the emittable types referenced by a type. */ +function discoverTypes($: Typekit, type: Type, collector: TypeCollector, visited: Set): void { if (visited.has(type)) return; visited.add(type); - if (type.kind === "Model") { - if ($.array.is(type)) { - if (type.indexer?.value) { - discoverModelsInType($, type.indexer.value, addModel, visited); + switch (type.kind) { + case "Model": + if ($.array.is(type) || $.record.is(type)) { + if (type.indexer?.value) { + discoverTypes($, type.indexer.value, collector, visited); + } + return; } - } else if (!$.record.is(type)) { - addModel(type); + collector.addModel(type); for (const prop of type.properties.values()) { - discoverModelsInType($, prop.type, addModel, visited); + discoverTypes($, prop.type, collector, visited); } if (type.baseModel) { - discoverModelsInType($, type.baseModel, addModel, visited); + discoverTypes($, type.baseModel, collector, visited); } if (type.templateMapper) { for (const arg of type.templateMapper.args) { if (arg.entityKind === "Type") { - discoverModelsInType($, arg, addModel, visited); + discoverTypes($, arg, collector, visited); } } } - } - } else if (type.kind === "Union") { - for (const variant of type.variants.values()) { - discoverModelsInType($, variant.type, addModel, visited); - } + return; + case "Union": + collector.addUnion(type); + for (const variant of type.variants.values()) { + discoverTypes($, variant.type, collector, visited); + } + return; + case "UnionVariant": + discoverTypes($, type.type, collector, visited); + return; + case "Enum": + collector.addEnum(type); + return; + case "EnumMember": + collector.addEnum(type.enum); + return; + case "ModelProperty": + discoverTypes($, type.type, collector, visited); + return; + case "Tuple": + for (const value of type.values) { + discoverTypes($, value, collector, visited); + } + return; + default: + return; } } -function collectModelsFromNamespace( - $: Typekit, - ns: TspNamespace, - models: Model[], - seen: Set, - authModels: Set, -): void { - for (const model of ns.models?.values() ?? []) { - if (!seen.has(model) && !authModels.has(model) && shouldEmitModel($, model)) { - seen.add(model); - models.push(model); - } - } - for (const childNs of ns.namespaces?.values() ?? []) { - collectModelsFromNamespace($, childNs, models, seen, authModels); - } +/** Whether a namespace belongs to the TypeSpec standard library. */ +function isBuiltInNamespace(ns: TspNamespace | undefined): boolean { + if (!ns) return false; + if (isStdNamespace(ns)) return true; + const nsName = getNamespaceFullName(ns); + return nsName === "TypeSpec" || nsName.startsWith("TypeSpec."); } /** From a534e6a9e8a1d007e1d3d64f68baf92582a384c2 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Sat, 1 Aug 2026 09:22:11 -0400 Subject: [PATCH 2/2] chore: keep wording generic --- ...emit-referenced-types-2026-7-31-18-15-0.md | 2 +- .../http-server-csharp/src/diagnostics.ts | 4 ++-- .../src/service-discovery.ts | 5 ++-- .../src/service-resolution.test.ts | 24 +++++++++---------- .../src/service-resolution.ts | 9 ++++--- 5 files changed, 21 insertions(+), 23 deletions(-) diff --git a/.chronus/changes/http-server-csharp-only-emit-referenced-types-2026-7-31-18-15-0.md b/.chronus/changes/http-server-csharp-only-emit-referenced-types-2026-7-31-18-15-0.md index 7c51b4b9dc2..19edf1e70ec 100644 --- a/.chronus/changes/http-server-csharp-only-emit-referenced-types-2026-7-31-18-15-0.md +++ b/.chronus/changes/http-server-csharp-only-emit-referenced-types-2026-7-31-18-15-0.md @@ -4,4 +4,4 @@ packages: - "@typespec/http-server-csharp" --- -Only emit types that belong to the service. Previously every model, enum and union declared in any imported library (`Azure.Core`, `Azure.ResourceManager`, ...) was emitted, producing large amounts of dead C# code and `anonymous-model` warnings for library types the spec author cannot change. Types declared outside the service namespace are now emitted only when the service references them, directly or transitively, and diagnostics are reported only for types the service itself declares. +Only emit types that belong to the service. Types declared outside the service namespace, for example in an imported library, are now emitted only when the service references them, directly or transitively. diff --git a/packages/http-server-csharp/src/diagnostics.ts b/packages/http-server-csharp/src/diagnostics.ts index de46ceb6ab5..352bbce82ea 100644 --- a/packages/http-server-csharp/src/diagnostics.ts +++ b/packages/http-server-csharp/src/diagnostics.ts @@ -10,8 +10,8 @@ import { isValidCSharpIdentifier } from "./utils/naming.js"; * Reports diagnostic warnings for models, scalars, and operations. * This pre-pass mirrors the old emitter behavior so that `tester.diagnose()` tests pass. * - * Only types declared by the service itself are checked: types coming from imported - * libraries are not actionable for the spec author, and most of them are never emitted. + * Only types declared by the service itself are checked, since diagnostics on + * types declared elsewhere are not actionable for the spec author. */ export function reportEmitterDiagnostics( program: Program, diff --git a/packages/http-server-csharp/src/service-discovery.ts b/packages/http-server-csharp/src/service-discovery.ts index f3c7eb98b02..1006defa819 100644 --- a/packages/http-server-csharp/src/service-discovery.ts +++ b/packages/http-server-csharp/src/service-discovery.ts @@ -14,9 +14,8 @@ import { getCSharpIdentifier, NameCasingType } from "./utils/naming.js"; * Collects the namespaces whose declarations are emitted even when nothing references them. * * When the spec declares one or more services, only the service namespaces and their - * sub-namespaces qualify. Types coming from imported libraries (`Azure.Core`, - * `Azure.ResourceManager`, ...) live outside of those namespaces and are therefore only - * emitted when the service actually references them. + * sub-namespaces qualify; types declared elsewhere are emitted only when the service + * references them. * * When no service is declared there is nothing to anchor the emit on, so every * non-standard namespace is treated as part of the service. diff --git a/packages/http-server-csharp/src/service-resolution.test.ts b/packages/http-server-csharp/src/service-resolution.test.ts index 04a06a7c523..4a80144616b 100644 --- a/packages/http-server-csharp/src/service-resolution.test.ts +++ b/packages/http-server-csharp/src/service-resolution.test.ts @@ -19,14 +19,14 @@ async function resolve(code: string): Promise { it("excludes models declared outside the service namespace that are not referenced", async () => { const resolution = await resolve(` - namespace Library { + namespace Other { model Used { name: string; } model Unused { name: string; } } @service namespace Contoso { - model Widget { id: string; used: Library.Used; } + model Widget { id: string; used: Other.Used; } op read(): Widget; } `); @@ -36,7 +36,7 @@ it("excludes models declared outside the service namespace that are not referenc it("excludes enums and union enums declared outside the service namespace that are not referenced", async () => { const resolution = await resolve(` - namespace Library { + namespace Other { enum UsedEnum { red, blue } enum UnusedEnum { up, down } union UsedUnion { "on", "off" } @@ -45,7 +45,7 @@ it("excludes enums and union enums declared outside the service namespace that a @service namespace Contoso { - model Widget { color: Library.UsedEnum; state: Library.UsedUnion; } + model Widget { color: Other.UsedEnum; state: Other.UsedUnion; } op read(): Widget; } `); @@ -54,9 +54,9 @@ it("excludes enums and union enums declared outside the service namespace that a expect(resolution.unionEnums.map((u) => u.name)).toEqual(["UsedUnion"]); }); -it("discovers types referenced transitively through operations only", async () => { +it("discovers types referenced transitively by an operation", async () => { const resolution = await resolve(` - namespace Library { + namespace Other { model Envelope { detail: Detail; } model Detail { kind: Kind; } enum Kind { simple, complex } @@ -65,7 +65,7 @@ it("discovers types referenced transitively through operations only", async () = @service namespace Contoso { - op read(): Library.Envelope; + op read(): Other.Envelope; } `); @@ -75,9 +75,9 @@ it("discovers types referenced transitively through operations only", async () = it("does not create interfaces for operations declared outside the service namespace", async () => { const resolution = await resolve(` - namespace Library { - interface LibraryOps { - @route("/lib") libRead(): void; + namespace Other { + interface OtherOps { + @route("/other") otherRead(): void; } } @@ -92,9 +92,9 @@ it("does not create interfaces for operations declared outside the service names expect(resolution.interfaces.map((i) => i.name)).toEqual(["Widgets"]); }); -it("emits every user namespace when no service is declared", async () => { +it("emits every namespace when no service is declared", async () => { const resolution = await resolve(` - namespace Library { + namespace Other { model Standalone { name: string; } } diff --git a/packages/http-server-csharp/src/service-resolution.ts b/packages/http-server-csharp/src/service-resolution.ts index 54d39e10344..b0a6dc40afa 100644 --- a/packages/http-server-csharp/src/service-resolution.ts +++ b/packages/http-server-csharp/src/service-resolution.ts @@ -53,8 +53,8 @@ export interface ServiceTypeResolution { * namespace traversals that previously occurred in individual components. * * Only types declared in the service namespace(s) are emitted unconditionally. - * Types coming from imported libraries are emitted only when the service - * references them, directly or transitively. + * Types declared elsewhere are emitted only when the service references them, + * directly or transitively. * * Ordering: * 1. Service namespace discovery @@ -152,9 +152,8 @@ interface TypeCollector { * Collects every type that should be emitted: the declarations of the service * namespace(s) plus everything the service references, directly or transitively. * - * Types declared by imported libraries (`Azure.Core`, `Azure.ResourceManager`, ...) - * are only emitted when the service actually references them; emitting the rest - * would generate dead code for constructs the service never exposes. + * Types declared outside those namespaces are only emitted when referenced, so + * that the emitter does not generate dead code for types the service never exposes. * * @param authModels Models that back authentication schemes; these are excluded * from emission because they represent protocol metadata rather than payloads.