Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ES private field check #44648

Merged
merged 45 commits into from
Sep 24, 2021
Merged

ES private field check #44648

merged 45 commits into from
Sep 24, 2021

Conversation

acutmore
Copy link
Contributor

@acutmore acutmore commented Jun 18, 2021

Ergonomic brand checks for Private Fields

This PR implements the TC39 Stage-4 Ergonomic brand checks for Private Fields proposal

Fixes #42574

Remaining Work

Type Checking

Checking the presence of a private field provides a strong type narrowing hint.

class C {
  #field = 1;

  static getFieldOrUndefined(value: object) {
      if (#field in value) {
          // 'value' is narrowed from 'object' to 'C'
          return value.#field;
      }
      return undefined;
  }
}

Unsoundness

Whilst the runtime check is accurate, this static check is not fool-proof. The super constructor return pattern can add a private field to an object that is not an instance of the class (different prototype). This PR opts to narrow the type without checking for this case under the assumption that this pattern is uncommon and discoverable by looking at the constructor of the super class.

Example
class Base {
    constructor(o: object) {
        return o;
    }
}

class Sub extends Base {
    #field = 1;

    constructor(o: object) {
        super(o);
    }

    method() {}

    static run(o: object) {
        if (#field in o) {
            // 'o' is now of type 'Sub' but...
            o.method(); // Throws 'o.method is not a function'
        }
    }
}

const o = {};
new Sub(o);
Sub.run(o);

Downlevel

Builds on top of the existing downlevel support for ES private class elements. If the private field is an instance field then the coresponding WeakMap is consulted. Private methods and accessors consult the WeakSet. Private static fields/methods/accessors do a direct equality check with the class constructor.

Example transform

TypeScript

class Foo {
    #field = 1;
    #method() {}
    static #staticField = 2;
    static #staticMethod() {}

    check(v: any) {
        #field in v;
        #method in v;
        #staticField in v;
        #staticMethod in v;
    }
}

function syntaxError(v: Foo) {
    return #field in v;
}

JavaScript - ES2020 emit

var __classPrivateFieldIn = (this && this.__classPrivateFieldIn) || function(receiver, state) {
    if (receiver === null || (typeof receiver !== "object" && typeof receiver !== "function")) throw new TypeError("Cannot use 'in' operator on non-object");
    return typeof state === "function" ? receiver === state : state.has(receiver);
};
var _Foo_instances, _a, _Foo_field, _Foo_method, _Foo_staticField, _Foo_staticMethod, _Bar_field;
class Foo {
    constructor() {
        _Foo_instances.add(this);
        _Foo_field.set(this, 1);
    }
    check(v) {
        __classPrivateFieldIn(v, _Foo_field);
        __classPrivateFieldIn(v, _Foo_instances);
        __classPrivateFieldIn(v, _a);
        __classPrivateFieldIn(v, _a);
    }
}
_a = Foo, _Foo_field = new WeakMap(), _Foo_instances = new WeakSet(), _Foo_method = function _Foo_method() { }, _Foo_staticMethod = function _Foo_staticMethod() { };
_Foo_staticField = { value: 2 };

function syntaxError(v) {
    return  in v;
}

References

Credits

This PR includes contributions from the following Bloomberg engineers:

add support for the 'private-fields-in-in' TC39 proposal

Signed-off-by: Ashley Claymore <[email protected]>
@typescript-bot
Copy link
Collaborator

The TypeScript team hasn't accepted the linked issue #42574. If you can get it accepted, this PR will have a better chance of being reviewed.

@typescript-bot typescript-bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Jun 18, 2021
@acutmore
Copy link
Contributor Author

I'm looking into the CI failures. Tests were passing locally before I pushed. I'll start with a fresh checkout this time to make sure I don't miss something.

src/compiler/types.ts Outdated Show resolved Hide resolved
@sandersn sandersn self-requested a review August 12, 2021 23:26
@typescript-bot typescript-bot added For Backlog Bug PRs that fix a backlog bug and removed For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Aug 12, 2021
@acutmore
Copy link
Contributor Author

Hi @sandersn, I can see the 4.5 beta release is scheduled for 2 weeks time here.
I wanted to be sure to mark off time in my calendar to respond to review comments, do you know roughly when you would have time to take a look? Thanks!

Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly questions in the checker, plus a background question in emitHelpers.

