blob: 63c77347926f39cbfe22ab037f2e7f1f014ad627 [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/analysis/features.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/error/error.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/source/source_range.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/dart/element/type.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';
/// State information captured by [NullSafetyDeadCodeVerifier.for_conditionEnd]
/// for later use by [NullSafetyDeadCodeVerifier.for_updaterBegin].
class DeadCodeForPartsState {
/// The value of [NullSafetyDeadCodeVerifier._firstDeadNode] at the time of
/// the call to [NullSafetyDeadCodeVerifier.for_conditionEnd]
final AstNode? _firstDeadNodeAsOfConditionEnd;
DeadCodeForPartsState._({required AstNode? firstDeadNodeAsOfConditionEnd})
: _firstDeadNodeAsOfConditionEnd = firstDeadNodeAsOfConditionEnd;
}
/// 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;
/// Whether the `wildcard_variables` feature is enabled.
final bool _wildCardVariablesEnabled;
DeadCodeVerifier(this._errorReporter, LibraryElement library)
: _wildCardVariablesEnabled = library.featureSet.isEnabled(
Feature.wildcard_variables,
);
@override
void visitBreakStatement(BreakStatement node) {
_labelTracker?.recordUsage(node.label?.name);
}
@override
void visitContinueStatement(ContinueStatement node) {
_labelTracker?.recordUsage(node.label?.name);
}
@override
void visitExportDirective(covariant ExportDirectiveImpl node) {
var libraryExport = node.libraryExport;
if (libraryExport != null) {
// The element is null when the URI is invalid.
var library = libraryExport.exportedLibrary2;
if (library != null && !library.isSynthetic) {
for (Combinator combinator in node.combinators) {
_checkCombinator(library, combinator);
}
}
}
super.visitExportDirective(node);
}
@override
void visitFunctionDeclaration(FunctionDeclaration node) {
var element = node.declaredFragment!.element;
// TODO(pq): ask the FunctionElement once implemented
if (_wildCardVariablesEnabled &&
element is LocalFunctionElement &&
element.name3 == '_') {
_errorReporter.atNode(node, WarningCode.DEAD_CODE);
}
super.visitFunctionDeclaration(node);
}
@override
void visitImportDirective(covariant ImportDirectiveImpl node) {
var libraryImport = node.libraryImport;
if (libraryImport != null) {
// The element is null when the URI is invalid, but not when the URI is
// valid but refers to a nonexistent file.
var library = libraryImport.importedLibrary2;
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);
});
}
@override
void visitVariableDeclaration(VariableDeclaration node) {
var initializer = node.initializer;
if (initializer != null && node.isLate) {
var element = node.declaredFragment!.element;
// TODO(pq): ask the LocalVariableElement once implemented
if (_wildCardVariablesEnabled &&
element is LocalVariableElement &&
element.name3 == '_') {
_errorReporter.atNode(
initializer,
WarningCode.DEAD_CODE_LATE_WILDCARD_VARIABLE_INITIALIZER,
);
}
}
super.visitVariableDeclaration(node);
}
/// Resolve the names in the given [combinator] in the scope of the given
/// [library].
void _checkCombinator(LibraryElementImpl library, Combinator combinator) {
Namespace namespace = NamespaceBuilder().createExportNamespaceForLibrary(
library,
);
NodeList<SimpleIdentifier> names;
DiagnosticCode 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.get2(nameStr);
element ??= namespace.get2("$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) {
// Note that `firstDeadNode` could be a node that will later be replaced
// in the syntax tree. It's not safe to query whether it is _equal_ to, for
// example, another node's child.
// TODO(srawlins): Change this code to avoid this issue.
var firstDeadNode = _firstDeadNode;
if (firstDeadNode == null) {
return;
}
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 if (parent is ConstructorDeclaration &&
firstDeadNode is EmptyFunctionBody) {
// Don't report "dead code" for an unreachable, but syntacically required,
// semicolon that follows one or more constructor initializers.
} else if (parent is ConstructorDeclaration &&
firstDeadNode is BlockFunctionBody &&
firstDeadNode.block.statements.isEmpty) {
// Don't report "dead code" for an unreachable, but empty block body that
// follows one or more constructor initializers.
} 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) {
offset = parent.operator.offset;
}
if (parent is ConstructorInitializer) {
_errorReporter.atOffset(
offset: parent.offset,
length: parent.end - parent.offset,
diagnosticCode: WarningCode.DEAD_CODE,
);
offset = node.end;
} else if (parent is DoStatement) {
var whileOffset = parent.whileKeyword.offset;
var whileEnd = parent.semicolon.end;
var body = parent.body;
if (body is Block) {
whileOffset = body.rightBracket.offset;
}
_errorReporter.atOffset(
offset: whileOffset,
length: whileEnd - whileOffset,
diagnosticCode: 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 BinaryExpression) {
offset = parent.operator.offset;
node = parent.rightOperand;
} else if (parent is LogicalOrPattern &&
firstDeadNode == parent.rightOperand) {
offset = parent.operator.offset;
}
var length = node.end - offset;
if (length > 0) {
_errorReporter.atOffset(
offset: offset,
length: length,
diagnosticCode: WarningCode.DEAD_CODE,
);
}
}
_firstDeadNode = null;
}
/// Performs the necessary dead code analysis when reaching the end of the
/// `condition` part of [ForParts].
///
/// Some state is returned, which should be passed to [for_updaterBegin] after
/// visiting the body of the loop.
DeadCodeForPartsState for_conditionEnd() {
// Capture the state of this class so that `for_updaterBegin` can use it to
// decide whether it's necessary to create an extra dead code report for the
// updaters.
return DeadCodeForPartsState._(
firstDeadNodeAsOfConditionEnd: _firstDeadNode,
);
}
/// Performs the necessary dead code analysis when reaching the beginning of
/// the `updaters` part of [ForParts].
///
/// [state] should be the state returned by [for_conditionEnd].
void for_updaterBegin(
NodeListImpl<ExpressionImpl> updaters,
DeadCodeForPartsState state,
) {
var isReachable = _flowAnalysis?.flow?.isReachable ?? true;
if (!isReachable && state._firstDeadNodeAsOfConditionEnd == null) {
// A dead code range started either at the beginning of the loop body or
// somewhere inside it, and so the updaters are dead. Since the updaters
// appear textually before the loop body, they need their own dead code
// warning.
var beginToken = updaters.beginToken;
var endToken = updaters.endToken;
if (beginToken != null && endToken != null) {
_errorReporter.atOffset(
offset: beginToken.offset,
length: endToken.end - beginToken.offset,
diagnosticCode: WarningCode.DEAD_CODE,
);
}
}
}
/// Rewites [_firstDeadNode] if it is equal to [oldNode], as [oldNode] is
/// being rewritten into [newNode] in the syntax tree.
void maybeRewriteFirstDeadNode(AstNode oldNode, AstNode newNode) {
if (_firstDeadNode == oldNode) {
_firstDeadNode = newNode;
}
}
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,
diagnosticCode: 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(CatchClauseImpl 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 _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.element;
if (element is PromotableElementImpl2 &&
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 void Function(
CatchClause first,
CatchClause last,
DiagnosticCode,
List<Object> arguments,
)
_reportDiagnostic;
final List<CatchClause> catchClauses;
bool _done = false;
final List<TypeImpl> _visitedTypes = [];
_CatchClausesVerifier(
this._typeSystem,
this._reportDiagnostic,
this.catchClauses,
);
void nextCatchClause(CatchClauseImpl 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);
_reportDiagnostic(
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)) {
_reportDiagnostic(
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;
}
}