|
| 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 | +} |
0 commit comments