Skip to content

Commit d989d8b

Browse files
fix(tspd): document generated auto decorator accessors
Use the decorator's own description so libraries re-exporting the accessors satisfy api-extractor.
1 parent dc6982f commit d989d8b

5 files changed

Lines changed: 202 additions & 30 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
changeKind: internal
3+
packages:
4+
- "@typespec/graphql"
5+
---
6+
7+
Regenerate the decorator signatures to pick up the doc comments now emitted for auto decorator
8+
accessors.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
changeKind: fix
3+
packages:
4+
- "@typespec/tspd"
5+
---
6+
7+
Document the generated auto decorator accessors with the description of the decorator they read or
8+
write, so libraries re-exporting them satisfy api-extractor's `ae-undocumented` rule.
9+
10+
```ts
11+
/** Mark a model as a GraphQL input type in the emitted schema. */
12+
export function isInputType(program: Program, target: Model): boolean {
13+
return hasAutoDecorator(program, "TypeSpec.GraphQL.inputType", target);
14+
}
15+
```

packages/graphql/generated-defs/TypeSpec.GraphQL.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,40 +184,92 @@ export type TypeSpecGraphQLDecorators = {
184184
specifiedBy: SpecifiedByDecorator;
185185
};
186186

187+
/**
188+
* Mark a model as a GraphQL input type in the emitted schema.
189+
*
190+
* This decorator is applied automatically by the mutation engine when it produces
191+
* a model that is used in input position. The emitter uses this to emit the model
192+
* as an `input` type rather than an object `type`.
193+
*/
187194
export function isInputType(program: Program, target: Model): boolean {
188195
return hasAutoDecorator(program, "TypeSpec.GraphQL.inputType", target);
189196
}
190197

198+
/**
199+
* Mark a model as a GraphQL input type in the emitted schema.
200+
*
201+
* This decorator is applied automatically by the mutation engine when it produces
202+
* a model that is used in input position. The emitter uses this to emit the model
203+
* as an `input` type rather than an object `type`.
204+
*/
191205
export function setInputType(program: Program, target: Model): void {
192206
setAutoDecorator(program, "TypeSpec.GraphQL.inputType", target);
193207
}
194208

209+
/**
210+
* Mark a field, operation, or type as nullable in the emitted GraphQL schema.
211+
*
212+
* Applied automatically by the mutation engine when it strips `| null` from
213+
* union types, and can also be applied directly in TypeSpec source.
214+
*/
195215
export function isNullable(
196216
program: Program,
197217
target: ModelProperty | Operation | Union | Model,
198218
): boolean {
199219
return hasAutoDecorator(program, "TypeSpec.GraphQL.nullable", target);
200220
}
201221

222+
/**
223+
* Mark a field, operation, or type as nullable in the emitted GraphQL schema.
224+
*
225+
* Applied automatically by the mutation engine when it strips `| null` from
226+
* union types, and can also be applied directly in TypeSpec source.
227+
*/
202228
export function setNullable(
203229
program: Program,
204230
target: ModelProperty | Operation | Union | Model,
205231
): void {
206232
setAutoDecorator(program, "TypeSpec.GraphQL.nullable", target);
207233
}
208234

235+
/**
236+
* Mark a field or operation as having nullable array elements in the emitted GraphQL schema.
237+
*
238+
* Applied automatically by the mutation engine when it detects `Array<T | null>`
239+
* patterns. Causes the emitter to emit `[T]` instead of `[T!]`.
240+
*/
209241
export function isNullableElements(program: Program, target: ModelProperty | Operation): boolean {
210242
return hasAutoDecorator(program, "TypeSpec.GraphQL.nullableElements", target);
211243
}
212244

245+
/**
246+
* Mark a field or operation as having nullable array elements in the emitted GraphQL schema.
247+
*
248+
* Applied automatically by the mutation engine when it detects `Array<T | null>`
249+
* patterns. Causes the emitter to emit `[T]` instead of `[T!]`.
250+
*/
213251
export function setNullableElements(program: Program, target: ModelProperty | Operation): void {
214252
setAutoDecorator(program, "TypeSpec.GraphQL.nullableElements", target);
215253
}
216254

