Skip to content

Commit

Permalink
feat: add flags, type, typeguards files
Browse files Browse the repository at this point in the history
  • Loading branch information
JoshuaKGoldberg committed Feb 6, 2023
1 parent cb573b1 commit 555ca46
Show file tree
Hide file tree
Showing 7 changed files with 333 additions and 10 deletions.
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"lcov",
"quickstart",
"tsutils",
"typeguards",
"wontfix"
]
}
28 changes: 28 additions & 0 deletions src/flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Code largely based on /~https://github.com/ajafff/tsutils
// Original license: /~https://github.com/ajafff/tsutils/blob/26b195358ec36d59f00333115aa3ffd9611ca78b/LICENSE

import * as ts from "typescript";

function isFlagSet(obj: { flags: number }, flag: number) {
return (obj.flags & flag) !== 0;
}

export const isNodeFlagSet: (node: ts.Node, flag: ts.NodeFlags) => boolean =
isFlagSet;
export const isTypeFlagSet: (type: ts.Type, flag: ts.TypeFlags) => boolean =
isFlagSet;
export const isSymbolFlagSet: (
symbol: ts.Symbol,
flag: ts.SymbolFlags
) => boolean = isFlagSet;

export function isModifierFlagSet(node: ts.Node, flag: ts.ModifierFlags) {
return (ts.getCombinedModifierFlags(node as ts.Declaration) & flag) !== 0;
}

export function isObjectFlagSet(
objectType: ts.ObjectType,
flag: ts.ObjectFlags
) {
return (objectType.objectFlags & flag) !== 0;
}
6 changes: 2 additions & 4 deletions src/forEachComment.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
// 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
// Code largely based on /~https://github.com/ajafff/tsutils
// Original license: /~https://github.com/ajafff/tsutils/blob/26b195358ec36d59f00333115aa3ffd9611ca78b/LICENSE

import * as ts from "typescript";

Expand Down
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 "./forEachComment.js";
export * from "./types.js";
export * from "./type.js";
243 changes: 243 additions & 0 deletions src/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
// Code largely based on /~https://github.com/ajafff/tsutils
// Original license: /~https://github.com/ajafff/tsutils/blob/26b195358ec36d59f00333115aa3ffd9611ca78b/LICENSE

import * as ts from "typescript";

import {
isModifierFlagSet,
isNodeFlagSet,
isObjectFlagSet,
isSymbolFlagSet,
isTypeFlagSet,
} from "./flags";
import {
isConstAssertion,
isEntityNameExpression,
isIntersectionType,
isNumericOrStringLikeLiteral,
isNumericPropertyName,
isObjectType,
isUnionType,
} from "./typeguards";

export function getPropertyOfType(type: ts.Type, name: ts.__String) {
if (!(name as string).startsWith("__"))
return type.getProperty(name as string);
return type.getProperties().find((s) => s.escapedName === name);
}

/** Determines whether a call to `Object.defineProperty` is statically analyzable. */
export function isBindableObjectDefinePropertyCall(node: ts.CallExpression) {
return (
node.arguments.length === 3 &&
isEntityNameExpression(node.arguments[0]) &&
isNumericOrStringLikeLiteral(node.arguments[1]) &&
ts.isPropertyAccessExpression(node.expression) &&
node.expression.name.escapedText === "defineProperty" &&
ts.isIdentifier(node.expression.expression) &&
node.expression.expression.escapedText === "Object"
);
}

/** Determines whether the given type is a boolean literal type and matches the given boolean literal (true or false). */
export function isBooleanLiteralType(type: ts.Type, literal: boolean) {
return (
isTypeFlagSet(type, ts.TypeFlags.BooleanLiteral) &&
(type as unknown as { intrinsicName: string }).intrinsicName ===
(literal ? "true" : "false")
);
}

