blob: 1026b0a941aac3313f35286629dae38a14b442a0 [file] [log] [blame]
// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
// for details. 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:analysis_server/src/utilities/flutter.dart';
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/constant/value.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/src/dart/constant/value.dart' show GenericState;
import 'package:analyzer/src/dart/element/inheritance_manager3.dart';
import 'package:analyzer/src/dart/element/type_system.dart';
import 'package:analyzer/src/lint/linter.dart';
import 'package:collection/collection.dart';
/// Computer for dart:ui/Flutter Color references.
class ColorComputer {
final ResolvedUnitResult resolvedUnit;
final LinterContext _linterContext;
final List<ColorReference> _colors = [];
final Flutter _flutter = Flutter.instance;
ColorComputer(this.resolvedUnit)
: _linterContext = LinterContextImpl(
[], // unused
LinterContextUnit(resolvedUnit.content, resolvedUnit.unit),
resolvedUnit.session.declaredVariables,
resolvedUnit.typeProvider,
resolvedUnit.typeSystem as TypeSystemImpl,
InheritanceManager3(), // unused
resolvedUnit.session.analysisContext.analysisOptions,
null,
);
/// Returns information about the color references in [resolvedUnit].
///
/// This method should only be called once for any instance of this class.
List<ColorReference> compute() {
final visitor = _ColorBuilder(this);
resolvedUnit.unit.accept(visitor);
return _colors;
}
/// Tries to add a color for the [expression].
///
/// If [target] is supplied, will be used instead of [expression] allowing
/// a value to be read from the member [memberName] or from a swatch value
/// with index [index].
bool tryAddColor(
Expression expression, {
Expression? target,
String? memberName,
int? index,
}) {
if (!_isColor(expression.staticType)) return false;
target ??= expression;
// Try to evaluate the constant target.
final colorConstResult = _linterContext.evaluateConstant(target);
var colorConst = colorConstResult.value;
if (colorConstResult.errors.isNotEmpty || colorConst == null) return false;
// If we want a specific member or swatch index, read that.
if (memberName != null) {
colorConst = _getMember(colorConst, memberName);
} else if (index != null) {
colorConst = _getSwatchValue(colorConst, index);
}
return _tryRecordColor(expression, colorConst);
}
/// Tries to add a color for the instance creation [expression].
///
/// This handles constructor calls that cannot be evaluated (for example
/// because they are not const) but are simple well-known dart:ui/Flutter
/// color constructors that we can manually parse.
bool tryAddKnownColorConstructor(InstanceCreationExpression expression) {
if (!_isColor(expression.staticType)) return false;
final constructor = expression.constructorName;
final staticElement = constructor.staticElement;
final classElement = staticElement?.enclosingElement;
final className = classElement?.name;
final constructorName = constructor.name?.name;
final constructorArgs = expression.argumentList.arguments
.map((e) => e is Literal ? e : null)
.toList();
int? colorValue;
if (_isDartUi(classElement) && className == 'Color') {
colorValue = _getDartUiColorValue(constructorName, constructorArgs);
} else if (_isFlutterPainting(classElement) && className == 'ColorSwatch') {
colorValue =
_getFlutterSwatchColorValue(constructorName, constructorArgs);
} else if (_isFlutterMaterial(classElement) &&
className == 'MaterialAccentColor') {
colorValue =
_getFlutterMaterialAccentColorValue(constructorName, constructorArgs);
}
return _tryRecordColorValue(expression, colorValue);
}
/// Creates a [ColorInformation] by extracting the argb values from
/// [value] encoded as 0xAARRGGBB as in the dart:ui Color class.
ColorInformation _colorInformationForColorValue(int value) {
// Extract color information according to dart:ui Color values.
final alpha = (0xff000000 & value) >> 24;
final red = (0x00ff0000 & value) >> 16;
final blue = (0x000000ff & value) >> 0;
final green = (0x0000ff00 & value) >> 8;
return ColorInformation(alpha, red, green, blue);
}
/// Extracts the integer color value from the dart:ui Color constant [color].
int? _colorValueForColorConst(DartObject? color) {
if (color == null || color.isNull) return null;
// If the object has a "color" field, walk down to that, because some colors
// like CupertinoColors have a "value=0" with an overridden getter that
// would always result in a value representing black.
color = color.getFieldFromHierarchy('color') ?? color;
return color.getFieldFromHierarchy('value')?.toIntValue();
}
/// Converts ARGB values into a single int value as 0xAARRGGBB as used by
/// the dart:ui Color class.
int _colorValueForComponents(int alpha, int red, int green, int blue) {
return (alpha << 24) | (red << 16) | (green << 8) | (blue << 0);
}
/// Extracts the color value from dart:ui Color constructor args.
int? _getDartUiColorValue(String? name, List<Literal?> args) {
if (name == null && args.length == 1) {
final arg0 = args[0];
return arg0 is IntegerLiteral ? arg0.value : null;
} else if (name == 'fromARGB' && args.length == 4) {
final arg0 = args[0];
final arg1 = args[1];
final arg2 = args[2];
final arg3 = args[3];
final alpha = arg0 is IntegerLiteral ? arg0.value : null;
final red = arg1 is IntegerLiteral ? arg1.value : null;
final green = arg2 is IntegerLiteral ? arg2.value : null;
final blue = arg3 is IntegerLiteral ? arg3.value : null;
return alpha != null && red != null && green != null && blue != null
? _colorValueForComponents(alpha, red, green, blue)
: null;
} else if (name == 'fromRGBO' && args.length == 4) {
final arg0 = args[0];
final arg1 = args[1];
final arg2 = args[2];
final arg3 = args[3];
final red = arg0 is IntegerLiteral ? arg0.value : null;
final green = arg1 is IntegerLiteral ? arg1.value : null;
final blue = arg2 is IntegerLiteral ? arg2.value : null;
final opacity = arg3 is IntegerLiteral
? arg3.value
: arg3 is DoubleLiteral
? arg3.value
: null;
final alpha = opacity != null ? (opacity * 255).toInt() : null;
return alpha != null && red != null && green != null && blue != null
? _colorValueForComponents(alpha, red, green, blue)
: null;
} else {
return null;
}
}
/// Extracts the color value from Flutter MaterialAccentColor constructor args.
int? _getFlutterMaterialAccentColorValue(String? name, List<Literal?> args) =>
// MaterialAccentColor is a subclass of SwatchColor and has the same
// constructor.
_getFlutterSwatchColorValue(name, args);
/// Extracts the color value from Flutter ColorSwatch constructor args.
int? _getFlutterSwatchColorValue(String? name, List<Literal?> args) {
if (name == null && args.isNotEmpty) {
final arg0 = args[0];
return arg0 is IntegerLiteral ? arg0.value : null;
} else {
return null;
}
}
/// Extracts a named member from a color.
///
/// Well-known getters like `shade500` will be mapped onto the swatch value
/// with a matching index.
DartObject? _getMember(DartObject target, String memberName) {
final colorValue = target.getFieldFromHierarchy(memberName);
if (colorValue != null) {
return colorValue;
}
// If we didn't get a value but it's a getter we know how to read from a
// swatch, try that.
if (memberName.startsWith('shade')) {
final shadeNumber = int.tryParse(memberName.substring(5));
if (shadeNumber != null) {
return _getSwatchValue(target, shadeNumber);
}
}
return null;
}
/// Extracts a specific shade index from a Flutter SwatchColor.
DartObject? _getSwatchValue(DartObject target, int swatchValue) {
final swatch = target.getFieldFromHierarchy('_swatch')?.toMapValue();
if (swatch == null) return null;
final key = swatch.keys.firstWhereOrNull(
(key) => key?.toIntValue() == swatchValue,
);
if (key == null) return null;
return swatch[key];
}
/// Checks whether [type] is - or extends - the dart:ui Color class.
bool _isColor(DartType? type) => type != null && _flutter.isColor(type);
/// Checks whether this elements library is dart:ui.
bool _isDartUi(Element? element) => element?.library?.name == 'dart.ui';
/// Checks whether this elements library is Flutter Material colors.
bool _isFlutterMaterial(Element? element) =>
element?.library?.identifier ==
'package:flutter/src/material/colors.dart';
/// Checks whether this elements library is Flutter Painting colors.
bool _isFlutterPainting(Element? element) =>
element?.library?.identifier ==
'package:flutter/src/painting/colors.dart';
/// Tries to record a color value from [colorConst] for [expression].
///
/// Returns whether a valid color was found and recorded.
bool _tryRecordColor(Expression expression, DartObject? colorConst) =>
_tryRecordColorValue(expression, _colorValueForColorConst(colorConst));
/// Tries to record the [colorValue] for [expression].
///
/// Returns whether a valid color was found and recorded.
bool _tryRecordColorValue(Expression expression, int? colorValue) {
if (colorValue == null) return false;
// Build color information from the Color value.
final color = _colorInformationForColorValue(colorValue);
// Record the color against the original entire expression.
_colors.add(ColorReference(expression.offset, expression.length, color));
return true;
}
}
/// Information about a color that is present in a document.
class ColorInformation {
final int alpha;
final int red;
final int green;
final int blue;
ColorInformation(this.alpha, this.red, this.green, this.blue);
}
/// Information about a specific known location of a [ColorInformation]
/// reference in a document.
class ColorReference {
final int offset;
final int length;
final ColorInformation color;
ColorReference(this.offset, this.length, this.color);
}
class _ColorBuilder extends RecursiveAstVisitor<void> {
final ColorComputer computer;
_ColorBuilder(this.computer);
@override
void visitIndexExpression(IndexExpression node) {
// Colors.redAccent[500].
final index = node.index;
final indexValue = index is IntegerLiteral ? index.value : null;
if (indexValue != null) {
if (computer.tryAddColor(
node,
target: node.realTarget,
index: indexValue,
)) {
return;
}
}
super.visitIndexExpression(node);
}
@override
void visitInstanceCreationExpression(InstanceCreationExpression node) {
// Usually we return after finding a color, but constructors can
// have nested colors in their arguments so we walk all the way down.
if (!computer.tryAddColor(node)) {
// If we couldn't evaluate the constant, try the well-known color
// constructors for dart:ui/Flutter.
computer.tryAddKnownColorConstructor(node);
}
super.visitInstanceCreationExpression(node);
}
@override
void visitPrefixedIdentifier(PrefixedIdentifier node) {
// Try the whole node as a constant (eg. `MyThemeClass.staticField`).
if (computer.tryAddColor(node)) {
return;
}
// Try a field of a static, (eg. `const MyThemeClass().instanceField`).
if (computer.tryAddColor(
node,
target: node.prefix,
memberName: node.identifier.name,
)) {
return;
}
super.visitPrefixedIdentifier(node);
}
@override
void visitPropertyAccess(PropertyAccess node) {
// Handle things like CupterinoColors.activeBlue.darkColor where we can't
// evaluate the whole expression, but can evaluate CupterinoColors.activeBlue
// and read the darkColor.
if (computer.tryAddColor(
node,
target: node.realTarget,
memberName: node.propertyName.name,
)) {
return;
}
super.visitPropertyAccess(node);
}
}
extension _DartObjectExtensions on DartObject {
/// Reads the value of the field [field] from this object.
///
/// If the field is not found, recurses up the super classes.
DartObject? getFieldFromHierarchy(String fieldName) =>
getField(fieldName) ??
getField(GenericState.SUPERCLASS_FIELD)?.getFieldFromHierarchy(fieldName);
}