255+
/**
256+
* Mark a model as a `@oneOf` input object in the emitted GraphQL schema.
257+
*
258+
* This decorator is applied automatically by the mutation engine when it converts
259+
* a union type in input context to a synthetic input object (since GraphQL unions
260+
* are output-only). The emitter uses this to emit the `@oneOf` directive.
261+
*/
217262
export function isOneOf(program: Program, target: Model): boolean {
218263
return hasAutoDecorator(program, "TypeSpec.GraphQL.oneOf", target);
219264
}
220265

266+
/**
267+
* Mark a model as a `@oneOf` input object in the emitted GraphQL schema.
268+
*
269+
* This decorator is applied automatically by the mutation engine when it converts
270+
* a union type in input context to a synthetic input object (since GraphQL unions
271+
* are output-only). The emitter uses this to emit the `@oneOf` directive.
272+
*/
221273
export function setOneOf(program: Program, target: Model): void {
222274
setAutoDecorator(program, "TypeSpec.GraphQL.oneOf", target);
223275
}

packages/tspd/src/gen-extern-signatures/components/auto-decorator-accessors.tsx

Lines changed: 84 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { code, For, List } from "@alloy-js/core";
22
import * as ts from "@alloy-js/typescript";
3+
import type { Decorator } from "@typespec/compiler";
34
import { typespecCompiler } from "../external-packages/compiler.js";
45
import type { DecoratorSignature } from "../types.js";
56
import { ParameterTsType, TargetParameterTsType } from "./decorator-signature-type.js";
@@ -51,17 +52,20 @@ function AutoDecoratorReader(props: Readonly<AutoDecoratorAccessorProps>) {
5152
if (params.length === 0) {
5253
// No-arg auto decorator — generate `is*` function
5354
return (
54-
<ts.FunctionDeclaration
55-
export
56-
name={`is${capitalizedName}`}
57-
parameters={[
58-
{ name: "program", type: typespecCompiler.Program },
59-
{ name: decorator.target.name, type: targetType },
60-
]}
61-
returnType="boolean"
62-
>
63-
{code`return ${typespecCompiler.hasAutoDecorator}(program, "${fqn}", ${decorator.target.name});`}
64-
</ts.FunctionDeclaration>
55+
<List hardline>
56+
<AccessorDoc decorator={decorator} />
57+
<ts.FunctionDeclaration
58+
export
59+
name={`is${capitalizedName}`}
60+
parameters={[
61+
{ name: "program", type: typespecCompiler.Program },
62+
{ name: decorator.target.name, type: targetType },
63+
]}
64+
returnType="boolean"
65+
>
66+
{code`return ${typespecCompiler.hasAutoDecorator}(program, "${fqn}", ${decorator.target.name});`}
67+
</ts.FunctionDeclaration>
68+
</List>
6569
);
6670
}
6771

@@ -99,17 +103,20 @@ function AutoDecoratorReader(props: Readonly<AutoDecoratorAccessorProps>) {
99103
}
100104

101105
return (
102-
<ts.FunctionDeclaration
103-
export
104-
name={`get${capitalizedName}`}
105-
parameters={[
106-
{ name: "program", type: typespecCompiler.Program },
107-
{ name: decorator.target.name, type: targetType },
108-
]}
109-
returnType={returnType}
110-
>
111-
{body}
112-
</ts.FunctionDeclaration>
106+
<List hardline>
107+
<AccessorDoc decorator={decorator} />
108+
<ts.FunctionDeclaration
109+
export
110+
name={`get${capitalizedName}`}
111+
parameters={[
112+
{ name: "program", type: typespecCompiler.Program },
113+
{ name: decorator.target.name, type: targetType },
114+
]}
115+
returnType={returnType}
116+
>
117+
{body}
118+
</ts.FunctionDeclaration>
119+
</List>
113120
);
114121
}
115122

@@ -161,13 +168,60 @@ function AutoDecoratorSetter(props: Readonly<AutoDecoratorAccessorProps>) {
161168
}
162169