/** Detects whether an expression is affected by an enclosing 'as const' assertion and therefore treated literally. */
export function isInConstContext(node: ts.Expression) {
let current: ts.Node = node;
while (true) {
const parent = current.parent;
outer: switch (parent.kind) {
case ts.SyntaxKind.TypeAssertionExpression:
case ts.SyntaxKind.AsExpression:
return isConstAssertion(parent as ts.AssertionExpression);
case ts.SyntaxKind.PrefixUnaryExpression:
if (current.kind !== ts.SyntaxKind.NumericLiteral) return false;
switch ((parent as ts.PrefixUnaryExpression).operator) {
case ts.SyntaxKind.PlusToken:
case ts.SyntaxKind.MinusToken:
current = parent;
break outer;
default:
return false;
}
case ts.SyntaxKind.PropertyAssignment:
if ((parent as ts.PropertyAssignment).initializer !== current)
return false;
current = parent.parent!;
break;
case ts.SyntaxKind.ShorthandPropertyAssignment:
current = parent.parent!;
break;
case ts.SyntaxKind.ParenthesizedExpression:
case ts.SyntaxKind.ArrayLiteralExpression:
case ts.SyntaxKind.ObjectLiteralExpression:
case ts.SyntaxKind.TemplateExpression:
current = parent;
break;
default:
return false;
}
}
}

/** Determines if writing to a certain property of a given type is allowed. */
export function isPropertyReadonlyInType(
type: ts.Type,
name: ts.__String,
checker: ts.TypeChecker
): boolean {
let seenProperty = false;
let seenReadonlySignature = false;
for (const t of unionTypeParts(type)) {
if (getPropertyOfType(t, name) === undefined) {
// property is not present in this part of the union -> check for readonly index signature
const index =
(isNumericPropertyName(name)
? checker.getIndexInfoOfType(t, ts.IndexKind.Number)
: undefined) ?? checker.getIndexInfoOfType(t, ts.IndexKind.String);
if (index?.isReadonly) {
if (seenProperty) return true;
seenReadonlySignature = true;
}
} else if (
seenReadonlySignature ||
isReadonlyPropertyIntersection(t, name, checker)
) {
return true;
} else {
seenProperty = true;
}
}
return false;
}

/** Returns true for `Object.defineProperty(o, 'prop', {value, writable: false})` and `Object.defineProperty(o, 'prop', {get: () => 1})`*/
export function isReadonlyAssignmentDeclaration(
node: ts.CallExpression,
checker: ts.TypeChecker
) {
if (!isBindableObjectDefinePropertyCall(node)) return false;
const descriptorType = checker.getTypeAtLocation(node.arguments[2]);
if (descriptorType.getProperty("value") === undefined)
return descriptorType.getProperty("set") === undefined;
const writableProp = descriptorType.getProperty("writable");
if (writableProp === undefined) return false;
const writableType =
writableProp.valueDeclaration !== undefined &&
ts.isPropertyAssignment(writableProp.valueDeclaration)
? checker.getTypeAtLocation(writableProp.valueDeclaration.initializer)
: checker.getTypeOfSymbolAtLocation(writableProp, node.arguments[2]);
return isBooleanLiteralType(writableType, false);
}

function isReadonlyPropertyIntersection(
type: ts.Type,
name: ts.__String,
checker: ts.TypeChecker
) {
return someTypePart(type, isIntersectionType, (t): boolean => {
const prop = getPropertyOfType(t, name);
if (prop === undefined) return false;
if (prop.flags & ts.SymbolFlags.Transient) {
if (/^(?:[1-9]\d*|0)$/.test(name as string) && isTupleTypeReference(t))
return t.target.readonly;
switch (isReadonlyPropertyFromMappedType(t, name, checker)) {
case true:
return true;
case false:
return false;
default:
// `undefined` falls through
}
}
return !!(
// members of namespace import
(
isSymbolFlagSet(prop, ts.SymbolFlags.ValueModule) ||
// we unwrapped every mapped type, now we can check the actual declarations
symbolHasReadonlyDeclaration(prop, checker)
)
);
});
}

