diff --git a/.chronus/changes/josh-experimental-diagnostic-id-2026-7-14-15-54-50.md b/.chronus/changes/josh-experimental-diagnostic-id-2026-7-14-15-54-50.md new file mode 100644 index 00000000000..db14f58c0fa --- /dev/null +++ b/.chronus/changes/josh-experimental-diagnostic-id-2026-7-14-15-54-50.md @@ -0,0 +1,12 @@ +--- +changeKind: feature +packages: + - "@typespec/http-client" +--- + +Add diagnostic and dependency metadata to `@experimental`. + +```typespec +@experimental(#{ diagnosticId: "C", dependsOn: #["A", "B"] }) +op bar(): void; +``` \ No newline at end of file diff --git a/packages/http-client/generated-defs/TypeSpec.HttpClient.ts b/packages/http-client/generated-defs/TypeSpec.HttpClient.ts index 9e5f21202e3..4f769d2126b 100644 --- a/packages/http-client/generated-defs/TypeSpec.HttpClient.ts +++ b/packages/http-client/generated-defs/TypeSpec.HttpClient.ts @@ -2,6 +2,8 @@ import type { DecoratorContext, DecoratorValidatorCallbacks, Type } from "@types export interface FeatureLifecycleOptions { readonly emitterScope?: string; + readonly diagnosticId?: string; + readonly dependsOn?: readonly string[]; } /** diff --git a/packages/http-client/lib/decorators.tsp b/packages/http-client/lib/decorators.tsp index 3e3b9a5396c..77282c83240 100644 --- a/packages/http-client/lib/decorators.tsp +++ b/packages/http-client/lib/decorators.tsp @@ -8,6 +8,17 @@ namespace TypeSpec.HttpClient; */ model FeatureLifecycleOptions { ...ClientDecoratorOptions; + + /** + * The diagnostic identifier surfaced by emitters for this experimental API. + */ + diagnosticId?: string; + + /** + * Diagnostic identifiers for experimental features this API depends on. These dependencies do + * not automatically remove this API's experimental status when they become generally available. + */ + dependsOn?: string[]; } /** diff --git a/packages/http-client/src/decorators/experimental.ts b/packages/http-client/src/decorators/experimental.ts index 2849b6846a4..5ce091ebc60 100644 --- a/packages/http-client/src/decorators/experimental.ts +++ b/packages/http-client/src/decorators/experimental.ts @@ -10,9 +10,15 @@ const featureLifecycleStateSymbol = createStateSymbol("featureLifecycleState"); export type FeatureLifecycleStage = "Experimental"; +export interface FeatureLifecycleDetails { + stage: FeatureLifecycleStage; + diagnosticId?: string; + dependsOn: readonly string[]; +} + const [getFeatureLifecycleState, setFeatureLifecycleState] = useStateMap< Type, - ScopedValue + ScopedValue >(featureLifecycleStateSymbol); export const $experimental: ExperimentalDecorator = (context, target, options) => { @@ -27,7 +33,11 @@ export const $experimental: ExperimentalDecorator = (context, target, options) = } setFeatureLifecycleState(context.program, target, { emitterFilter: scopeFilter, - value: "Experimental", + value: { + stage: "Experimental", + diagnosticId: options?.diagnosticId, + dependsOn: options?.dependsOn ?? [], + }, }); }; @@ -39,6 +49,15 @@ export function getClientFeatureLifecycle( target: Type, options: GetFeatureLifecycleOptions = {}, ): DiagnosticResult { + const [details, diagnostics] = getClientFeatureLifecycleDetails(program, target, options); + return [details?.stage, diagnostics]; +} + +export function getClientFeatureLifecycleDetails( + program: Program, + target: Type, + options: GetFeatureLifecycleOptions = {}, +): DiagnosticResult { const diagnostics = createDiagnosticCollector(); const lifecycle = getFeatureLifecycleState(program, target); diff --git a/packages/http-client/src/index.ts b/packages/http-client/src/index.ts index d8c0f943aaa..140224f638f 100644 --- a/packages/http-client/src/index.ts +++ b/packages/http-client/src/index.ts @@ -1,5 +1,10 @@ export const namespace = "TypeSpec.HttpClient"; export * from "./context/index.js"; +export type { + FeatureLifecycleDetails, + FeatureLifecycleStage, + GetFeatureLifecycleOptions, +} from "./decorators/experimental.js"; export type * from "./interfaces.js"; export { $lib } from "./lib.js"; export { $decorators } from "./tsp-index.js"; diff --git a/packages/http-client/src/typekit/kits/client.ts b/packages/http-client/src/typekit/kits/client.ts index 742926878cd..0d66812cce8 100644 --- a/packages/http-client/src/typekit/kits/client.ts +++ b/packages/http-client/src/typekit/kits/client.ts @@ -9,8 +9,14 @@ import { createDiagnosable, defineKit } from "@typespec/compiler/typekit"; import type { HttpOperation, HttpServer, HttpServiceAuthentication } from "@typespec/http"; import { getHttpService, getServers, resolveAuthentication } from "@typespec/http"; import "@typespec/http/experimental/typekit"; -import type { GetFeatureLifecycleOptions } from "../../decorators/experimental.js"; -import { getClientFeatureLifecycle } from "../../decorators/experimental.js"; +import type { + FeatureLifecycleDetails, + GetFeatureLifecycleOptions, +} from "../../decorators/experimental.js"; +import { + getClientFeatureLifecycle, + getClientFeatureLifecycleDetails, +} from "../../decorators/experimental.js"; import type { InternalClient } from "../../interfaces.js"; import { reportDiagnostic } from "../../lib.js"; import { createBaseConstructor, getConstructors } from "../../utils/client-helpers.js"; @@ -26,6 +32,14 @@ interface ClientKit extends NameKit { getFeatureLifecycle: Diagnosable< (type: Type, options?: GetFeatureLifecycleOptions) => string | undefined >; + /** + * Get the feature lifecycle details for a given type. + * @param type The type to get the feature lifecycle details for + * @param options The options to use when getting the feature lifecycle details + */ + getFeatureLifecycleDetails: Diagnosable< + (type: Type, options?: GetFeatureLifecycleOptions) => FeatureLifecycleDetails | undefined + >; /** * Get the parent of a client * @param type The client to get the parent of @@ -103,6 +117,9 @@ defineKit({ getFeatureLifecycle: createDiagnosable(function (type, options) { return getClientFeatureLifecycle(this.program, type, options); }), + getFeatureLifecycleDetails: createDiagnosable(function (type, options) { + return getClientFeatureLifecycleDetails(this.program, type, options); + }), getParent(client) { const type = client.kind === "Client" ? client.type : client; if (type.namespace && type.namespace !== this.program.getGlobalNamespaceType()) { diff --git a/packages/http-client/test/lib/experimental-decorator.test.ts b/packages/http-client/test/lib/experimental-decorator.test.ts index 11b87fdf27b..eaf0ab052cf 100644 --- a/packages/http-client/test/lib/experimental-decorator.test.ts +++ b/packages/http-client/test/lib/experimental-decorator.test.ts @@ -32,6 +32,60 @@ it("should get the feature lifecycle for a model property", async () => { expect(featureLifecycle).toBe("Experimental"); }); +it("should get experimental lifecycle details", async () => { + const { betaProp, program } = await runner.compile(t.code` + namespace Test; + + model MyModel { + @experimental(#{ + diagnosticId: "C", + dependsOn: #["A", "B"] + }) + ${t.modelProperty("betaProp")}: string; + } + `); + + const featureLifecycle = $(program).client.getFeatureLifecycle(betaProp); + const details = $(program).client.getFeatureLifecycleDetails(betaProp); + + expect(featureLifecycle).toBe("Experimental"); + expect(details).toEqual({ + stage: "Experimental", + diagnosticId: "C", + dependsOn: ["A", "B"], + }); +}); + +it("should filter experimental lifecycle details by emitter scope", async () => { + const { betaProp, program } = await runner.compile(t.code` + namespace Test; + + model MyModel { + @experimental(#{ + emitterScope: "myEmitter", + diagnosticId: "C", + dependsOn: #["A", "B"] + }) + ${t.modelProperty("betaProp")}: string; + } + `); + + expect( + $(program).client.getFeatureLifecycleDetails(betaProp, { + emitterName: "myEmitter", + }), + ).toEqual({ + stage: "Experimental", + diagnosticId: "C", + dependsOn: ["A", "B"], + }); + expect( + $(program).client.getFeatureLifecycleDetails(betaProp, { + emitterName: "otherEmitter", + }), + ).toBeUndefined(); +}); + it("should get the feature lifecycle for a model property within scope", async () => { const { betaProp, program } = await runner.compile(t.code` namespace Test;