Skip to content

Commit

Permalink
feat: forEachComment
Browse files Browse the repository at this point in the history
  • Loading branch information
JoshuaKGoldberg committed Feb 6, 2023
1 parent 2cd08dc commit f901bec
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 68 deletions.
5 changes: 5 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ module.exports = {
rules: {
// These off-by-default rules work well for this repo and we like them on.
"deprecation/deprecation": "error",

// TODO?
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unnecessary-condition": "off",
"no-constant-condition": "off",
},
},
{
Expand Down
9 changes: 3 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<h1 align="center">TypeScript API Tools</h1>

<p align="center">Utility functions for working with TypeScript's API. Based on the wonderful ajafff/tsutils.</p>
<p align="center">Utility functions for working with TypeScript's API. Based on the wonderful [ajafff/tsutils](/~https://github.com/ajafff/tsutils).</p>

<p align="center">
<a href="#contributors" target="_blank">
Expand Down Expand Up @@ -32,11 +32,8 @@
npm i ts-api-tools
```

```ts
import { greet } from "ts-api-tools";

greet("Hello, world!");
```
> ⚠️ This package is a fork of and drop-in replacement for [`tsutils`](/~https://github.com/ajafff/tsutils) ([original license: MIT](/~https://github.com/ajafff/tsutils/blob/26b195358ec36d59f00333115aa3ffd9611ca78b/LICENSE)).
> It's very early stage - most functions are not implemented yet! ⚠️
## Development

Expand Down
2 changes: 2 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"script/*.json"
],
"words": [
"ajafff",
"Codecov",
"codespace",
"commitlint",
Expand All @@ -19,6 +20,7 @@
"knip",
"lcov",
"quickstart",
"tsutils",
"wontfix"
]
}
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@
"main": "./lib/index.js",
"name": "ts-api-tools",
"packageManager": "pnpm@7.26.3",
"peerDependencies": {
"typescript": ">=4"
},
"repository": {
"type": "git",
"url": "/~https://github.com/JoshuaKGoldberg/ts-api-tools"
Expand Down
92 changes: 92 additions & 0 deletions src/forEachComment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Code largely based on ajafff/tsutils:
// /~https://github.com/ajafff/tsutils/blob/03b4df15d14702f9c7a128ac3fae77171778d613/util/util.ts
// Original license MIT:
// /~https://github.com/ajafff/tsutils/blob/26b195358ec36d59f00333115aa3ffd9611ca78b/LICENSE

import * as ts from "typescript";

import { forEachToken } from "./forEachToken";

/** Exclude trailing positions that would lead to scanning for trivia inside JsxText */
function canHaveTrailingTrivia(token: ts.Node): boolean {
switch (token.kind) {
case ts.SyntaxKind.CloseBraceToken:
// after a JsxExpression inside a JsxElement's body can only be other JsxChild, but no trivia
return (
token.parent.kind !== ts.SyntaxKind.JsxExpression ||
!isJsxElementOrFragment(token.parent.parent)
);
case ts.SyntaxKind.GreaterThanToken:
switch (token.parent.kind) {
case ts.SyntaxKind.JsxOpeningElement:
// if end is not equal, this is part of the type arguments list. in all other cases it would be inside the element body
return token.end !== token.parent.end;
case ts.SyntaxKind.JsxOpeningFragment:
return false; // would be inside the fragment
case ts.SyntaxKind.JsxSelfClosingElement:
return (
token.end !== token.parent.end || // if end is not equal, this is part of the type arguments list
!isJsxElementOrFragment(token.parent.parent)
); // there's only trailing trivia if it's the end of the top element
case ts.SyntaxKind.JsxClosingElement:
case ts.SyntaxKind.JsxClosingFragment:
// there's only trailing trivia if it's the end of the top element
return !isJsxElementOrFragment(token.parent.parent.parent);
}
}
return true;
}

function isJsxElementOrFragment(
node: ts.Node
): node is ts.JsxElement | ts.JsxFragment {
return (
node.kind === ts.SyntaxKind.JsxElement ||
node.kind === ts.SyntaxKind.JsxFragment
);
}

export type ForEachCommentCallback = (
fullText: string,
comment: ts.CommentRange
) => void;

/** Iterate over all comments owned by `node` or its children */
export function forEachComment(
node: ts.Node,
cb: ForEachCommentCallback,
sourceFile: ts.SourceFile = node.getSourceFile()
) {
/* Visit all tokens and skip trivia.
Comment ranges between tokens are parsed without the need of a scanner.
forEachTokenWithWhitespace does intentionally not pay attention to the correct comment ownership of nodes as it always
scans all trivia before each token, which could include trailing comments of the previous token.
Comment onwership is done right in this function*/
const fullText = sourceFile.text;
const notJsx = sourceFile.languageVariant !== ts.LanguageVariant.JSX;
return forEachToken(
node,
(token) => {
if (token.pos === token.end) return;
if (token.kind !== ts.SyntaxKind.JsxText)
ts.forEachLeadingCommentRange(
fullText,
// skip shebang at position 0
// TODO: Investigate
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
token.pos === 0 ? (ts.getShebang(fullText) || "").length : token.pos,
commentCallback
);
if (notJsx || canHaveTrailingTrivia(token))
return ts.forEachTrailingCommentRange(
fullText,
token.end,
commentCallback
);
},
sourceFile
);
function commentCallback(pos: number, end: number, kind: ts.CommentKind) {
cb(fullText, { pos, end, kind });
}
}
36 changes: 36 additions & 0 deletions src/forEachToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Code largely based on ajafff/tsutils:
// /~https://github.com/ajafff/tsutils/blob/03b4df15d14702f9c7a128ac3fae77171778d613/util/util.ts
// Original license MIT:
// /~https://github.com/ajafff/tsutils/blob/26b195358ec36d59f00333115aa3ffd9611ca78b/LICENSE

import * as ts from "typescript";

/**
* Iterate over all tokens of `node`
*
* @param node The node whose tokens should be visited
* @param cb Is called for every token contained in `node`
*/
export function forEachToken(
node: ts.Node,
cb: (node: ts.Node) => void,
sourceFile: ts.SourceFile = node.getSourceFile()
) {
const queue = [];
while (true) {
if (ts.isTokenKind(node.kind)) {
cb(node);
// TODO: Investigate?
// eslint-disable-next-line deprecation/deprecation
} else if (node.kind !== ts.SyntaxKind.JSDocComment) {
const children = node.getChildren(sourceFile);
if (children.length === 1) {
node = children[0];
continue;
}
for (let i = children.length - 1; i >= 0; --i) queue.push(children[i]); // add children in reverse order, when we pop the next element from the queue, it's the first child
}
if (queue.length === 0) break;
node = queue.pop()!;
}
}
46 changes: 0 additions & 46 deletions src/greet.test.ts

This file was deleted.

15 changes: 0 additions & 15 deletions src/greet.ts

This file was deleted.

7 changes: 7 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { describe, expect, it } from "vitest";

describe("index", () => {
it("TODO", () => {
expect(2 + 2).not.toBe("fish");
});
});
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from "./greet.js";
export * from "./forEachComment.js";
export * from "./types.js";

0 comments on commit f901bec

Please sign in to comment.