Skip to content

Commit ea4e4c0

Browse files
feat: add iterateComments method (#758)
<!-- 👋 Hi, thanks for sending a PR to ts-api-utils! 💖. Please fill out all fields below and make sure each item is true and [x] checked. Otherwise we may not be able to review your PR. --> ## PR Checklist - [x] Addresses an existing open issue: fixes #755 - [x] That issue was marked as [`status: accepting prs`](https://github.com/JoshuaKGoldberg/ts-api-utils/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22) - [x] Steps in [CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/ts-api-utils/blob/main/.github/CONTRIBUTING.md) were taken ## Overview <!-- Description of what is changed and how the code change does that. --> This contains change from #756, if we decide to add them one by one, should merge #756 first. --------- Co-authored-by: Josh Goldberg ✨ <git@joshuakgoldberg.com>
1 parent f6fda9b commit ea4e4c0

2 files changed

Lines changed: 110 additions & 12 deletions

File tree

src/comments.test.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,53 @@
11
import ts from "typescript";
22
import { describe, expect, it, vitest } from "vitest";
33

4-
import { forEachComment } from "./comments";
4+
import { forEachComment, iterateComments } from "./comments";
55
import { createNodeAndSourceFile } from "./test/utils";
66

7+
describe("iterateComments", () => {
8+
it("Should iterate all comments", () => {
9+
const { node, sourceFile } = createNodeAndSourceFile(`
10+
// line comment${" "}
11+
/*
12+
block comment line 1${" "}
13+
block comment line 2${" "}
14+
*/
15+
let value;
16+
`);
17+
18+
const generator = iterateComments(node, sourceFile);
19+
expect(typeof generator[Symbol.iterator]).toBe("function");
20+
expect(generator.next()).toEqual({
21+
done: false,
22+
value: {
23+
end: 21,
24+
kind: ts.SyntaxKind.SingleLineCommentTrivia,
25+
pos: 4,
26+
text: "// line comment ",
27+
value: " line comment ",
28+
},
29+
});
30+
expect(generator.next()).toEqual({
31+
done: false,
32+
value: {
33+
end: 85,
34+
kind: ts.SyntaxKind.MultiLineCommentTrivia,
35+
pos: 25,
36+
text: `/*
37+
block comment line 1${" "}
38+
block comment line 2${" "}
39+
*/`,
40+
value: `
41+
block comment line 1${" "}
42+
block comment line 2${" "}
43+
`,
44+
},
45+
});
46+
expect(generator.next()).toEqual({ done: true, value: undefined });
47+
});
48+
});
49+
50+
// TODO: Move tests into `iterateComments`
751
describe("forEachComment", () => {
852
it("does not call the callback when the source is a variable with no comments", () => {
953
const { node, sourceFile } = createNodeAndSourceFile("let value;");

src/comments.ts

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ import ts from "typescript";
55

66
import { iterateTokens } from "./tokens";
77

8+
/**
9+
* Descriptive data for a comment as yielded by {@link iterateComments}.
10+
*/
11+
export type Comment = ts.CommentRange & {
12+
text: string;
13+
value: string;
14+
};
15+
816
/**
917
* Callback type used for {@link forEachComment}.
1018
* @category Callbacks
@@ -39,6 +47,28 @@ export function forEachComment(
3947
callback: ForEachCommentCallback,
4048
sourceFile: ts.SourceFile = node.getSourceFile(),
4149
): void {
50+
const fullText = sourceFile.text;
51+
for (const { end, kind, pos } of iterateComments(node, sourceFile)) {
52+
callback(fullText, { end, kind, pos });
53+
}
54+
}
55+
56+
/**
57+
* Iterates over all comments owned by `node` or its children.
58+
* @category Nodes - Other Utilities
59+
* @example
60+
* ```ts
61+
* declare const node: ts.Node;
62+
*
63+
* for (const {pos, text} of iterateComment(node) {
64+
* console.log(`Found comment at position ${pos}: '${text}'.`);
65+
* };
66+
* ```
67+
*/
68+
export function* iterateComments(
69+
node: ts.Node,
70+
sourceFile: ts.SourceFile = node.getSourceFile(),
71+
): Generator<Comment> {
4272
/* Visit all tokens and skip trivia.
4373
Comment ranges between tokens are parsed without the need of a scanner.
4474
forEachTokenWithWhitespace does intentionally not pay attention to the correct comment ownership of nodes as it always
@@ -53,22 +83,22 @@ export function forEachComment(
5383
}
5484

5585
if (token.kind !== ts.SyntaxKind.JsxText) {
56-
ts.forEachLeadingCommentRange(
57-
fullText,
58-
// skip shebang at position 0
59-
token.pos === 0 ? (ts.getShebang(fullText) ?? "").length : token.pos,
60-
commentCallback,
61-
);
86+
yield* collectComments((callback) => {
87+
ts.forEachLeadingCommentRange(
88+
fullText,
89+
// skip shebang at position 0
90+
token.pos === 0 ? (ts.getShebang(fullText) ?? "").length : token.pos,
91+
callback,
92+
);
93+
}, fullText);
6294
}
6395

6496
if (notJsx || canHaveTrailingTrivia(token)) {
65-
ts.forEachTrailingCommentRange(fullText, token.end, commentCallback);
97+
yield* collectComments((callback) => {
98+
ts.forEachTrailingCommentRange(fullText, token.end, callback);
99+
}, fullText);
66100
}
67101
}
68-
69-
function commentCallback(pos: number, end: number, kind: ts.CommentKind) {
70-
callback(fullText, { end, kind, pos });
71-
}
72102
}
73103

74104
/**
@@ -105,6 +135,30 @@ function canHaveTrailingTrivia(token: ts.Node): boolean {
105135
return true;
106136
}
107137

138+
/**
139+
* Collect comments by `ts.{forEachLeadingCommentRange,forEachTrailingCommentRange}`
140+
* @internal
141+
*/
142+
function collectComments(
143+
execute: (
144+
callback: (pos: number, end: number, kind: ts.CommentKind) => void,
145+
) => void,
146+
fullText: string,
147+
) {
148+
const comments: Comment[] = [];
149+
150+
execute((pos: number, end: number, kind: ts.CommentKind) => {
151+
const text = fullText.slice(pos, end);
152+
const value = text.slice(
153+
2,
154+
kind === ts.SyntaxKind.SingleLineCommentTrivia ? undefined : -2,
155+
);
156+
comments.push({ end, kind, pos, text, value });
157+
});
158+
159+
return comments;
160+
}
161+
108162
/**
109163
* Test if a node is a `JsxElement` or `JsxFragment`.
110164
* @internal

0 commit comments

Comments
 (0)