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
Conversation
Hi @about-code, I'm your friendly neighborhood Microsoft Pull Request Bot (You can call me MSBOT). Thanks for your contribution! TTYL, MSBOT; |
… not included in prior commit).
Not on the TypeScript dev team. That said, the error message looks fantastic 🌹❤️ |
…tionInStrictMode1.errors.txt after rebuilding and testing compiler.
@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.
There was a problem hiding this 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 |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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
.
@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. |
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)); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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") { |
There was a problem hiding this comment.
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(/*...*/);
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.
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:
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 |
There's also a tc39 proposal for private fields. |
Hi @about-code, I'm your friendly neighborhood Microsoft Pull Request Bot (You can call me MSBOT). Thanks for your contribution! The agreement was validated by Microsoft and real humans are currently evaluating your PR. TTYL, MSBOT; |
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 |
Thanks @about-code! |
[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
will be issued.
Conflicting method declarations
For example
produces
for compile target
es5
or lower. Because ES2015 runtimes seem to be able to handle potentially conflicting static method declarations natively (apart fromprototype
), by internally setting the native function property towritable: true
, there won't be an error for targetes6
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 targetinges6
.Conflicting property declarations
is impossible to fix for target
es5
because ofFunction.name
being non-configurable and non-writable by spec. and at least in somees5
runtimes. Ines6
targetsFunction.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 fores6
runtimes and above, using an emit likeUnfortunately 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
:static prototype = ""
static arguments = ""
static caller = ""
static length = ""
static name = ""
static prototype(){...}
static arguments(){...}
static caller(){...}
static length(){...}
static name(){...}
Target
es6
and higher:static prototype = ""
static arguments = ""
static caller = ""
static length = ""
static name = ""
static prototype(){...}
static arguments(){...}
static caller(){...}
static length(){...}
static name(){...}
* 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.
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