function isReadonlyPropertyFromMappedType(
type: ts.Type,
name: ts.__String,
checker: ts.TypeChecker
): boolean | undefined {
if (!isObjectType(type) || !isObjectFlagSet(type, ts.ObjectFlags.Mapped))
return;
const declaration = type.symbol.declarations![0] as ts.MappedTypeNode;
// well-known symbols are not affected by mapped types
if (
declaration.readonlyToken !== undefined &&
!/^__@[^@]+$/.test(name as string)
)
return declaration.readonlyToken.kind !== ts.SyntaxKind.MinusToken;
return isPropertyReadonlyInType(
(type as unknown as { modifiersType: ts.Type }).modifiersType,
name,
checker
);
}

export function isTupleType(type: ts.Type): type is ts.TupleType {
return (
(type.flags & ts.TypeFlags.Object &&
(type as ts.ObjectType).objectFlags & ts.ObjectFlags.Tuple) !== 0
);
}

export function isTupleTypeReference(
type: ts.Type
): type is ts.TypeReference & { target: ts.TupleType } {
return isTypeReference(type) && isTupleType(type.target);
}

export function isTypeReference(type: ts.Type): type is ts.TypeReference {
return (
(type.flags & ts.TypeFlags.Object) !== 0 &&
((type as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference) !== 0
);
}

export function someTypePart(
type: ts.Type,
predicate: (t: ts.Type) => t is ts.UnionOrIntersectionType,
cb: (t: ts.Type) => boolean
) {
return predicate(type) ? type.types.some(cb) : cb(type);
}

export function symbolHasReadonlyDeclaration(
symbol: ts.Symbol,
checker: ts.TypeChecker
) {
return (
(symbol.flags & ts.SymbolFlags.Accessor) === ts.SymbolFlags.GetAccessor ||
symbol.declarations?.some(
(node) =>
isModifierFlagSet(node, ts.ModifierFlags.Readonly) ||
(ts.isVariableDeclaration(node) &&
isNodeFlagSet(node.parent, ts.NodeFlags.Const)) ||
(ts.isCallExpression(node) &&
isReadonlyAssignmentDeclaration(node, checker)) ||
ts.isEnumMember(node) ||
((ts.isPropertyAssignment(node) ||
ts.isShorthandPropertyAssignment(node)) &&
isInConstContext(node.parent))
)
);
}

export function unionTypeParts(type: ts.Type): ts.Type[] {
return isUnionType(type) ? type.types : [type];
}
58 changes: 58 additions & 0 deletions src/typeguards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Code largely based on /~https://github.com/ajafff/tsutils
// Original license: /~https://github.com/ajafff/tsutils/blob/26b195358ec36d59f00333115aa3ffd9611ca78b/LICENSE

import * as ts from "typescript";

export function isConditionalType(type: ts.Type): type is ts.ConditionalType {
return (type.flags & ts.TypeFlags.Conditional) !== 0;
}

export function isConstAssertion(node: ts.AssertionExpression) {
return (
ts.isTypeReferenceNode(node.type) &&
node.type.typeName.kind === ts.SyntaxKind.Identifier &&
node.type.typeName.escapedText === "const"
);
}

export function isEntityNameExpression(
node: ts.Node
): node is ts.EntityNameExpression {
return (
node.kind === ts.SyntaxKind.Identifier ||
(ts.isPropertyAccessExpression(node) &&
isEntityNameExpression(node.expression))
);
}

export function isIntersectionType(type: ts.Type): type is ts.IntersectionType {
return (type.flags & ts.TypeFlags.Intersection) !== 0;
}

export function isNumericPropertyName(name: string | ts.__String): boolean {
return String(+name) === name;
}

export function isNumericOrStringLikeLiteral(
node: ts.Node
): node is
| ts.NumericLiteral
| ts.StringLiteral
| ts.NoSubstitutionTemplateLiteral {
switch (node.kind) {
case ts.SyntaxKind.StringLiteral:
case ts.SyntaxKind.NumericLiteral:
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
return true;
default:
return false;
}
}

export function isObjectType(type: ts.Type): type is ts.ObjectType {
return (type.flags & ts.TypeFlags.Object) !== 0;
}

export function isUnionType(type: ts.Type): type is ts.UnionType {
return (type.flags & ts.TypeFlags.Union) !== 0;
}
5 changes: 0 additions & 5 deletions src/types.ts

This file was deleted.

0 comments on commit 555ca46

Please sign in to comment.