Skip to content

Commit

Permalink
feat: support class.hasInstance
Browse files Browse the repository at this point in the history
  • Loading branch information
Jack-Works committed Feb 1, 2022
1 parent 9d5f62a commit 2384d95
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 32 deletions.
72 changes: 71 additions & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31431,6 +31431,12 @@ namespace ts {
return checkImportMetaProperty(node);
}

if (node.keywordToken === SyntaxKind.ClassKeyword) {
const prop = getPropertyOfType(checkClassHasInstanceProperty(node), node.name.escapedText);
if (!prop) return errorType;
return getTypeOfSymbol(prop);
}

return Debug.assertNever(node.keywordToken);
}

Expand All @@ -31441,6 +31447,8 @@ namespace ts {
case SyntaxKind.NewKeyword:
const type = checkNewTargetMetaProperty(node);
return isErrorType(type) ? errorType : createNewTargetExpressionType(type);
case SyntaxKind.ClassKeyword:
return checkClassHasInstanceProperty(node);
default:
Debug.assertNever(node.keywordToken);
}
Expand Down Expand Up @@ -31476,6 +31484,19 @@ namespace ts {
return node.name.escapedText === "meta" ? getGlobalImportMetaType() : errorType;
}


function checkClassHasInstanceProperty(node: MetaProperty) {
const container = getContainingClass(node);
if (!container) {
error(node, Diagnostics.Meta_property_0_is_only_allowed_in_the_body_of_a_class, "class." + node.name.escapedText);
return errorType;
}
else {
const symbol = getSymbolOfNode(container)!;
return createClassMetaPropertyExpressionType(getTypeOfSymbol(symbol));
}
}

