Skip to content

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

Merged
sandersn merged 17 commits into
microsoft:masterfrom
about-code:master
Jan 17, 2017
Merged

Fixing #442: Impossible to define static 'length' function on class#12065
sandersn merged 17 commits into
microsoft:masterfrom
about-code:master

Conversation

@about-code

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

Copy link
Copy Markdown
Contributor

[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

msftclas commented Nov 5, 2016

Copy link
Copy Markdown

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

basarat commented Nov 5, 2016

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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

@sandersn sandersn left a comment

Copy link
Copy Markdown
Member

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?

Comment thread src/compiler/checker.ts Outdated
}
}

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

Copy link
Copy Markdown
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.

Comment thread src/compiler/checker.ts Outdated
}

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

Copy link
Copy Markdown
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?

Comment thread src/compiler/checker.ts Outdated
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
Copy Markdown
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
Copy Markdown
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.

Comment thread src/compiler/checker.ts Outdated
// 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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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

sandersn commented Jan 3, 2017

Copy link
Copy Markdown
Member

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.

Comment thread src/compiler/checker.ts Outdated
*/
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
Copy Markdown
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
Copy Markdown
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.

Comment thread src/compiler/checker.ts
const isStatic = getModifierFlags(member) & ModifierFlags.Static;
const isMethod = member.kind === SyntaxKind.MethodDeclaration;
const memberNameNode = member.name;
if (isStatic && memberNameNode) {

Copy link
Copy Markdown
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;
}

Comment thread src/compiler/checker.ts Outdated
memberName === "name" ||
memberName === "length" ||
memberName === "caller" ||
memberName === "arguments"

Copy link
Copy Markdown
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.

Comment thread src/compiler/checker.ts Outdated
}
}
else { // ES6+
if (memberName === "prototype") {

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown

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
Copy Markdown
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
Copy Markdown
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