-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Add override keyword #13217
Add override keyword #13217
Conversation
src/compiler/commandLineParser.ts
Outdated
@@ -145,6 +145,11 @@ namespace ts { | |||
description: Diagnostics.Raise_error_on_expressions_and_declarations_with_an_implied_any_type, | |||
}, | |||
{ | |||
name: "noImplicitOverride", | |||
type: "boolean", | |||
description: Diagnostics.Member_function_0_was_marked_override_but_no_matching_definition_was_found_in_the_superclass_chain_1, |
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.
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'"
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.
Done
src/compiler/checker.ts
Outdated
} | ||
|
||
function checkOverrideMethodDeclaration(node: MethodDeclaration) { | ||
forEachEnclosingClass(node, enclosingClass => { |
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.
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
@@ -15712,6 +15712,21 @@ namespace ts { | |||
if (getModifierFlags(node) & ModifierFlags.Abstract && node.body) { | |||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
Got it.
src/compiler/types.ts
Outdated
@@ -155,6 +155,7 @@ namespace ts { | |||
PrivateKeyword, | |||
ProtectedKeyword, | |||
PublicKeyword, | |||
OverrideKeyword, |
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.
override is not an ES reserved word. move it to the contextual keyword section.
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.
Done.
src/compiler/checker.ts
Outdated
@@ -20964,6 +20985,25 @@ namespace ts { | |||
flags |= ModifierFlags.Async; | |||
lastAsync = modifier; | |||
break; | |||
|
|||
case SyntaxKind.OverrideKeyword: |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also add a check in checkAccessorDeclaration
for accessors matching in override
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 a check if declarations are consecutive, see checkFunctionOrConstructorSymbol
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should follow the abstract
modifier checks. override
is only applicable on a method declaration in a class or a class expression.
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.
Done.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also check that an override method can not be marked as optional.
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.
Done.
src/compiler/checker.ts
Outdated
// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i would move this to checkKindsOfPropertyMemberOverrides
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.
Done.
src/compiler/emitter.ts
Outdated
@@ -324,6 +324,7 @@ namespace ts { | |||
case SyntaxKind.PrivateKeyword: | |||
case SyntaxKind.ProtectedKeyword: | |||
case SyntaxKind.PublicKeyword: | |||
case SyntaxKind.OverrideKeyword: |
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.
you also need to handle this in the declarationEmitter.ts, and please add declaration emit tests.
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.
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
@@ -717,6 +717,9 @@ namespace ts { | |||
} | |||
|
|||
function emitClassMemberDeclarationFlags(flags: ModifierFlags) { | |||
if (flags & ModifierFlags.Override) { |
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.
this should be after protected
. also please add a test for that.
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.
and after static.
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.
OK
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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() {}
}
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.
OK
src/compiler/checker.ts
Outdated
if (flags & ModifierFlags.Override) { | ||
return grammarErrorOnNode(modifier, Diagnostics._0_modifier_already_seen, "override"); | ||
} | ||
else if (flags & ModifierFlags.Static) { |
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 allowed
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.
Done.
src/compiler/checker.ts
Outdated
if (!objectType) { | ||
return; | ||
} | ||
for (const name in propertiesToCheck) { |
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.
we should disable this check in ambient contexts.
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.
Done.
|
||
// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
disable this in ambient contexts.
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.
Done.
…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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
src/compiler/checker.ts
Outdated
for (const prop of getPropertiesOfObjectType(type)) { | ||
properties[prop.name] = prop; | ||
} | ||
checkAugmentedPropertyMemberOverrides(type, 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.
checkAugmentedPropertyMemberOverrides(type, resolveStructuredTypeMembers(type).members);
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.
Got it, done
src/compiler/checker.ts
Outdated
return; | ||
} | ||
|
||
const objectType = getSymbol(globals, "Object", SymbolFlags.Type); |
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.
const objectType = resolveStructuredTypeMembers(globalObjectType);
. No need to test for non-null.
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.
OK, done
src/compiler/checker.ts
Outdated
return; | ||
} | ||
|
||
for (const name in propertiesToCheck) { |
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.
propertiesToCheck.forEach(propertyToCheck => {
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.
Done
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
objectType.members.get(propertyToCheck.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.
Fixed
src/compiler/checker.ts
Outdated
@@ -18372,9 +18456,19 @@ namespace ts { | |||
// derived class instance member variables and accessors, but not by other kinds of members. | |||
|
|||
// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const onlyInDerived = cloneMap(resolveStructuredTypeMembers(type).members);
(no need for for
loop)
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.
Done
src/compiler/checker.ts
Outdated
@@ -16104,6 +16127,9 @@ namespace ts { | |||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remember to modify const flagsToCheck
below to include override
. And make a test for this.
src/compiler/checker.ts
Outdated
symbolToString(derived), | ||
symbolToString(found) | ||
); | ||
if (!isPropertyIdenticalTo(derived, foundSymbol)) { |
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.
This checks for identical properties, but the error message says "assignable to".
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.
Removed check (was unnecessary)
@@ -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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.)
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.
Fixed
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)
}
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.
Agreed
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.
Done
} | ||
} | ||
|
||
class RejectWhenOverrideMissingOnAugmentedProperty { |
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.
Include a positive version of this test. class A { override toString() { return ""; } }
should succeed with no errors.
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.
Done
Thanks @andy-ms for your review. I'll merge from master and begin addressing your comments over the weekend. |
return grammarErrorOnNode(modifier, Diagnostics.Abstract_methods_can_only_appear_within_an_abstract_class); | ||
} | ||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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()); // 1 In 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.