Skip to content

Commit 41cd21d

Browse files
fix: handle const Type Parameters (#519)
<!-- 👋 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 #518 - [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 Initial attempt at resolving the issue. I've got no idea if this is the correct approach or not. There might be some cases which this PR doesn't currently handle. --------- Co-authored-by: Josh Goldberg ✨ <git@joshuakgoldberg.com>
1 parent 72d7e63 commit 41cd21d

8 files changed

Lines changed: 157 additions & 28 deletions

File tree

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"@phenomnomnominal/tsquery": "^6.1.3",
5454
"@release-it/conventional-changelog": "^8.0.1",
5555
"@types/eslint": "^8.56.5",
56+
"@types/semver": "^7.5.8",
5657
"@typescript-eslint/eslint-plugin": "^7.3.1",
5758
"@typescript-eslint/parser": "^7.3.1",
5859
"@typescript/vfs": "^1.5.0",
@@ -81,6 +82,7 @@
8182
"prettier-plugin-curly": "^0.2.1",
8283
"prettier-plugin-packagejson": "^2.4.7",
8384
"release-it": "^17.0.1",
85+
"semver": "^7.6.2",
8486
"sentences-per-line": "^0.2.1",
8587
"should-semantic-release": "^0.3.0",
8688
"tsup": "^8.0.2",

pnpm-lock.yaml

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

src/flags.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,17 @@ export const isSymbolFlagSet: (
8989
flag: ts.SymbolFlags,
9090
) => boolean = isFlagSetOnObject;
9191

92+
/**
93+
* Test if the given symbol's links has the given `CheckFlags` set.
94+
* @internal
95+
*/
96+
export function isTransientSymbolLinksFlagSet(
97+
links: ts.TransientSymbolLinks,
98+
flag: ts.CheckFlags,
99+
): boolean {
100+
return isFlagSet(links.checkFlags, flag);
101+
}
102+
92103
/**
93104
* Test if the given node has the given `TypeFlags` set.
94105
* @category Nodes - Flag Utilities

src/nodes/utilities.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import ts from "typescript";
55

6+
import { isTransientSymbolLinksFlagSet } from "../flags";
67
import {
78
isConstAssertionExpression,
89
isEntityNameExpression,
@@ -28,10 +29,13 @@ export function isBindableObjectDefinePropertyCall(
2829
}
2930

3031
/**
31-
* Detects whether an expression is affected by an enclosing `as const` assertion and therefore treated literally.
32+
* Detects whether the property assignment is affected by an enclosing `as const` assertion or const type parameter and therefore treated literally.
3233
* @internal
3334
*/
34-
export function isInConstContext(node: ts.Expression): boolean {
35+
export function isInConstContext(
36+
node: ts.PropertyAssignment | ts.ShorthandPropertyAssignment,
37+
typeChecker: ts.TypeChecker,
38+
): boolean {
3539
let current: ts.Node = node;
3640
while (true) {
3741
const parent = current.parent;
@@ -69,6 +73,45 @@ export function isInConstContext(node: ts.Expression): boolean {
6973
case ts.SyntaxKind.TemplateExpression:
7074
current = parent;
7175
break;
76+
case ts.SyntaxKind.CallExpression:
77+
if (!ts.isExpression(current)) {
78+
return false;
79+
}
80+
81+
const functionSignature = typeChecker.getResolvedSignature(
82+
parent as ts.CallExpression,
83+
);
84+
if (functionSignature === undefined) {
85+
return false;
86+
}
87+
88+
const argumentIndex = (parent as ts.CallExpression).arguments.indexOf(
89+
current,
90+
);
91+
if (argumentIndex < 0) {
92+
return false;
93+
}
94+
95+
const parameterSymbol =
96+
functionSignature.getParameters()[argumentIndex];
97+
if (parameterSymbol === undefined || !("links" in parameterSymbol)) {
98+
return false;
99+
}
100+
101+
const parameterSymbolLinks = (parameterSymbol as ts.TransientSymbol)
102+
.links;
103+
104+
const propertySymbol =
105+
parameterSymbolLinks.type?.getProperties()?.[argumentIndex];
106+
if (propertySymbol === undefined || !("links" in propertySymbol)) {
107+
return false;
108+
}
109+
110+
// I believe we only need to check one level deep, regardless of how deep `node` is.
111+
return isTransientSymbolLinksFlagSet(
112+
(propertySymbol as ts.TransientSymbol).links,
113+
ts.CheckFlags.Readonly,
114+
);
72115
default:
73116
return false;
74117
}

0 commit comments

Comments
 (0)