| // 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); |
| } |
| } |