blob: 6b28876661fddf37b4cedbc2fb32e760c60ebba3 [file] [log] [blame]
// Copyright (c) 2019, 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/token.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/source/source_range.dart';
import 'package:analyzer/src/dart/element/type_system.dart';
import 'package:analyzer/src/dart/resolver/flow_analysis_visitor.dart';
import 'package:analyzer/src/dart/resolver/scope.dart';
import 'package:analyzer/src/error/codes.dart';
typedef _CatchClausesVerifierReporter = void Function(
CatchClause first,
CatchClause last,
ErrorCode,
List<Object> arguments,
);
/// A visitor that finds dead code, other than unreachable code that is
/// handled in [NullSafetyDeadCodeVerifier].
class DeadCodeVerifier extends RecursiveAstVisitor<void> {
/// The error reporter by which errors will be reported.
final ErrorReporter _errorReporter;
/// The object used to track the usage of labels within a given label scope.
_LabelTracker? _labelTracker;
DeadCodeVerifier(this._errorReporter);
@override
void visitBreakStatement(BreakStatement node) {
_labelTracker?.recordUsage(node.label?.name);
}
@override
void visitContinueStatement(ContinueStatement node) {
_labelTracker?.recordUsage(node.label?.name);
}
@override
void visitExportDirective(ExportDirective node) {
final exportElement = node.element;
if (exportElement != null) {
// The element is null when the URI is invalid.
LibraryElement? library = exportElement.exportedLibrary;
if (library != null && !library.isSynthetic) {
for (Combinator combinator in node.combinators) {
_checkCombinator(library, combinator);
}
}
}
super.visitExportDirective(node);
}
@override
void visitImportDirective(ImportDirective node) {
final importElement = node.element;
if (importElement != null) {
// The element is null when the URI is invalid, but not when the URI is
// valid but refers to a nonexistent file.
LibraryElement? library = importElement.importedLibrary;
if (library != null && !library.isSynthetic) {
for (Combinator combinator in node.combinators) {
_checkCombinator(library, combinator);
}
}
}
super.visitImportDirective(node);
}
@override
void visitLabeledStatement(LabeledStatement node) {
_withLabelTracker(node.labels, () {
super.visitLabeledStatement(node);
});
}
@override
void visitSwitchStatement(SwitchStatement node) {
List<Label> labels = <Label>[];
for (SwitchMember member in node.members) {
labels.addAll(member.labels);
}
_withLabelTracker(labels, () {
super.visitSwitchStatement(node);
});
}
/// Resolve the names in the given [combinator] in the scope of the given
/// [library].
void _checkCombinator(LibraryElement library, Combinator combinator) {
Namespace namespace =
NamespaceBuilder().createExportNamespaceForLibrary(library);
NodeList<SimpleIdentifier> names;
ErrorCode warningCode;
if (combinator is HideCombinator) {
names = combinator.hiddenNames;
warningCode = WarningCode.UNDEFINED_HIDDEN_NAME;
} else {
names = (combinator as ShowCombinator).shownNames;
warningCode = WarningCode.UNDEFINED_SHOWN_NAME;
}
for (SimpleIdentifier name in names) {
String nameStr = name.name;
Element? element = namespace.get(nameStr);
element ??= namespace.get("$nameStr=");
if (element == null) {
_errorReporter.atNode(
name,
warningCode,
arguments: [library.identifier, nameStr],
);
}
}
}
void _withLabelTracker(List<Label> labels, void Function() f) {
var labelTracker = _LabelTracker(_labelTracker, labels);
try {
_labelTracker = labelTracker;
f();
} finally {
for (Label label in labelTracker.unusedLabels()) {
_errorReporter.atNode(
label,
WarningCode.UNUSED_LABEL,
arguments: [label.label.name],
);
}
_labelTracker = labelTracker.outerTracker;
}
}
}
/// Helper for tracking dead code - [CatchClause]s and unreachable code.
///
/// [CatchClause]s are checked separately, as we visit AST we may make some
/// of them as dead, and record [_deadCatchClauseRanges].
///
/// When an unreachable node is found, and [_firstDeadNode] is `null`, we
/// set [_firstDeadNode], so start a new dead nodes interval. The dead code
/// interval ends when [flowEnd] is invoked with a node that is the start
/// node, or contains it. So, we end the end of the covering control flow.
class NullSafetyDeadCodeVerifier {
final TypeSystemImpl _typeSystem;
final ErrorReporter _errorReporter;
final FlowAnalysisHelper? _flowAnalysis;
/// The stack of verifiers of (potentially nested) try statements.
final List<_CatchClausesVerifier> _catchClausesVerifiers = [];
/// When a sequence [CatchClause]s is found to be dead, we don't want to
/// report additional dead code inside of already dead code.
final List<SourceRange> _deadCatchClauseRanges = [];
/// When this field is `null`, we are in reachable code.
/// Once we find the first unreachable node, we store it here.
///
/// When this field is not `null`, and we see an unreachable node, this new
/// node is ignored, because it continues the same dead code range.
AstNode? _firstDeadNode;
NullSafetyDeadCodeVerifier(
this._typeSystem,
this._errorReporter,
this._flowAnalysis,
);
/// The [node] ends a basic block in the control flow. If [_firstDeadNode] is
/// not `null`, and is covered by the [node], then we reached the end of
/// the current dead code interval.
void flowEnd(AstNode node) {
var firstDeadNode = _firstDeadNode;
if (firstDeadNode != null) {
if (!_containsFirstDeadNode(node)) {
return;
}
if (node is SwitchMember && node == firstDeadNode) {
_errorReporter.atToken(
node.keyword,
WarningCode.DEAD_CODE,
);
_firstDeadNode = null;
return;
}
var parent = firstDeadNode.parent;
if (parent is Assertion && identical(firstDeadNode, parent.message)) {
// Don't report "dead code" for the message part of an assert statement,
// because this causes nuisance warnings for redundant `!= null`
// asserts.
} else {
var offset = firstDeadNode.offset;
// We know that [node] is the first dead node, or contains it.
// So, technically the code interval ends at the end of [node].
// But we trim it to the last statement for presentation purposes.
if (node != firstDeadNode) {
if (node is FunctionDeclaration) {
node = node.functionExpression.body;
}
if (node is FunctionExpression) {
node = node.body;
}
if (node is MethodDeclaration) {
node = node.body;
}
if (node is BlockFunctionBody) {
node = node.block;
}
if (node is Block && node.statements.isNotEmpty) {
node = node.statements.last;
}
if (node is SwitchMember && node.statements.isNotEmpty) {
node = node.statements.last;
}
} else if (parent is BinaryExpression && node == parent.rightOperand) {
offset = parent.operator.offset;
}
if (parent is DoStatement) {
var doOffset = parent.doKeyword.offset;
var doEnd = parent.doKeyword.end;
var whileOffset = parent.whileKeyword.offset;
var whileEnd = parent.semicolon.end;
var body = parent.body;
if (body is Block) {
doEnd = body.leftBracket.end;
whileOffset = body.rightBracket.offset;
}
_errorReporter.atOffset(
offset: doOffset,
length: doEnd - doOffset,
errorCode: WarningCode.DEAD_CODE,
);
_errorReporter.atOffset(
offset: whileOffset,
length: whileEnd - whileOffset,
errorCode: WarningCode.DEAD_CODE,
);
offset = parent.semicolon.next!.offset;
if (parent.hasBreakStatement) {
offset = node.end;
}
} else if (parent is ForParts) {
node = parent.updaters.last;
} else if (parent is ForStatement) {
_reportForUpdaters(parent);
} else if (parent is Block) {
var grandParent = parent.parent;
if (grandParent is ForStatement) {
_reportForUpdaters(grandParent);
}
} else if (parent is LogicalOrPattern && node == parent.rightOperand) {
offset = parent.operator.offset;
}
var length = node.end - offset;
if (length > 0) {
_errorReporter.atOffset(
offset: offset,
length: length,
errorCode: WarningCode.DEAD_CODE,
);
}
}
_firstDeadNode = null;
}
}
void tryStatementEnter(TryStatement node) {
var verifier = _CatchClausesVerifier(
_typeSystem,
(first, last, errorCode, arguments) {
var offset = first.offset;
var length = last.end - offset;
_errorReporter.atOffset(
offset: offset,
length: length,
errorCode: errorCode,
arguments: arguments,
);
_deadCatchClauseRanges.add(SourceRange(offset, length));
},
node.catchClauses,
);
_catchClausesVerifiers.add(verifier);
}
void tryStatementExit(TryStatement node) {
_catchClausesVerifiers.removeLast();
}
void verifyCascadeExpression(CascadeExpression node) {
var first = node.cascadeSections.firstOrNull;
if (first is PropertyAccess) {
_verifyUnassignedSimpleIdentifier(node, node.target, first.operator);
} else if (first is MethodInvocation) {
_verifyUnassignedSimpleIdentifier(node, node.target, first.operator);
} else if (first is IndexExpression) {
_verifyUnassignedSimpleIdentifier(node, node.target, first.period);
}
}
void verifyCatchClause(CatchClause node) {
var verifier = _catchClausesVerifiers.last;
if (verifier._done) return;
verifier.nextCatchClause(node);
}
void verifyIndexExpression(IndexExpression node) {
_verifyUnassignedSimpleIdentifier(node, node.target, node.question);
}
void verifyMethodInvocation(MethodInvocation node) {
_verifyUnassignedSimpleIdentifier(node, node.target, node.operator);
}
void verifyPropertyAccess(PropertyAccess node) {
_verifyUnassignedSimpleIdentifier(node, node.target, node.operator);
}
void visitNode(AstNode node) {
// Comments are visited after bodies of functions.
// So, they look unreachable, but this does not make sense.
if (node is Comment) return;
var flowAnalysis = _flowAnalysis;
if (flowAnalysis == null) return;
flowAnalysis.checkUnreachableNode(node);
// If the first dead node is not `null`, even if this new node is
// unreachable, we can ignore it as it is part of the same dead code
// range anyway.
if (_firstDeadNode != null) return;
var flow = flowAnalysis.flow;
if (flow == null) return;
if (flow.isReachable) return;
// If in a dead `CatchClause`, no need to report dead code.
for (var range in _deadCatchClauseRanges) {
if (range.contains(node.offset)) {
return;
}
}
_firstDeadNode = node;
}
bool _containsFirstDeadNode(AstNode parent) {
for (var node = _firstDeadNode; node != null; node = node.parent) {
if (node == parent) return true;
}
return false;
}
void _reportForUpdaters(ForStatement node) {
var forParts = node.forLoopParts;
if (forParts is ForParts) {
var updaters = forParts.updaters;
var beginToken = updaters.beginToken;
var endToken = updaters.endToken;
if (beginToken != null && endToken != null) {
_errorReporter.atOffset(
offset: beginToken.offset,
length: endToken.end - beginToken.offset,
errorCode: WarningCode.DEAD_CODE,
);
}
}
}
void _verifyUnassignedSimpleIdentifier(
AstNode node, Expression? target, Token? operator) {
var flowAnalysis = _flowAnalysis;
if (flowAnalysis == null) return;
var operatorType = operator?.type;
if (operatorType != TokenType.QUESTION &&
operatorType != TokenType.QUESTION_PERIOD &&
operatorType != TokenType.QUESTION_PERIOD_PERIOD) {
return;
}
if (target?.staticType?.nullabilitySuffix != NullabilitySuffix.question) {
return;
}
target = target?.unParenthesized;
if (target is SimpleIdentifier) {
var element = target.staticElement;
if (element is PromotableElement &&
flowAnalysis.isDefinitelyUnassigned(target, element)) {
var parent = node.parent;
while (parent is MethodInvocation ||
parent is PropertyAccess ||
parent is IndexExpression) {
node = parent!;
parent = node.parent;
}
_errorReporter.atNode(
node,
WarningCode.DEAD_CODE,
);
}
}
}
}
/// A visitor that finds a [BreakStatement] for a specified [DoStatement].
class _BreakDoStatementVisitor extends RecursiveAstVisitor<void> {
bool hasBreakStatement = false;
final DoStatement doStatement;
_BreakDoStatementVisitor(this.doStatement);
@override
void visitBreakStatement(BreakStatement node) {
if (node.target == doStatement) {
hasBreakStatement = true;
}
}
}
class _CatchClausesVerifier {
final TypeSystemImpl _typeSystem;
final _CatchClausesVerifierReporter _errorReporter;
final List<CatchClause> catchClauses;
bool _done = false;
final List<DartType> _visitedTypes = <DartType>[];
_CatchClausesVerifier(
this._typeSystem,
this._errorReporter,
this.catchClauses,
);
void nextCatchClause(CatchClause catchClause) {
var currentType = catchClause.exceptionType?.type;
// Found catch clause that doesn't have an exception type.
// Generate an error on any following catch clauses.
if (currentType == null || currentType.isDartCoreObject) {
if (catchClause != catchClauses.last) {
var index = catchClauses.indexOf(catchClause);
_errorReporter(
catchClauses[index + 1],
catchClauses.last,
WarningCode.DEAD_CODE_CATCH_FOLLOWING_CATCH,
const [],
);
_done = true;
}
return;
}
// An on-catch clause was found; verify that the exception type is not a
// subtype of a previous on-catch exception type.
for (var type in _visitedTypes) {
if (_typeSystem.isSubtypeOf(currentType, type)) {
_errorReporter(
catchClause,
catchClauses.last,
WarningCode.DEAD_CODE_ON_CATCH_SUBTYPE,
[currentType, type],
);
_done = true;
return;
}
}
_visitedTypes.add(currentType);
}
}
/// An object used to track the usage of labels within a single label scope.
class _LabelTracker {
/// The tracker for the outer label scope.
final _LabelTracker? outerTracker;
/// The labels whose usage is being tracked.
final List<Label> labels;
/// A list of flags corresponding to the list of [labels] indicating whether
/// the corresponding label has been used.
late final List<bool> used;
/// A map from the names of labels to the index of the label in [labels].
final Map<String, int> labelMap = <String, int>{};
/// Initialize a newly created label tracker.
_LabelTracker(this.outerTracker, this.labels) {
used = List.filled(labels.length, false);
for (int i = 0; i < labels.length; i++) {
labelMap[labels[i].label.name] = i;
}
}
/// Record that the label with the given [labelName] has been used.
void recordUsage(String? labelName) {
if (labelName != null) {
var index = labelMap[labelName];
if (index != null) {
used[index] = true;
} else {
outerTracker?.recordUsage(labelName);
}
}
}
/// Return the unused labels.
Iterable<Label> unusedLabels() sync* {
for (int i = 0; i < labels.length; i++) {
if (!used[i]) {
yield labels[i];
}
}
}
}
extension DoStatementExtension on DoStatement {
bool get hasBreakStatement {
var visitor = _BreakDoStatementVisitor(this);
body.visitChildren(visitor);
return visitor.hasBreakStatement;
}
}