blob: f42679e76f222405e73341c8cc61deb19b897fa1 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. 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/results.dart';
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:path/path.dart' as path;
import '../utils.dart';
import 'analyze.dart';
// The comment pattern representing the "flutter_ignore" inline directive that
// indicates the line should be exempt from the stopwatch check.
final Pattern _ignoreStopwatch = RegExp(r'// flutter_ignore: .*stopwatch .*\(see analyze\.dart\)');
/// Use of Stopwatches can introduce test flakes as the logical time of a
/// stopwatch can fall out of sync with the mocked time of FakeAsync in testing.
/// The Clock object provides a safe stopwatch instead, which is paired with
/// FakeAsync as part of the test binding.
final AnalyzeRule noStopwatches = _NoStopwatches();
class _NoStopwatches implements AnalyzeRule {
final Map<ResolvedUnitResult, List<AstNode>> _errors = <ResolvedUnitResult, List<AstNode>>{};
@override
void applyTo(ResolvedUnitResult unit) {
final _StopwatchVisitor visitor = _StopwatchVisitor(unit);
unit.unit.visitChildren(visitor);
final List<AstNode> violationsInUnit = visitor.stopwatchAccessNodes;
if (violationsInUnit.isNotEmpty) {
_errors.putIfAbsent(unit, () => <AstNode>[]).addAll(violationsInUnit);
}
}
@override
void reportViolations(String workingDirectory) {
if (_errors.isEmpty) {
return;
}
String locationInFile(ResolvedUnitResult unit, AstNode node) {
return '${path.relative(path.relative(unit.path, from: workingDirectory))}:${unit.lineInfo.getLocation(node.offset).lineNumber}';
}
foundError(<String>[
for (final MapEntry<ResolvedUnitResult, List<AstNode>> entry in _errors.entries)
for (final AstNode node in entry.value)
'${locationInFile(entry.key, node)}: ${node.parent}',
'\n${bold}Stopwatches introduce flakes by falling out of sync with the FakeAsync used in testing.$reset',
'A Stopwatch that stays in sync with FakeAsync is available through the Gesture or Test bindings, through samplingClock.'
]);
}
@override
String toString() => 'No "Stopwatch"';
}
// This visitor finds invocation sites of Stopwatch (and subclasses) constructors
// and references to "external" functions that return a Stopwatch (and subclasses),
// including constructors, and put them in the stopwatchAccessNodes list.
class _StopwatchVisitor extends RecursiveAstVisitor<void> {
_StopwatchVisitor(this.compilationUnit);
final ResolvedUnitResult compilationUnit;
final List<AstNode> stopwatchAccessNodes = <AstNode>[];
final Map<ClassElement, bool> _isStopwatchClassElementCache = <ClassElement, bool>{};
bool _checkIfImplementsStopwatchRecursively(ClassElement classElement) {
if (classElement.library.isDartCore) {
return classElement.name == 'Stopwatch';
}
return classElement.allSupertypes.any((InterfaceType interface) {
final InterfaceElement interfaceElement = interface.element;
return interfaceElement is ClassElement && _implementsStopwatch(interfaceElement);
});
}
// The cached version, call this method instead of _checkIfImplementsStopwatchRecursively.
bool _implementsStopwatch(ClassElement classElement) {
return classElement.library.isDartCore
? classElement.name == 'Stopwatch'
:_isStopwatchClassElementCache.putIfAbsent(classElement, () => _checkIfImplementsStopwatchRecursively(classElement));
}
bool _isInternal(LibraryElement libraryElement) {
return path.isWithin(
compilationUnit.session.analysisContext.contextRoot.root.path,
libraryElement.source.fullName,
);
}
bool _hasTrailingFlutterIgnore(AstNode node) {
return compilationUnit.content
.substring(node.offset + node.length, compilationUnit.lineInfo.getOffsetOfLineAfter(node.offset + node.length))
.contains(_ignoreStopwatch);
}
// We don't care about directives or comments, skip them.
@override
void visitImportDirective(ImportDirective node) { }
@override
void visitExportDirective(ExportDirective node) { }
@override
void visitComment(Comment node) { }
@override
void visitConstructorName(ConstructorName node) {
final Element? element = node.staticElement;
if (element is! ConstructorElement) {
assert(false, '$element of $node is not a ConstructorElement.');
return;
}
final bool isAllowed = switch (element.returnType) {
InterfaceType(element: final ClassElement classElement) => !_implementsStopwatch(classElement),
InterfaceType(element: InterfaceElement()) => true,
};
if (isAllowed || _hasTrailingFlutterIgnore(node)) {
return;
}
stopwatchAccessNodes.add(node);
}
@override
void visitSimpleIdentifier(SimpleIdentifier node) {
final bool isAllowed = switch (node.staticElement) {
ExecutableElement(
returnType: DartType(element: final ClassElement classElement),
library: final LibraryElement libraryElement
) => _isInternal(libraryElement) || !_implementsStopwatch(classElement),
Element() || null => true,
};
if (isAllowed || _hasTrailingFlutterIgnore(node)) {
return;
}
stopwatchAccessNodes.add(node);
}
}