Skip to content

Commit 8559778

Browse files
Doc comment used to set @doc value (microsoft#1921)
1 parent db8dd8e commit 8559778

17 files changed

+334
-37
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@typespec/compiler",
5+
"comment": "**Feature** Doc comment will be applied as the doc for types unless an explicit @doc is provided.",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@typespec/compiler"
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@typespec/http",
5+
"comment": "Uptake doc comment changes",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@typespec/http"
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@typespec/openapi3",
5+
"comment": "Uptake doc comment changes. Standard built-in scalar will not have the description included as they are inlined.",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@typespec/openapi3"
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@typespec/protobuf",
5+
"comment": "Uptake doc comment changes",
6+
"type": "minor"
7+
}
8+
],
9+
"packageName": "@typespec/protobuf"
10+
}

packages/compiler/core/checker.ts

+59-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getDeprecated, getIndexer } from "../lib/decorators.js";
1+
import { $docFromComment, getDeprecated, getIndexer } from "../lib/decorators.js";
22
import { createSymbol, createSymbolTable } from "./binder.js";
33
import { ProjectionError, compilerAssert } from "./diagnostics.js";
44
import { validateInheritanceDiscriminatedUnions } from "./helpers/discriminator-utils.js";
@@ -29,6 +29,7 @@ import {
2929
DecoratorExpressionNode,
3030
Diagnostic,
3131
DiagnosticTarget,
32+
DocContent,
3233
Enum,
3334
EnumMember,
3435
EnumMemberNode,
@@ -2959,7 +2960,20 @@ export function createChecker(program: Program): Checker {
29592960
type.decorators = checkDecorators(type, prop, mapper);
29602961
const parentTemplate = getParentTemplateNode(prop);
29612962
linkMapper(type, mapper);
2963+
29622964
if (!parentTemplate || shouldCreateTypeForTemplate(parentTemplate, mapper)) {
2965+
if (
2966+
prop.parent?.parent?.kind === SyntaxKind.OperationSignatureDeclaration &&
2967+
prop.parent.parent.parent?.kind === SyntaxKind.OperationStatement
2968+
) {
2969+
const doc = extractParamDoc(prop.parent.parent.parent, type.name);
2970+
if (doc) {
2971+
type.decorators.unshift({
2972+
decorator: $docFromComment,
2973+
args: [{ value: createLiteralType(doc) }],
2974+
});
2975+
}
2976+
}
29632977
finishType(type);
29642978
}
29652979

@@ -5270,12 +5284,56 @@ function linkMapper<T extends Type>(typeDef: T, mapper?: TypeMapper) {
52705284
}
52715285
}
52725286

5287+
function extractMainDoc(type: Type): string | undefined {
5288+
if (type.node?.docs === undefined) {
5289+
return undefined;
5290+
}
5291+
let mainDoc: string = "";
5292+
for (const doc of type.node.docs) {
5293+
mainDoc += getDocContent(doc.content);
5294+
}
5295+
return mainDoc;
5296+
}
5297+
5298+
function extractParamDoc(node: OperationStatementNode, paramName: string): string | undefined {
5299+
if (node.docs === undefined) {
5300+
return undefined;
5301+
}
5302+
for (const doc of node.docs) {
5303+
for (const tag of doc.tags) {
5304+
if (tag.kind === SyntaxKind.DocParamTag && tag.paramName.sv === paramName) {
5305+
return getDocContent(tag.content);
5306+
}
5307+
}
5308+
}
5309+
return undefined;
5310+
}
5311+
5312+
function getDocContent(content: readonly DocContent[]) {
5313+
const docs = [];
5314+
for (const node of content) {
5315+
compilerAssert(
5316+
node.kind === SyntaxKind.DocText,
5317+
"No other doc content node kinds exist yet. Update this code appropriately when more are added."
5318+
);
5319+
docs.push(node.text);
5320+
}
5321+
return docs.join("");
5322+
}
5323+
52735324
function finishTypeForProgramAndChecker<T extends Type>(
52745325
program: Program,
52755326
typePrototype: TypePrototype,
52765327
typeDef: T
52775328
): T {
52785329
if ("decorators" in typeDef) {
5330+
const docComment = extractMainDoc(typeDef);
5331+
if (docComment) {
5332+
typeDef.decorators.unshift({
5333+
decorator: $docFromComment,
5334+
args: [{ value: program.checker.createLiteralType(docComment) }],
5335+
});
5336+
}
52795337
for (const decApp of typeDef.decorators) {
52805338
applyDecoratorToType(program, decApp, typeDef);
52815339
}

packages/compiler/core/parser.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2210,7 +2210,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
22102210
}
22112211

22122212
function parseDocList(): [pos: number, nodes: DocNode[]] {
2213-
if (docRanges.length === 0 || !options.docs) {
2213+
if (docRanges.length === 0 || options.docs === false) {
22142214
return [tokenPos(), []];
22152215
}
22162216
const docs: DocNode[] = [];

packages/compiler/formatter/parser.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export function parse(
88
parsers: { [parserName: string]: Parser },
99
opts: ParserOptions & { parentParser?: string }
1010
): TypeSpecScriptNode {
11-
const result = typespecParse(text, { comments: true });
11+
const result = typespecParse(text, { comments: true, docs: false });
1212
const errors = result.parseDiagnostics.filter((x) => x.severity === "error");
1313
if (errors.length > 0 && !result.printable) {
1414
throw new PrettierParserError(errors[0]);

packages/compiler/lib/decorators.ts

+49-18
Original file line numberDiff line numberDiff line change
@@ -44,21 +44,6 @@ function replaceTemplatedStringFromProperties(formatString: string, sourceObject
4444
});
4545
}
4646

47-
function setTemplatedStringProperty(
48-
key: symbol,
49-
program: Program,
50-
target: Type,
51-
text: string,
52-
sourceObject?: Type
53-
) {
54-
// If an object was passed in, use it to format the documentation string
55-
if (sourceObject) {
56-
text = replaceTemplatedStringFromProperties(text, sourceObject);
57-
}
58-
59-
program.stateMap(key).set(target, text);
60-
}
61-
6247
function createStateSymbol(name: string) {
6348
return Symbol.for(`TypeSpec.${name}`);
6449
}
@@ -79,14 +64,31 @@ export function $summary(
7964
text: string,
8065
sourceObject: Type
8166
) {
82-
setTemplatedStringProperty(summaryKey, context.program, target, text, sourceObject);
67+
if (sourceObject) {
68+
text = replaceTemplatedStringFromProperties(text, sourceObject);
69+
}
70+
71+
context.program.stateMap(summaryKey).set(target, text);
8372
}
8473

8574
export function getSummary(program: Program, type: Type): string | undefined {
8675
return program.stateMap(summaryKey).get(type);
8776
}
8877

8978
const docsKey = createStateSymbol("docs");
79+
export interface DocData {
80+
/**
81+
* Doc value.
82+
*/
83+
value: string;
84+
85+
/**
86+
* How was the doc set.
87+
* - `@doc` means the `@doc` decorator was used
88+
* - `comment` means it was set from a `/** comment * /`
89+
*/
90+
source: "@doc" | "comment";
91+
}
9092
/**
9193
* @doc attaches a documentation string. Works great with multi-line string literals.
9294
*
@@ -97,13 +99,42 @@ const docsKey = createStateSymbol("docs");
9799
*/
98100
export function $doc(context: DecoratorContext, target: Type, text: string, sourceObject?: Type) {
99101
validateDecoratorUniqueOnNode(context, target, $doc);
100-
setTemplatedStringProperty(docsKey, context.program, target, text, sourceObject);
102+
if (sourceObject) {
103+
text = replaceTemplatedStringFromProperties(text, sourceObject);
104+
}
105+
setDocData(context.program, target, { value: text, source: "@doc" });
101106
}
102107

103-
export function getDoc(program: Program, target: Type): string | undefined {
108+
/**
109+
* @internal to be used to set the `@doc` from doc comment.
110+
*/
111+
export function $docFromComment(context: DecoratorContext, target: Type, text: string) {
112+
setDocData(context.program, target, { value: text, source: "comment" });
113+
}
114+
115+
function setDocData(program: Program, target: Type, data: DocData) {
116+
program.stateMap(docsKey).set(target, data);
117+
}
118+
/**
119+
* Get the documentation information for the given type. In most cases you probably just want to use {@link getDoc}
120+
* @param program Program
121+
* @param target Type
122+
* @returns Doc data with source information.
123+
*/
124+
export function getDocData(program: Program, target: Type): DocData | undefined {
104125
return program.stateMap(docsKey).get(target);
105126
}
106127

128+
/**
129+
* Get the documentation string for the given type.
130+
* @param program Program
131+
* @param target Type
132+
* @returns Documentation value
133+
*/
134+
export function getDoc(program: Program, target: Type): string | undefined {
135+
return getDocData(program, target)?.value;
136+
}
137+
107138
export function $inspectType(program: Program, target: Type, text: string) {
108139
// eslint-disable-next-line no-console
109140
if (text) console.log(text);

packages/compiler/server/type-details.ts

+12-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
import { compilerAssert, DocContent, Program, SyntaxKind, Type } from "../core/index.js";
2-
import { getDoc } from "../lib/decorators.js";
1+
import {
2+
compilerAssert,
3+
DocContent,
4+
getDocData,
5+
Program,
6+
SyntaxKind,
7+
Type,
8+
} from "../core/index.js";
39
import { getTypeSignature } from "./type-signature.js";
410

511
/**
@@ -51,9 +57,10 @@ function getTypeDocumentation(program: Program, type: Type) {
5157
}
5258

5359
// Add @doc(...) API docs
54-
const apiDocs = getDoc(program, type);
55-
if (apiDocs) {
56-
docs.push(apiDocs);
60+
const apiDocs = getDocData(program, type);
61+
// The doc comment is already included above we don't want to duplicate
62+
if (apiDocs && apiDocs.source === "@doc") {
63+
docs.push(apiDocs.value);
5764
}
5865

5966
return docs.join("\n\n");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { ok, strictEqual } from "assert";
2+
import { Model, Operation } from "../../core/index.js";
3+
import { getDoc } from "../../lib/decorators.js";
4+
import { BasicTestRunner, createTestRunner } from "../../testing/index.js";
5+
6+
describe("compiler: checker: doc comments", () => {
7+
let runner: BasicTestRunner;
8+
beforeEach(async () => {
9+
runner = await createTestRunner();
10+
});
11+
12+
const expectedMainDoc = "This is a doc comment.";
13+
const docComment = `/**
14+
* ${expectedMainDoc}
15+
*/`;
16+
function testMainDoc(name: string, code: string) {
17+
it(name, async () => {
18+
const { target } = await runner.compile(code);
19+
ok(target, `Make sure to have @test("target") in code.`);
20+
21+
strictEqual(getDoc(runner.program, target), expectedMainDoc);
22+
});
23+
}
24+
25+
describe("main doc apply to", () => {
26+
testMainDoc(
27+
"model",
28+
`${docComment}
29+
@test("target") model Foo {}`
30+
);
31+
32+
testMainDoc(
33+
"model property",
34+
`
35+
model Foo {
36+
${docComment}
37+
@test("target") foo: string;
38+
}
39+
`
40+
);
41+
42+
testMainDoc(
43+
"scalar",
44+
`${docComment}
45+
@test("target") scalar foo;`
46+
);
47+
48+
testMainDoc(
49+
"enum",
50+
`${docComment}
51+
@test("target") enum Foo {}`
52+
);
53+
54+
testMainDoc(
55+
"enum memember",
56+
`
57+
enum Foo {
58+
${docComment}
59+
@test("target") foo,
60+
}
61+
`
62+
);
63+
testMainDoc(
64+
"operation",
65+
`${docComment}
66+
@test("target") op test(): string;`
67+
);
68+
69+
testMainDoc(
70+
"interface",
71+
`${docComment}
72+
@test("target") interface Foo {}`
73+
);
74+
});
75+
76+
it("using @doc() decorator will override the doc comment", async () => {
77+
const { Foo } = (await runner.compile(`
78+
79+
/**
80+
* This is a doc comment.
81+
*/
82+
@doc("This is the actual doc.")
83+
@test model Foo {}
84+
`)) as { Foo: Model };
85+
86+
strictEqual(getDoc(runner.program, Foo), "This is the actual doc.");
87+
});
88+
89+
it("using @param in doc comment of operation applies doc on the parameters", async () => {
90+
const { addUser } = (await runner.compile(`
91+
92+
/**
93+
* This is the operation doc.
94+
* @param name This is the name param doc.
95+
* @param age This is the age param doc.
96+
*/
97+
@test op addUser(name: string, age: string): void;
98+
`)) as { addUser: Operation };
99+
100+
strictEqual(getDoc(runner.program, addUser), "This is the operation doc.");
101+
strictEqual(
102+
getDoc(runner.program, addUser.parameters.properties.get("name")!),
103+
"This is the name param doc."
104+
);
105+
strictEqual(
106+
getDoc(runner.program, addUser.parameters.properties.get("age")!),
107+
"This is the age param doc."
108+
);
109+
});
110+
});

0 commit comments

Comments
 (0)