Skip to content

Commit

Permalink
fix: handle const Type Parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
RebeccaStevens committed May 23, 2024
1 parent c4018e4 commit 4622196
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 3 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"@phenomnomnominal/tsquery": "^6.1.3",
"@release-it/conventional-changelog": "^8.0.1",
"@types/eslint": "^8.56.5",
"@types/semver": "^7.5.8",
"@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1",
"@typescript/vfs": "^1.5.0",
Expand Down Expand Up @@ -81,6 +82,7 @@
"prettier-plugin-curly": "^0.2.1",
"prettier-plugin-packagejson": "^2.4.7",
"release-it": "^17.0.1",
"semver": "^7.6.2",
"sentences-per-line": "^0.2.1",
"should-semantic-release": "^0.3.0",
"tsup": "^8.0.2",
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

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

11 changes: 11 additions & 0 deletions src/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,17 @@ export const isSymbolFlagSet: (
flag: ts.SymbolFlags,
) => boolean = isFlagSetOnObject;

/**
* Test if the given symbol's links has the given `ModifierFlags` set.
* @internal
*/
export function isSymbolLinkFlagSet(
symbol: ts.Symbol,
flag: ts.ModifierFlags,
): boolean {
return symbol.links !== undefined && isFlagSet(symbol.links.checkFlags, flag);
}

/**
* Test if the given node has the given `TypeFlags` set.
* @category Nodes - Flag Utilities
Expand Down
55 changes: 54 additions & 1 deletion src/nodes/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import ts from "typescript";

import { isSymbolLinkFlagSet } from "../flags";
import {
isConstAssertionExpression,
isEntityNameExpression,
Expand All @@ -27,11 +28,24 @@ export function isBindableObjectDefinePropertyCall(
);
}

/**
* Detects whether the property assignment is affected by an enclosing `as const` assertion or const type parameter and therefore treated literally.
*/
export function isInConstContext(
node: ts.PropertyAssignment | ts.ShorthandPropertyAssignment,
typeChecker: ts.TypeChecker,
): boolean {
return (
isInAsConstContext(node.parent) ||
isInConstTypeParameterContext(node, typeChecker)
);
}

/**
* Detects whether an expression is affected by an enclosing `as const` assertion and therefore treated literally.
* @internal
*/
export function isInConstContext(node: ts.Expression): boolean {
function isInAsConstContext(node: ts.Expression): boolean {
let current: ts.Node = node;
while (true) {
const parent = current.parent;
Expand Down Expand Up @@ -74,3 +88,42 @@ export function isInConstContext(node: ts.Expression): boolean {
}
}
}

/**
* Detects whether a property assignment is affected by a const type parameter and therefore treated literally.
* @internal
*/
function isInConstTypeParameterContext(
node: ts.PropertyAssignment | ts.ShorthandPropertyAssignment,
typeChecker: ts.TypeChecker,
): boolean {
let current: ts.PropertyAssignment | ts.ShorthandPropertyAssignment = node;
let callExpression: ts.CallExpression;

while (true) {
if (
ts.isPropertyAssignment(current.parent.parent) ||
ts.isShorthandPropertyAssignment(current.parent.parent)
) {
current = current.parent.parent;
continue;
}

if (ts.isCallExpression(current.parent.parent)) {
callExpression = current.parent.parent;
break;
}

return false;
}

const type = typeChecker.getTypeAtLocation(callExpression);
const property = type
.getProperties()
.find((property) => property.getName() === current.name.getText());

return (
property !== undefined &&
isSymbolLinkFlagSet(property, ts.ModifierFlags.Readonly)
);
}
27 changes: 27 additions & 0 deletions src/types/utilities.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import semver from "semver";
import ts from "typescript";
import { describe, expect, it } from "vitest";

Expand Down Expand Up @@ -131,6 +132,32 @@ describe("symbolHasReadonlyDeclaration", () => {

expect(symbolHasReadonlyDeclaration(symbol, typeChecker)).toBe(expected);
});

if (semver.gte(ts.version, "5.0.0")) {
it("returns true when the symbol belongs to a property of a nested object literal directly passed into a function that declares the parameter with a const type parameter", () => {
const { sourceFile, typeChecker } = createSourceFileAndTypeChecker(`
declare const fn: <const A>(param: A) => A;
const bar = { baz: 1 };
fn({ foo: { bar } });
`);

const statement = sourceFile.statements.at(-1) as ts.ExpressionStatement;
const callExpression = statement.expression as ts.CallExpression;
const objectLiteral1 = callExpression
.arguments[0] as ts.ObjectLiteralExpression;
const foo = objectLiteral1.properties[0] as ts.PropertyAssignment;
const fooSymbol = (foo as { symbol?: ts.Symbol }).symbol!;
const objectLiteral2 = foo.initializer as ts.ObjectLiteralExpression;
const bar = objectLiteral2.properties[0] as ts.PropertyAssignment;
const barSymbol = (bar as { symbol?: ts.Symbol }).symbol!;

expect(fooSymbol).toBeDefined();
expect(barSymbol).toBeDefined();
expect(symbolHasReadonlyDeclaration(fooSymbol, typeChecker)).toBe(true);
expect(symbolHasReadonlyDeclaration(barSymbol, typeChecker)).toBe(true);
});
}
});

describe("isFalsyType", () => {
Expand Down
2 changes: 1 addition & 1 deletion src/types/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ export function symbolHasReadonlyDeclaration(
ts.isEnumMember(node) ||
((ts.isPropertyAssignment(node) ||
ts.isShorthandPropertyAssignment(node)) &&
isInConstContext(node.parent)),
isInConstContext(node, typeChecker)),
)
);
}
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@
"strict": true,
"target": "ES2021"
},
"include": ["src"]
"include": ["src", "typings"]
}
11 changes: 11 additions & 0 deletions typings/typescript.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import "typescript";

declare module "typescript" {
// internal TS APIs

interface Symbol {
readonly links?: {
readonly checkFlags: ModifierFlags;
};
}
}

0 comments on commit 4622196

Please sign in to comment.