Skip to content

Commit

Permalink
feat: add collectVariableUsage API (#274)
Browse files Browse the repository at this point in the history
## 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
  • Loading branch information
JoshuaKGoldberg authored Sep 7, 2023
1 parent 3f3a485 commit b6a40ea
Show file tree
Hide file tree
Showing 18 changed files with 1,895 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ module.exports = {
"@typescript-eslint/explicit-module-boundary-types": "error",

// TODO?
"@typescript-eslint/prefer-literal-enum-member": "off",
"@typescript-eslint/no-confusing-void-expression": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unnecessary-condition": "off",
Expand Down Expand Up @@ -72,6 +73,10 @@ module.exports = {
root: true,
rules: {
// These off-by-default rules work well for this repo and we like them on.
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", caughtErrors: "all" },
],
"import/extensions": ["error"],
"import/no-useless-path-segments": [
"error",
Expand Down
2 changes: 2 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
"konamimojisplosion",
"lcov",
"packagejson",
"phenomnomnominal",
"quickstart",
"tsquery",
"tsup",
"tsutils",
"tsvfs",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"*": "prettier --ignore-unknown --write"
},
"devDependencies": {
"@phenomnomnominal/tsquery": "^6.1.3",
"@typescript-eslint/eslint-plugin": "^6.4.0",
"@typescript-eslint/parser": "6.5.0",
"@typescript/vfs": "^1.5.0",
Expand Down
23 changes: 23 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from "./scopes";
export * from "./syntax";
export * from "./tokens";
export * from "./types";
export * from "./usage";
4 changes: 2 additions & 2 deletions src/modifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import ts from "typescript";
* ```
*/
export function includesModifier(
modifiers: Iterable<ts.Modifier> | undefined,
modifiers: Iterable<ts.ModifierLike> | undefined,
...kinds: ts.ModifierSyntaxKind[]
): boolean {
if (modifiers === undefined) return false;
for (const modifier of modifiers)
if (kinds.includes(modifier.kind)) return true;
if (kinds.includes(modifier.kind as ts.ModifierSyntaxKind)) return true;
return false;
}
15 changes: 10 additions & 5 deletions src/test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ import ts from "typescript";
export function createNodeAndSourceFile<Node extends ts.Node>(
sourceText: string,
): { node: Node; sourceFile: ts.SourceFile } {
const sourceFile = ts.createSourceFile(
"file.tsx",
sourceText,
ts.ScriptTarget.ESNext,
);
const sourceFile = createSourceFile(sourceText);
const statement = sourceFile.statements.at(-1)!;

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

export function createSourceFile(sourceText: string): ts.SourceFile {
return ts.createSourceFile(
"file.tsx",
sourceText,
ts.ScriptTarget.ESNext,
true,
);
}

export function createNode<Node extends ts.Node>(
nodeOrSourceText: Node | string,
): Node {
Expand Down
72 changes: 72 additions & 0 deletions src/usage/Scope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Code largely based on https://github.com/ajafff/tsutils
// Original license: https://github.com/ajafff/tsutils/blob/26b195358ec36d59f00333115aa3ffd9611ca78b/LICENSE

import ts from "typescript";

import { isFunctionScopeBoundary } from "../scopes";
import { DeclarationDomain } from "./declarations";
import type { EnumScope, NamespaceScope } from "./scopes";
import { InternalUsageInfo, Usage, UsageInfoCallback } from "./usage";

export enum ScopeBoundary {
None = 0,
Function = 1,
Block = 2,
Type = 4,
ConditionalType = 8,
}

export enum ScopeBoundarySelector {
Function = ScopeBoundary.Function,
Block = ScopeBoundarySelector.Function | ScopeBoundary.Block,
Type = ScopeBoundarySelector.Block | ScopeBoundary.Type,
InferType = ScopeBoundary.ConditionalType,
}

export interface Scope {
addUse(use: Usage, scope?: Scope): void;
addVariable(
identifier: string,
name: ts.PropertyName,
selector: ScopeBoundarySelector,
exported: boolean,
domain: DeclarationDomain,
): void;
createOrReuseEnumScope(name: string, exported: boolean): EnumScope;
createOrReuseNamespaceScope(
name: string,
exported: boolean,
ambient: boolean,
hasExportStatement: boolean,
): NamespaceScope;
end(cb: UsageInfoCallback): void;
getDestinationScope(selector: ScopeBoundarySelector): Scope;
getFunctionScope(): Scope;
getVariables(): Map<string, InternalUsageInfo>;
markExported(name: ts.Identifier, as?: ts.Identifier): void;
}

export function isBlockScopeBoundary(node: ts.Node): ScopeBoundary {
switch (node.kind) {
case ts.SyntaxKind.Block: {
const parent = node.parent;
return parent.kind !== ts.SyntaxKind.CatchClause &&
// blocks inside SourceFile are block scope boundaries
(parent.kind === ts.SyntaxKind.SourceFile ||
// blocks that are direct children of a function scope boundary are no scope boundary
// for example the FunctionBlock is part of the function scope of the containing function
!isFunctionScopeBoundary(parent))
? ScopeBoundary.Block
: ScopeBoundary.None;
}
case ts.SyntaxKind.ForStatement:
case ts.SyntaxKind.ForInStatement:
case ts.SyntaxKind.ForOfStatement:
case ts.SyntaxKind.CaseBlock:
case ts.SyntaxKind.CatchClause:
case ts.SyntaxKind.WithStatement:
return ScopeBoundary.Block;
default:
return ScopeBoundary.None;
}
}
Loading

0 comments on commit b6a40ea

Please sign in to comment.