Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .chronus/changes/feat-response-doc-2025-11-22-13-13-51.md
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the change to the parser/compiler will require some more discussion, this might introduce some ambiguity in the grammar. It also is something that decorator and directive might need to align with.

On the other hand the change to http package I think can be done separately and should unblock the named union case described in the original issue.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
changeKind: feature
packages:
- "@typespec/compiler"
---

Parse inline doc comments.
Change formatter behavior to improve the readability of inline doc comments.
7 changes: 7 additions & 0 deletions .chronus/changes/feat-response-doc-2025-11-25-23-40-21.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@typespec/http"
---

Associate parsed inline doc comments with response descriptions.
51 changes: 42 additions & 9 deletions packages/compiler/src/core/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,19 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
return { pos, docs, directives, decorators };
}

function parseInlineDocComments(): {
pos: number;
docs: DocNode[];
} {
const docs: DocNode[] = [];
const [pos, addedDocs] = parseDocList();
for (const doc of addedDocs) {
docs.push(doc);
}

return { pos, docs };
}

function parseTypeSpecScriptItemList(): Statement[] {
const stmts: Statement[] = [];
let seenBlocklessNs = false;
Expand Down Expand Up @@ -841,7 +854,9 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
if (token === Token.OpenParen) {
const parameters = parseOperationParameters();
parseExpected(Token.Colon);
const returnType = parseExpression();
// try to parse inline docs
const { docs } = parseInlineDocComments();
const returnType = parseExpression(docs);

signature = {
kind: SyntaxKind.OperationSignatureDeclaration,
Expand Down Expand Up @@ -1228,7 +1243,9 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
const { items: templateParameters, range: templateParametersRange } =
parseTemplateParameterList();
parseExpected(Token.Equals);
const value = parseExpression();
// try to parse inline docs
const { docs } = parseInlineDocComments();
const value = parseExpression(docs);
parseExpected(Token.Semicolon);
return {
kind: SyntaxKind.AliasStatement,
Expand Down Expand Up @@ -1263,23 +1280,39 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
return undefined;
}

function parseExpression(): Expression {
return parseUnionExpressionOrHigher();
function parseExpression(docs: DocNode[] = []): Expression {
return parseUnionExpressionOrHigher(docs);
}

function parseUnionExpressionOrHigher(): Expression {
const pos = tokenPos();
function parseUnionExpressionOrHigher(exprDocs: DocNode[]): Expression {
parseOptional(Token.Bar);
// try to parse inline docs
const { docs: rideSideDocs, pos } = parseInlineDocComments();
// doc comments right side of `|` take precedence over left side
// e.g.
// op foo: /** exprDocs */ | /** rideSideDocs */ MyModel;
const docs = rideSideDocs.length > 0 ? rideSideDocs : exprDocs;
const node: Expression = parseIntersectionExpressionOrHigher();
const expr = {
...node,
docs,
pos,
};

if (token() !== Token.Bar) {
return node;
return expr;
}

const options = [node];
const options = [expr];
while (parseOptional(Token.Bar)) {
// try to parse inline docs
const { docs, pos } = parseInlineDocComments();
const expr = parseIntersectionExpressionOrHigher();
options.push(expr);
options.push({
...expr,
docs,
pos,
});
}

return {
Expand Down
20 changes: 19 additions & 1 deletion packages/compiler/src/formatter/print/printer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
EnumMemberNode,
EnumSpreadMemberNode,
EnumStatementNode,
Expression,
FunctionDeclarationStatementNode,
FunctionParameterNode,
IdentifierNode,
Expand Down Expand Up @@ -1267,12 +1268,29 @@ export function printNamespaceStatement(
return [decorators, `namespace `, join(".", names), suffix];
}

function shouldWrapOperationSignatureDeclarationInNewLines(returnType: Expression) {
if (
returnType.kind === SyntaxKind.TypeReference ||
returnType.kind === SyntaxKind.ModelExpression
) {
return returnType.docs && returnType.docs.length > 0;
}
return false;
}

export function printOperationSignatureDeclaration(
path: AstPath<OperationSignatureDeclarationNode>,
options: TypeSpecPrettierOptions,
print: PrettierChildPrint,
) {
return ["(", path.call(print, "parameters"), "): ", path.call(print, "returnType")];
const node = path.node;
let closeParenColon = "): ";
// if inline doc comments on return type, move to new line
const shouldAddNewLine = shouldWrapOperationSignatureDeclarationInNewLines(node.returnType);
if (shouldAddNewLine) {
closeParenColon = "):\n";
}
return ["(", path.call(print, "parameters"), closeParenColon, path.call(print, "returnType")];
}

export function printOperationSignatureReference(
Expand Down
79 changes: 79 additions & 0 deletions packages/compiler/test/formatter/formatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,85 @@ op foo(
});
});
});

describe("inline doc comments", () => {
it("single inline expression", async () => {
await assertFormat({
code: `
op foo(): /** inline doc comment */ { @statusCode _: 200; content: string; };`,
expected: `
op foo():
/** inline doc comment */
{
@statusCode _: 200;
content: string;
};`,
});
});

it("single model statement", async () => {
await assertFormat({
code: `
model SuccessResponse {
@statusCode _: 200;
content: string;
}
op foo(): /** inline doc comment */
SuccessResponse;`,
expected: `
model SuccessResponse {
@statusCode _: 200;
content: string;
}
op foo():
/** inline doc comment */
SuccessResponse;`,
});
});

it("union expression", async () => {
await assertFormat({
code: `
model Success201 { @statusCode _: 201; content: string; }
model Success204 { @statusCode _: 204; content: string; }
model Error400 { @statusCode _: 400; content: string; }

op foo():
/** 200 */
{ @statusCode _: 200; content: string; } |
/**
* 201
*/
Success201 | Success204 | Error400;`,
expected: `
model Success201 {
@statusCode _: 201;
content: string;
}
model Success204 {
@statusCode _: 204;
content: string;
}
model Error400 {
@statusCode _: 400;
content: string;
}

op foo():
| /** 200 */
{
@statusCode _: 200;
content: string;
}
| /**
* 201
*/
Success201
| Success204
| Error400;`,
});
});
});
});

describe("scalar", () => {
Expand Down
76 changes: 76 additions & 0 deletions packages/compiler/test/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1091,6 +1091,82 @@ describe("compiler: parser", () => {
strictEqual(comments[0].parsedAsDocs, true);
});

it("mark used block comment with parsedAsDocs at operation", () => {
const script = parse(
`
op foo(): /** One-liner */ |
{ bar: string };
`,
{ docs: true, comments: true },
);
const comments = script.comments;
strictEqual(comments[0].kind, SyntaxKind.BlockComment);
strictEqual(comments[0].parsedAsDocs, true);
});

describe("mark used block comment with parsedAsDocs at union", () => {
parseEach(
[
[
`
op foo(): | /** First-union */
{ bar: string } | /** Second-union */ { baz: string };
`,
(script) => {
const comments = script.comments;
strictEqual(comments[0].kind, SyntaxKind.BlockComment);
strictEqual(comments[0].parsedAsDocs, true);
strictEqual(comments[1].kind, SyntaxKind.BlockComment);
strictEqual(comments[1].parsedAsDocs, true);
},
],
[
`
alias MyResponse = /** First-union */ { bar: string } | /** Second-union */ { baz: string };
op foo(): MyResponse;
`,
(script) => {
const comments = script.comments;
strictEqual(comments[0].kind, SyntaxKind.BlockComment);
strictEqual(comments[0].parsedAsDocs, true);
strictEqual(comments[1].kind, SyntaxKind.BlockComment);
strictEqual(comments[1].parsedAsDocs, true);
},
],
[
`
model Second { baz: string };
union MyResponse { /** First-union */ { bar: string }; /** Second-union */ Second; };
op foo(): MyResponse;
`,
(script) => {
const comments = script.comments;
strictEqual(comments[0].kind, SyntaxKind.BlockComment);
strictEqual(comments[0].parsedAsDocs, true);
strictEqual(comments[1].kind, SyntaxKind.BlockComment);
strictEqual(comments[1].parsedAsDocs, true);
},
],
[
`
/** This is Second model */
model Second { baz: string };
union MyResponse { /** First-union */ { bar: string }; /** Second-union */ Second; };
op foo(): /** Override MyResponse union */ MyResponse | /** Override third-union */ { qux: string };
`,
(script) => {
const comments = script.comments;
strictEqual(comments[0].kind, SyntaxKind.BlockComment);
strictEqual(comments[0].parsedAsDocs, true);
strictEqual(comments[1].kind, SyntaxKind.BlockComment);
strictEqual(comments[1].parsedAsDocs, true);
},
],
],
{ docs: true, comments: true },
);
});

it("other comments are not marked with parsedAsDocs", () => {
const script = parse(
`
Expand Down
Loading