src/compiler/factory/emitHelpers.ts Show resolved Hide resolved
src/compiler/checker.ts Outdated Show resolved Hide resolved
src/compiler/checker.ts Outdated Show resolved Hide resolved
src/compiler/checker.ts Outdated Show resolved Hide resolved
Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove the lexical lookup code and open an issue+PR pair to discuss how it should work in general. I don't think it should be private-identifier specific.

src/compiler/checker.ts Outdated Show resolved Hide resolved
tests/cases/fourslash/codeFixSpellingPrivateNameInIn.ts Outdated Show resolved Hide resolved
Copy link
Member

@rbuckton rbuckton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to do a more thorough review, but my primary concern is that I don't think we need to add a new node for this.

}
}
if (!symbolResolved) {
error(privId, Diagnostics.Private_identifiers_are_not_allowed_outside_class_bodies);
Copy link
Member

@rbuckton rbuckton Sep 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like something we should be reporting this as a grammar error. Something like:

Private identifiers are only allowed in class bodies and may only be used as part of a class member declaration, property access, or on the left-hand-side of an 'in' expression

@DanielRosenwasser any thoughts on wording?

checkExternalEmitHelpers(left, ExternalEmitHelpers.ClassPrivateFieldIn);
}
// Unlike in 'checkPrivateIdentifierExpression' we now have access to the RHS type
// which provides us with the oppotunity to emit more detailed errors
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// which provides us with the oppotunity to emit more detailed errors
// which provides us with the opportunity to emit more detailed errors

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regretting that when I disabled all of my extensions to free up some CPU I also disabled the code-spell-checker. I'll reenable, as mistakes like this shouldn't be getting through to the PR.

Comment on lines 32082 to 32084
if (leftType === silentNeverType || rightType === silentNeverType) {
return silentNeverType;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have a silentNeverType, we're generally in an inference path and bail out of checking. Instead of splitting up the function body around privIdOnLeft, should we just move this to the top of the function?

Copy link
Contributor Author

@acutmore acutmore Sep 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ve been meaning to look into the difference between never and silentNever, thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've moved this to the top now. I hadn't done that because I thought we wouldn't get the checkExternalEmitHelpers error when the RHS was never but I checked and as you suggested - this is still OK as the silentNeverType has a different use case.


const privateId = expr.left;
Debug.assertNode(privateId, isPrivateIdentifier);
const symbol = lookupSymbolForPrivateIdentifierDeclaration(privateId.escapedText, privateId);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For find-all-refs, you're possibly being tripped up by the fact PrivateIdentifier isn't handled by isExpressionNode:

export function isExpressionNode(node: Node): boolean {

called from here:

https://github.com/microsoft/TypeScript/blob/7c49552cd516da620b903edccc61af54d1872c3b/src/compiler/checker.ts#L40232

For an Identifier, we would perform a normal name resolution:

https://github.com/microsoft/TypeScript/blob/7c49552cd516da620b903edccc61af54d1872c3b/src/compiler/checker.ts#L40245

Instead, you're storing the symbol on the parent binary expression and then looking it up at https://github.com/microsoft/TypeScript/blob/7c49552cd516da620b903edccc61af54d1872c3b/src/compiler/checker.ts#L40284, which seems strange. We don't normally store the symbol on Identifier references, since they can have multiple meanings, so it seems a bit strange to cache it for PrivateIdentifier (despite @sandersn's comment). However, since a PrivateIdentifier can't have anything other than a value meaning currently, we could probably store it on the NodeLinks for the id itself, rather than its parent expression.

src/compiler/parser.ts Outdated Show resolved Hide resolved
@acutmore
Copy link
Contributor Author

tslib PR opened microsoft/tslib#157

Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few more requests for the parser and checker.

src/compiler/parser.ts Outdated Show resolved Hide resolved
src/compiler/parser.ts Outdated Show resolved Hide resolved
src/compiler/checker.ts Show resolved Hide resolved
src/compiler/checker.ts Show resolved Hide resolved
src/compiler/checker.ts Show resolved Hide resolved
Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new changes look good to me. I'll sign off once the new message baselines are updated and then we'll be ready to merge once @rbuckton signs off.

@sandersn
Copy link
Member

@rbuckton is out for a family emergency so I checked that his comments were resolved. I'm going to merge this now and look at the tslib change next.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
For Backlog Bug PRs that fix a backlog bug
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

Proposal private-fields-in-in is stage-3
8 participants