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.
sandersn
left a comment
There was a problem hiding this comment.
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?
| } | ||
| } | ||
|
|
||
| // Static members may conflict with non-configurable non-writable built-in Function object properties |
| } | ||
|
|
||
| // 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.
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.
should be getModifierFlags(node) & ModifierFlags.Static -- this calculates the modifiers once and then caches them
There was a problem hiding this comment.
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.
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.
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.
Can you add a test for default-exported classes and anonymous class expressions?
There was a problem hiding this comment.
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.
Reduce nesting by negating this:
if (!isStatic || !memberNameNode) {
continue;
}| memberName === "name" || | ||
| memberName === "length" || | ||
| memberName === "caller" || | ||
| memberName === "arguments" |
There was a problem hiding this comment.
Consider a switch/case here instead.
| } | ||
| } | ||
| else { // ES6+ | ||
| if (memberName === "prototype") { |
There was a problem hiding this comment.
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
masterbranch[x] You've successfully run
jake runtestslocally[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
es5or 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 targetes6in 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
es5because ofFunction.namebeing non-configurable and non-writable by spec. and at least in somees5runtimes. Ines6targetsFunction.nameis 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 fores6runtimes 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
es6and 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