Skip to content

Commit 2001b00

Browse files
DanTupcommit-bot@chromium.org
authored andcommitted
[analysis_server] Add ColorComputer for locating color references in user code
Change-Id: I166f87135a9b15821e5f65cfea33b8a15fa7aa03 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/219101 Reviewed-by: Brian Wilkerson <[email protected]> Commit-Queue: Brian Wilkerson <[email protected]>
1 parent 2f325ab commit 2001b00

File tree

9 files changed

+902
-0
lines changed

9 files changed

+902
-0
lines changed
Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:analysis_server/src/utilities/flutter.dart';
6+
import 'package:analyzer/dart/analysis/results.dart';
7+
import 'package:analyzer/dart/ast/ast.dart';
8+
import 'package:analyzer/dart/ast/visitor.dart';
9+
import 'package:analyzer/dart/constant/value.dart';
10+
import 'package:analyzer/dart/element/element.dart';
11+
import 'package:analyzer/dart/element/type.dart';
12+
import 'package:analyzer/src/dart/constant/value.dart' show GenericState;
13+
import 'package:analyzer/src/dart/element/inheritance_manager3.dart';
14+
import 'package:analyzer/src/dart/element/type_system.dart';
15+
import 'package:analyzer/src/lint/linter.dart';
16+
import 'package:collection/collection.dart';
17+
18+
/// Computer for dart:ui/Flutter Color references.
19+
class ColorComputer {
20+
final ResolvedUnitResult resolvedUnit;
21+
final LinterContext _linterContext;
22+
final List<ColorReference> _colors = [];
23+
final Flutter _flutter = Flutter.instance;
24+
25+
ColorComputer(this.resolvedUnit)
26+
: _linterContext = LinterContextImpl(
27+
[], // unused
28+
LinterContextUnit(resolvedUnit.content, resolvedUnit.unit),
29+
resolvedUnit.session.declaredVariables,
30+
resolvedUnit.typeProvider,
31+
resolvedUnit.typeSystem as TypeSystemImpl,
32+
InheritanceManager3(), // unused
33+
resolvedUnit.session.analysisContext.analysisOptions,
34+
null,
35+
);
36+
37+
/// Returns information about the color references in [resolvedUnit].
38+
///
39+
/// This method should only be called once for any instance of this class.
40+
List<ColorReference> compute() {
41+
final visitor = _ColorBuilder(this);
42+
resolvedUnit.unit.accept(visitor);
43+
return _colors;
44+
}
45+
46+
/// Tries to add a color for the [expression].
47+
///
48+
/// If [target] is supplied, will be used instead of [expression] allowing
49+
/// a value to be read from the member [memberName] or from a swatch value
50+
/// with index [index].
51+
bool tryAddColor(
52+
Expression expression, {
53+
Expression? target,
54+
String? memberName,
55+
int? index,
56+
}) {
57+
if (!_isColor(expression.staticType)) return false;
58+
59+
target ??= expression;
60+
61+
// Try to evaluate the constant target.
62+
final colorConstResult = _linterContext.evaluateConstant(target);
63+
var colorConst = colorConstResult.value;
64+
if (colorConstResult.errors.isNotEmpty || colorConst == null) return false;
65+
66+
// If we want a specific member or swatch index, read that.
67+
if (memberName != null) {
68+
colorConst = _getMember(colorConst, memberName);
69+
} else if (index != null) {
70+
colorConst = _getSwatchValue(colorConst, index);
71+
}
72+
73+
return _tryRecordColor(expression, colorConst);
74+
}
75+
76+
/// Tries to add a color for the instance creation [expression].
77+
///
78+
/// This handles constructor calls that cannot be evaluated (for example
79+
/// because they are not const) but are simple well-known dart:ui/Flutter
80+
/// color constructors that we can manually parse.
81+
bool tryAddKnownColorConstructor(InstanceCreationExpression expression) {
82+
if (!_isColor(expression.staticType)) return false;
83+
84+
final constructor = expression.constructorName;
85+
final staticElement = constructor.staticElement;
86+
final classElement = staticElement?.enclosingElement;
87+
final className = classElement?.name;
88+
final constructorName = constructor.name?.name;
89+
final constructorArgs = expression.argumentList.arguments
90+
.map((e) => e is Literal ? e : null)
91+
.toList();
92+
93+
int? colorValue;
94+
if (_isDartUi(classElement) && className == 'Color') {
95+
colorValue = _getDartUiColorValue(constructorName, constructorArgs);
96+
} else if (_isFlutterPainting(classElement) && className == 'ColorSwatch') {
97+
colorValue =
98+
_getFlutterSwatchColorValue(constructorName, constructorArgs);
99+
} else if (_isFlutterMaterial(classElement) &&
100+
className == 'MaterialAccentColor') {
101+
colorValue =
102+
_getFlutterMaterialAccentColorValue(constructorName, constructorArgs);
103+
}
104+
105+
return _tryRecordColorValue(expression, colorValue);
106+
}
107+
108+
/// Creates a [ColorInformation] by extracting the argb values from
109+
/// [value] encoded as 0xAARRGGBB as in the dart:ui Color class.
110+
ColorInformation _colorInformationForColorValue(int value) {
111+
// Extract color information according to dart:ui Color values.
112+
final alpha = (0xff000000 & value) >> 24;
113+
final red = (0x00ff0000 & value) >> 16;
114+
final blue = (0x000000ff & value) >> 0;
115+
final green = (0x0000ff00 & value) >> 8;
116+
117+
return ColorInformation(alpha, red, green, blue);
118+
}
119+
120+
/// Extracts the integer color value from the dart:ui Color constant [color].
121+
int? _colorValueForColorConst(DartObject? color) {
122+
if (color == null || color.isNull) return null;
123+
124+
// If the object has a "color" field, walk down to that, because some colors
125+
// like CupertinoColors have a "value=0" with an overridden getter that
126+
// would always result in a value representing black.
127+
color = color.getFieldFromHierarchy('color') ?? color;
128+
129+
return color.getFieldFromHierarchy('value')?.toIntValue();
130+
}
131+
132+
/// Converts ARGB values into a single int value as 0xAARRGGBB as used by
133+
/// the dart:ui Color class.
134+
int _colorValueForComponents(int alpha, int red, int green, int blue) {
135+
return (alpha << 24) | (red << 16) | (green << 8) | (blue << 0);
136+
}
137+
138+
/// Extracts the color value from dart:ui Color constructor args.
139+
int? _getDartUiColorValue(String? name, List<Literal?> args) {
140+
if (name == null && args.length == 1) {
141+
final arg0 = args[0];
142+
return arg0 is IntegerLiteral ? arg0.value : null;
143+
} else if (name == 'fromARGB' && args.length == 4) {
144+
final arg0 = args[0];
145+
final arg1 = args[1];
146+
final arg2 = args[2];
147+
final arg3 = args[3];
148+
149+
final alpha = arg0 is IntegerLiteral ? arg0.value : null;
150+
final red = arg1 is IntegerLiteral ? arg1.value : null;
151+
final green = arg2 is IntegerLiteral ? arg2.value : null;
152+
final blue = arg3 is IntegerLiteral ? arg3.value : null;
153+
154+
return alpha != null && red != null && green != null && blue != null
155+
? _colorValueForComponents(alpha, red, green, blue)
156+
: null;
157+
} else if (name == 'fromRGBO' && args.length == 4) {
158+
final arg0 = args[0];
159+
final arg1 = args[1];
160+
final arg2 = args[2];
161+
final arg3 = args[3];
162+
163+
final red = arg0 is IntegerLiteral ? arg0.value : null;
164+
final green = arg1 is IntegerLiteral ? arg1.value : null;
165+
final blue = arg2 is IntegerLiteral ? arg2.value : null;
166+
final opacity = arg3 is IntegerLiteral
167+
? arg3.value
168+
: arg3 is DoubleLiteral
169+
? arg3.value
170+
: null;
171+
final alpha = opacity != null ? (opacity * 255).toInt() : null;
172+
173+
return alpha != null && red != null && green != null && blue != null
174+
? _colorValueForComponents(alpha, red, green, blue)
175+
: null;
176+
}
177+
}
178+
179+
/// Extracts the color value from Flutter MaterialAccentColor constructor args.
180+
int? _getFlutterMaterialAccentColorValue(String? name, List<Literal?> args) =>
181+
// MaterialAccentColor is a subclass of SwatchColor and has the same
182+
// constructor.
183+
_getFlutterSwatchColorValue(name, args);
184+
185+
/// Extracts the color value from Flutter ColorSwatch constructor args.
186+
int? _getFlutterSwatchColorValue(String? name, List<Literal?> args) {
187+
if (name == null && args.isNotEmpty) {
188+
final arg0 = args[0];
189+
return arg0 is IntegerLiteral ? arg0.value : null;
190+
}
191+
}
192+
193+
/// Extracts a named member from a color.
194+
///
195+
/// Well-known getters like `shade500` will be mapped onto the swatch value
196+
/// with a matching index.
197+
DartObject? _getMember(DartObject target, String memberName) {
198+
final colorValue = target.getFieldFromHierarchy(memberName);
199+
if (colorValue != null) {
200+
return colorValue;
201+
}
202+
203+
// If we didn't get a value but it's a getter we know how to read from a
204+
// swatch, try that.
205+
if (memberName.startsWith('shade')) {
206+
final shadeNumber = int.tryParse(memberName.substring(5));
207+
if (shadeNumber != null) {
208+
return _getSwatchValue(target, shadeNumber);
209+
}
210+
}
211+
}
212+
213+
/// Extracts a specific shade index from a Flutter SwatchColor.
214+
DartObject? _getSwatchValue(DartObject target, int swatchValue) {
215+
final swatch = target.getFieldFromHierarchy('_swatch')?.toMapValue();
216+
if (swatch == null) return null;
217+
218+
final key = swatch.keys.firstWhereOrNull(
219+
(key) => key?.toIntValue() == swatchValue,
220+
);
221+
if (key == null) return null;
222+
223+
return swatch[key];
224+
}
225+
226+
/// Checks whether [type] is - or extends - the dart:ui Color class.
227+
bool _isColor(DartType? type) => type != null && _flutter.isColor(type);
228+
229+
/// Checks whether this elements library is dart:ui.
230+
bool _isDartUi(Element? element) => element?.library?.name == 'dart.ui';
231+
232+
/// Checks whether this elements library is Flutter Material colors.
233+
bool _isFlutterMaterial(Element? element) =>
234+
element?.library?.identifier ==
235+
'package:flutter/src/material/colors.dart';
236+
237+
/// Checks whether this elements library is Flutter Painting colors.
238+
bool _isFlutterPainting(Element? element) =>
239+
element?.library?.identifier ==
240+
'package:flutter/src/painting/colors.dart';
241+
242+
/// Tries to record a color value from [colorConst] for [expression].
243+
///
244+
/// Returns whether a valid color was found and recorded.
245+
bool _tryRecordColor(Expression expression, DartObject? colorConst) =>
246+
_tryRecordColorValue(expression, _colorValueForColorConst(colorConst));
247+
248+
/// Tries to record the [colorValue] for [expression].
249+
///
250+
/// Returns whether a valid color was found and recorded.
251+
bool _tryRecordColorValue(Expression expression, int? colorValue) {
252+
if (colorValue == null) return false;
253+
254+
// Build color information from the Color value.
255+
final color = _colorInformationForColorValue(colorValue);
256+
257+
// Record the color against the original entire expression.
258+
_colors.add(ColorReference(expression.offset, expression.length, color));
259+
return true;
260+
}
261+
}
262+
263+
/// Information about a color that is present in a document.
264+
class ColorInformation {
265+
final int alpha;
266+
final int red;
267+
final int green;
268+
final int blue;
269+
270+
ColorInformation(this.alpha, this.red, this.green, this.blue);
271+
}
272+
273+
/// Information about a specific known location of a [ColorInformation]
274+
/// reference in a document.
275+
class ColorReference {
276+
final int offset;
277+
final int length;
278+
final ColorInformation color;
279+
280+
ColorReference(this.offset, this.length, this.color);
281+
}
282+
283+
class _ColorBuilder extends RecursiveAstVisitor<void> {
284+
final ColorComputer computer;
285+
286+
_ColorBuilder(this.computer);
287+
288+
@override
289+
void visitIndexExpression(IndexExpression node) {
290+
// Colors.redAccent[500].
291+
final index = node.index;
292+
final indexValue = index is IntegerLiteral ? index.value : null;
293+
if (indexValue != null) {
294+
if (computer.tryAddColor(
295+
node,
296+
target: node.realTarget,
297+
index: indexValue,
298+
)) {
299+
return;
300+
}
301+
}
302+
super.visitIndexExpression(node);
303+
}
304+
305+
@override
306+
void visitInstanceCreationExpression(InstanceCreationExpression node) {
307+
// Usually we return after finding a color, but constructors can
308+
// have nested colors in their arguments so we walk all the way down.
309+
if (!computer.tryAddColor(node)) {
310+
// If we couldn't evaluate the constant, try the well-known color
311+
// constructors for dart:ui/Flutter.
312+
computer.tryAddKnownColorConstructor(node);
313+
}
314+
315+
super.visitInstanceCreationExpression(node);
316+
}
317+
318+
@override
319+
void visitPrefixedIdentifier(PrefixedIdentifier node) {
320+
// Try the whole node as a constant (eg. `MyThemeClass.staticField`).
321+
if (computer.tryAddColor(node)) {
322+
return;
323+
}
324+
325+
// Try a field of a static, (eg. `const MyThemeClass().instanceField`).
326+
if (computer.tryAddColor(
327+
node,
328+
target: node.prefix,
329+
memberName: node.identifier.name,
330+
)) {
331+
return;
332+
}
333+
334+
super.visitPrefixedIdentifier(node);
335+
}
336+
337+
@override
338+
void visitPropertyAccess(PropertyAccess node) {
339+
// Handle things like CupterinoColors.activeBlue.darkColor where we can't
340+
// evaluate the whole expression, but can evaluate CupterinoColors.activeBlue
341+
// and read the darkColor.
342+
if (computer.tryAddColor(
343+
node,
344+
target: node.realTarget,
345+
memberName: node.propertyName.name,
346+
)) {
347+
return;
348+
}
349+
350+
super.visitPropertyAccess(node);
351+
}
352+
}
353+
354+
extension _DartObjectExtensions on DartObject {
355+
/// Reads the value of the field [field] from this object.
356+
///
357+
/// If the field is not found, recurses up the super classes.
358+
DartObject? getFieldFromHierarchy(String fieldName) =>
359+
getField(fieldName) ??
360+
getField(GenericState.SUPERCLASS_FIELD)?.getFieldFromHierarchy(fieldName);
361+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Copyright 2021 The Chromium 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+
5+
export 'src/cupertino/colors.dart';

pkg/analysis_server/test/mock_packages/flutter/lib/material.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// found in the LICENSE file.
44

55
export 'src/material/app_bar.dart';
6+
export 'src/material/colors.dart';
67
export 'src/material/icons.dart';
78
export 'src/material/scaffold.dart';
89
export 'widgets.dart';

pkg/analysis_server/test/mock_packages/flutter/lib/painting.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
export 'src/painting/alignment.dart';
66
export 'src/painting/basic_types.dart';
77
export 'src/painting/box_decoration.dart';
8+
export 'src/painting/colors.dart';
89
export 'src/painting/decoration.dart';
910
export 'src/painting/edge_insets.dart';
1011
export 'src/painting/text_painter.dart';

0 commit comments

Comments
 (0)