Add override keyword#13217
Add override keyword#13217pcj wants to merge 10 commits intomicrosoft:masterfrom windward-studios:override
Conversation
| }, | ||
| { | ||
| name: "noImplicitOverride", | ||
| type: "boolean", |
There was a problem hiding this comment.
Description here is what you would see on the commandline under -?, i.e. this should be a string like "Raise an error when overriding class methods are not marked with 'overide'"
src/compiler/checker.ts
Outdated
| } | ||
|
|
||
| function checkOverrideMethodDeclaration(node: MethodDeclaration) { | ||
| forEachEnclosingClass(node, enclosingClass => { |
There was a problem hiding this comment.
You'll want to only checking the containing class. The function forEachEnclosingClass is for dealing with nesting of classes, not inheritance chains
src/compiler/checker.ts
Outdated
| error(node, Diagnostics.Method_0_cannot_have_an_implementation_because_it_is_marked_abstract, declarationNameToString(node.name)); | ||
| } | ||
|
|
||
| // Is this the correct time to make assertions against the inheritance chain? |
There was a problem hiding this comment.
Around line 18164 (look for checkKindsOfPropertyMemberOverrides(type, baseType);) / 18261 might be better; here the base type members are already in scope and we are iterating through the class properties.
src/compiler/types.ts
Outdated
| PrivateKeyword, | ||
| ProtectedKeyword, | ||
| PublicKeyword, | ||
| OverrideKeyword, |
There was a problem hiding this comment.
override is not an ES reserved word. move it to the contextual keyword section.
src/compiler/checker.ts
Outdated
| lastAsync = modifier; | ||
| break; | ||
|
|
||
| case SyntaxKind.OverrideKeyword: |
There was a problem hiding this comment.
you also need to add a check in checkFlagAgreementBetweenOverloads to make sure all overloads are marked as override.
There was a problem hiding this comment.
Also add a check in checkAccessorDeclaration for accessors matching in override
There was a problem hiding this comment.
Also a check if declarations are consecutive, see checkFunctionOrConstructorSymbol
There was a problem hiding this comment.
Done for (1), (2). As for (3), I still don't really understand what consecutive declaration checks mean. Please propose a test class that illustrates it if possible.
src/compiler/checker.ts
Outdated
| else if (flags & ModifierFlags.Abstract) { | ||
| return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_be_used_with_1_modifier, "abstract", "override"); | ||
| } | ||
| else if (node.parent.kind === SyntaxKind.ModuleBlock || node.parent.kind === SyntaxKind.SourceFile) { |
There was a problem hiding this comment.
this should follow the abstract modifier checks. override is only applicable on a method declaration in a class or a class expression.
src/compiler/checker.ts
Outdated
|
|
||
| function checkOverrideMethodDeclaration(node: MethodDeclaration) { | ||
| forEachEnclosingClass(node, enclosingClass => { | ||
| // TODO: save the methodDeclaration node here in a cache and |
There was a problem hiding this comment.
also check that an override method can not be marked as optional.
| // Is this the correct time to make assertions against the inheritance chain? | ||
| // Have all other methods been resolved? Probably need to record that an override exists | ||
| // and perform the actual resolution later. | ||
| if (getModifierFlags(node) & ModifierFlags.Override) { |
There was a problem hiding this comment.
i would move this to checkKindsOfPropertyMemberOverrides
src/compiler/emitter.ts
Outdated
| case SyntaxKind.PrivateKeyword: | ||
| case SyntaxKind.ProtectedKeyword: | ||
| case SyntaxKind.PublicKeyword: | ||
| case SyntaxKind.OverrideKeyword: |
There was a problem hiding this comment.
you also need to handle this in the declarationEmitter.ts, and please add declaration emit tests.
There was a problem hiding this comment.
This has not been done yet. I was not able to identify an appropriate declaration emitter test to mimic. Can you suggest one to investigate?
|
Thank you for all the excellent feedback, will begin addressing your comments and submit a new iteration over the next day or so. |
|
See comments above. In addition, consider the following issues:
abstract class Base {
abstract getMeaningOfLife(): number;
}
abstract class Derived extends Base {
// Here, override keyword is not adding any utility (compiler
// already reports inappropriate usage with abstract keyword alone),
// but it may be semantically correct. I see pro/con of each but defer to
// language designers for decision. My bias is to not use override in
// conjunction with non-concrete members.
// Current implementation rejects
// simultaneous use of both override and abstract.
override abstract getMeaningOfLife(): number;
}As an extension to this line of thought, the
class Foo {
toLocaleString(): string { 'foo' };
}I have addressed this with the new |
|
In addition, the transpile test for the compiler option is failing as follows. This does not get resolved with 1) Transpile Supports setting 'noImplicitOverride' Correct output for undefined:
Error: The baseline file transpile/Supports setting noImplicitOverride.js has changed.
at writeComparison (built/local/run.js:90502:23) |
|
See comments above. In addition, consider the following issues:
what you need is
This is only applicable in JS files, i would get this done in a separate PR.
Do not see why you can not have a
I would say no. you can just drop the
I do not think it makes much sense. the only use case, is the same as the
the properties from |
Excluding augmented methods from override would be a glaring omission IMHO. Unless I'm missing something, the implementation does not seem particularly onerous. Look forward to hearing your thoughts about this following the design meeting. |
src/compiler/declarationEmitter.ts
Outdated
| } | ||
|
|
||
| function emitClassMemberDeclarationFlags(flags: ModifierFlags) { | ||
| if (flags & ModifierFlags.Override) { |
There was a problem hiding this comment.
this should be after protected. also please add a test for that.
src/compiler/checker.ts
Outdated
| else if (flags & ModifierFlags.Abstract) { | ||
| return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_be_used_with_1_modifier, "abstract", "override"); | ||
| } | ||
| else if (node.parent.kind === SyntaxKind.ModuleBlock || node.parent.kind === SyntaxKind.SourceFile) { |
There was a problem hiding this comment.
still check too wide. you want to follow the abstract check above. basically override is only allowed on class property/method/accessor declarations.
There was a problem hiding this comment.
here is the order we should enforce/check for:
[public | protected | private] [abstract | override] [static] [readonly | async] [get | set] identifier
class C {
public override static readonly p: number;
protected overide static async method() {}
}
src/compiler/checker.ts
Outdated
| if (flags & ModifierFlags.Override) { | ||
| return grammarErrorOnNode(modifier, Diagnostics._0_modifier_already_seen, "override"); | ||
| } | ||
| else if (flags & ModifierFlags.Static) { |
src/compiler/checker.ts
Outdated
| if (!objectType) { | ||
| return; | ||
| } | ||
| for (const name in propertiesToCheck) { |
There was a problem hiding this comment.
we should disable this check in ambient contexts.
|
|
||
| // Before accepting the correct case, ensure 'override' is marked if --noImplicitOverride is true. | ||
| // Abstract members are an exception as override checks are suspended until the implementation solidifies. | ||
| if (compilerOptions.noImplicitOverride |
There was a problem hiding this comment.
disable this in ambient contexts.
…ler option. * Moves `OverrideKeyword` to the more appropriate contextual keyword section (types.ts) * Introduces the `CompilerOptions.noImplicitOverride` boolean option (types.ts). * Moves the location for performing override assertions to `checkKindsOfPropertyMemberOverrides` as suggested (checker.ts). * Adds several other diagnostic messages (diagnosticMessages.json). * Improves the breadth of the `overrideKeyword.ts` test case. * Adds a new `noImplicitOverride.ts` test case.
* Rename check{Implicit -> Augmented}PropertyMemberOverrides.
* Disable augment checks in ambients contexts.
* Allow override on static methods.
* Add override keyword to spec.md.
* Use chained diagnostics when possible.
* Remove hardcoded diagnotic-related strings in checker.ts.
* Split tests into separate files overrideKeyword{es5 + es6}.ts.
|
Thanks for your patience... Comments addressed. Force-pushed 3143818 after rebase to 2711303 (Jan 12). A few more issues, but I think we are getting close. Just want to say again how much I appreciate the excellent reviews.
|
|
Friendly ping. |
|
@pcj looks like the new baseline tasks don't handle the |
|
Thanks @RyanCavanaugh that did it. |
|
@mhegazy @RyanCavanaugh, do we have a roadmap on moving this forward? |
Some one needs to review it, and we need to discuss it in the design meeting. I will put it on the agenda for this week. |
|
Appreciate posting the design meeting notes at #13729. Two questions, (one big, one small):
Thanks! |
There was a problem hiding this comment.
Sorry for the long delay!
Please add this test (seems to work without any modification):
// @noImplicitOverride: true
const mixin = <BC extends new (...args: any[]) => {}>(Base: BC) => class extends Base {
mixedIn() {}
};
class A {
normal() {}
}
class B extends mixin(A) {
normal() {} // Error: needs 'override'
mixedIn() {} // Error: needs 'override'
}Also, merge from master before reading my comments because they may only apply to the latest version of the compiler. In a few places it looks like you were using old-style maps and now we use es6 maps, so my comments could help you update.
| for (const prop of getPropertiesOfObjectType(type)) { | ||
| properties[prop.name] = prop; | ||
| } | ||
| checkAugmentedPropertyMemberOverrides(type, properties); |
There was a problem hiding this comment.
checkAugmentedPropertyMemberOverrides(type, resolveStructuredTypeMembers(type).members);
src/compiler/checker.ts
Outdated
| return; | ||
| } | ||
|
|
||
| const objectType = getSymbol(globals, "Object", SymbolFlags.Type); |
There was a problem hiding this comment.
const objectType = resolveStructuredTypeMembers(globalObjectType);. No need to test for non-null.
src/compiler/checker.ts
Outdated
| return; | ||
| } | ||
|
|
||
| for (const name in propertiesToCheck) { |
There was a problem hiding this comment.
propertiesToCheck.forEach(propertyToCheck => {
src/compiler/checker.ts
Outdated
| for (const name in propertiesToCheck) { | ||
| const derived = getTargetSymbol(propertiesToCheck[name]); | ||
| const derivedDeclarationFlags = getDeclarationModifierFlagsFromSymbol(derived); | ||
| const found = objectType.members[name]; |
There was a problem hiding this comment.
objectType.members.get(propertyToCheck.name)
src/compiler/checker.ts
Outdated
| // NOTE: assignability is checked in checkClassDeclaration | ||
|
|
||
| // Track which symbols in the derived class have not been seen. | ||
| const onlyInDerived = createMap<Symbol>(); |
There was a problem hiding this comment.
const onlyInDerived = cloneMap(resolveStructuredTypeMembers(type).members); (no need for for loop)
| else if (deviation & ModifierFlags.Abstract) { | ||
| error(o.name, Diagnostics.Overload_signatures_must_all_be_abstract_or_non_abstract); | ||
| } | ||
| else if (deviation & ModifierFlags.Override) { |
There was a problem hiding this comment.
Remember to modify const flagsToCheck below to include override. And make a test for this.
| symbolToString(derived), | ||
| symbolToString(found) | ||
| ); | ||
| if (!isPropertyIdenticalTo(derived, foundSymbol)) { |
There was a problem hiding this comment.
This checks for identical properties, but the error message says "assignable to".
| @@ -0,0 +1,86 @@ | |||
| tests/cases/compiler/noImplicitOverride.ts(11,5): error TS90034: Class member 'toString' must be marked 'override' when noImplicitOverride is enabled (augmented from Object.Object) | |||
There was a problem hiding this comment.
Something's wrong with the way this is being rendered. We shouldn't have Object. in front of all of these. (Except e.g. Object.toString which is actually a property of Object.)
src/compiler/checker.ts
Outdated
| return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_be_used_with_1_modifier, "private", "override"); | ||
| } | ||
| else if (node.kind === SyntaxKind.Parameter) { | ||
| return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_appear_on_a_parameter, "override"); |
There was a problem hiding this comment.
Actually, I think we should allow override as a marker of a parameter property. E.g.:
abstract class A {
abstract x: number;
}
class B extends A {
constructor(override x: number)
}| } | ||
| } | ||
|
|
||
| class RejectWhenOverrideMissingOnAugmentedProperty { |
There was a problem hiding this comment.
Include a positive version of this test. class A { override toString() { return ""; } } should succeed with no errors.
|
Thanks @Andy-MS for your review. I'll merge from master and begin addressing your comments over the weekend. |
| } | ||
| if (flags & ModifierFlags.Static) { | ||
| else if (flags & ModifierFlags.Override) { | ||
| return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_be_used_with_1_modifier, "override", "abstract"); |
There was a problem hiding this comment.
In this situation, I think it would make sense to put an override modifier on the subclass' method:
abstract class A {
abstract m(): void;
}
abstract class B extends A {
abstract m(): void;
}|
@pcj bump? |
|
@pcj bump |
|
@Andy-MS Haven't forgot, sorry for the delay! Still working on this. |
|
Isn't this PR close to finished? Was it abandoned? I would really like this functionality... so what are the community's next steps here? Do you need people to step in and help out? |
|
Sorry guys, this got de-prioritized on my end when other stuff started rolling. If I can't get his going again in the next two weeks, someone else should take over. |
|
OK, this PR is now sync'd back up to master. Will allocate some time this weekend to address some of @Andy-MS review comments. |
Test output looking reasonable, no errors seen.
|
OK, I have this mostly up to speed. Still need to address some of @Andy-MS comments. Issue I am still working out is thse cases: class A {
protected static type: string;
}
class B extends A {
protected static type: string = 'B';
}In this case, the compiler should (maybe?) detect that The second one is somewhat esoteric that I got from @ahejlsberg in #5989: class A {
"constructor": typeof A; // Explicitly declare constructor property
protected static type: string;
public log() {
console.log(this.constructor.type); // Access actual constructor object
}
}
class B extends A {
protected static type: string = 'B';
}
class C extends A {
protected static type: string = 'C';
}In this case, I am not certain this is valid syntax anymore. Also, putting |
|
Sorry to loop back on this after long delay, but I'm personally not seeing the point of using override with static members. @mhegazy you mentioned it should be possible (yes), but what is the use case for it? Aren't users always specifying the base type constructor (like |
|
@pcj A static member on a subclass overrides a static member on a superclass with the same name. class C {
static a(): number {
// 'this' = the class from the LHS of '.', like with an instance member.
// So 'this.b' doesn't always mean 'C.b'.
return this.b();
}
static b(): number {
return 0;
}
}
class D extends C {
static b() {
return 1;
}
}
console.log(D.a()); // 1In the example you gave above the compiler can currently detect an error if you write As for the other error, you shouldn't be getting |
|
Whoa. Okay, I didn't know that! (static members accessible through the |
|
Hi @pcj. I'm sorry that we've let this PR linger without more direct team feedback for as long as it has. We discussed the issue and PR in our last design meeting (#20724), and after having discussed it more broadly among members of the team, we didn't feel entirely confident as to whether Again, I apologize that we didn't address this PR earlier. We reiterated the importance of transparently communicating our own positions outside of the core team. The work you've done here is great, so I hope this experience doesn't dissuade you from continuing to participate in the community and on our issue tracker. |
|
Hi @DanielRosenwasser I understand your concerns. The feature as-is is largely complete with the exception of static / constructor inheritance that I may push up as a WIP. If there is future support for it, it can be resurrected from here. |
|
For me personally, as a developer with experience in Java, C# and others, |
|
@DanielRosenwasser I know what you mean about the nature of Javascript - but the same could be argued about typing - typing is a bad fit for Javascript. But Typescript fixed that (for the most part) in the same way that it could fix the lack of override (#2000). I feel like it is a valid feature request. |
|
It is sad to see that such must-to-have feature has sunk in multiple year discussions about theoretical corner cases and unimportant implementation details, especially given that many mature languages have already figured all details out and there is no need to reinvent the wheel. It was much better to make a breaking change and a flag which turns it off by default. I guess many people will be happy to turn such flag on and spend few days refactoring the entire codebase and refactor again when (and if..) the compiler 'override' implementation changes, instead of not having it at all for many years and foreseeable future. |
Fixes #2000
This initial commit simply introduces the keyword and performs some initial modifier flags checks. The actual checks against the inheritance chain are not implemented yet.
@RyanCavanaugh or other: Can you suggest when/where the appropriate time to perform these checks are?
No formal review required at this time (obviously), but would appreciate any strategy recommendations or other suggestions.