// 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 '../utils.dart'; import 'analyze.dart'; /// Verify that we use clampDouble instead of double.clamp for performance /// reasons. /// /// See also: /// * https://github.com/flutter/flutter/pull/103559 /// * https://github.com/flutter/flutter/issues/103917 final AnalyzeRule noDoubleClamp = _NoDoubleClamp(); class _NoDoubleClamp implements AnalyzeRule { final Map<ResolvedUnitResult, List<AstNode>> _errors = <ResolvedUnitResult, List<AstNode>>{}; @override void applyTo(ResolvedUnitResult unit) { final _DoubleClampVisitor visitor = _DoubleClampVisitor(); unit.unit.visitChildren(visitor); final List<AstNode> violationsInUnit = visitor.clampAccessNodes; if (violationsInUnit.isNotEmpty) { _errors.putIfAbsent(unit, () => <AstNode>[]).addAll(violationsInUnit); } } @override void reportViolations(String workingDirectory) { if (_errors.isEmpty) { return; } foundError(<String>[ for (final MapEntry<ResolvedUnitResult, List<AstNode>> entry in _errors.entries) for (final AstNode node in entry.value) '${locationInFile(entry.key, node, workingDirectory)}: ${node.parent}', '\n${bold}For performance reasons, we use a custom "clampDouble" function instead of using "double.clamp".$reset', ]); } @override String toString() => 'No "double.clamp"'; } class _DoubleClampVisitor extends RecursiveAstVisitor<void> { final List<AstNode> clampAccessNodes = <AstNode>[]; // We don't care about directives or comments. @override void visitImportDirective(ImportDirective node) { } @override void visitExportDirective(ExportDirective node) { } @override void visitComment(Comment node) { } @override void visitSimpleIdentifier(SimpleIdentifier node) { if (node.name != 'clamp' || node.staticElement is! MethodElement) { return; } final bool isAllowed = switch (node.parent) { // PropertyAccess matches num.clamp in tear-off form. Always prefer // doubleClamp over tear-offs: even when all 3 operands are int literals, // the return type doesn't get promoted to int: // final x = 1.clamp(0, 2); // The inferred return type is int, where as: // final f = 1.clamp; // final y = f(0, 2) // The inferred return type is num. PropertyAccess( target: Expression(staticType: DartType(isDartCoreDouble: true) || DartType(isDartCoreNum: true) || DartType(isDartCoreInt: true)), ) => false, // Expressions like `final int x = 1.clamp(0, 2);` should be allowed. MethodInvocation( target: Expression(staticType: DartType(isDartCoreInt: true)), argumentList: ArgumentList(arguments: [Expression(staticType: DartType(isDartCoreInt: true)), Expression(staticType: DartType(isDartCoreInt: true))]), ) => true, // Otherwise, disallow num.clamp() invocations. MethodInvocation( target: Expression(staticType: DartType(isDartCoreDouble: true) || DartType(isDartCoreNum: true) || DartType(isDartCoreInt: true)), ) => false, _ => true, }; if (!isAllowed) { clampAccessNodes.add(node); } } }