Skip to content

Commit b6a40ea

Browse files
feat: add collectVariableUsage API (#274)
## PR Checklist - [x] Addresses an existing open issue: fixes #263 - [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 Directly ports the exported public `collectVariableUsage` function from `tsutils`. Adds in some unit test coverage too. I'd wanted to refactor its source to be less reliant on class hierarchies... but there's a lot going on there. Out of scope for now. I did apply a few touchups: * Changed "soft" privates to use `#`. * Renamed `VariableInfo` to `UsageInfo`. * It's used both for types and values, so referring to them as "variables" was confusing to me. * Please shout at me if you can think of a better name or believe the original name to be better! * Switches the `const enum`s to just `enum`s. * Adds test coverage for the most common cases. * It's not super thorough, so if you spot some logic you think should be tested, shout at me and I'm happy to add it in if I can
1 parent 3f3a485 commit b6a40ea

18 files changed

Lines changed: 1895 additions & 7 deletions

.eslintrc.cjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ module.exports = {
3232
"@typescript-eslint/explicit-module-boundary-types": "error",
3333

3434
// TODO?
35+
"@typescript-eslint/prefer-literal-enum-member": "off",
3536
"@typescript-eslint/no-confusing-void-expression": "off",
3637
"@typescript-eslint/no-non-null-assertion": "off",
3738
"@typescript-eslint/no-unnecessary-condition": "off",
@@ -72,6 +73,10 @@ module.exports = {
7273
root: true,
7374
rules: {
7475
// These off-by-default rules work well for this repo and we like them on.
76+
"@typescript-eslint/no-unused-vars": [
77+
"error",
78+
{ argsIgnorePattern: "^_", caughtErrors: "all" },
79+
],
7580
"import/extensions": ["error"],
7681
"import/no-useless-path-segments": [
7782
"error",

cspell.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
"konamimojisplosion",
2525
"lcov",
2626
"packagejson",
27+
"phenomnomnominal",
2728
"quickstart",
29+
"tsquery",
2830
"tsup",
2931
"tsutils",
3032
"tsvfs",

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"*": "prettier --ignore-unknown --write"
5050
},
5151
"devDependencies": {
52+
"@phenomnomnominal/tsquery": "^6.1.3",
5253
"@typescript-eslint/eslint-plugin": "^6.4.0",
5354
"@typescript-eslint/parser": "6.5.0",
5455
"@typescript/vfs": "^1.5.0",

pnpm-lock.yaml

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export * from "./scopes";
77
export * from "./syntax";
88
export * from "./tokens";
99
export * from "./types";
10+
export * from "./usage";

src/modifiers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ import ts from "typescript";
1515
* ```
1616
*/
1717
export function includesModifier(
18-
modifiers: Iterable<ts.Modifier> | undefined,
18+
modifiers: Iterable<ts.ModifierLike> | undefined,
1919
...kinds: ts.ModifierSyntaxKind[]
2020
): boolean {
2121
if (modifiers === undefined) return false;
2222
for (const modifier of modifiers)
23-
if (kinds.includes(modifier.kind)) return true;
23+
if (kinds.includes(modifier.kind as ts.ModifierSyntaxKind)) return true;
2424
return false;
2525
}

src/test/utils.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,7 @@ import ts from "typescript";
44
export function createNodeAndSourceFile<Node extends ts.Node>(
55
sourceText: string,
66
): { node: Node; sourceFile: ts.SourceFile } {
7-
const sourceFile = ts.createSourceFile(
8-
"file.tsx",
9-
sourceText,
10-
ts.ScriptTarget.ESNext,
11-
);
7+
const sourceFile = createSourceFile(sourceText);
128
const statement = sourceFile.statements.at(-1)!;
139

1410
const node = (ts.isExpressionStatement(statement)
@@ -18,6 +14,15 @@ export function createNodeAndSourceFile<Node extends ts.Node>(
1814
return { node, sourceFile };
1915
}
2016

17+
export function createSourceFile(sourceText: string): ts.SourceFile {
18+
return ts.createSourceFile(
19+
"file.tsx",
20+
sourceText,
21+
ts.ScriptTarget.ESNext,
22+
true,
23+
);
24+
}
25+
2126
export function createNode<Node extends ts.Node>(
2227
nodeOrSourceText: Node | string,
2328
): Node {

src/usage/Scope.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Code largely based on https://github.com/ajafff/tsutils
2+
// Original license: https://github.com/ajafff/tsutils/blob/26b195358ec36d59f00333115aa3ffd9611ca78b/LICENSE
3+
4+
import ts from "typescript";
5+
6+
import { isFunctionScopeBoundary } from "../scopes";
7+
import { DeclarationDomain } from "./declarations";
8+
import type { EnumScope, NamespaceScope } from "./scopes";
9+
import { InternalUsageInfo, Usage, UsageInfoCallback } from "./usage";
10+
11+
export enum ScopeBoundary {
12+
None = 0,
13+
Function = 1,
14+
Block = 2,
15+
Type = 4,
16+
ConditionalType = 8,
17+
}
18+
19+
export enum ScopeBoundarySelector {
20+
Function = ScopeBoundary.Function,
21+
Block = ScopeBoundarySelector.Function | ScopeBoundary.Block,
22+
Type = ScopeBoundarySelector.Block | ScopeBoundary.Type,
23+
InferType = ScopeBoundary.ConditionalType,
24+
}
25+
26+
export interface Scope {
27+
addUse(use: Usage, scope?: Scope): void;
28+
addVariable(
29+
identifier: string,
30+
name: ts.PropertyName,
31+
selector: ScopeBoundarySelector,
32+
exported: boolean,
33+
domain: DeclarationDomain,
34+
): void;
35+
createOrReuseEnumScope(name: string, exported: boolean): EnumScope;
36+
createOrReuseNamespaceScope(
37+
name: string,
38+
exported: boolean,
39+
ambient: boolean,
40+
hasExportStatement: boolean,
41+
): NamespaceScope;
42+
end(cb: UsageInfoCallback): void;
43+
getDestinationScope(selector: ScopeBoundarySelector): Scope;
44+
getFunctionScope(): Scope;
45+
getVariables(): Map<string, InternalUsageInfo>;
46+
markExported(name: ts.Identifier, as?: ts.Identifier): void;
47+
}
48+
49+
export function isBlockScopeBoundary(node: ts.Node): ScopeBoundary {
50+
switch (node.kind) {
51+
case ts.SyntaxKind.Block: {
52+
const parent = node.parent;
53+
return parent.kind !== ts.SyntaxKind.CatchClause &&
54+
// blocks inside SourceFile are block scope boundaries
55+
(parent.kind === ts.SyntaxKind.SourceFile ||
56+
// blocks that are direct children of a function scope boundary are no scope boundary
57+
// for example the FunctionBlock is part of the function scope of the containing function
58+
!isFunctionScopeBoundary(parent))
59+
? ScopeBoundary.Block
60+
: ScopeBoundary.None;
61+
}
62+
case ts.SyntaxKind.ForStatement:
63+
case ts.SyntaxKind.ForInStatement:
64+
case ts.SyntaxKind.ForOfStatement:
65+
case ts.SyntaxKind.CaseBlock:
66+
case ts.SyntaxKind.CatchClause:
67+
case ts.SyntaxKind.WithStatement:
68+
return ScopeBoundary.Block;
69+
default:
70+
return ScopeBoundary.None;
71+
}
72+
}

0 commit comments

Comments
 (0)