blob: 40d859f7f36b1262474f09f11c5db5a353a1af8d [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/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/type.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/src/dart/element/type.dart';
import 'package:analyzer/src/dart/resolver/exit_detector.dart';
import 'package:analyzer/src/dart/resolver/scope.dart';
import 'package:analyzer/src/error/codes.dart';
import 'package:analyzer/src/generated/constant.dart';
import 'package:analyzer/src/generated/resolver.dart';
import 'package:analyzer/src/generated/type_system.dart';
import 'package:analyzer/src/task/strong/checker.dart';
/// A visitor that finds dead code and unused labels.
class DeadCodeVerifier extends RecursiveAstVisitor<void> {
/// The error reporter by which errors will be reported.
final ErrorReporter _errorReporter;
/// The type system for this visitor
final TypeSystemImpl _typeSystem;
/// The object used to track the usage of labels within a given label scope.
_LabelTracker labelTracker;
/// Is `true` if the library being analyzed is non-nullable by default.
final bool _isNonNullableByDefault;
/// Initialize a newly created dead code verifier that will report dead code
/// to the given [errorReporter] and will use the given [typeSystem] if one is
/// provided.
DeadCodeVerifier(this._errorReporter, FeatureSet featureSet,
{TypeSystemImpl typeSystem})
: this._typeSystem = typeSystem ??
TypeSystemImpl(
implicitCasts: true,
isNonNullableByDefault: false,
strictInference: false,
typeProvider: null,
),
_isNonNullableByDefault = featureSet.isEnabled(Feature.non_nullable);
@override
void visitAssignmentExpression(AssignmentExpression node) {
TokenType operatorType = node.operator.type;
if (operatorType == TokenType.QUESTION_QUESTION_EQ) {
_checkForDeadNullCoalesce(
getReadType(node.leftHandSide), node.rightHandSide);
}
super.visitAssignmentExpression(node);
}
@override
void visitBinaryExpression(BinaryExpression node) {
Token operator = node.operator;
bool isAmpAmp = operator.type == TokenType.AMPERSAND_AMPERSAND;
bool isBarBar = operator.type == TokenType.BAR_BAR;
bool isQuestionQuestion = operator.type == TokenType.QUESTION_QUESTION;
if (isAmpAmp || isBarBar) {
Expression lhsCondition = node.leftOperand;
if (!_isDebugConstant(lhsCondition)) {
EvaluationResultImpl lhsResult = _getConstantBooleanValue(lhsCondition);
if (lhsResult != null) {
bool value = lhsResult.value.toBoolValue();
if (value == true && isBarBar) {
// Report error on "else" block: true || !e!
_errorReporter.reportErrorForNode(
HintCode.DEAD_CODE, node.rightOperand);
// Only visit the LHS:
lhsCondition?.accept(this);
return;
} else if (value == false && isAmpAmp) {
// Report error on "if" block: false && !e!
_errorReporter.reportErrorForNode(
HintCode.DEAD_CODE, node.rightOperand);
// Only visit the LHS:
lhsCondition?.accept(this);
return;
}
}
}
// How do we want to handle the RHS? It isn't dead code, but "pointless"
// or "obscure"...
// Expression rhsCondition = node.getRightOperand();
// ValidResult rhsResult = getConstantBooleanValue(rhsCondition);
// if (rhsResult != null) {
// if (rhsResult == ValidResult.RESULT_TRUE && isBarBar) {
// // report error on else block: !e! || true
// errorReporter.reportError(HintCode.DEAD_CODE, node.getRightOperand());
// // only visit the RHS:
// rhsCondition?.accept(this);
// return null;
// } else if (rhsResult == ValidResult.RESULT_FALSE && isAmpAmp) {
// // report error on if block: !e! && false
// errorReporter.reportError(HintCode.DEAD_CODE, node.getRightOperand());
// // only visit the RHS:
// rhsCondition?.accept(this);
// return null;
// }
// }
} else if (isQuestionQuestion) {
_checkForDeadNullCoalesce(
getReadType(node.leftOperand), node.rightOperand);
}
super.visitBinaryExpression(node);
}
/// For each block, this method reports and error on all statements between
/// the end of the block and the first return statement (assuming there it is
/// not at the end of the block.)
@override
void visitBlock(Block node) {
NodeList<Statement> statements = node.statements;
_checkForDeadStatementsInNodeList(statements);
}
@override
void visitBreakStatement(BreakStatement node) {
labelTracker?.recordUsage(node.label?.name);
}
@override
void visitConditionalExpression(ConditionalExpression node) {
Expression conditionExpression = node.condition;
conditionExpression?.accept(this);
if (!_isDebugConstant(conditionExpression)) {
EvaluationResultImpl result =
_getConstantBooleanValue(conditionExpression);
if (result != null) {
if (result.value.toBoolValue() == true) {
// Report error on "else" block: true ? 1 : !2!
_errorReporter.reportErrorForNode(
HintCode.DEAD_CODE, node.elseExpression);
node.thenExpression?.accept(this);
return;
} else {
// Report error on "if" block: false ? !1! : 2
_errorReporter.reportErrorForNode(
HintCode.DEAD_CODE, node.thenExpression);
node.elseExpression?.accept(this);
return;
}
}
}
super.visitConditionalExpression(node);
}
@override
void visitContinueStatement(ContinueStatement node) {
labelTracker?.recordUsage(node.label?.name);
}
@override
void visitExportDirective(ExportDirective node) {
ExportElement 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 visitIfElement(IfElement node) {
Expression conditionExpression = node.condition;
conditionExpression?.accept(this);
if (!_isDebugConstant(conditionExpression)) {
EvaluationResultImpl result =
_getConstantBooleanValue(conditionExpression);
if (result != null) {
if (result.value.toBoolValue() == true) {
// Report error on else block: if(true) {} else {!}
CollectionElement elseElement = node.elseElement;
if (elseElement != null) {
_errorReporter.reportErrorForNode(HintCode.DEAD_CODE, elseElement);
node.thenElement?.accept(this);
return;
}
} else {
// Report error on if block: if (false) {!} else {}
_errorReporter.reportErrorForNode(
HintCode.DEAD_CODE, node.thenElement);
node.elseElement?.accept(this);
return;
}
}
}
super.visitIfElement(node);
}
@override
void visitIfStatement(IfStatement node) {
Expression conditionExpression = node.condition;
conditionExpression?.accept(this);
if (!_isDebugConstant(conditionExpression)) {
EvaluationResultImpl result =
_getConstantBooleanValue(conditionExpression);
if (result != null) {
if (result.value.toBoolValue() == true) {
// Report error on else block: if(true) {} else {!}
Statement elseStatement = node.elseStatement;
if (elseStatement != null) {
_errorReporter.reportErrorForNode(
HintCode.DEAD_CODE, elseStatement);
node.thenStatement?.accept(this);
return;
}
} else {
// Report error on if block: if (false) {!} else {}
_errorReporter.reportErrorForNode(
HintCode.DEAD_CODE, node.thenStatement);
node.elseStatement?.accept(this);
return;
}
}
}
super.visitIfStatement(node);
}
@override
void visitImportDirective(ImportDirective node) {
ImportElement 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 non-existent 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) {
_pushLabels(node.labels);
try {
super.visitLabeledStatement(node);
} finally {
_popLabels();
}
}
@override
void visitSwitchCase(SwitchCase node) {
_checkForDeadStatementsInNodeList(node.statements, allowMandated: true);
super.visitSwitchCase(node);
}
@override
void visitSwitchDefault(SwitchDefault node) {
_checkForDeadStatementsInNodeList(node.statements, allowMandated: true);
super.visitSwitchDefault(node);
}
@override
void visitSwitchStatement(SwitchStatement node) {
List<Label> labels = <Label>[];
for (SwitchMember member in node.members) {
labels.addAll(member.labels);
}
_pushLabels(labels);
try {
super.visitSwitchStatement(node);
} finally {
_popLabels();
}
}
@override
void visitTryStatement(TryStatement node) {
node.body?.accept(this);
node.finallyBlock?.accept(this);
NodeList<CatchClause> catchClauses = node.catchClauses;
int numOfCatchClauses = catchClauses.length;
List<DartType> visitedTypes = <DartType>[];
for (int i = 0; i < numOfCatchClauses; i++) {
CatchClause catchClause = catchClauses[i];
if (catchClause.onKeyword != null) {
// An on-catch clause was found; verify that the exception type is not a
// subtype of a previous on-catch exception type.
DartType currentType = catchClause.exceptionType?.type;
if (currentType != null) {
if (currentType.isObject) {
// Found catch clause clause that has Object as an exception type,
// this is equivalent to having a catch clause that doesn't have an
// exception type, visit the block, but generate an error on any
// following catch clauses (and don't visit them).
catchClause?.accept(this);
if (i + 1 != numOfCatchClauses) {
// This catch clause is not the last in the try statement.
CatchClause nextCatchClause = catchClauses[i + 1];
CatchClause lastCatchClause = catchClauses[numOfCatchClauses - 1];
int offset = nextCatchClause.offset;
int length = lastCatchClause.end - offset;
_errorReporter.reportErrorForOffset(
HintCode.DEAD_CODE_CATCH_FOLLOWING_CATCH, offset, length);
return;
}
}
int length = visitedTypes.length;
for (int j = 0; j < length; j++) {
DartType type = visitedTypes[j];
if (_typeSystem.isSubtypeOf2(currentType, type)) {
CatchClause lastCatchClause = catchClauses[numOfCatchClauses - 1];
int offset = catchClause.offset;
int length = lastCatchClause.end - offset;
_errorReporter.reportErrorForOffset(
HintCode.DEAD_CODE_ON_CATCH_SUBTYPE,
offset,
length,
[currentType, type]);
return;
}
}
visitedTypes.add(currentType);
}
catchClause?.accept(this);
} else {
// Found catch clause clause that doesn't have an exception type,
// visit the block, but generate an error on any following catch clauses
// (and don't visit them).
catchClause?.accept(this);
if (i + 1 != numOfCatchClauses) {
// This catch clause is not the last in the try statement.
CatchClause nextCatchClause = catchClauses[i + 1];
CatchClause lastCatchClause = catchClauses[numOfCatchClauses - 1];
int offset = nextCatchClause.offset;
int length = lastCatchClause.end - offset;
_errorReporter.reportErrorForOffset(
HintCode.DEAD_CODE_CATCH_FOLLOWING_CATCH, offset, length);
return;
}
}
}
}
@override
void visitWhileStatement(WhileStatement node) {
Expression conditionExpression = node.condition;
conditionExpression?.accept(this);
if (!_isDebugConstant(conditionExpression)) {
EvaluationResultImpl result =
_getConstantBooleanValue(conditionExpression);
if (result != null) {
if (result.value.toBoolValue() == false) {
// Report error on while block: while (false) {!}
_errorReporter.reportErrorForNode(HintCode.DEAD_CODE, node.body);
return;
}
}
}
node.body?.accept(this);
}
/// 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 hintCode;
if (combinator is HideCombinator) {
names = combinator.hiddenNames;
hintCode = HintCode.UNDEFINED_HIDDEN_NAME;
} else {
names = (combinator as ShowCombinator).shownNames;
hintCode = HintCode.UNDEFINED_SHOWN_NAME;
}
for (SimpleIdentifier name in names) {
String nameStr = name.name;
Element element = namespace.get(nameStr);
if (element == null) {
element = namespace.get("$nameStr=");
}
if (element == null) {
_errorReporter
.reportErrorForNode(hintCode, name, [library.identifier, nameStr]);
}
}
}
void _checkForDeadNullCoalesce(TypeImpl lhsType, Expression rhs) {
if (!_isNonNullableByDefault) return;
if (_typeSystem.isStrictlyNonNullable(lhsType)) {
_errorReporter.reportErrorForNode(
StaticWarningCode.DEAD_NULL_COALESCE,
rhs,
);
}
}
/// Given some list of [statements], loop through the list searching for dead
/// statements. If [allowMandated] is true, then allow dead statements that
/// are mandated by the language spec. This allows for a final break,
/// continue, return, or throw statement at the end of a switch case, that are
/// mandated by the language spec.
void _checkForDeadStatementsInNodeList(NodeList<Statement> statements,
{bool allowMandated = false}) {
bool statementExits(Statement statement) {
if (statement is BreakStatement) {
return statement.label == null;
} else if (statement is ContinueStatement) {
return statement.label == null;
}
return ExitDetector.exits(statement);
}
int size = statements.length;
for (int i = 0; i < size; i++) {
Statement currentStatement = statements[i];
currentStatement?.accept(this);
if (statementExits(currentStatement) && i != size - 1) {
Statement nextStatement = statements[i + 1];
Statement lastStatement = statements[size - 1];
// If mandated statements are allowed, and only the last statement is
// dead, and it's a BreakStatement, then assume it is a statement
// mandated by the language spec, there to avoid a
// CASE_BLOCK_NOT_TERMINATED error.
if (allowMandated && i == size - 2 && nextStatement is BreakStatement) {
return;
}
int offset = nextStatement.offset;
int length = lastStatement.end - offset;
_errorReporter.reportErrorForOffset(HintCode.DEAD_CODE, offset, length);
return;
}
}
}
/// Given some [expression], return [ValidResult.RESULT_TRUE] if it is `true`,
/// [ValidResult.RESULT_FALSE] if it is `false`, or `null` if the expression
/// is not a constant boolean value.
EvaluationResultImpl _getConstantBooleanValue(Expression expression) {
if (expression is BooleanLiteral) {
return EvaluationResultImpl(
DartObjectImpl(
_typeSystem,
_typeSystem.typeProvider.boolType,
BoolState.from(expression.value),
),
);
}
// Don't consider situations where we could evaluate to a constant boolean
// expression with the ConstantVisitor
// else {
// EvaluationResultImpl result = expression.accept(new ConstantVisitor());
// if (result == ValidResult.RESULT_TRUE) {
// return ValidResult.RESULT_TRUE;
// } else if (result == ValidResult.RESULT_FALSE) {
// return ValidResult.RESULT_FALSE;
// }
// return null;
// }
return null;
}
/// Return `true` if the given [expression] is resolved to a constant
/// variable.
bool _isDebugConstant(Expression expression) {
Element element;
if (expression is Identifier) {
element = expression.staticElement;
} else if (expression is PropertyAccess) {
element = expression.propertyName.staticElement;
}
if (element is PropertyAccessorElement) {
PropertyInducingElement variable = element.variable;
return variable != null && variable.isConst;
}
return false;
}
/// Exit the most recently entered label scope after reporting any labels that
/// were not referenced within that scope.
void _popLabels() {
for (Label label in labelTracker.unusedLabels()) {
_errorReporter
.reportErrorForNode(HintCode.UNUSED_LABEL, label, [label.label.name]);
}
labelTracker = labelTracker.outerTracker;
}
/// Enter a new label scope in which the given [labels] are defined.
void _pushLabels(List<Label> labels) {
labelTracker = _LabelTracker(labelTracker, labels);
}
}
/// 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.
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) {
int index = labelMap[labelName];
if (index != null) {
used[index] = true;
} else if (outerTracker != null) {
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];
}
}
}
}