1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:analyzer/dart/analysis/results.dart';
6import 'package:analyzer/dart/ast/ast.dart';
7import 'package:analyzer/dart/ast/visitor.dart';
8import 'package:analyzer/dart/element/element.dart';
9import 'package:analyzer/dart/element/type.dart';
10
11import '../utils.dart';
12import 'analyze.dart';
13
14/// Verify that we use clampDouble instead of double.clamp for performance
15/// reasons.
16///
17/// See also:
18/// * https://github.com/flutter/flutter/pull/103559
19/// * https://github.com/flutter/flutter/issues/103917
20final AnalyzeRule noDoubleClamp = _NoDoubleClamp();
21
22class _NoDoubleClamp implements AnalyzeRule {
23 final Map<ResolvedUnitResult, List<AstNode>> _errors = <ResolvedUnitResult, List<AstNode>>{};
24
25 @override
26 void applyTo(ResolvedUnitResult unit) {
27 final _DoubleClampVisitor visitor = _DoubleClampVisitor();
28 unit.unit.visitChildren(visitor);
29 final List<AstNode> violationsInUnit = visitor.clampAccessNodes;
30 if (violationsInUnit.isNotEmpty) {
31 _errors.putIfAbsent(unit, () => <AstNode>[]).addAll(violationsInUnit);
32 }
33 }
34
35 @override
36 void reportViolations(String workingDirectory) {
37 if (_errors.isEmpty) {
38 return;
39 }
40
41 foundError(<String>[
42 for (final MapEntry<ResolvedUnitResult, List<AstNode>> entry in _errors.entries)
43 for (final AstNode node in entry.value)
44 '${locationInFile(entry.key, node, workingDirectory)}: ${node.parent}',
45 '\n${bold}For performance reasons, we use a custom "clampDouble" function instead of using "double.clamp".$reset',
46 ]);
47 }
48
49 @override
50 String toString() => 'No "double.clamp"';
51}
52
53class _DoubleClampVisitor extends RecursiveAstVisitor<void> {
54 final List<AstNode> clampAccessNodes = <AstNode>[];
55
56 // We don't care about directives or comments.
57 @override
58 void visitImportDirective(ImportDirective node) {}
59
60 @override
61 void visitExportDirective(ExportDirective node) {}
62
63 @override
64 void visitComment(Comment node) {}
65
66 @override
67 void visitSimpleIdentifier(SimpleIdentifier node) {
68 if (node.name != 'clamp' || node.staticElement is! MethodElement) {
69 return;
70 }
71 final bool isAllowed = switch (node.parent) {
72 // PropertyAccess matches num.clamp in tear-off form. Always prefer
73 // doubleClamp over tear-offs: even when all 3 operands are int literals,
74 // the return type doesn't get promoted to int:
75 // final x = 1.clamp(0, 2); // The inferred return type is int, where as:
76 // final f = 1.clamp;
77 // final y = f(0, 2) // The inferred return type is num.
78 PropertyAccess(
79 target: Expression(
80 staticType: DartType(isDartCoreDouble: true) ||
81 DartType(isDartCoreNum: true) ||
82 DartType(isDartCoreInt: true),
83 ),
84 ) =>
85 false,
86
87 // Expressions like `final int x = 1.clamp(0, 2);` should be allowed.
88 MethodInvocation(
89 target: Expression(staticType: DartType(isDartCoreInt: true)),
90 argumentList: ArgumentList(
91 arguments: [
92 Expression(staticType: DartType(isDartCoreInt: true)),
93 Expression(staticType: DartType(isDartCoreInt: true)),
94 ],
95 ),
96 ) =>
97 true,
98
99 // Otherwise, disallow num.clamp() invocations.
100 MethodInvocation(
101 target: Expression(
102 staticType: DartType(isDartCoreDouble: true) ||
103 DartType(isDartCoreNum: true) ||
104 DartType(isDartCoreInt: true),
105 ),
106 ) =>
107 false,
108
109 _ => true,
110 };
111 if (!isAllowed) {
112 clampAccessNodes.add(node);
113 }
114 }
115}
116