Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for @prop doc comment tag to describe model properties #3527

Merged
merged 10 commits into from
Jun 11, 2024
8 changes: 8 additions & 0 deletions .chronus/changes/feature-prop-doc-2024-5-5-21-1-58.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: feature
packages:
- "@typespec/compiler"
---

Add support for `@prop` doc comment tag to describe model properties
2 changes: 1 addition & 1 deletion grammars/typespec.json
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@
},
"doc-comment-param": {
"name": "comment.block.tsp",
"match": "(?x)((@)(?:param|template))\\s+(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)\\b",
"match": "(?x)((@)(?:param|template|prop))\\s+(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)\\b",
"captures": {
"1": {
"name": "keyword.tag.tspdoc"
Expand Down
58 changes: 42 additions & 16 deletions packages/compiler/src/core/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2501,8 +2501,9 @@ export function createChecker(program: Program): Checker {

if (parameterModelSym?.members) {
const members = getOrCreateAugmentedSymbolTable(parameterModelSym.members);
const paramDocs = extractParamDocs(node);
for (const [name, memberSym] of members) {
const doc = extractParamDoc(node, name);
const doc = paramDocs[name];
if (doc) {
docFromCommentForSym.set(memberSym, doc);
}
Expand Down Expand Up @@ -3844,6 +3845,18 @@ export function createChecker(program: Program): Checker {
derivedModels: [],
});
linkType(links, type, mapper);

if (node.symbol.members) {
const members = getOrCreateAugmentedSymbolTable(node.symbol.members);
const propDocs = extractPropDocs(node);
for (const [name, memberSym] of members) {
const doc = propDocs[name];
if (doc) {
docFromCommentForSym.set(memberSym, doc);
}
}
}

const isBase = checkModelIs(node, node.is, mapper);

if (isBase) {
Expand All @@ -3859,7 +3872,8 @@ export function createChecker(program: Program): Checker {

if (isBase) {
for (const prop of isBase.properties.values()) {
const newProp = cloneType(prop, {
const memberSym = getMemberSymbol(node.symbol, prop.name)!;
const newProp = cloneTypeForSymbol(memberSym, prop, {
sourceProperty: prop,
model: type,
});
Expand Down Expand Up @@ -5125,7 +5139,8 @@ export function createChecker(program: Program): Checker {
prop: ModelPropertyNode,
mapper: TypeMapper | undefined
): ModelProperty {
const symId = getSymbolId(getSymbolForMember(prop)!);
const sym = getSymbolForMember(prop)!;
const symId = getSymbolId(sym);
const links = getSymbolLinksForMember(prop);

if (links && links.declaredType && mapper === undefined) {
Expand Down Expand Up @@ -5172,14 +5187,9 @@ export function createChecker(program: Program): Checker {
linkMapper(type, mapper);

if (!parentTemplate || shouldCreateTypeForTemplate(parentTemplate, mapper)) {
if (
prop.parent?.parent?.kind === SyntaxKind.OperationSignatureDeclaration &&
prop.parent.parent.parent?.kind === SyntaxKind.OperationStatement
) {
const doc = extractParamDoc(prop.parent.parent.parent, type.name);
if (doc) {
type.decorators.unshift(createDocFromCommentDecorator("self", doc));
}
const docComment = docFromCommentForSym.get(sym);
if (docComment) {
type.decorators.unshift(createDocFromCommentDecorator("self", docComment));
}
finishType(type);
}
Expand Down Expand Up @@ -8442,18 +8452,34 @@ function extractReturnsDocs(type: Type): {
return result;
}

function extractParamDoc(node: OperationStatementNode, paramName: string): string | undefined {
function extractParamDocs(node: OperationStatementNode): Record<string, string> {
if (node.docs === undefined) {
return undefined;
return {};
}
const paramDocs: Record<string, string> = {};
for (const doc of node.docs) {
for (const tag of doc.tags) {
if (tag.kind === SyntaxKind.DocParamTag && tag.paramName.sv === paramName) {
return getDocContent(tag.content);
if (tag.kind === SyntaxKind.DocParamTag) {
paramDocs[tag.paramName.sv] = getDocContent(tag.content);
}
}
}
return undefined;
return paramDocs;
}

function extractPropDocs(node: ModelStatementNode): Record<string, string> {
if (node.docs === undefined) {
return {};
}
const propDocs: Record<string, string> = {};
for (const doc of node.docs) {
for (const tag of doc.tags) {
if (tag.kind === SyntaxKind.DocPropTag) {
propDocs[tag.propName.sv] = getDocContent(tag.content);
}
}
}
return propDocs;
}

function getDocContent(content: readonly DocContent[]) {
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/core/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ const diagnostics = {
default: "Invalid identifier.",
tag: "Invalid tag name. Use backticks around code if this was not meant to be a tag.",
param: "Invalid parameter name.",
prop: "Invalid property name.",
templateParam: "Invalid template parameter name.",
},
},
Expand Down
33 changes: 30 additions & 3 deletions packages/compiler/src/core/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ import {
DocErrorsTagNode,
DocNode,
DocParamTagNode,
DocPropTagNode,
DocReturnsTagNode,
DocTag,
DocTemplateTagNode,
DocTextNode,
DocUnknownTagNode,
EmptyStatementNode,
EnumMemberNode,
Expand Down Expand Up @@ -2891,6 +2893,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
return parseDocParamLikeTag(pos, tagName, SyntaxKind.DocParamTag, "param");
case "template":
return parseDocParamLikeTag(pos, tagName, SyntaxKind.DocTemplateTag, "templateParam");
case "prop":
return parseDocPropTag(pos, tagName);
case "return":
case "returns":
return parseDocSimpleTag(pos, tagName, SyntaxKind.DocReturnsTag);
Expand All @@ -2911,9 +2915,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
kind: ParamLikeTag["kind"],
messageId: keyof CompilerDiagnostics["doc-invalid-identifier"]
): ParamLikeTag {
const name = parseDocIdentifier(messageId);
parseOptionalHyphenDocParamLikeTag();
const content = parseDocContent();
const { name, content } = parseDocParamLikeTagInternal(messageId);

return {
kind,
Expand All @@ -2924,6 +2926,27 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
};
}

function parseDocPropTag(pos: number, tagName: IdentifierNode): DocPropTagNode {
const { name, content } = parseDocParamLikeTagInternal("prop");

return {
kind: SyntaxKind.DocPropTag,
tagName,
propName: name,
content,
...finishNode(pos),
};
}

function parseDocParamLikeTagInternal(
messageId: keyof CompilerDiagnostics["doc-invalid-identifier"]
): { name: IdentifierNode; content: DocTextNode[] } {
const name = parseDocIdentifier(messageId);
parseOptionalHyphenDocParamLikeTag();
const content = parseDocContent();
return { name, content };
}

/**
* Handles the optional hyphen in param-like documentation comment tags.
*
Expand Down Expand Up @@ -3669,6 +3692,10 @@ export function visitChildren<T>(node: Node, cb: NodeCallback<T>): T | undefined
return (
visitNode(cb, node.tagName) || visitNode(cb, node.paramName) || visitEach(cb, node.content)
);
case SyntaxKind.DocPropTag:
return (
visitNode(cb, node.tagName) || visitNode(cb, node.propName) || visitEach(cb, node.content)
);
case SyntaxKind.DocReturnsTag:
case SyntaxKind.DocErrorsTag:
case SyntaxKind.DocUnknownTag:
Expand Down
7 changes: 7 additions & 0 deletions packages/compiler/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,7 @@ export enum SyntaxKind {
Doc,
DocText,
DocParamTag,
DocPropTag,
DocReturnsTag,
DocErrorsTag,
DocTemplateTag,
Expand Down Expand Up @@ -1903,6 +1904,7 @@ export type DocTag =
| DocReturnsTagNode
| DocErrorsTagNode
| DocParamTagNode
| DocPropTagNode
| DocTemplateTagNode
| DocUnknownTagNode;
export type DocContent = DocTextNode;
Expand All @@ -1925,6 +1927,11 @@ export interface DocParamTagNode extends DocTagBaseNode {
readonly paramName: IdentifierNode;
}

export interface DocPropTagNode extends DocTagBaseNode {
readonly kind: SyntaxKind.DocPropTag;
readonly propName: IdentifierNode;
}

export interface DocTemplateTagNode extends DocTagBaseNode {
readonly kind: SyntaxKind.DocTemplateTag;
readonly paramName: IdentifierNode;
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/formatter/print/printer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ export function printNode(
return printDoc(path as AstPath<DocNode>, options, print);
case SyntaxKind.DocText:
case SyntaxKind.DocParamTag:
case SyntaxKind.DocPropTag:
case SyntaxKind.DocTemplateTag:
case SyntaxKind.DocReturnsTag:
case SyntaxKind.DocErrorsTag:
Expand Down
4 changes: 4 additions & 0 deletions packages/compiler/src/server/classify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,10 @@ export function getSemanticTokens(ast: TypeSpecScriptNode): SemanticToken[] {
classifyDocTag(node.tagName, SemanticTokenKind.DocCommentTag);
classifyOverride(node.paramName, SemanticTokenKind.Variable);
break;
case SyntaxKind.DocPropTag:
classifyDocTag(node.tagName, SemanticTokenKind.DocCommentTag);
classifyOverride(node.propName, SemanticTokenKind.Variable);
break;
case SyntaxKind.DocReturnsTag:
classifyDocTag(node.tagName, SemanticTokenKind.DocCommentTag);
break;
Expand Down
4 changes: 3 additions & 1 deletion packages/compiler/src/server/tmlanguage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,13 +191,14 @@ const blockComment: BeginEndRule = {
const docCommentParam: MatchRule = {
key: "doc-comment-param",
scope: "comment.block.tsp",
match: `(?x)((@)(?:param|template))\\s+(${identifier})\\b`,
match: `(?x)((@)(?:param|template|prop))\\s+(${identifier})\\b`,
captures: {
"1": { scope: "keyword.tag.tspdoc" },
"2": { scope: "keyword.tag.tspdoc" },
"3": { scope: "variable.name.tsp" },
},
};

const docCommentReturn: MatchRule = {
key: "doc-comment-return-tag",
scope: "comment.block.tsp",
Expand All @@ -207,6 +208,7 @@ const docCommentReturn: MatchRule = {
"2": { scope: "keyword.tag.tspdoc" },
},
};

const docCommentUnknownTag: MatchRule = {
key: "doc-comment-unknown-tag",
scope: "comment.block.tsp",
Expand Down
Loading
Loading