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 no RenderBox subclasses call compute* instead of get* for
15/// computing the intrinsic dimensions. The [candidates] variable contains the
16/// full list of RenderBox intrinsic method invocations checked by this rule.
17final AnalyzeRule renderBoxIntrinsicCalculation = _RenderBoxIntrinsicCalculationRule();
18
19const Map<String, String> candidates = <String, String>{
20 'computeDryBaseline': 'getDryBaseline',
21 'computeDryLayout': 'getDryLayout',
22 'computeDistanceToActualBaseline': 'getDistanceToBaseline, or getDistanceToActualBaseline',
23 'computeMaxIntrinsicHeight': 'getMaxIntrinsicHeight',
24 'computeMinIntrinsicHeight': 'getMinIntrinsicHeight',
25 'computeMaxIntrinsicWidth': 'getMaxIntrinsicWidth',
26 'computeMinIntrinsicWidth': 'getMinIntrinsicWidth',
27};
28
29class _RenderBoxIntrinsicCalculationRule implements AnalyzeRule {
30 final Map<ResolvedUnitResult, List<(AstNode, String)>> _errors =
31 <ResolvedUnitResult, List<(AstNode, String)>>{};
32
33 @override
34 void applyTo(ResolvedUnitResult unit) {
35 final _RenderBoxSubclassVisitor visitor = _RenderBoxSubclassVisitor();
36 unit.unit.visitChildren(visitor);
37 final List<(AstNode, String)> violationsInUnit = visitor.violationNodes;
38 if (violationsInUnit.isNotEmpty) {
39 _errors.putIfAbsent(unit, () => <(AstNode, String)>[]).addAll(violationsInUnit);
40 }
41 }
42
43 @override
44 void reportViolations(String workingDirectory) {
45 if (_errors.isEmpty) {
46 return;
47 }
48
49 foundError(<String>[
50 for (final MapEntry<ResolvedUnitResult, List<(AstNode, String)>> entry in _errors.entries)
51 for (final (AstNode node, String suggestion) in entry.value)
52 '${locationInFile(entry.key, node, workingDirectory)}: ${node.parent}. Consider calling $suggestion instead.',
53 '\n${bold}Typically the get* methods should be used to obtain the intrinsics of a RenderBox.$reset',
54 ]);
55 }
56
57 @override
58 String toString() => 'RenderBox subclass intrinsic calculation best practices';
59}
60
61class _RenderBoxSubclassVisitor extends RecursiveAstVisitor<void> {
62 final List<(AstNode, String)> violationNodes = <(AstNode, String)>[];
63
64 static final Map<InterfaceElement, bool> _isRenderBoxClassElementCache =
65 <InterfaceElement, bool>{};
66 // The cached version, call this method instead of _checkIfImplementsRenderBox.
67 static bool _implementsRenderBox(InterfaceElement interfaceElement) {
68 // Framework naming convention: a RenderObject subclass names have "Render" in its name.
69 if (!interfaceElement.name.contains('Render')) {
70 return false;
71 }
72 return interfaceElement.name == 'RenderBox' ||
73 _isRenderBoxClassElementCache.putIfAbsent(
74 interfaceElement,
75 () => _checkIfImplementsRenderBox(interfaceElement),
76 );
77 }
78
79 static bool _checkIfImplementsRenderBox(InterfaceElement element) {
80 return element.allSupertypes.any(
81 (InterfaceType interface) => _implementsRenderBox(interface.element),
82 );
83 }
84
85 // We don't care about directives, comments, or asserts.
86 @override
87 void visitImportDirective(ImportDirective node) {}
88 @override
89 void visitExportDirective(ExportDirective node) {}
90 @override
91 void visitComment(Comment node) {}
92 @override
93 void visitAssertStatement(AssertStatement node) {}
94
95 @override
96 void visitClassDeclaration(ClassDeclaration node) {
97 // Ignore the RenderBox class implementation: that's the only place the
98 // compute* methods are supposed to be called.
99 if (node.name.lexeme != 'RenderBox') {
100 super.visitClassDeclaration(node);
101 }
102 }
103
104 @override
105 void visitSimpleIdentifier(SimpleIdentifier node) {
106 final String? correctMethodName = candidates[node.name];
107 if (correctMethodName == null) {
108 return;
109 }
110 final bool isCallingSuperImplementation = switch (node.parent) {
111 PropertyAccess(target: SuperExpression()) ||
112 MethodInvocation(target: SuperExpression()) => true,
113 _ => false,
114 };
115 if (isCallingSuperImplementation) {
116 return;
117 }
118 final Element? declaredInClassElement = node.staticElement?.declaration?.enclosingElement3;
119 if (declaredInClassElement is InterfaceElement &&
120 _implementsRenderBox(declaredInClassElement)) {
121 violationNodes.add((node, correctMethodName));
122 }
123 }
124}
125