Skip to content
Merged
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
28 changes: 17 additions & 11 deletions packages/typescript/src/ast/jsdoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {

/** Get all JSDoc tags related to a node, including those on parent nodes. */
export function getJSDocTags(node: Node): readonly JSDocTag[] {
return getJSDocCommentsAndTags(node);
return getJSDocCommentsAndTags(node).flatMap(j => isJSDoc(j) ? j.tags ?? [] : j);
}

/** Gets all JSDoc tags that match a specified predicate */
Expand Down Expand Up @@ -95,21 +95,27 @@ function ownsJSDocTag(hostNode: Node, tag: JSDocTag): boolean {
|| tag.parent.parent === hostNode;
}

function filterOwnedJSDocTags(hostNode: Node, comments: JSDoc[]): JSDocTag[] {
const result: JSDocTag[] = [];
function filterOwnedJSDocTags(hostNode: Node, comments: JSDoc[]): (JSDoc | JSDocTag)[] {
const result: (JSDoc | JSDocTag)[] = [];
const lastJSDoc = comments[comments.length - 1];
for (const jsDoc of comments) {
if (!jsDoc.tags) {
continue;
}
if (jsDoc === lastJSDoc) {
for (const tag of jsDoc.tags) {
if (ownsJSDocTag(hostNode, tag)) {
result.push(tag);
const onlyOwnTags = jsDoc.tags?.every(t => ownsJSDocTag(hostNode, t)) ?? true;
if (!onlyOwnTags && jsDoc.tags) {
for (const tag of jsDoc.tags) {
if (ownsJSDocTag(hostNode, tag)) {
result.push(tag);
}
}
}
else {
result.push(jsDoc);
}
}
else {
if (!jsDoc.tags) {
continue;
}
// Tags from earlier comments only contribute their `@overload` tags.
for (const tag of jsDoc.tags) {
if (isJSDocOverloadTag(tag)) {
Expand Down Expand Up @@ -181,8 +187,8 @@ function getNextJSDocCommentLocation(node: Node): Node | undefined {
return undefined;
}

function getJSDocCommentsAndTags(hostNode: Node): JSDocTag[] {
const result: JSDocTag[] = [];
export function getJSDocCommentsAndTags(hostNode: Node): (JSDoc | JSDocTag)[] {
const result: (JSDoc | JSDocTag)[] = [];
// Pull parameter comments from a declaring initializer (e.g. `var x = function () {}`).
if (isVariableLike(hostNode)) {
const initializer = (hostNode as { initializer?: Node; }).initializer;
Expand Down
152 changes: 152 additions & 0 deletions packages/typescript/test/async/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ import {
cast,
escapeLeadingUnderscores,
type Expression,
getJSDocCommentsAndTags,
getJSDocTags,
getSynthesizedDeepClone,
getTextOfJSDocComment,
InternalSymbolName,
isCallExpression,
isExpressionStatement,
isFunctionDeclaration,
isIdentifier,
isImportDeclaration,
isInterfaceDeclaration,
isJSDoc,
isJSDocParameterTag,
isModuleDeclaration,
isNamedImports,
Expand Down Expand Up @@ -5739,6 +5742,155 @@ const cast = /** @type {number} */ (someValue);
});
});

describe("ast - getJSDocCommentsAndTags", { concurrency }, () => {
test("returns the whole JSDoc comment when the host node owns all of its tags", async () => {
await using api = spawnAPI({
"/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }),
"/src/main.ts": `
/**
* The answer to everything.
* @deprecated use theAnswer instead
*/
export const answer = 42;
`,
});

const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" });
const project = snapshot.getConfiguredProject("/tsconfig.json")!;
const sourceFile = await project.program.getSourceFile("/src/main.ts");
assert.ok(sourceFile);
const answer = [...sourceFile.statements].filter(isVariableStatement)[0].declarationList.declarations[0];
assert.ok(answer);

// Every tag on `answer`'s JSDoc comment is owned by `answer`, so the whole
// comment is returned as a single entry rather than its individual tags.
const commentsAndTags = getJSDocCommentsAndTags(answer);
assert.equal(commentsAndTags.length, 1);
assert.ok(isJSDoc(commentsAndTags[0]));

// Flattening still yields the same tags as getJSDocTags.
assert.deepEqual(getJSDocTags(answer).map(t => t.tagName.text), ["deprecated"]);
});

test("returns the whole JSDoc comment when it has a description but no tags", async () => {
await using api = spawnAPI({
"/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }),
"/src/main.ts": `
/**
* The answer to everything.
*/
export const answer = 42;
`,
});

const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" });
const project = snapshot.getConfiguredProject("/tsconfig.json")!;
const sourceFile = await project.program.getSourceFile("/src/main.ts");
assert.ok(sourceFile);
const answer = [...sourceFile.statements].filter(isVariableStatement)[0].declarationList.declarations[0];
assert.ok(answer);

// With no tags to discard, the JSDoc node (and its description) is
// still returned rather than an empty tag list.
const commentsAndTags = getJSDocCommentsAndTags(answer);
assert.equal(commentsAndTags.length, 1);
assert.ok(isJSDoc(commentsAndTags[0]));
assert.equal(getTextOfJSDocComment(commentsAndTags[0].comment), "The answer to everything.");

assert.deepEqual(getJSDocTags(answer), []);
});

test("returns individual tags when the host node does not own all of them", async () => {
Comment thread
andrewbranch marked this conversation as resolved.
await using api = spawnAPI({
"/tsconfig.json": JSON.stringify({ compilerOptions: { allowJs: true, checkJs: true } }),
"/src/main.js": `
/** @type {string} */
const value = "hello";

const cast = /** @type {number} */ (someValue);
`,
});

const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" });
const project = snapshot.getConfiguredProject("/tsconfig.json")!;
const sourceFile = await project.program.getSourceFile("/src/main.js");
assert.ok(sourceFile);
const statements = [...sourceFile.statements].filter(isVariableStatement);

// The @type cast tag is not owned by `castDecl`, so no tags qualify and
// the JSDoc comment is not returned at all.
const castDecl = statements[1].declarationList.declarations[0];
assert.deepEqual(getJSDocCommentsAndTags(castDecl), []);
});

test("returns an individual JSDocTag for a parameter matched by name", async () => {
await using api = spawnAPI({
"/tsconfig.json": JSON.stringify({ compilerOptions: { allowJs: true, checkJs: true } }),
"/src/main.js": `
/**
* @param {string} name the name to measure
* @returns {number} the length
*/
var measure = function (name) {
return name.length;
};
`,
});

const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" });
const project = snapshot.getConfiguredProject("/tsconfig.json")!;
const sourceFile = await project.program.getSourceFile("/src/main.js");
assert.ok(sourceFile);
const variable = sourceFile.statements.find(isVariableStatement);
assert.ok(variable);
const funcExpr = variable.declarationList.declarations[0].initializer;
assert.ok(funcExpr);
const param = (funcExpr as unknown as { parameters: NodeArray<Node>; }).parameters[0];

// A Parameter host node returns its matching @param tag directly as a
// JSDocTag, not wrapped in the enclosing JSDoc comment.
const commentsAndTags = getJSDocCommentsAndTags(param);
assert.equal(commentsAndTags.length, 1);
const [tag] = commentsAndTags;
assert.ok(isJSDocParameterTag(tag));
assert.ok(isIdentifier(tag.name));
assert.equal(tag.name.text, "name");
});

test("returns the whole JSDoc comment when the host node owns multiple tags", async () => {
await using api = spawnAPI({
"/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }),
"/src/main.ts": `
/**
* Adds two numbers.
* @param a the first number
* @param b the second number
* @returns the sum
*/
export function add(a: number, b: number): number {
return a + b;
}
`,
});

const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" });
const project = snapshot.getConfiguredProject("/tsconfig.json")!;
const sourceFile = await project.program.getSourceFile("/src/main.ts");
assert.ok(sourceFile);
const add = [...sourceFile.statements].find(isFunctionDeclaration);
assert.ok(add);

// All three tags on `add`'s JSDoc comment are owned by `add`, so the
// whole comment is returned as a single entry rather than three tags.
const commentsAndTags = getJSDocCommentsAndTags(add);
assert.equal(commentsAndTags.length, 1);
assert.ok(isJSDoc(commentsAndTags[0]));

// Flattening still yields all three tags, in order, via getJSDocTags.
assert.deepEqual(getJSDocTags(add).map(t => t.tagName.text), ["param", "param", "returns"]);
});
});

describe("Checker - getPropertiesOfType", { concurrency }, () => {
test("returns properties of an object type", async () => {
await using api = spawnAPI({
Expand Down
Loading
Loading