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

Fixing #442: Impossible to define static 'length' function on class #12065

Merged
merged 17 commits into from Jan 17, 2017
Merged

Fixing #442: Impossible to define static 'length' function on class #12065

merged 17 commits into from Jan 17, 2017

Conversation

about-code
Copy link
Contributor

@about-code about-code commented Nov 5, 2016

[x] There is an associated issue that is labelled 'Bug' or 'Accepting PRs' or is in the Community milestone
[x] Code is up-to-date with the master branch
[x] You've successfully run jake runtests locally
[x] You've signed the CLA
[x] There are new or updated unit tests validating the change

Fixes #442

By accepting this pull request when a static property name conflict is detected a new error message

TS2699: Static property 'X' conflicts with built-in property 'Function.X' of constructor function 'Y'

will be issued.

Conflicting method declarations

For example

class Foo {
    static name() {
        return "Bar";
    }
}

produces

TS2699: Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'Foo'

for compile target es5 or lower. Because ES2015 runtimes seem to be able to handle potentially conflicting static method declarations natively (apart from prototype), by internally setting the native function property to writable: true, there won't be an error for target es6 in the current implementation. The absence of an error in this case just indicates, that it will be handled without a runtime error. It may still lead to unexpected results if a user is not aware of overwriting the class constructor's name property which he might want to use elsewhere to read the class name (see examples on MDN). So I leave open to discussions if we should also issue an error for these cases when targeting es6.

Conflicting property declarations

class Foo {
    static name = "Bar";
}

is impossible to fix for target es5 because of Function.name being non-configurable and non-writable by spec. and at least in some es5 runtimes. In es6 targets Function.name is configurable but non-writable by default. Without changes to the emitter above value would not be assigned. It seems like it could be made possible for es6 runtimes and above, using an emit like

Object.defineProperty(Foo, "name", {
   writable: true,
   value: "Bar"
});

Unfortunately I am not able to contribute such change myself. The pragmatic solution will be to also raise a transpile error which some may favour over a transpile, anyway.

Eventually, here's a matrix of the various static member declarations and when a transpile error will be raised:

Target es3, es5:

member declaration configurable writable How handled
static prototype = "" no no error
static arguments = "" no no error
static caller = "" no no error
static length = "" no no error
static name = "" no no error
method declaration configurable writable How handled
static prototype(){...} no no error
static arguments(){...} no no error
static caller(){...} no no error
static length(){...} no no error
static name(){...} no no error

Target es6 and higher:

member declaration configurable writable How handled
static prototype = "" no no error
static arguments = "" yes no error*
static caller = "" yes no error*
static length = "" yes no error*
static name = "" yes no error*
method declaration configurable writable How handled
static prototype(){...} no no error
static arguments(){...} yes no** ok
static caller(){...} yes no** ok
static length(){...} yes no** ok
static name(){...} yes no** ok

* could be enabled by changing emitter output
** property made writable internally by the runtime, once a method declaration exists

Prior work

TypeScript has been preventing the declaration of a static class member prototype since the first public commit by @mhegazy and since then issued a TS2300: Duplicate identifier 'prototype' error.

class Foo {
    static property: any;
}

There were no changes made to the existing implementation. However, having a static prototype property falls into the same category of errors as to be handled for fixing #442. Hence when running the tests for this pull request the baseline for test case tests/cases/conformance/classes/propertyMemberDeclarations/propertyNamedPrototype.ts changed and now also contains a static property conflict error. I accepted this as a new baseline.

Another attempt to fix #442 is PR GH-5396

@msftclas
Copy link

msftclas commented Nov 5, 2016

Hi @about-code, I'm your friendly neighborhood Microsoft Pull Request Bot (You can call me MSBOT). Thanks for your contribution!
You've already signed the contribution license agreement. Thanks!
We will now validate the agreement and then real humans will evaluate your PR.

TTYL, MSBOT;

@basarat
Copy link
Contributor

basarat commented Nov 5, 2016

Not on the TypeScript dev team. That said, the error message looks fantastic 🌹❤️

…tionInStrictMode1.errors.txt

after rebuilding and testing compiler.
@about-code
Copy link
Contributor Author

@basarat you're as visible and active in the TS community as if you were part of the dev team 😄 Have got in touch with TS thanks to you and atom-typescript. So this 🌹 from 🇩🇪 is for you. Glad to here, you like the message.

Disallow non-function properties `static arguments` and `static caller`, though.
@about-code about-code closed this Nov 6, 2016
@about-code about-code reopened this Nov 9, 2016
@sandersn sandersn self-assigned this Dec 15, 2016
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.

Overall the change looks good. I am not sure the method/property distinction is worthwhile to make in ES6. Not only would it be simpler to explain to people, it would simplify the code as well.

@rbuckton, can you take a look at the proposed semantics? Do you have an opinion about whether we should follow ES6 semantics exactly or simplify them so that using these names is always an error?

@@ -14771,6 +14771,49 @@ namespace ts {
}
}

// Static members may conflict with non-configurable non-writable built-in Function object properties
Copy link
Member

Choose a reason for hiding this comment

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

Please use jsdoc format.

@@ -14771,6 +14771,49 @@ namespace ts {
}
}

// Static members may conflict with non-configurable non-writable built-in Function object properties
// see https://github.com/microsoft/typescript/issues/442.
Copy link
Member

Choose a reason for hiding this comment

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