function getTypeOfParameter(symbol: Symbol) {
const type = getTypeOfSymbol(symbol);
if (strictNullChecks) {
Expand Down Expand Up @@ -31849,6 +31870,39 @@ namespace ts {
return createAnonymousType(symbol, members, emptyArray, emptyArray, emptyArray);
}

// cache this type on the class symbol?
function createClassMetaPropertyExpressionType(targetClass: Type): Type {
// Create a synthetic type `ClassMetaProperty { hasInstance(value: unknown): unknown is TargetClass }`
const symbol = createSymbol(SymbolFlags.None, "ClassMetaProperty" as __String);

// class.hasInstance
const hasInstanceSymbol = createSymbol(SymbolFlags.Property, "hasInstance" as __String, CheckFlags.Readonly);
{
hasInstanceSymbol.parent = symbol;

const arg0 = createSymbol(SymbolFlags.None, "value" as __String);
arg0.type = unknownType;
const classConstructor = getSingleSignature(targetClass, SignatureKind.Construct, /** allow members */ true);
const classInstance = classConstructor ? getReturnTypeOfSignature(classConstructor) : errorType;
const hasInstanceReturnType = createTypePredicate(TypePredicateKind.Identifier, "value", 0, classInstance);
const signature = createSignature(
/** declaration */ undefined,
/** generics */ undefined,
/** this */ undefined,
[arg0],
booleanType,
hasInstanceReturnType,
/** minArgCount */ 1,
SignatureFlags.None,
);
hasInstanceSymbol.type = createAnonymousType(/** symbol */ undefined, emptySymbols, [signature], emptyArray, emptyArray);
}

const members = createSymbolTable([hasInstanceSymbol]);
symbol.members = members;
return createAnonymousType(symbol, members, emptyArray, emptyArray, emptyArray);
}

function getReturnTypeFromBody(func: FunctionLikeDeclaration, checkMode?: CheckMode): Type {
if (!func.body) {
return errorType;
Expand Down Expand Up @@ -41230,6 +41284,11 @@ namespace ts {
if (parent.keywordToken === SyntaxKind.NewKeyword) {
return checkNewTargetMetaProperty(parent).symbol;
}
if (parent.keywordToken === SyntaxKind.ClassKeyword) {
const meta = checkClassHasInstanceProperty(parent).symbol;
const prop = meta.members?.get(parent.name.escapedText);
return prop;
}
}
}

Expand Down Expand Up @@ -41297,8 +41356,9 @@ namespace ts {
case SyntaxKind.DefaultKeyword:
case SyntaxKind.FunctionKeyword:
case SyntaxKind.EqualsGreaterThanToken:
case SyntaxKind.ClassKeyword:
return getSymbolOfNode(node.parent);
case SyntaxKind.ClassKeyword:
return isMetaProperty(node.parent) ? checkMetaPropertyKeyword(node.parent).symbol : getSymbolOfNode(node.parent);
case SyntaxKind.ImportType:
return isLiteralImportTypeNode(node) ? getSymbolAtLocation(node.argument.literal, ignoreErrors) : undefined;

Expand Down Expand Up @@ -43846,6 +43906,16 @@ namespace ts {
return grammarErrorOnNode(node.name, Diagnostics._0_is_not_a_valid_meta_property_for_keyword_1_Did_you_mean_2, node.name.escapedText, tokenToString(node.keywordToken), "meta");
}
break;
case SyntaxKind.ClassKeyword:
if (escapedText !== "hasInstance") {
return grammarErrorOnNode(node.name, Diagnostics._0_is_not_a_valid_meta_property_for_keyword_1_Did_you_mean_2, node.name.escapedText, tokenToString(node.keywordToken), "hasInstance");
}
else {
if (node.parent.kind !== SyntaxKind.CallExpression) {
return grammarErrorOnNode(node, Diagnostics._0_expected, "class.hasInstance()");
}
}
break;
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -6264,6 +6264,10 @@
"category": "Error",
"code": 17018
},
"Meta-property '{0}' is only allowed in the body of a class.": {
"category": "Error",
"code": 17019
},
"Circularity detected while resolving configuration: {0}": {
"category": "Error",
"code": 18000
Expand Down
1 change: 1 addition & 0 deletions src/compiler/factory/nodeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3129,6 +3129,7 @@ namespace ts {
node.transformFlags |= TransformFlags.ContainsES2015;
break;
case SyntaxKind.ImportKeyword:
case SyntaxKind.ClassKeyword:
node.transformFlags |= TransformFlags.ContainsESNext;
break;
default:
Expand Down
19 changes: 16 additions & 3 deletions src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4111,10 +4111,9 @@ namespace ts {
}

function isStartOfExpressionStatement(): boolean {
// As per the grammar, none of '{' or 'function' or 'class' can start an expression statement.
// As per the grammar, none of '{' or 'function' can start an expression statement.
return token() !== SyntaxKind.OpenBraceToken &&
token() !== SyntaxKind.FunctionKeyword &&
token() !== SyntaxKind.ClassKeyword &&
token() !== SyntaxKind.AtToken &&
isStartOfExpression();
}
Expand Down Expand Up @@ -5615,6 +5614,9 @@ namespace ts {

return parseFunctionExpression();
case SyntaxKind.ClassKeyword:
if (lookAhead(nextTokenIsDot)) {
return parseClassMetaProperty();
};
return parseClassExpression();
case SyntaxKind.FunctionKeyword:
return parseFunctionExpression();
Expand Down Expand Up @@ -6165,9 +6167,10 @@ namespace ts {
case SyntaxKind.LetKeyword:
case SyntaxKind.ConstKeyword:
case SyntaxKind.FunctionKeyword:
case SyntaxKind.ClassKeyword:
case SyntaxKind.EnumKeyword:
return true;
case SyntaxKind.ClassKeyword:
return !lookAhead(nextTokenIsDot);

// 'declare', 'module', 'namespace', 'interface'* and 'type' are all legal JavaScript identifiers;
// however, an identifier cannot be followed by another identifier on the same line. This is what we
Expand Down Expand Up @@ -6330,6 +6333,9 @@ namespace ts {
case SyntaxKind.FunctionKeyword:
return parseFunctionDeclaration(getNodePos(), hasPrecedingJSDocComment(), /*decorators*/ undefined, /*modifiers*/ undefined);
case SyntaxKind.ClassKeyword:
if (lookAhead(nextTokenIsDot)) {
return parseExpressionOrLabeledStatement();
}
return parseClassDeclaration(getNodePos(), hasPrecedingJSDocComment(), /*decorators*/ undefined, /*modifiers*/ undefined);
case SyntaxKind.IfKeyword:
return parseIfStatement();
Expand Down Expand Up @@ -6997,6 +7003,13 @@ namespace ts {
return Debug.fail("Should not have attempted to parse class member declaration.");
}

function parseClassMetaProperty(): MetaProperty {
const pos = getNodePos();
nextToken(); // advance past the 'class'
nextToken(); // advance past the dot
return finishNode(factory.createMetaProperty(SyntaxKind.ClassKeyword, parseIdentifierName()), pos);
}

function parseClassExpression(): ClassExpression {
return parseClassDeclarationOrExpression(getNodePos(), hasPrecedingJSDocComment(), /*decorators*/ undefined, /*modifiers*/ undefined, SyntaxKind.ClassExpression) as ClassExpression;
}
Expand Down
143 changes: 122 additions & 21 deletions src/compiler/transformers/esnext.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,125 @@
/*@internal*/
namespace ts {
export function transformESNext(context: TransformationContext) {
return chainBundle(context, transformSourceFile);

function transformSourceFile(node: SourceFile) {
if (node.isDeclarationFile) {
return node;
}

return visitEachChild(node, visitor, context);
}

function visitor(node: Node): VisitResult<Node> {
if ((node.transformFlags & TransformFlags.ContainsESNext) === 0) {
return node;
}
switch (node.kind) {
default:
return visitEachChild(node, visitor, context);
}
}
}
export function transformESNext(context: TransformationContext) {
let currentClassHasInstanceTracker: Identifier | undefined;
return chainBundle(context, transformSourceFile);

function transformSourceFile(node: SourceFile) {
if (node.isDeclarationFile) {
return node;
}

return visitEachChild(node, visitor, context);
}

function visitor(node: Node): VisitResult<Node> {
if ((node.transformFlags & TransformFlags.ContainsESNext) === 0) {
return node;
}
switch (node.kind) {
case SyntaxKind.ClassDeclaration:
case SyntaxKind.ClassExpression:
return visitClassLike(node as ClassDeclaration | ClassExpression);
case SyntaxKind.CallExpression:
return visitCallExpression(node as CallExpression);
default:
return visitEachChild(node, visitor, context);
}
}

function visitClassLike(node: ClassDeclaration | ClassExpression) {
const oldClassHasInstanceTracker = currentClassHasInstanceTracker;
currentClassHasInstanceTracker = undefined;
const updated = visitEachChild(node, visitor, context);
if (!currentClassHasInstanceTracker) {
currentClassHasInstanceTracker = oldClassHasInstanceTracker;
return updated;
}

// var tracker; tracker = new WeakSet();
context.hoistVariableDeclaration(currentClassHasInstanceTracker);
context.addInitializationStatement(
factory.createExpressionStatement(
factory.createAssignment(
currentClassHasInstanceTracker,
factory.createNewExpression(factory.createIdentifier("WeakSet"), /** generics */ undefined, /** args */ undefined)
)
)
);

// tracker.add(this);
const track = factory.createExpressionStatement(factory.createCallExpression(factory.createPropertyAccessExpression(currentClassHasInstanceTracker, "add"), /** generics */ undefined, [factory.createThis()]));

const originalConstructor = getFirstConstructorWithBody(node);
let updatedConstructor: ConstructorDeclaration = originalConstructor || createDefaultConstructor(isClassExtended(node), [track]);
if (originalConstructor) {
const body = updatedConstructor.body!;
const updatedBody = isClassExtended(node) ?
// extended class, add track after super()
visitEachChild(body, function visitor(node): VisitResult<Node> {
if (node.kind === SyntaxKind.ClassDeclaration || node.kind === SyntaxKind.ClassExpression || node.kind === SyntaxKind.FunctionDeclaration || node.kind === SyntaxKind.FunctionExpression) return node;
if (isCallExpression(node) && node.expression.kind === SyntaxKind.SuperKeyword) {
return factory.createCommaListExpression([node, track.expression, factory.createThis()]);
}
return visitEachChild(node, visitor, context);
}, context) :
// plain class, add track at the top
factory.updateBlock(body, [track, ...body.statements]);
updatedConstructor = factory.updateConstructorDeclaration(updatedConstructor, updatedConstructor.decorators, updatedConstructor.modifiers, updatedConstructor.parameters, updatedBody);
}

const updatedMembers = originalConstructor ? updated.members.map(element => element === originalConstructor ? updatedConstructor : element) : [updatedConstructor, ...updated.members];

currentClassHasInstanceTracker = oldClassHasInstanceTracker;
if (isClassDeclaration(updated)) {
return factory.updateClassDeclaration(
updated,
updated.decorators,
updated.modifiers,
updated.name,
updated.typeParameters,
updated.heritageClauses,
updatedMembers,
);
}
else {
return factory.updateClassExpression(
updated,
updated.decorators,
updated.modifiers,
updated.name,
updated.typeParameters,
updated.heritageClauses,
updatedMembers,
);
}
}

function visitCallExpression(node: CallExpression) {
const lhs = node.expression as MetaProperty;
if (lhs.kind !== SyntaxKind.MetaProperty || lhs.keywordToken !== SyntaxKind.ClassKeyword) return visitEachChild(node, visitor, context);
if (!currentClassHasInstanceTracker) {
currentClassHasInstanceTracker = factory.createTempVariable(noop, /** reserveNested */ true);
}
// tracker.has()
const trackerDotHas = factory.createPropertyAccessChain(currentClassHasInstanceTracker, /** ?. */ undefined, "has");
const arg0 = visitEachChild(node.arguments[0] || factory.createNull(), visitor, context);
return factory.createCallExpression(trackerDotHas, /** generics */ undefined, [arg0]);
}
}
function isClassExtended(node: ClassLikeDeclaration) {
return some(node.heritageClauses, node => node.token === SyntaxKind.ExtendsKeyword);
}
function createDefaultConstructor(isExtended: boolean, additionalStatements: Statement[]) {
const params: ParameterDeclaration[] = [];
const statements: Statement[] = [];
if (isExtended) {
const rest = factory.createTempVariable(noop);
const param = factory.createParameterDeclaration(/** deco */ undefined, /** mod */ undefined, factory.createToken(SyntaxKind.DotDotDotToken), rest);
params.push(param);
statements.push(factory.createExpressionStatement(factory.createCallExpression(factory.createSuper(), /** generics */ undefined, [factory.createSpreadElement(rest)])));
}
statements.push(...additionalStatements);
return factory.createConstructorDeclaration(/** deco */ undefined, /** modifiers */ undefined, params, factory.createBlock(statements));
}
}
2 changes: 1 addition & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2504,7 +2504,7 @@ namespace ts {
// for the same reasons we treat NewExpression as a PrimaryExpression.
export interface MetaProperty extends PrimaryExpression {
readonly kind: SyntaxKind.MetaProperty;
readonly keywordToken: SyntaxKind.NewKeyword | SyntaxKind.ImportKeyword;
readonly keywordToken: SyntaxKind.NewKeyword | SyntaxKind.ImportKeyword | SyntaxKind.ClassKeyword;
readonly name: Identifier;
}

Expand Down
3 changes: 1 addition & 2 deletions src/services/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1877,7 +1877,7 @@ namespace ts.Completions {
break;
case SyntaxKind.MetaProperty:
node = parent.getFirstToken(sourceFile)!;
Debug.assert(node.kind === SyntaxKind.ImportKeyword || node.kind === SyntaxKind.NewKeyword);
Debug.assert(node.kind === SyntaxKind.ImportKeyword || node.kind === SyntaxKind.NewKeyword || node.kind === SyntaxKind.ClassKeyword);
break;
default:
// There is nothing that precedes the dot, so this likely just a stray character
Expand Down Expand Up @@ -4064,4 +4064,3 @@ namespace ts.Completions {
}

}

4 changes: 2 additions & 2 deletions tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1323,7 +1323,7 @@ declare namespace ts {
}
export interface MetaProperty extends PrimaryExpression {
readonly kind: SyntaxKind.MetaProperty;
readonly keywordToken: SyntaxKind.NewKeyword | SyntaxKind.ImportKeyword;
readonly keywordToken: SyntaxKind.NewKeyword | SyntaxKind.ImportKeyword | SyntaxKind.ClassKeyword;
readonly name: Identifier;
}
export interface JsxElement extends PrimaryExpression {
Expand Down Expand Up @@ -10945,7 +10945,7 @@ declare namespace ts {
/** @deprecated Use `factory.updateNonNullChain` or the factory supplied by your transformation context instead. */
const updateNonNullChain: (node: NonNullChain, expression: Expression) => NonNullChain;
/** @deprecated Use `factory.createMetaProperty` or the factory supplied by your transformation context instead. */
const createMetaProperty: (keywordToken: SyntaxKind.ImportKeyword | SyntaxKind.NewKeyword, name: Identifier) => MetaProperty;
const createMetaProperty: (keywordToken: SyntaxKind.ClassKeyword | SyntaxKind.ImportKeyword | SyntaxKind.NewKeyword, name: Identifier) => MetaProperty;
/** @deprecated Use `factory.updateMetaProperty` or the factory supplied by your transformation context instead. */
const updateMetaProperty: (node: MetaProperty, name: Identifier) => MetaProperty;
/** @deprecated Use `factory.createTemplateSpan` or the factory supplied by your transformation context instead. */
Expand Down
Loading

0 comments on commit 2384d95

Please sign in to comment.