diff --git a/.chronus/changes/graphql-regen-auto-accessor-docs-2026-9-2.md b/.chronus/changes/graphql-regen-auto-accessor-docs-2026-9-2.md new file mode 100644 index 00000000000..4e77fe62a66 --- /dev/null +++ b/.chronus/changes/graphql-regen-auto-accessor-docs-2026-9-2.md @@ -0,0 +1,8 @@ +--- +changeKind: internal +packages: + - "@typespec/graphql" +--- + +Regenerate the decorator signatures to pick up the doc comments now emitted for auto decorator +accessors. diff --git a/.chronus/changes/tspd-auto-accessor-docs-2026-9-2.md b/.chronus/changes/tspd-auto-accessor-docs-2026-9-2.md new file mode 100644 index 00000000000..c444bc8cac2 --- /dev/null +++ b/.chronus/changes/tspd-auto-accessor-docs-2026-9-2.md @@ -0,0 +1,15 @@ +--- +changeKind: fix +packages: + - "@typespec/tspd" +--- + +Document the generated auto decorator accessors with the description of the decorator they read or +write, so libraries re-exporting them satisfy api-extractor's `ae-undocumented` rule. + +```ts +/** Mark a model as a GraphQL input type in the emitted schema. */ +export function isInputType(program: Program, target: Model): boolean { + return hasAutoDecorator(program, "TypeSpec.GraphQL.inputType", target); +} +``` diff --git a/.chronus/changes/tspd-honor-library-config-2026-9-2.md b/.chronus/changes/tspd-honor-library-config-2026-9-2.md new file mode 100644 index 00000000000..e9b8ea3c1d8 --- /dev/null +++ b/.chronus/changes/tspd-honor-library-config-2026-9-2.md @@ -0,0 +1,9 @@ +--- +changeKind: fix +packages: + - "@typespec/tspd" +--- + +Honor the library's own `tspconfig.yaml` when generating signatures and reference documentation, so +libraries that opt into a compiler feature (such as `auto-decorators`) no longer report errors during +`gen-extern-signature` and `doc`. diff --git a/packages/graphql/generated-defs/TypeSpec.GraphQL.ts b/packages/graphql/generated-defs/TypeSpec.GraphQL.ts index c854d6aa9f0..45595962d2c 100644 --- a/packages/graphql/generated-defs/TypeSpec.GraphQL.ts +++ b/packages/graphql/generated-defs/TypeSpec.GraphQL.ts @@ -184,14 +184,34 @@ export type TypeSpecGraphQLDecorators = { specifiedBy: SpecifiedByDecorator; }; +/** + * Mark a model as a GraphQL input type in the emitted schema. + * + * This decorator is applied automatically by the mutation engine when it produces + * a model that is used in input position. The emitter uses this to emit the model + * as an `input` type rather than an object `type`. + */ export function isInputType(program: Program, target: Model): boolean { return hasAutoDecorator(program, "TypeSpec.GraphQL.inputType", target); } +/** + * Mark a model as a GraphQL input type in the emitted schema. + * + * This decorator is applied automatically by the mutation engine when it produces + * a model that is used in input position. The emitter uses this to emit the model + * as an `input` type rather than an object `type`. + */ export function setInputType(program: Program, target: Model): void { setAutoDecorator(program, "TypeSpec.GraphQL.inputType", target); } +/** + * Mark a field, operation, or type as nullable in the emitted GraphQL schema. + * + * Applied automatically by the mutation engine when it strips `| null` from + * union types, and can also be applied directly in TypeSpec source. + */ export function isNullable( program: Program, target: ModelProperty | Operation | Union | Model, @@ -199,6 +219,12 @@ export function isNullable( return hasAutoDecorator(program, "TypeSpec.GraphQL.nullable", target); } +/** + * Mark a field, operation, or type as nullable in the emitted GraphQL schema. + * + * Applied automatically by the mutation engine when it strips `| null` from + * union types, and can also be applied directly in TypeSpec source. + */ export function setNullable( program: Program, target: ModelProperty | Operation | Union | Model, @@ -206,18 +232,44 @@ export function setNullable( setAutoDecorator(program, "TypeSpec.GraphQL.nullable", target); } +/** + * Mark a field or operation as having nullable array elements in the emitted GraphQL schema. + * + * Applied automatically by the mutation engine when it detects `Array` + * patterns. Causes the emitter to emit `[T]` instead of `[T!]`. + */ export function isNullableElements(program: Program, target: ModelProperty | Operation): boolean { return hasAutoDecorator(program, "TypeSpec.GraphQL.nullableElements", target); } +/** + * Mark a field or operation as having nullable array elements in the emitted GraphQL schema. + * + * Applied automatically by the mutation engine when it detects `Array` + * patterns. Causes the emitter to emit `[T]` instead of `[T!]`. + */ export function setNullableElements(program: Program, target: ModelProperty | Operation): void { setAutoDecorator(program, "TypeSpec.GraphQL.nullableElements", target); } +/** + * Mark a model as a `@oneOf` input object in the emitted GraphQL schema. + * + * This decorator is applied automatically by the mutation engine when it converts + * a union type in input context to a synthetic input object (since GraphQL unions + * are output-only). The emitter uses this to emit the `@oneOf` directive. + */ export function isOneOf(program: Program, target: Model): boolean { return hasAutoDecorator(program, "TypeSpec.GraphQL.oneOf", target); } +/** + * Mark a model as a `@oneOf` input object in the emitted GraphQL schema. + * + * This decorator is applied automatically by the mutation engine when it converts + * a union type in input context to a synthetic input object (since GraphQL unions + * are output-only). The emitter uses this to emit the `@oneOf` directive. + */ export function setOneOf(program: Program, target: Model): void { setAutoDecorator(program, "TypeSpec.GraphQL.oneOf", target); } diff --git a/packages/tspd/src/gen-extern-signatures/components/auto-decorator-accessors.tsx b/packages/tspd/src/gen-extern-signatures/components/auto-decorator-accessors.tsx index 37aa16099c9..9b84a6f0880 100644 --- a/packages/tspd/src/gen-extern-signatures/components/auto-decorator-accessors.tsx +++ b/packages/tspd/src/gen-extern-signatures/components/auto-decorator-accessors.tsx @@ -1,5 +1,6 @@ import { code, For, List } from "@alloy-js/core"; import * as ts from "@alloy-js/typescript"; +import type { Decorator } from "@typespec/compiler"; import { typespecCompiler } from "../external-packages/compiler.js"; import type { DecoratorSignature } from "../types.js"; import { ParameterTsType, TargetParameterTsType } from "./decorator-signature-type.js"; @@ -51,17 +52,20 @@ function AutoDecoratorReader(props: Readonly) { if (params.length === 0) { // No-arg auto decorator — generate `is*` function return ( - - {code`return ${typespecCompiler.hasAutoDecorator}(program, "${fqn}", ${decorator.target.name});`} - + + + + {code`return ${typespecCompiler.hasAutoDecorator}(program, "${fqn}", ${decorator.target.name});`} + + ); } @@ -99,17 +103,20 @@ function AutoDecoratorReader(props: Readonly) { } return ( - - {body} - + + + + {body} + + ); } @@ -161,13 +168,60 @@ function AutoDecoratorSetter(props: Readonly) { } return ( - - {body} - + + + + {body} + + ); } + +/** + * Render the decorator's own documentation as the accessor doc comment. + * + * Only the description is carried over: the `@param` tags of the decorator describe its TypeSpec + * parameters, which do not line up with the accessor signatures. + * + * The comment is rendered standalone rather than through the `doc` prop of + * `ts.FunctionDeclaration`, because that also emits `@param {Type}` tags whose type references count + * as value usages and would turn the type-only imports of this file into value imports. + */ +function AccessorDoc(props: Readonly<{ decorator: Decorator }>) { + const description = getDocDescription(props.decorator); + if (description === undefined) { + return null; + } + const lines = description.split("\n"); + const comment = + lines.length === 1 + ? `/** ${lines[0]} */` + : [`/**`, ...lines.map((line) => ` * ${line}`.trimEnd()), ` */`].join("\n"); + return <>{comment}; +} + +/** Get the description of a decorator, excluding any doc tag. */ +function getDocDescription(decorator: Decorator): string | undefined { + const docs = decorator.node?.docs; + if (docs === undefined || docs.length === 0) { + return undefined; + } + + const lines: string[] = []; + for (const doc of docs) { + for (const content of doc.content) { + for (const line of content.text.split("\n")) { + // Issue to escape @internal and other tsdoc tags https://github.com/microsoft/TypeScript/issues/47679 + lines.push(line.replaceAll("@internal", "@_internal")); + } + } + } + + const description = lines.join("\n").trim(); + return description === "" ? undefined : description; +} diff --git a/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts b/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts index ef10780f977..e30af6a81ee 100644 --- a/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts +++ b/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts @@ -25,6 +25,7 @@ import { } from "@typespec/compiler"; import prettier from "prettier"; import { createDiagnostic } from "../ref-doc/lib.js"; +import { resolveLibraryCompilerOptions } from "../utils/library-config.js"; import { generateSignatures } from "./components/entity-signatures.js"; import type { DecoratorSignature, EntitySignature, FunctionSignature } from "./types.js"; @@ -155,6 +156,7 @@ export async function generateExternSignatureForExports( for (const entry of exports) { programs.push( await compile(host, entry.typespecEntrypoint, { + ...(await resolveLibraryCompilerOptions(host, entry.typespecEntrypoint)), parseOptions: { comments: true, docs: true }, }), ); diff --git a/packages/tspd/src/ref-doc/experimental.ts b/packages/tspd/src/ref-doc/experimental.ts index 58726dd8271..6cdefbd2236 100644 --- a/packages/tspd/src/ref-doc/experimental.ts +++ b/packages/tspd/src/ref-doc/experimental.ts @@ -2,6 +2,7 @@ import type { Diagnostic } from "@typespec/compiler"; import { compile, createDiagnosticCollector, joinPaths, NodeHost } from "@typespec/compiler"; import { mkdir, writeFile } from "fs/promises"; import prettier from "prettier"; +import { resolveLibraryCompilerOptions } from "../utils/library-config.js"; import { generateJsApiDocs } from "./api-docs.js"; import { renderReadme } from "./emitters/markdown.js"; import { renderToAstroStarlightMarkdown } from "./emitters/starlight.js"; @@ -74,6 +75,7 @@ export async function resolveLibraryRefDocsBase( if (pkgJson.tspMain) { const main = joinPaths(libraryPath, pkgJson.tspMain); const program = await compile(NodeHost, main, { + ...(await resolveLibraryCompilerOptions(NodeHost, main)), parseOptions: { comments: true, docs: true }, }); const refDoc = diagnostics.pipe(extractRefDocs(program, options)); diff --git a/packages/tspd/src/ref-doc/extractor.ts b/packages/tspd/src/ref-doc/extractor.ts index 9d5e7f2ec28..a1620b8d7f7 100644 --- a/packages/tspd/src/ref-doc/extractor.ts +++ b/packages/tspd/src/ref-doc/extractor.ts @@ -46,6 +46,7 @@ import { import { SyntaxKind, type DocUnknownTagNode } from "@typespec/compiler/ast"; import { readFile } from "fs/promises"; import { pathToFileURL } from "url"; +import { resolveLibraryCompilerOptions } from "../utils/library-config.js"; import { createDiagnostic, reportDiagnostic } from "./lib.js"; import type { DecoratorRefDoc, @@ -109,6 +110,7 @@ export async function extractLibraryRefDocs( if (tspMain) { const main = resolvePath(libraryPath, tspMain); const program = await compile(NodeHost, main, { + ...(await resolveLibraryCompilerOptions(NodeHost, main)), parseOptions: { comments: true, docs: true }, }); mainSourceFiles = new Set(program.sourceFiles.keys()); @@ -203,6 +205,7 @@ async function extractSubExports( const main = resolvePath(libraryPath, tspEntry); try { const program = await compile(NodeHost, main, { + ...(await resolveLibraryCompilerOptions(NodeHost, main)), parseOptions: { comments: true, docs: true }, }); const subRefDoc = diagnostics.pipe( diff --git a/packages/tspd/src/utils/library-config.ts b/packages/tspd/src/utils/library-config.ts new file mode 100644 index 00000000000..079ade2b722 --- /dev/null +++ b/packages/tspd/src/utils/library-config.ts @@ -0,0 +1,19 @@ +import { + resolveCompilerOptions, + type CompilerHost, + type CompilerOptions, +} from "@typespec/compiler"; + +/** + * Resolve the compiler options from the library's own `tspconfig.yaml`, so that features it opts + * into (such as `auto-decorators`) apply when tspd compiles it. + * + * tspd only ever inspects a library, so emitting is always disabled. + */ +export async function resolveLibraryCompilerOptions( + host: CompilerHost, + entrypoint: string, +): Promise { + const [options] = await resolveCompilerOptions(host, { cwd: process.cwd(), entrypoint }); + return { ...options, noEmit: true }; +} diff --git a/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts b/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts index e9678c07a78..4dc9a33ad12 100644 --- a/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts +++ b/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts @@ -531,6 +531,49 @@ export function setMyMeta(program: Program, target: Model, value: { name: string }); }); + it("documents accessors with the decorator description, dropping doc tags", async () => { + await expectSignatures({ + code: ` + /** + * Specify the minimum number of instances the array must contain. + * + * @param value The minimum number of instances. + */ + auto dec myMin(target: Model, value: valueof int32); + `, + expected: ` +import { getAutoDecoratorValue, type Model, type Program, setAutoDecorator } from "@typespec/compiler"; + +/** Specify the minimum number of instances the array must contain. */ +export function getMyMin(program: Program, target: Model): number | undefined { + return getAutoDecoratorValue(program, "myMin", target)?.["value"] as any; +} + +/** Specify the minimum number of instances the array must contain. */ +export function setMyMin(program: Program, target: Model, value: number): void { + setAutoDecorator(program, "myMin", target, { value: value }); +} + `, + }); + }); + + it("renders a multi line description as a block comment", async () => { + const result = await generateDecoratorSignatures(` + /** + * First line. + * + * Second line. + */ + auto dec myFlag(target: Model); + `); + expect(result).toContain(`/** + * First line. + * + * Second line. + */ +export function isMyFlag`); + }); + it("generates accessor with fully-qualified name for namespaced auto decorator", async () => { const [{ program }] = await Tester.compileAndDiagnose( `