blob: d41097cbbf25fe87c78d5b599c346c84d035d9bd [file] [log] [blame]
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:collection';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/error/codes.g.dart';
class MustCallSuperVerifier {
final ErrorReporter _errorReporter;
MustCallSuperVerifier(this._errorReporter);
void checkMethodDeclaration(MethodDeclaration node) {
if (node.isStatic || node.isAbstract) {
return;
}
var element = node.declaredFragment!.element;
var overridden = _findOverriddenMemberWithMustCallSuper(element);
if (overridden == null) {
return;
}
var overriddenName = overridden.name3;
if (overriddenName == null) {
return;
}
if (element is MethodElement2 && _hasConcreteSuperMethod(element)) {
_verifySuperIsCalled(
node,
overriddenName,
overridden.enclosingElement2?.name3,
);
return;
}
var enclosingElement = element.enclosingElement2;
if (enclosingElement is! ClassElement2) {
return;
}
if (element is GetterElement) {
var inheritedConcreteGetter = enclosingElement
.lookupInheritedConcreteMember(element);
if (inheritedConcreteGetter is GetterElement) {
_verifySuperIsCalled(
node,
overriddenName,
overridden.enclosingElement2?.name3,
);
}
return;
}
if (element is SetterElement) {
var inheritedConcreteSetter = enclosingElement
.lookupInheritedConcreteMember(element);
if (inheritedConcreteSetter is SetterElement) {
var name = overriddenName;
// For a setter, give the name without the trailing '=' to the verifier,
// in order to check against property access.
if (name.endsWith('=')) {
name = name.substring(0, name.length - 1);
}
_verifySuperIsCalled(node, name, overridden.enclosingElement2?.name3);
}
}
}
/// Finds a method which is overridden by [element] and which is annotated
/// with `@mustCallSuper`.
///
/// As per the definition of `mustCallSuper` [1], every method which overrides
/// a method annotated with `@mustCallSuper` is implicitly annotated with
/// `@mustCallSuper`.
///
/// [1]: https://pub.dev/documentation/meta/latest/meta/mustCallSuper-constant.html
ExecutableElement2? _findOverriddenMemberWithMustCallSuper(
ExecutableElement2 element,
) {
var classElement = element.enclosingElement2;
if (classElement is! InterfaceElement2) {
return null;
}
var name = element.name3;
if (name == null) {
return null;
}
// Walk up the type hierarchy from [classElement], ignoring direct
// interfaces.
var superclasses = Queue<InterfaceElement2?>();
void addToQueue(InterfaceElement2 element) {
superclasses.addAll(element.mixins.map((i) => i.element3));
superclasses.add(element.supertype?.element3);
if (element is MixinElement2) {
superclasses.addAll(
element.superclassConstraints.map((i) => i.element3),
);
}
}
var visitedClasses = <InterfaceElement2>{};
addToQueue(classElement);
while (superclasses.isNotEmpty) {
var ancestor = superclasses.removeFirst();
if (ancestor == null || !visitedClasses.add(ancestor)) {
continue;
}
ExecutableElement2? member;
switch (element) {
case MethodElement2():
member = ancestor.getMethod2(name);
case GetterElement():
member = ancestor.getMethod2(name) ?? ancestor.getGetter2(name);
case SetterElement():
member = ancestor.getSetter2(name);
}
if (member is MethodElement2 && member.metadata2.hasMustCallSuper) {
return member;
}
if (member is PropertyAccessorElement2 &&
member.metadata2.hasMustCallSuper) {
return member;
}
// TODO(srawlins): What about a field annotated with `@mustCallSuper`?
// This might seem a legitimate case, but is not called out in the
// documentation of [mustCallSuper].
addToQueue(ancestor);
}
return null;
}
/// Returns whether [element] overrides a concrete method.
bool _hasConcreteSuperMethod(ExecutableElement2 element) {
var classElement = element.enclosingElement2 as InterfaceElement2;
var name = element.name3;
if (name == null) {
return true;
}
if (classElement.supertype.isConcrete(name)) {
return true;
}
if (classElement.mixins.any((m) => m.isConcrete(name))) {
return true;
}
if (classElement is MixinElement2 &&
classElement.superclassConstraints.any((c) => c.isConcrete(name))) {
return true;
}
return false;
}
void _verifySuperIsCalled(
MethodDeclaration node,
String? methodName,
String? overriddenEnclosingName,
) {
var declaredFragment = node.declaredFragment!;
var declaredElement = declaredFragment.element as ExecutableElementImpl2;
if (!declaredElement.invokesSuperSelf) {
// Overridable elements are always enclosed in named elements, so it is
// safe to assume [overriddenEnclosingName] is non-`null`.
_errorReporter.atToken(
node.name,
WarningCode.MUST_CALL_SUPER,
arguments: [overriddenEnclosingName!],
);
}
return;
}
}
extension on InterfaceElement2 {
ExecutableElement2? lookupInheritedConcreteMember(
ExecutableElement2 element,
) {
var nameObj = Name.forElement(element);
if (nameObj == null) {
return null;
}
var library = element.library2 as LibraryElementImpl;
var inheritanceManager = library.session.inheritanceManager;
return inheritanceManager.getMember4(this, nameObj, forSuper: true);
}
}
extension on InterfaceType? {
bool isConcrete(String name) {
var self = this;
if (self == null) return false;
var element = self.element3;
var library = element.library2 as LibraryElementImpl;
var inheritanceManager = library.session.inheritanceManager;
var concrete = inheritanceManager.getMember4(
element,
Name.forLibrary(library, name),
concrete: true,
);
return concrete != null;
}
}