blob: 5e4fa1b4c9c34523a4479af0e02dfc803f9b9522 [file] [log] [blame]
// Copyright (c) 2023, 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 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart';
import '../analyzer.dart';
const _desc = r"Don't implicitly reopen classes.";
// TODO(pq): link out to (upcoming) dart.dev docs.
// https://github.com/dart-lang/site-www/issues/4833
const _details = r'''
Using an `interface`, `base`, `final`, or `sealed` modifier on a class,
or a `base` modifier on a mixin,
authors can control whether classes and mixins allow being implemented,
extended, and/or mixed in from outside of the library where they're defined.
In some cases, it's possible for an author to inadvertently relax these controls
and implicitly "reopen" a class. (A similar reopening cannot occur with a mixin.)
This lint guards against unintentionally reopening a class by requiring such
cases to be made explicit with the
[`@reopen`](https://pub.dev/documentation/meta/latest/meta/reopen-constant.html)
annotation in `package:meta`.
**BAD:**
```dart
interface class I {}
class C extends I {} // LINT
```
**GOOD:**
```dart
interface class I {}
final class C extends I {}
```
```dart
import 'package:meta/meta.dart';
interface class I {}
@reopen
class C extends I {}
```
''';
class ImplicitReopen extends LintRule {
static const LintCode code = LintCode('implicit_reopen',
"The {0} '{1}' reopens '{2}' because it is not marked '{3}'",
correctionMessage:
"Try marking '{1}' '{3}' or annotating it with '@reopen'");
ImplicitReopen()
: super(
name: 'implicit_reopen',
description: _desc,
details: _details,
state: State.experimental(),
categories: {Category.errorProne});
@override
LintCode get lintCode => code;
@override
void registerNodeProcessors(
NodeLintRegistry registry, LinterContext context) {
var visitor = _Visitor(this);
registry.addClassDeclaration(this, visitor);
registry.addClassTypeAlias(this, visitor);
}
}
class _Visitor extends SimpleAstVisitor {
final LintRule rule;
_Visitor(this.rule);
void checkElement(InterfaceElement? element, NamedCompilationUnitMember node,
{required String type}) {
if (element is! ClassElement) return;
if (element.hasReopen) return;
if (element.isSealed) return;
if (element.isMixinClass) return;
var library = element.library;
var supertype = element.supertype?.element;
if (supertype is! ClassElement) return;
if (supertype.library != library) return;
if (element.isBase) {
if (supertype.isFinal) {
reportLint(node,
target: element, other: supertype, reason: 'final', type: type);
return;
} else if (supertype.isInterface) {
reportLint(node,
target: element, other: supertype, reason: 'interface', type: type);
return;
}
} else if (element.hasNoModifiers) {
if (supertype.isInterface) {
reportLint(node,
target: element, other: supertype, reason: 'interface', type: type);
return;
}
}
}
void reportLint(
NamedCompilationUnitMember member, {
required String type,
required InterfaceElement target,
required InterfaceElement other,
required String reason,
}) {
rule.reportLintForToken(member.name,
arguments: [type, target.name, other.name, reason]);
}
@override
void visitClassDeclaration(ClassDeclaration node) {
checkElement(node.declaredElement, node, type: 'class');
}
@override
visitClassTypeAlias(ClassTypeAlias node) {
checkElement(node.declaredElement, node, type: 'class');
}
}
extension on ClassElement {
bool get hasNoModifiers => !isInterface && !isBase && !isSealed && !isFinal;
}