blob: 1f389d6b6471a097606264a92302625ee1bf4063 [file] [log] [blame]
// Copyright (c) 2020, 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/error/error.dart';
import '../analyzer.dart';
import '../util/dart_type_utilities.dart';
const _desc = r'Use late for private members with non-nullable type.';
const _details = r'''
Use late for private members with non-nullable types that are always expected to
be non-null. Thus it's clear that the field is not expected to be `null` and it
avoids null checks.
**BAD:**
```dart
int? _i;
m() {
_i!.abs();
}
```
**GOOD:**
```dart
late int _i;
m() {
_i.abs();
}
```
''';
bool _isPrivateExtension(AstNode parent) {
if (parent is! ExtensionDeclaration) {
return false;
}
var parentName = parent.name?.name;
return parentName == null || Identifier.isPrivateName(parentName);
}
class UseLateForPrivateFieldsAndVariables extends LintRule
implements NodeLintRule {
UseLateForPrivateFieldsAndVariables()
: super(
name: 'use_late_for_private_fields_and_variables',
description: _desc,
details: _details,
maturity: Maturity.experimental,
group: Group.style,
);
@override
void registerNodeProcessors(
NodeLintRegistry registry, LinterContext context) {
var visitor = _Visitor(this, context);
registry.addCompilationUnit(this, visitor);
}
}
class _Visitor extends UnifyingAstVisitor<void> {
static final lateables =
<CompilationUnitElement, List<VariableDeclaration>>{};
static final nullableAccess = <CompilationUnitElement, Set<Element>>{};
final LintRule rule;
final LinterContext context;
CompilationUnitElement? currentUnit;
_Visitor(this.rule, this.context);
@override
void visitCompilationUnit(CompilationUnit node) {
if (node.featureSet.isEnabled(Feature.non_nullable)) {
var declaredElement = node.declaredElement;
if (declaredElement == null) {
return;
}
lateables.putIfAbsent(declaredElement, () => []);
nullableAccess.putIfAbsent(declaredElement, () => {});
currentUnit = declaredElement;
super.visitCompilationUnit(node);
var unitsInContext =
context.allUnits.map((e) => e.unit.declaredElement).toSet();
var libraryUnitsInContext =
declaredElement.library.units.where(unitsInContext.contains).toSet();
var areAllLibraryUnitsVisited =
libraryUnitsInContext.every(lateables.containsKey);
if (areAllLibraryUnitsVisited) {
_checkAccess(libraryUnitsInContext);
// clean up
for (var unit in libraryUnitsInContext) {
lateables.remove(unit);
nullableAccess.remove(unit);
}
}
}
}
@override
void visitFieldDeclaration(FieldDeclaration node) {
for (var variable in node.fields.variables) {
var parent = node.parent;
// see https://github.com/dart-lang/linter/pull/2189#issuecomment-660115569
// We could also include public members in private classes but to do that
// we'd need to ensure that there are no instances of either the
// enclosing class or any subclass of the enclosing class that are ever
// accessible outside this library.
if (parent != null &&
(Identifier.isPrivateName(variable.name.name) ||
_isPrivateExtension(parent))) {
_visit(variable);
}
}
super.visitFieldDeclaration(node);
}
@override
void visitNode(AstNode node) {
var parent = node.parent;
Element? element;
if (parent is AssignmentExpression && parent.leftHandSide == node) {
element = DartTypeUtilities.getCanonicalElement(parent.writeElement);
} else {
element = DartTypeUtilities.getCanonicalElementFromIdentifier(node);
}
if (element != null) {
if (parent is Expression) {
parent = parent.unParenthesized;
}
if (node is SimpleIdentifier && node.inDeclarationContext()) {
// ok
} else if (parent is PostfixExpression &&
parent.operand == node &&
parent.operator.type == TokenType.BANG) {
// ok non-null access
} else if (parent is AssignmentExpression &&
parent.operator.type == TokenType.EQ &&
DartTypeUtilities.isNonNullable(
context, parent.rightHandSide.staticType)) {
// ok non-null access
} else {
nullableAccess[currentUnit]?.add(element);
}
}
super.visitNode(node);
}
@override
void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) {
for (var variable in node.variables.variables) {
if (Identifier.isPrivateName(variable.name.name)) {
_visit(variable);
}
}
super.visitTopLevelVariableDeclaration(node);
}
void _checkAccess(Iterable<CompilationUnitElement> units) {
var allNullableAccess =
units.expand((unit) => nullableAccess[unit] ?? const {}).toSet();
for (var unit in units) {
for (var variable in lateables[unit] ?? const <VariableDeclaration>[]) {
if (!allNullableAccess.contains(variable.declaredElement)) {
rule.reporter.reportError(AnalysisError(
unit.source, variable.offset, variable.length, rule.lintCode));
}
}
}
}
void _visit(VariableDeclaration variable) {
if (variable.isLate) {
return;
}
if (variable.isSynthetic) {
return;
}
var declaredElement = variable.declaredElement;
if (declaredElement == null ||
context.typeSystem.isNonNullable(declaredElement.type)) {
return;
}
lateables[currentUnit]?.add(variable);
}
}