blob: 1d272ac3a9613f6e0194f010354491362f79ac13 [file] [log] [blame]
// Copyright (c) 2016, 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 'package:analyzer/dart/element/type.dart';
import 'package:meta/meta.dart';
import '../analyzer.dart';
import '../ast.dart';
/// Builds a function that reports a variable node if none of the [predicates]
/// return `true` for any node inside the [container] node.
_VisitVariableDeclaration _buildVariableReporter(
AstNode container,
LintRule rule,
Map<DartTypePredicate, String> predicates, {
required _VariableType variableType,
}) => (VariableDeclaration variable) {
if (variable.equals != null && variable.initializer is SimpleIdentifier) {
return;
}
var variableElement = variable.declaredFragment?.element;
if (variableElement == null) {
return;
}
if (!predicates.keys.any((DartTypePredicate p) => p(variableElement.type))) {
return;
}
var visitor = _ValidUseVisitor(
variable,
variableElement,
predicates,
variableType: variableType,
);
container.accept(visitor);
if (visitor.containsValidUse) {
return;
}
rule.reportAtNode(variable);
};
/// Whether any of the [predicates] applies to [methodName] and holds true for
/// [type].
bool _hasMatch(
Map<DartTypePredicate, String> predicates,
DartType type,
String methodName,
) => predicates.keys.any((p) => predicates[p] == methodName && p(type));
bool _isElementEqualToVariable(
Element? propertyElement,
VariableElement? variableElement,
) =>
propertyElement == variableElement ||
propertyElement.matches(variableElement);
bool _isInvocationThroughCascadeExpression(
MethodInvocation invocation,
VariableElement variableElement,
) {
if (invocation.realTarget is! SimpleIdentifier) {
return false;
}
var identifier = invocation.realTarget;
if (identifier is SimpleIdentifier) {
return identifier.element.matches(variableElement);
}
return false;
}
bool _isPostfixExpressionOperandEqualToVariable(
AstNode? n,
VariableElement variableElement,
) {
if (n is PostfixExpression) {
var operand = n.operand;
return operand is SimpleIdentifier &&
_isElementEqualToVariable(operand.element, variableElement);
}
return false;
}
bool _isPropertyAccessThroughThis(
Expression? n,
VariableElement variableElement,
) {
if (n is! PropertyAccess) {
return false;
}
var target = n.realTarget;
if (target is! ThisExpression) {
return false;
}
var propertyElement = n.propertyName.element;
return _isElementEqualToVariable(propertyElement, variableElement);
}
bool _isSimpleIdentifierElementEqualToVariable(
AstNode? n,
VariableElement variableElement,
) =>
n is SimpleIdentifier &&
_isElementEqualToVariable(n.element, variableElement);
typedef DartTypePredicate = bool Function(DartType type);
typedef _VisitVariableDeclaration = void Function(VariableDeclaration node);
abstract class LeakDetectorProcessors extends SimpleAstVisitor<void> {
final LintRule rule;
LeakDetectorProcessors(this.rule);
@protected
Map<DartTypePredicate, String> get predicates;
@override
void visitFieldDeclaration(FieldDeclaration node) {
var unit = getCompilationUnit(node);
if (unit != null) {
// When visiting a field declaration, we want to check tree under the
// containing unit for ConstructorFieldInitializers and FieldFormalParameters.
node.fields.variables.forEach(
_buildVariableReporter(
unit,
rule,
predicates,
variableType: _VariableType.field,
),
);
}
}
@override
void visitVariableDeclarationStatement(VariableDeclarationStatement node) {
var function = node.thisOrAncestorOfType<FunctionBody>();
if (function != null) {
// When visiting a variable declaration, we want to check tree under the
// containing function for ReturnStatements. If an interesting variable
// is returned, don't report it.
node.variables.variables.forEach(
_buildVariableReporter(
function,
rule,
predicates,
variableType: _VariableType.local,
),
);
}
}
}
/// A visitor that tracks _any_ valid use of [variable].
///
/// A valid use may calling a method or tearing off a method on [variable] as
/// per [predicates].
///
/// A valid use may also just be the variable "escaping" the scope, for example,
/// being returned by a function, or being passed as an argument to a function.
class _ValidUseVisitor extends RecursiveAstVisitor<void> {
/// The variable under consideration.
final VariableDeclaration variable;
/// The element of the variable under consideration; stored here as a non-
/// `null` value.
final VariableElement variableElement;
/// The predicates that determine whether a method call or method tear-off is
/// a valid use.
final Map<DartTypePredicate, String> predicates;
/// The type of variable, which determines a few specifics about variable
/// use.
final _VariableType variableType;
/// Whether the node tree, after being visited, was determined to contain a
/// valid use.
var containsValidUse = false;
_ValidUseVisitor(
this.variable,
this.variableElement,
this.predicates, {
required this.variableType,
});
@override
void visitAssignmentExpression(AssignmentExpression node) {
// Being assigned another reference.
if (node.rightHandSide is SimpleIdentifier) {
if (_isElementEqualToVariable(
node.writeElement,
variable.declaredFragment?.element,
)) {
containsValidUse = true;
return;
}
// Assignment to VariableDeclaration as setter.
var leftHandSide = node.leftHandSide;
if (leftHandSide is PropertyAccess &&
leftHandSide.propertyName.token.lexeme == variable.name.lexeme) {
containsValidUse = true;
return;
}
}
super.visitAssignmentExpression(node);
}
@override
void visitConstructorFieldInitializer(ConstructorFieldInitializer node) {
if (node.fieldName.element == variableElement) {
containsValidUse = true;
return;
}
super.visitConstructorFieldInitializer(node);
}
@override
void visitFieldFormalParameter(FieldFormalParameter node) {
if (variableType == _VariableType.field) {
var staticElement = node.declaredFragment?.element;
if (staticElement is FieldFormalParameterElement &&
staticElement.field2 == variableElement) {
containsValidUse = true;
return;
}
}
super.visitFieldFormalParameter(node);
}
@override
void visitMethodInvocation(MethodInvocation node) {
if (_hasMatch(predicates, variableElement.type, node.methodName.name) &&
(_isSimpleIdentifierElementEqualToVariable(
node.realTarget,
variableElement,
) ||
_isPostfixExpressionOperandEqualToVariable(
node.realTarget,
variableElement,
) ||
_isPropertyAccessThroughThis(node.realTarget, variableElement) ||
(node.thisOrAncestorMatching((a) => a == variable) != null))) {
containsValidUse = true;
return;
}
if (_isInvocationThroughCascadeExpression(node, variableElement)) {
containsValidUse = true;
return;
}
if (node.argumentList.arguments
.whereType<SimpleIdentifier>()
.map((e) => e.element)
.contains(variableElement)) {
// If any function is invoked with our variable, we suppress lints. This
// is because it is not so uncommon to invoke the target method there. We
// might not have access to the body of such function at analysis time, so
// we cannot infer whether the target method is invoked.
// TODO(alexeidiaz): Should there be another, stricter lint rule that
// omits this step?
containsValidUse = true;
return;
}
super.visitMethodInvocation(node);
}
@override
void visitPrefixedIdentifier(PrefixedIdentifier node) {
if (node.prefix.element == variableElement &&
_hasMatch(
predicates,
variableElement.type,
node.identifier.token.lexeme,
)) {
containsValidUse = true;
return;
}
super.visitPrefixedIdentifier(node);
}
@override
void visitReturnStatement(ReturnStatement node) {
if (variableType == _VariableType.local) {
var expression = node.expression;
if (expression is SimpleIdentifier &&
expression.element == variableElement) {
containsValidUse = true;
return;
}
}
super.visitReturnStatement(node);
}
}
/// The type of variable being assessed.
enum _VariableType { field, local }
extension on Element? {
bool matches(VariableElement? variable) => switch (this) {
PropertyAccessorElement(:var variable3) => variable3 == variable,
_ => false,
};
}