163170
return (
164-
<ts.FunctionDeclaration
165-
export
166-
name={`set${capitalizedName}`}
167-
parameters={parameters}
168-
returnType="void"
169-
>
170-
{body}
171-
</ts.FunctionDeclaration>
171+
<List hardline>
172+
<AccessorDoc decorator={decorator} />
173+
<ts.FunctionDeclaration
174+
export
175+
name={`set${capitalizedName}`}
176+
parameters={parameters}
177+
returnType="void"
178+
>
179+
{body}
180+
</ts.FunctionDeclaration>
181+
</List>
172182
);
173183
}
184+
185+
/**
186+
* Render the decorator's own documentation as the accessor doc comment.
187+
*
188+
* Only the description is carried over: the `@param` tags of the decorator describe its TypeSpec
189+
* parameters, which do not line up with the accessor signatures.
190+
*
191+
* The comment is rendered standalone rather than through the `doc` prop of
192+
* `ts.FunctionDeclaration`, because that also emits `@param {Type}` tags whose type references count
193+
* as value usages and would turn the type-only imports of this file into value imports.
194+
*/
195+
function AccessorDoc(props: Readonly<{ decorator: Decorator }>) {
196+
const description = getDocDescription(props.decorator);
197+
if (description === undefined) {
198+
return null;
199+
}
200+
const lines = description.split("\n");
201+
const comment =
202+
lines.length === 1
203+
? `/** ${lines[0]} */`
204+
: [`/**`, ...lines.map((line) => ` * ${line}`.trimEnd()), ` */`].join("\n");
205+
return <>{comment}</>;
206+
}
207+
208+
/** Get the description of a decorator, excluding any doc tag. */
209+
function getDocDescription(decorator: Decorator): string | undefined {
210+
const docs = decorator.node?.docs;
211+
if (docs === undefined || docs.length === 0) {
212+
return undefined;
213+
}
214+
215+
const lines: string[] = [];
216+
for (const doc of docs) {
217+
for (const content of doc.content) {
218+
for (const line of content.text.split("\n")) {
219+
// Issue to escape @internal and other tsdoc tags https://github.com/microsoft/TypeScript/issues/47679
220+
lines.push(line.replaceAll("@internal", "@_internal"));
221+
}
222+
}
223+
}
224+
225+
const description = lines.join("\n").trim();
226+
return description === "" ? undefined : description;
227+
}

packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,49 @@ export function setMyMeta(program: Program, target: Model, value: { name: string
531531
});
532532
});
533533

534+
it("documents accessors with the decorator description, dropping doc tags", async () => {
535+
await expectSignatures({
536+
code: `
537+
/**
538+
* Specify the minimum number of instances the array must contain.
539+
*
540+
* @param value The minimum number of instances.
541+
*/
542+
auto dec myMin(target: Model, value: valueof int32);
543+
`,
544+
expected: `
545+
import { getAutoDecoratorValue, type Model, type Program, setAutoDecorator } from "@typespec/compiler";
546+
547+
/** Specify the minimum number of instances the array must contain. */
548+
export function getMyMin(program: Program, target: Model): number | undefined {
549+
return getAutoDecoratorValue(program, "myMin", target)?.["value"] as any;
550+
}
551+
552+
/** Specify the minimum number of instances the array must contain. */
553+
export function setMyMin(program: Program, target: Model, value: number): void {
554+
setAutoDecorator(program, "myMin", target, { value: value });
555+
}
556+
`,
557+
});
558+
});
559+
560+
it("renders a multi line description as a block comment", async () => {
561+
const result = await generateDecoratorSignatures(`
562+
/**
563+
* First line.
564+
*
565+
* Second line.
566+
*/
567+
auto dec myFlag(target: Model);
568+
`);
569+
expect(result).toContain(`/**
570+
* First line.
571+
*
572+
* Second line.
573+
*/
574+
export function isMyFlag`);
575+
});
576+
534577
it("generates accessor with fully-qualified name for namespaced auto decorator", async () => {
535578
const [{ program }] = await Tester.compileAndDiagnose(
536579
`

0 commit comments

Comments
 (0)