instead of a github issue number, can you reference the spec the way you do below instead?

const message = Diagnostics.Static_property_0_conflicts_with_built_in_property_Function_0_of_constructor_function_1;
const className = getSymbolOfNode(node).name;
for (const member of node.members) {
const isStatic = forEach(member.modifiers, (m: Modifier) => m.kind === SyntaxKind.StaticKeyword);
Copy link
Member

Choose a reason for hiding this comment

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

should be getModifierFlags(node) & ModifierFlags.Static -- this calculates the modifiers once and then caches them

Copy link
Member

Choose a reason for hiding this comment

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

Yikes, looks like checkClassForDuplicateDeclarations also uses this code. We should change that pretty soon.

// see https://github.com/microsoft/typescript/issues/442.
function checkClassForStaticPropertyNameConflicts(node: ClassLikeDeclaration) {
const message = Diagnostics.Static_property_0_conflicts_with_built_in_property_Function_0_of_constructor_function_1;
const className = getSymbolOfNode(node).name;
Copy link
Member

Choose a reason for hiding this comment

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

getNameOfSymbol(getSymbolOfNode(node)) would be better here. You may have to move getNameOfSymbol to make it accessible outside getSymbolDisplayBuilder.

Copy link
Member

Choose a reason for hiding this comment

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

Or just node.name, but that doesn't work for anonymous classes, unfortunately. I think that means some of the other error reporting in checkClassLikeDeclaration is broken, because it uses node.name.

@about-code
Copy link
Contributor Author

@sandersn Thanks for your review and comments. I'll try to implement them as soon as possible. I fully agree with your concerns regarding potential confusion about member/method distinction for es6.

@sandersn
Copy link
Member

sandersn commented Jan 3, 2017

I just talked to @rbuckton and we both agree that we should simplify ES6' semantics and get rid of the method/property distinction. Using these names should always be an error.

*/
function checkClassForStaticPropertyNameConflicts(node: ClassLikeDeclaration) {
const message = Diagnostics.Static_property_0_conflicts_with_built_in_property_Function_0_of_constructor_function_1;
const className = getNameOfSymbol(getSymbolOfNode(node));
Copy link
Member

Choose a reason for hiding this comment

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

Can you add a test for default-exported classes and anonymous class expressions?

Copy link
Member

Choose a reason for hiding this comment

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

Also, I would also only get the information you need when you actually report the error. Consider extracting that into a helper function.

const isStatic = getModifierFlags(member) & ModifierFlags.Static;
const isMethod = member.kind === SyntaxKind.MethodDeclaration;
const memberNameNode = member.name;
if (isStatic && memberNameNode) {
Copy link
Member

Choose a reason for hiding this comment

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

Reduce nesting by negating this:

if (!isStatic || !memberNameNode) {
    continue;
}

memberName === "name" ||
memberName === "length" ||
memberName === "caller" ||
memberName === "arguments"
Copy link
Member

Choose a reason for hiding this comment

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

Consider a switch/case here instead.

}
}
else { // ES6+
if (memberName === "prototype") {
Copy link
Member

Choose a reason for hiding this comment

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

switch (memberName) {
    case "name":
    case "length":
    case "caller":
    case "arguments":
        if (isMethod) {
            continue;
        }
        // fall through
    case "prototype":
        error(/*...*/);

@DanielRosenwasser
Copy link
Member

Just saw @sandersn's feedback - feel free to simplify on top of any style nits I left behind based on that.

Adding tests with default export and anonymous class expressions.
Adding tests with default export and anonymous class expressions.
@about-code
Copy link
Contributor Author

Like @sandersn noted, by omitting es6 method/property distinction we eventually resolve any differences between the currently supported targets. So I changed my proposal to implement the following semantics:

All currently supported targets:

member declaration method declaration How handled
static prototype = "" static prototype(){...} error
static arguments = "" static arguments(){...} error
static caller = "" static caller(){...} error
static length = "" static length(){...} error
static name = "" static name(){...} error

Future targets:

I recently found out about a tc39 stage2 proposal about class-public-fields. From your reading of the draft, do you think it correctly addresses potential conflicts? For example would a [[PublicFields]] internal slot as defined in the proposal prevent collisions in future versions of ECMAScript - at least for public static fields?

@about-code
Copy link
Contributor Author

There's also a tc39 proposal for private fields.

@about-code about-code closed this Jan 14, 2017
@about-code about-code reopened this Jan 14, 2017
@msftclas
Copy link

Hi @about-code, I'm your friendly neighborhood Microsoft Pull Request Bot (You can call me MSBOT). Thanks for your contribution!
You've already signed the contribution license agreement. Thanks!

The agreement was validated by Microsoft and real humans are currently evaluating your PR.

TTYL, MSBOT;

@sandersn
Copy link
Member

I think it's fine to put the error in as-is, and if/when the class-public-fields proposal moves to stage 3, we can remove the error for the --esnext target (or whatever edition the proposal makes it into the official spec).

You might raise your concerns with the spec authors, though. During class initialisation, everything in [[PublicFields]] gets passed to DefinePropertyOrThrow, which should have the same behaviour as ES6. I think. I'm not sure I'm reading it correctly.

@sandersn
Copy link
Member

Thanks @about-code!

@sandersn sandersn merged commit 899d512 into microsoft:master Jan 17, 2017
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Impossible to define static 'length' function on class
5 participants