Version 2.11.0-237.0.dev

Merge commit 'f3962367a5e1bda1f77b2102557c14833862e558' into 'dev'
diff --git a/pkg/analysis_server/lib/src/services/correction/fix/data_driven/value_generator.dart b/pkg/analysis_server/lib/src/services/correction/fix/data_driven/value_generator.dart
index 1214e8b..3bfb639 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix/data_driven/value_generator.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix/data_driven/value_generator.dart
@@ -51,6 +51,16 @@
       return node.argumentList;
     } else if (node is InstanceCreationExpression) {
       return node.argumentList;
+    } else if (node is SimpleIdentifier) {
+      var parent = node.parent;
+      if (parent is ConstructorName) {
+        var grandparent = parent.parent;
+        if (grandparent is InstanceCreationExpression) {
+          return grandparent.argumentList;
+        }
+      } else if (parent is MethodInvocation && parent.methodName == node) {
+        return parent.argumentList;
+      }
     } else if (node is TypeArgumentList) {
       var parent = node.parent;
       if (parent is InvocationExpression) {
diff --git a/pkg/analysis_server/test/src/services/correction/fix/data_driven/flutter_use_case_test.dart b/pkg/analysis_server/test/src/services/correction/fix/data_driven/flutter_use_case_test.dart
new file mode 100644
index 0000000..c26ea18
--- /dev/null
+++ b/pkg/analysis_server/test/src/services/correction/fix/data_driven/flutter_use_case_test.dart
@@ -0,0 +1,1835 @@
+// Copyright (c) 2020, 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:test_reflective_loader/test_reflective_loader.dart';
+
+import 'data_driven_test_support.dart';
+
+void main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(FlutterUseCaseTest);
+  });
+}
+
+@reflectiveTest
+class FlutterUseCaseTest extends DataDrivenFixProcessorTest {
+  @failingTest
+  Future<void>
+      test_cupertino_CupertinoDialog_toCupertinoAlertDialog_deprecated() async {
+    // This test fails because we don't rename the parameter to the constructor.
+    setPackageContent('''
+@deprecated
+class CupertinoDialog {
+  CupertinoDialog({String child}) {}
+}
+class CupertinoAlertDialog {
+  CupertinoAlertDialog({String content}) {}
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title:  'Replace with CupertinoAlertDialog'
+    date: 2020-09-24
+    bulkApply: false
+    element:
+      uris: ['$importUri']
+      class: 'CupertinoDialog'
+    changes:
+      - kind: 'rename'
+        newName: 'CupertinoAlertDialog'
+      - kind: 'renameParameter'
+        oldName: 'child'
+        newName: 'content'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f() {
+  CupertinoDialog(child: 'x');
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f() {
+  CupertinoAlertDialog(content: 'x');
+}
+''');
+  }
+
+  @failingTest
+  Future<void>
+      test_cupertino_CupertinoDialog_toCupertinoAlertDialog_removed() async {
+    // This test fails because we don't rename the parameter to the constructor.
+    setPackageContent('''
+class CupertinoAlertDialog {
+  CupertinoAlertDialog({String content}) {}
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title:  'Replace with CupertinoAlertDialog'
+    date: 2020-09-24
+    bulkApply: false
+    element:
+      uris: ['$importUri']
+      class: 'CupertinoDialog'
+    changes:
+      - kind: 'rename'
+        newName: 'CupertinoAlertDialog'
+      - kind: 'renameParameter'
+        oldName: 'child'
+        newName: 'content'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f() {
+  CupertinoDialog(child: 'x');
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f() {
+  CupertinoAlertDialog(content: 'x');
+}
+''', errorFilter: ignoreUnusedImport);
+  }
+
+  Future<void>
+      test_cupertino_CupertinoDialog_toCupertinoPopupSurface_deprecated() async {
+    setPackageContent('''
+@deprecated
+class CupertinoDialog {
+  CupertinoDialog({String child}) {}
+}
+class CupertinoPopupSurface {
+  CupertinoPopupSurface({String content}) {}
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Replace with CupertinoPopupSurface'
+    date: 2020-09-24
+    bulkApply: false
+    element:
+      uris: ['$importUri']
+      class: 'CupertinoDialog'
+    changes:
+      - kind: 'rename'
+        newName: 'CupertinoPopupSurface'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f() {
+  CupertinoDialog(child: 'x');
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f() {
+  CupertinoPopupSurface(child: 'x');
+}
+''');
+  }
+
+  Future<void>
+      test_cupertino_CupertinoDialog_toCupertinoPopupSurface_removed() async {
+    setPackageContent('''
+class CupertinoPopupSurface {
+  CupertinoPopupSurface({String content}) {}
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Replace with CupertinoPopupSurface'
+    date: 2020-09-24
+    bulkApply: false
+    element:
+      uris: ['$importUri']
+      class: 'CupertinoDialog'
+    changes:
+      - kind: 'rename'
+        newName: 'CupertinoPopupSurface'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f() {
+  CupertinoDialog(child: 'x');
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f() {
+  CupertinoPopupSurface(child: 'x');
+}
+''', errorFilter: ignoreUnusedImport);
+  }
+
+  Future<void>
+      test_cupertino_CupertinoTextThemeData_copyWith_deprecated() async {
+    setPackageContent('''
+class CupertinoTextThemeData {
+  copyWith({Color color, @deprecated Brightness brightness}) {}
+}
+class Color {}
+class Colors {
+  static Color blue = Color();
+}
+class Brightness {
+  static Brightness dark = Brightness();
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Removed brightness'
+    date: 2020-09-24
+    element:
+      uris: ['$importUri']
+      method: 'copyWith'
+      inClass: 'CupertinoTextThemeData'
+    changes:
+      - kind: 'removeParameter'
+        name: 'brightness'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(CupertinoTextThemeData data) {
+  data.copyWith(color: Colors.blue, brightness: Brightness.dark);
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(CupertinoTextThemeData data) {
+  data.copyWith(color: Colors.blue);
+}
+''');
+  }
+
+  Future<void> test_cupertino_CupertinoTextThemeData_copyWith_removed() async {
+    setPackageContent('''
+class CupertinoTextThemeData {
+  copyWith({Color color}) {}
+}
+class Color {}
+class Colors {
+  static Color blue = Color();
+}
+class Brightness {
+  static Brightness dark = Brightness();
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Removed brightness'
+    date: 2020-09-24
+    element:
+      uris: ['$importUri']
+      method: 'copyWith'
+      inClass: 'CupertinoTextThemeData'
+    changes:
+      - kind: 'removeParameter'
+        name: 'brightness'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(CupertinoTextThemeData data) {
+  data.copyWith(color: Colors.blue, brightness: Brightness.dark);
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(CupertinoTextThemeData data) {
+  data.copyWith(color: Colors.blue);
+}
+''');
+  }
+
+  Future<void>
+      test_cupertino_CupertinoTextThemeData_defaultConstructor_deprecated() async {
+    setPackageContent('''
+class CupertinoTextThemeData {
+  CupertinoTextThemeData({Color color, @deprecated Brightness brightness}) {}
+}
+class Color {}
+class Colors {
+  static Color blue = Color();
+}
+class Brightness {
+  static Brightness dark = Brightness();
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Removed brightness'
+    date: 2020-09-24
+    element:
+      uris: ['$importUri']
+      constructor: ''
+      inClass: 'CupertinoTextThemeData'
+    changes:
+      - kind: 'removeParameter'
+        name: 'brightness'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f() {
+  CupertinoTextThemeData(color: Colors.blue, brightness: Brightness.dark);
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f() {
+  CupertinoTextThemeData(color: Colors.blue);
+}
+''');
+  }
+
+  Future<void>
+      test_cupertino_CupertinoTextThemeData_defaultConstructor_removed() async {
+    setPackageContent('''
+class CupertinoTextThemeData {
+  CupertinoTextThemeData({Color color}) {}
+}
+class Color {}
+class Colors {
+  static Color blue = Color();
+}
+class Brightness {
+  static Brightness dark = Brightness();
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Removed brightness'
+    date: 2020-09-24
+    element:
+      uris: ['$importUri']
+      constructor: ''
+      inClass: 'CupertinoTextThemeData'
+    changes:
+      - kind: 'removeParameter'
+        name: 'brightness'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f() {
+  CupertinoTextThemeData(color: Colors.blue, brightness: Brightness.dark);
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f() {
+  CupertinoTextThemeData(color: Colors.blue);
+}
+''');
+  }
+
+  Future<void>
+      test_gestures_PointerEnterEvent_fromHoverEvent_deprecated() async {
+    setPackageContent('''
+class PointerEnterEvent {
+  @deprecated
+  PointerEnterEvent.fromHoverEvent(PointerHoverEvent event);
+  PointerEnterEvent.fromMouseEvent(PointerEvent event);
+}
+class PointerHoverEvent extends PointerEvent {}
+class PointerEvent {}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to fromMouseEvent'
+    date: 2020-09-24
+    element:
+      uris: ['$importUri']
+      constructor: 'fromHoverEvent'
+      inClass: 'PointerEnterEvent'
+    changes:
+      - kind: 'rename'
+        newName: 'fromMouseEvent'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(PointerHoverEvent event) {
+  PointerEnterEvent.fromHoverEvent(event);
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(PointerHoverEvent event) {
+  PointerEnterEvent.fromMouseEvent(event);
+}
+''');
+  }
+
+  Future<void> test_gestures_PointerEnterEvent_fromHoverEvent_removed() async {
+    setPackageContent('''
+class PointerEnterEvent {
+  PointerEnterEvent.fromMouseEvent(PointerEvent event);
+}
+class PointerHoverEvent extends PointerEvent {}
+class PointerEvent {}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to fromMouseEvent'
+    date: 2020-09-24
+    element:
+      uris: ['$importUri']
+      constructor: 'fromHoverEvent'
+      inClass: 'PointerEnterEvent'
+    changes:
+      - kind: 'rename'
+        newName: 'fromMouseEvent'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(PointerHoverEvent event) {
+  PointerEnterEvent.fromHoverEvent(event);
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(PointerHoverEvent event) {
+  PointerEnterEvent.fromMouseEvent(event);
+}
+''');
+  }
+
+  Future<void>
+      test_gestures_PointerExitEvent_fromHoverEvent_deprecated() async {
+    setPackageContent('''
+class PointerExitEvent {
+  @deprecated
+  PointerExitEvent.fromHoverEvent(PointerHoverEvent event);
+  PointerExitEvent.fromMouseEvent(PointerEvent event);
+}
+class PointerHoverEvent extends PointerEvent {}
+class PointerEvent {}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to fromMouseEvent'
+    date: 2020-09-24
+    element:
+      uris: ['$importUri']
+      constructor: 'fromHoverEvent'
+      inClass: 'PointerExitEvent'
+    changes:
+      - kind: 'rename'
+        newName: 'fromMouseEvent'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(PointerHoverEvent event) {
+  PointerExitEvent.fromHoverEvent(event);
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(PointerHoverEvent event) {
+  PointerExitEvent.fromMouseEvent(event);
+}
+''');
+  }
+
+  Future<void> test_gestures_PointerExitEvent_fromHoverEvent_removed() async {
+    setPackageContent('''
+class PointerExitEvent {
+  PointerExitEvent.fromMouseEvent(PointerEvent event);
+}
+class PointerHoverEvent extends PointerEvent {}
+class PointerEvent {}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to fromMouseEvent'
+    date: 2020-09-24
+    element:
+      uris: ['$importUri']
+      constructor: 'fromHoverEvent'
+      inClass: 'PointerExitEvent'
+    changes:
+      - kind: 'rename'
+        newName: 'fromMouseEvent'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(PointerHoverEvent event) {
+  PointerExitEvent.fromHoverEvent(event);
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(PointerHoverEvent event) {
+  PointerExitEvent.fromMouseEvent(event);
+}
+''');
+  }
+
+  Future<void>
+      test_material_Scaffold_resizeToAvoidBottomPadding_deprecated() async {
+    setPackageContent('''
+class Scaffold {
+  @deprecated
+  bool resizeToAvoidBottomPadding;
+  bool resizeToAvoidBottomInset;
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to resizeToAvoidBottomInset'
+    date: 2020-09-14
+    element:
+      uris: ['$importUri']
+      field: 'resizeToAvoidBottomPadding'
+      inClass: 'Scaffold'
+    changes:
+      - kind: 'rename'
+        newName: 'resizeToAvoidBottomInset'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(Scaffold scaffold) {
+  scaffold.resizeToAvoidBottomPadding;
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(Scaffold scaffold) {
+  scaffold.resizeToAvoidBottomInset;
+}
+''');
+  }
+
+  Future<void>
+      test_material_Scaffold_resizeToAvoidBottomPadding_removed() async {
+    setPackageContent('''
+class Scaffold {
+  bool resizeToAvoidBottomInset;
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to resizeToAvoidBottomInset'
+    date: 2020-09-14
+    element:
+      uris: ['$importUri']
+      field: 'resizeToAvoidBottomPadding'
+      inClass: 'Scaffold'
+    changes:
+      - kind: 'rename'
+        newName: 'resizeToAvoidBottomInset'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(Scaffold scaffold) {
+  scaffold.resizeToAvoidBottomPadding;
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(Scaffold scaffold) {
+  scaffold.resizeToAvoidBottomInset;
+}
+''');
+  }
+
+  @failingTest
+  Future<void> test_material_showDialog_deprecated() async {
+    // This test is failing because there is no way to specify that if a `child`
+    // was previously provided that a `builder` should be provided.
+    setPackageContent('''
+void showDialog({
+  @deprecated Widget child,
+  Widget Function(BuildContext) builder}) {}
+
+class Widget {}
+class BuildContext {}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Replace child with builder'
+    date: 2020-09-24
+    element:
+      uris: ['$importUri']
+      function: 'showDialog'
+    changes:
+      - kind: 'addParameter'
+        index: 0
+        name: 'builder'
+        style: optional_named
+        argumentValue:
+          expression: '(context) => {% widget %}'
+          variables:
+            widget:
+              kind: argument
+              name: 'child'
+      - kind: 'removeParameter'
+        name: 'child'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(Widget widget) {
+  showDialog(child: widget);
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(Widget widget) {
+  showDialog(builder: (context) => widget);
+}
+''');
+  }
+
+  @failingTest
+  Future<void> test_material_showDialog_removed() async {
+    // This test is failing because there is no way to specify that if a `child`
+    // was previously provided that a `builder` should be provided.
+    setPackageContent('''
+void showDialog({
+  @deprecated Widget child,
+  Widget Function(BuildContext) builder}) {}
+
+class Widget {}
+class BuildContext {}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Replace child with builder'
+    date: 2020-09-24
+    element:
+      uris: ['$importUri']
+      function: 'showDialog'
+    changes:
+      - kind: 'addParameter'
+        index: 0
+        name: 'builder'
+        style: optional_named
+        argumentValue:
+          expression: '(context) => {% widget %}'
+          variables:
+            widget:
+              kind: argument
+              name: 'child'
+      - kind: 'removeParameter'
+        name: 'child'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(Widget widget) {
+  showDialog(child: widget);
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(Widget widget) {
+  showDialog(builder: (context) => widget);
+}
+''');
+  }
+
+  Future<void> test_material_TextTheme_display4_deprecated() async {
+    setPackageContent('''
+class TextTheme {
+  @deprecated
+  int get display4 => 0;
+  int get headline1 => 0;
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to headline1'
+    date: 2020-09-24
+    element:
+      uris: ['$importUri']
+      getter: display4
+      inClass: 'TextTheme'
+    changes:
+      - kind: 'rename'
+        newName: 'headline1'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(TextTheme theme) {
+  theme.display4;
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(TextTheme theme) {
+  theme.headline1;
+}
+''');
+  }
+
+  Future<void> test_material_TextTheme_display4_removed() async {
+    setPackageContent('''
+class TextTheme {
+  int get headline1 => 0;
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to headline1'
+    date: 2020-09-24
+    element:
+      uris: ['$importUri']
+      getter: display4
+      inClass: 'TextTheme'
+    changes:
+      - kind: 'rename'
+        newName: 'headline1'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(TextTheme theme) {
+  theme.display4;
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(TextTheme theme) {
+  theme.headline1;
+}
+''');
+  }
+
+  Future<void> test_material_Typography_defaultConstructor_deprecated() async {
+    setPackageContent('''
+class Typography {
+  @deprecated
+  Typography();
+  Typography.material2014();
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Use Typography.material2014'
+    date: 2020-09-24
+    element:
+      uris: ['$importUri']
+      constructor: ''
+      inClass: 'Typography'
+    changes:
+      - kind: 'rename'
+        newName: 'material2014'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f() {
+  Typography();
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f() {
+  Typography.material2014();
+}
+''');
+  }
+
+  Future<void> test_material_Typography_defaultConstructor_removed() async {
+    setPackageContent('''
+class Typography {
+  Typography.material2014();
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Use Typography.material2014'
+    date: 2020-09-24
+    element:
+      uris: ['$importUri']
+      constructor: ''
+      inClass: 'Typography'
+    changes:
+      - kind: 'rename'
+        newName: 'material2014'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f() {
+  Typography();
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f() {
+  Typography.material2014();
+}
+''');
+  }
+
+  Future<void>
+      test_widgets_BuildContext_ancestorInheritedElementForWidgetOfExactType_deprecated() async {
+    setPackageContent('''
+class BuildContext {
+  @deprecated
+  void ancestorInheritedElementForWidgetOfExactType(Type t) {}
+  void getElementForInheritedWidgetOfExactType<T>() {}
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to getElementForInheritedWidgetOfExactType'
+    date: 2020-09-14
+    element:
+      uris: ['$importUri']
+      method: 'ancestorInheritedElementForWidgetOfExactType'
+      inClass: 'BuildContext'
+    changes:
+      - kind: 'rename'
+        newName: 'getElementForInheritedWidgetOfExactType'
+      - kind: 'addTypeParameter'
+        index: 0
+        name: 'T'
+        argumentValue:
+          expression: '{% type %}'
+          variables:
+            type:
+              kind: 'argument'
+              index: 0
+      - kind: 'removeParameter'
+        index: 0
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(BuildContext context) {
+  context.ancestorInheritedElementForWidgetOfExactType(String);
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(BuildContext context) {
+  context.getElementForInheritedWidgetOfExactType<String>();
+}
+''');
+  }
+
+  Future<void>
+      test_widgets_BuildContext_ancestorInheritedElementForWidgetOfExactType_removed() async {
+    setPackageContent('''
+class BuildContext {
+  void getElementForInheritedWidgetOfExactType<T>() {}
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to getElementForInheritedWidgetOfExactType'
+    date: 2020-09-14
+    element:
+      uris: ['$importUri']
+      method: 'ancestorInheritedElementForWidgetOfExactType'
+      inClass: 'BuildContext'
+    changes:
+      - kind: 'rename'
+        newName: 'getElementForInheritedWidgetOfExactType'
+      - kind: 'addTypeParameter'
+        index: 0
+        name: 'T'
+        argumentValue:
+          expression: '{% type %}'
+          variables:
+            type:
+              kind: 'argument'
+              index: 0
+      - kind: 'removeParameter'
+        index: 0
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(BuildContext context) {
+  context.ancestorInheritedElementForWidgetOfExactType(String);
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(BuildContext context) {
+  context.getElementForInheritedWidgetOfExactType<String>();
+}
+''');
+  }
+
+  Future<void>
+      test_widgets_BuildContext_ancestorWidgetOfExactType_deprecated() async {
+    setPackageContent('''
+class BuildContext {
+  @deprecated
+  void ancestorWidgetOfExactType(Type t) {}
+  void findAncestorWidgetOfExactType<T>() {}
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to getElementForInheritedWidgetOfExactType'
+    date: 2020-09-14
+    element:
+      uris: ['$importUri']
+      method: 'ancestorWidgetOfExactType'
+      inClass: 'BuildContext'
+    changes:
+      - kind: 'rename'
+        newName: 'findAncestorWidgetOfExactType'
+      - kind: 'addTypeParameter'
+        index: 0
+        name: 'T'
+        argumentValue:
+          expression: '{% type %}'
+          variables:
+            type:
+              kind: 'argument'
+              index: 0
+      - kind: 'removeParameter'
+        index: 0
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(BuildContext context) {
+  context.ancestorWidgetOfExactType(String);
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(BuildContext context) {
+  context.findAncestorWidgetOfExactType<String>();
+}
+''');
+  }
+
+  Future<void>
+      test_widgets_BuildContext_ancestorWidgetOfExactType_removed() async {
+    setPackageContent('''
+class BuildContext {
+  void findAncestorWidgetOfExactType<T>() {}
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to getElementForInheritedWidgetOfExactType'
+    date: 2020-09-14
+    element:
+      uris: ['$importUri']
+      method: 'ancestorWidgetOfExactType'
+      inClass: 'BuildContext'
+    changes:
+      - kind: 'rename'
+        newName: 'findAncestorWidgetOfExactType'
+      - kind: 'addTypeParameter'
+        index: 0
+        name: 'T'
+        argumentValue:
+          expression: '{% type %}'
+          variables:
+            type:
+              kind: 'argument'
+              index: 0
+      - kind: 'removeParameter'
+        index: 0
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(BuildContext context) {
+  context.ancestorWidgetOfExactType(String);
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(BuildContext context) {
+  context.findAncestorWidgetOfExactType<String>();
+}
+''');
+  }
+
+  Future<void> test_widgets_BuildContext_inheritFromElement_deprecated() async {
+    setPackageContent('''
+class BuildContext {
+  @deprecated
+  void inheritFromElement() {}
+  void dependOnInheritedElement() {}
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to dependOnInheritedElement'
+    date: 2020-09-24
+    element:
+      uris: ['$importUri']
+      method: 'inheritFromElement'
+      inClass: 'BuildContext'
+    changes:
+      - kind: 'rename'
+        newName: 'dependOnInheritedElement'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(BuildContext context) {
+  context.inheritFromElement();
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(BuildContext context) {
+  context.dependOnInheritedElement();
+}
+''');
+  }
+
+  Future<void> test_widgets_BuildContext_inheritFromElement_removed() async {
+    setPackageContent('''
+class BuildContext {
+  void dependOnInheritedElement() {}
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to dependOnInheritedElement'
+    date: 2020-09-24
+    element:
+      uris: ['$importUri']
+      method: 'inheritFromElement'
+      inClass: 'BuildContext'
+    changes:
+      - kind: 'rename'
+        newName: 'dependOnInheritedElement'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(BuildContext context) {
+  context.inheritFromElement();
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(BuildContext context) {
+  context.dependOnInheritedElement();
+}
+''');
+  }
+
+  Future<void>
+      test_widgets_BuildContext_inheritFromWidgetOfExactType_deprecated() async {
+    setPackageContent('''
+class BuildContext {
+  @deprecated
+  void inheritFromWidgetOfExactType(Type t) {}
+  void dependOnInheritedWidgetOfExactType<T>() {}
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to dependOnInheritedWidgetOfExactType'
+    date: 2020-09-14
+    element:
+      uris: ['$importUri']
+      method: 'inheritFromWidgetOfExactType'
+      inClass: 'BuildContext'
+    changes:
+      - kind: 'rename'
+        newName: 'dependOnInheritedWidgetOfExactType'
+      - kind: 'addTypeParameter'
+        index: 0
+        name: 'T'
+        argumentValue:
+          expression: '{% type %}'
+          variables:
+            type:
+              kind: 'argument'
+              index: 0
+      - kind: 'removeParameter'
+        index: 0
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(BuildContext context) {
+  context.inheritFromWidgetOfExactType(String);
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(BuildContext context) {
+  context.dependOnInheritedWidgetOfExactType<String>();
+}
+''');
+  }
+
+  Future<void>
+      test_widgets_BuildContext_inheritFromWidgetOfExactType_removed() async {
+    setPackageContent('''
+class BuildContext {
+  void dependOnInheritedWidgetOfExactType<T>() {}
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to dependOnInheritedWidgetOfExactType'
+    date: 2020-09-14
+    element:
+      uris: ['$importUri']
+      method: 'inheritFromWidgetOfExactType'
+      inClass: 'BuildContext'
+    changes:
+      - kind: 'rename'
+        newName: 'dependOnInheritedWidgetOfExactType'
+      - kind: 'addTypeParameter'
+        index: 0
+        name: 'T'
+        argumentValue:
+          expression: '{% type %}'
+          variables:
+            type:
+              kind: 'argument'
+              index: 0
+      - kind: 'removeParameter'
+        index: 0
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(BuildContext context) {
+  context.inheritFromWidgetOfExactType(String);
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(BuildContext context) {
+  context.dependOnInheritedWidgetOfExactType<String>();
+}
+''');
+  }
+
+  Future<void>
+      test_widgets_Element_ancestorInheritedElementForWidgetOfExactType_deprecated() async {
+    setPackageContent('''
+class Element {
+  @deprecated
+  void ancestorInheritedElementForWidgetOfExactType(Type t) {}
+  void getElementForInheritedWidgetOfExactType<T>() {}
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to getElementForInheritedWidgetOfExactType'
+    date: 2020-09-14
+    element:
+      uris: ['$importUri']
+      method: 'ancestorInheritedElementForWidgetOfExactType'
+      inClass: 'Element'
+    changes:
+      - kind: 'rename'
+        newName: 'getElementForInheritedWidgetOfExactType'
+      - kind: 'addTypeParameter'
+        index: 0
+        name: 'T'
+        argumentValue:
+          expression: '{% type %}'
+          variables:
+            type:
+              kind: 'argument'
+              index: 0
+      - kind: 'removeParameter'
+        index: 0
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(Element element) {
+  element.ancestorInheritedElementForWidgetOfExactType(String);
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(Element element) {
+  element.getElementForInheritedWidgetOfExactType<String>();
+}
+''');
+  }
+
+  Future<void>
+      test_widgets_Element_ancestorInheritedElementForWidgetOfExactType_removed() async {
+    setPackageContent('''
+class Element {
+  void getElementForInheritedWidgetOfExactType<T>() {}
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to getElementForInheritedWidgetOfExactType'
+    date: 2020-09-14
+    element:
+      uris: ['$importUri']
+      method: 'ancestorInheritedElementForWidgetOfExactType'
+      inClass: 'Element'
+    changes:
+      - kind: 'rename'
+        newName: 'getElementForInheritedWidgetOfExactType'
+      - kind: 'addTypeParameter'
+        index: 0
+        name: 'T'
+        argumentValue:
+          expression: '{% type %}'
+          variables:
+            type:
+              kind: 'argument'
+              index: 0
+      - kind: 'removeParameter'
+        index: 0
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(Element element) {
+  element.ancestorInheritedElementForWidgetOfExactType(String);
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(Element element) {
+  element.getElementForInheritedWidgetOfExactType<String>();
+}
+''');
+  }
+
+  Future<void>
+      test_widgets_Element_ancestorWidgetOfExactType_deprecated() async {
+    setPackageContent('''
+class Element {
+  @deprecated
+  void ancestorWidgetOfExactType(Type t) {}
+  void findAncestorWidgetOfExactType<T>() {}
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to getElementForInheritedWidgetOfExactType'
+    date: 2020-09-14
+    element:
+      uris: ['$importUri']
+      method: 'ancestorWidgetOfExactType'
+      inClass: 'Element'
+    changes:
+      - kind: 'rename'
+        newName: 'findAncestorWidgetOfExactType'
+      - kind: 'addTypeParameter'
+        index: 0
+        name: 'T'
+        argumentValue:
+          expression: '{% type %}'
+          variables:
+            type:
+              kind: 'argument'
+              index: 0
+      - kind: 'removeParameter'
+        index: 0
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(Element element) {
+  element.ancestorWidgetOfExactType(String);
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(Element element) {
+  element.findAncestorWidgetOfExactType<String>();
+}
+''');
+  }
+
+  Future<void> test_widgets_Element_ancestorWidgetOfExactType_removed() async {
+    setPackageContent('''
+class Element {
+  void findAncestorWidgetOfExactType<T>() {}
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to getElementForInheritedWidgetOfExactType'
+    date: 2020-09-14
+    element:
+      uris: ['$importUri']
+      method: 'ancestorWidgetOfExactType'
+      inClass: 'Element'
+    changes:
+      - kind: 'rename'
+        newName: 'findAncestorWidgetOfExactType'
+      - kind: 'addTypeParameter'
+        index: 0
+        name: 'T'
+        argumentValue:
+          expression: '{% type %}'
+          variables:
+            type:
+              kind: 'argument'
+              index: 0
+      - kind: 'removeParameter'
+        index: 0
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(Element element) {
+  element.ancestorWidgetOfExactType(String);
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(Element element) {
+  element.findAncestorWidgetOfExactType<String>();
+}
+''');
+  }
+
+  Future<void> test_widgets_Element_inheritFromElement_deprecated() async {
+    setPackageContent('''
+class Element {
+  @deprecated
+  void inheritFromElement() {}
+  void dependOnInheritedElement() {}
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to dependOnInheritedElement'
+    date: 2020-09-24
+    element:
+      uris: ['$importUri']
+      method: 'inheritFromElement'
+      inClass: 'Element'
+    changes:
+      - kind: 'rename'
+        newName: 'dependOnInheritedElement'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(Element element) {
+  element.inheritFromElement();
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(Element element) {
+  element.dependOnInheritedElement();
+}
+''');
+  }
+
+  Future<void> test_widgets_Element_inheritFromElement_removed() async {
+    setPackageContent('''
+class Element {
+  void dependOnInheritedElement() {}
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to dependOnInheritedElement'
+    date: 2020-09-24
+    element:
+      uris: ['$importUri']
+      method: 'inheritFromElement'
+      inClass: 'Element'
+    changes:
+      - kind: 'rename'
+        newName: 'dependOnInheritedElement'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(Element element) {
+  element.inheritFromElement();
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(Element element) {
+  element.dependOnInheritedElement();
+}
+''');
+  }
+
+  Future<void>
+      test_widgets_Element_inheritFromWidgetOfExactType_deprecated() async {
+    setPackageContent('''
+class Element {
+  @deprecated
+  void inheritFromWidgetOfExactType(Type t) {}
+  void dependOnInheritedWidgetOfExactType<T>() {}
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to dependOnInheritedWidgetOfExactType'
+    date: 2020-09-14
+    element:
+      uris: ['$importUri']
+      method: 'inheritFromWidgetOfExactType'
+      inClass: 'Element'
+    changes:
+      - kind: 'rename'
+        newName: 'dependOnInheritedWidgetOfExactType'
+      - kind: 'addTypeParameter'
+        index: 0
+        name: 'T'
+        argumentValue:
+          expression: '{% type %}'
+          variables:
+            type:
+              kind: 'argument'
+              index: 0
+      - kind: 'removeParameter'
+        index: 0
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(Element element) {
+  element.inheritFromWidgetOfExactType(String);
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(Element element) {
+  element.dependOnInheritedWidgetOfExactType<String>();
+}
+''');
+  }
+
+  Future<void>
+      test_widgets_Element_inheritFromWidgetOfExactType_removed() async {
+    setPackageContent('''
+class Element {
+  void dependOnInheritedWidgetOfExactType<T>() {}
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to dependOnInheritedWidgetOfExactType'
+    date: 2020-09-14
+    element:
+      uris: ['$importUri']
+      method: 'inheritFromWidgetOfExactType'
+      inClass: 'Element'
+    changes:
+      - kind: 'rename'
+        newName: 'dependOnInheritedWidgetOfExactType'
+      - kind: 'addTypeParameter'
+        index: 0
+        name: 'T'
+        argumentValue:
+          expression: '{% type %}'
+          variables:
+            type:
+              kind: 'argument'
+              index: 0
+      - kind: 'removeParameter'
+        index: 0
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(Element element) {
+  element.inheritFromWidgetOfExactType(String);
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(Element element) {
+  element.dependOnInheritedWidgetOfExactType<String>();
+}
+''');
+  }
+
+  Future<void>
+      test_widgets_ScrollPosition_jumpToWithoutSettling_deprecated() async {
+    setPackageContent('''
+class ScrollPosition {
+  @deprecated
+  void jumpToWithoutSettling(double d);
+  void jumpTo(double d);
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to jumpTo'
+    date: 2020-09-14
+    element:
+      uris: ['$importUri']
+      method: 'jumpToWithoutSettling'
+      inClass: 'ScrollPosition'
+    changes:
+      - kind: 'rename'
+        newName: 'jumpTo'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(ScrollPosition position) {
+  position.jumpToWithoutSettling(0.5);
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(ScrollPosition position) {
+  position.jumpTo(0.5);
+}
+''');
+  }
+
+  Future<void>
+      test_widgets_ScrollPosition_jumpToWithoutSettling_removed() async {
+    setPackageContent('''
+class ScrollPosition {
+  void jumpTo(double d);
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to jumpTo'
+    date: 2020-09-14
+    element:
+      uris: ['$importUri']
+      method: 'jumpToWithoutSettling'
+      inClass: 'ScrollPosition'
+    changes:
+      - kind: 'rename'
+        newName: 'jumpTo'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(ScrollPosition position) {
+  position.jumpToWithoutSettling(0.5);
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(ScrollPosition position) {
+  position.jumpTo(0.5);
+}
+''');
+  }
+
+  Future<void>
+      test_widgets_StatefulElement_inheritFromElement_deprecated() async {
+    setPackageContent('''
+class StatefulElement {
+  @deprecated
+  void inheritFromElement() {}
+  void dependOnInheritedElement() {}
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to dependOnInheritedElement'
+    date: 2020-09-24
+    element:
+      uris: ['$importUri']
+      method: 'inheritFromElement'
+      inClass: 'StatefulElement'
+    changes:
+      - kind: 'rename'
+        newName: 'dependOnInheritedElement'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(StatefulElement element) {
+  element.inheritFromElement();
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(StatefulElement element) {
+  element.dependOnInheritedElement();
+}
+''');
+  }
+
+  Future<void> test_widgets_StatefulElement_inheritFromElement_removed() async {
+    setPackageContent('''
+class StatefulElement {
+  void dependOnInheritedElement() {}
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to dependOnInheritedElement'
+    date: 2020-09-24
+    element:
+      uris: ['$importUri']
+      method: 'inheritFromElement'
+      inClass: 'StatefulElement'
+    changes:
+      - kind: 'rename'
+        newName: 'dependOnInheritedElement'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(StatefulElement element) {
+  element.inheritFromElement();
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(StatefulElement element) {
+  element.dependOnInheritedElement();
+}
+''');
+  }
+
+  Future<void>
+      test_widgets_WidgetsBinding_allowFirstFrameReport_deprecated() async {
+    setPackageContent('''
+class WidgetsBinding {
+  @deprecated
+  void allowFirstFrameReport() {}
+  void allowFirstFrame() {}
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to allowFirstFrame'
+    date: 2020-09-24
+    element:
+      uris: ['$importUri']
+      method: 'allowFirstFrameReport'
+      inClass: 'WidgetsBinding'
+    changes:
+      - kind: 'rename'
+        newName: 'allowFirstFrame'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(WidgetsBinding binding) {
+  binding.allowFirstFrameReport();
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(WidgetsBinding binding) {
+  binding.allowFirstFrame();
+}
+''');
+  }
+
+  Future<void>
+      test_widgets_WidgetsBinding_allowFirstFrameReport_removed() async {
+    setPackageContent('''
+class WidgetsBinding {
+  void allowFirstFrame() {}
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to allowFirstFrame'
+    date: 2020-09-24
+    element:
+      uris: ['$importUri']
+      method: 'allowFirstFrameReport'
+      inClass: 'WidgetsBinding'
+    changes:
+      - kind: 'rename'
+        newName: 'allowFirstFrame'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(WidgetsBinding binding) {
+  binding.allowFirstFrameReport();
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(WidgetsBinding binding) {
+  binding.allowFirstFrame();
+}
+''');
+  }
+
+  Future<void>
+      test_widgets_WidgetsBinding_deferFirstFrameReport_deprecated() async {
+    setPackageContent('''
+class WidgetsBinding {
+  @deprecated
+  void deferFirstFrameReport() {}
+  void deferFirstFrame() {}
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to deferFirstFrame'
+    date: 2020-09-24
+    element:
+      uris: ['$importUri']
+      method: 'deferFirstFrameReport'
+      inClass: 'WidgetsBinding'
+    changes:
+      - kind: 'rename'
+        newName: 'deferFirstFrame'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(WidgetsBinding binding) {
+  binding.deferFirstFrameReport();
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(WidgetsBinding binding) {
+  binding.deferFirstFrame();
+}
+''');
+  }
+
+  Future<void>
+      test_widgets_WidgetsBinding_deferFirstFrameReport_removed() async {
+    setPackageContent('''
+class WidgetsBinding {
+  void deferFirstFrame() {}
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  - title: 'Rename to deferFirstFrame'
+    date: 2020-09-24
+    element:
+      uris: ['$importUri']
+      method: 'deferFirstFrameReport'
+      inClass: 'WidgetsBinding'
+    changes:
+      - kind: 'rename'
+        newName: 'deferFirstFrame'
+''');
+    await resolveTestUnit('''
+import '$importUri';
+
+void f(WidgetsBinding binding) {
+  binding.deferFirstFrameReport();
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f(WidgetsBinding binding) {
+  binding.deferFirstFrame();
+}
+''');
+  }
+}
diff --git a/pkg/analysis_server/test/src/services/correction/fix/data_driven/test_all.dart b/pkg/analysis_server/test/src/services/correction/fix/data_driven/test_all.dart
index bd85ebe..5f2419e 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/data_driven/test_all.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/data_driven/test_all.dart
@@ -8,6 +8,7 @@
 import 'code_template_test.dart' as code_template;
 import 'diagnostics/test_all.dart' as diagnostics;
 import 'end_to_end_test.dart' as end_to_end;
+import 'flutter_use_case_test.dart' as flutter_use_case;
 import 'modify_parameters_test.dart' as modify_parameters;
 import 'rename_test.dart' as rename_change;
 import 'transform_set_manager_test.dart' as transform_set_manager;
@@ -19,6 +20,7 @@
     code_template.main();
     diagnostics.main();
     end_to_end.main();
+    flutter_use_case.main();
     modify_parameters.main();
     rename_change.main();
     transform_set_manager.main();
diff --git a/pkg/analyzer/CHANGELOG.md b/pkg/analyzer/CHANGELOG.md
index 13a6e34..e67947e 100644
--- a/pkg/analyzer/CHANGELOG.md
+++ b/pkg/analyzer/CHANGELOG.md
@@ -2,6 +2,8 @@
 * Deprecated `GenericTypeAliasElement`. Use `FunctionTypeAliasElement`.
 * Read imports, exports, and parts on demand in `AnalysisDriver`.
   Specifically, `parseFileSync` will not read any referenced files.
+* Types are not set anymore for classes/constructors/getters of
+  identifiers in metadata (still set in arguments).
 
 ## 0.40.4
 * Deprecated `IndexExpression.auxiliaryElements` and
diff --git a/pkg/analyzer/lib/src/dart/resolver/annotation_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/annotation_resolver.dart
index 92fef68..bd112a8 100644
--- a/pkg/analyzer/lib/src/dart/resolver/annotation_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/annotation_resolver.dart
@@ -76,7 +76,6 @@
         element = _resolver.toLegacyElement(element);
 
         identifier.staticElement = element;
-        identifier.staticType = element?.type ?? DynamicTypeImpl.instance;
         // TODO(scheglov) error?
       } else if (prefixElement is PrefixElement) {
         var resolver = PropertyElementResolver(_resolver);
@@ -108,14 +107,6 @@
 
         var element = result.readElement;
         identifier.staticElement = element;
-
-        DartType type;
-        if (element is PropertyAccessorElement && element.isGetter) {
-          type = element.returnType;
-        } else {
-          type = DynamicTypeImpl.instance;
-        }
-        identifier.staticType = type;
       }
     } else {
       var identifier = nodeName as SimpleIdentifier;
@@ -137,16 +128,6 @@
           [identifier.name],
         );
       }
-
-      DartType type;
-      if (element is ClassElement) {
-        type = _resolver.typeProvider.typeType;
-      } else if (element is PropertyAccessorElement && element.isGetter) {
-        type = element.returnType;
-      } else {
-        type = DynamicTypeImpl.instance;
-      }
-      identifier.staticType = type;
     }
 
     _resolveAnnotationElement(node);
diff --git a/pkg/analyzer/test/src/dart/analysis/driver_resolution_test.dart b/pkg/analyzer/test/src/dart/analysis/driver_resolution_test.dart
index 7f16523..e955fb1 100644
--- a/pkg/analyzer/test/src/dart/analysis/driver_resolution_test.dart
+++ b/pkg/analyzer/test/src/dart/analysis/driver_resolution_test.dart
@@ -151,7 +151,7 @@
 
       SimpleIdentifier identifier_1 = annotation.name;
       expect(identifier_1.staticElement, same(myElement.getter));
-      expect(identifier_1.staticType, typeProvider.intType);
+      expect(identifier_1.staticType, isNull);
     }
 
     {
@@ -202,7 +202,7 @@
 
     SimpleIdentifier aRef = annotation.name;
     assertElement(aRef, findElement.topGet('a'));
-    assertType(aRef, 'int');
+    assertTypeNull(aRef);
   }
 
   test_annotation_onDirective_import() async {
@@ -222,7 +222,7 @@
 
     SimpleIdentifier aRef = annotation.name;
     assertElement(aRef, findElement.topGet('a'));
-    assertType(aRef, 'int');
+    assertTypeNull(aRef);
   }
 
   test_annotation_onDirective_library() async {
@@ -242,7 +242,7 @@
 
     SimpleIdentifier aRef = annotation.name;
     assertElement(aRef, findElement.topGet('a'));
-    assertType(aRef, 'int');
+    assertTypeNull(aRef);
   }
 
   test_annotation_onDirective_part() async {
@@ -265,7 +265,7 @@
 
     SimpleIdentifier aRef = annotation.name;
     assertElement(aRef, findElement.topGet('a'));
-    assertType(aRef, 'int');
+    assertTypeNull(aRef);
   }
 
   test_annotation_onDirective_partOf() async {
@@ -288,7 +288,7 @@
 
     SimpleIdentifier aRef = annotation.name;
     assertElement(aRef, findElement.topGet('a'));
-    assertType(aRef, 'int');
+    assertTypeNull(aRef);
   }
 
   test_annotation_onFormalParameter_redirectingFactory() async {
@@ -310,7 +310,7 @@
 
       SimpleIdentifier ref = annotation.name;
       assertElement(ref, getter);
-      assertType(ref, 'int');
+      assertTypeNull(ref);
     }
 
     {
@@ -386,7 +386,7 @@
 
     SimpleIdentifier identifier_1 = annotation.name;
     expect(identifier_1.staticElement, same(myElement.getter));
-    expect(identifier_1.staticType, typeProvider.intType);
+    assertTypeNull(identifier_1);
   }
 
   test_annotation_prefixed_classField() async {
@@ -570,7 +570,7 @@
     assertTypeNull(prefixed.prefix);
 
     expect(prefixed.identifier.staticElement, same(aGetter));
-    expect(prefixed.identifier.staticType, typeProvider.intType);
+    assertTypeNull(prefixed.identifier);
 
     expect(annotation.constructorName, isNull);
     expect(annotation.arguments, isNull);
@@ -630,7 +630,7 @@
     assertTypeNull(prefixed.prefix);
 
     expect(prefixed.identifier.staticElement, same(constructor));
-    assertType(prefixed.identifier, 'A Function(int, {int b})');
+    assertTypeNull(prefixed.identifier);
 
     expect(annotation.constructorName, isNull);
 
@@ -699,14 +699,14 @@
 
     SimpleIdentifier identifier_1 = annotation_1.name;
     expect(identifier_1.staticElement, same(element_1.getter));
-    expect(identifier_1.staticType, typeProvider.intType);
+    assertTypeNull(identifier_1);
 
     Annotation annotation_2 = main.metadata[1];
     expect(annotation_2.element, same(element_2.getter));
 
     SimpleIdentifier identifier_2 = annotation_2.name;
     expect(identifier_2.staticElement, same(element_2.getter));
-    expect(identifier_2.staticType, typeProvider.intType);
+    assertTypeNull(identifier_2);
   }
 
   test_asExpression() async {
diff --git a/pkg/analyzer/test/src/dart/resolution/metadata_test.dart b/pkg/analyzer/test/src/dart/resolution/metadata_test.dart
index 0e2a55e..c325cf2 100644
--- a/pkg/analyzer/test/src/dart/resolution/metadata_test.dart
+++ b/pkg/analyzer/test/src/dart/resolution/metadata_test.dart
@@ -22,6 +22,120 @@
 
 @reflectiveTest
 class MetadataResolutionTest extends PubPackageResolutionTest {
+  test_genericClass_instanceGetter() async {
+    await resolveTestCode(r'''
+class A<T> {
+  T get foo {}
+}
+
+@A.foo
+void f() {}
+''');
+
+    _assertResolvedNodeText(findNode.annotation('@A'), r'''
+Annotation
+  element: self::@class::A::@getter::foo
+  name: PrefixedIdentifier
+    identifier: SimpleIdentifier
+      staticElement: self::@class::A::@getter::foo
+      staticType: null
+      token: foo
+    period: .
+    prefix: SimpleIdentifier
+      staticElement: self::@class::A
+      staticType: null
+      token: A
+    staticElement: self::@class::A::@getter::foo
+    staticType: null
+''');
+  }
+
+  test_genericClass_namedConstructor() async {
+    await assertNoErrorsInCode(r'''
+class A<T> {
+  const A.named();
+}
+
+@A.named()
+void f() {}
+''');
+
+    _assertResolvedNodeText(findNode.annotation('@A'), r'''
+Annotation
+  arguments: ArgumentList
+  element: ConstructorMember
+    base: self::@class::A::@constructor::named
+    substitution: {T: dynamic}
+  name: PrefixedIdentifier
+    identifier: SimpleIdentifier
+      staticElement: ConstructorMember
+        base: self::@class::A::@constructor::named
+        substitution: {T: dynamic}
+      staticType: null
+      token: named
+    period: .
+    prefix: SimpleIdentifier
+      staticElement: self::@class::A
+      staticType: null
+      token: A
+    staticElement: ConstructorMember
+      base: self::@class::A::@constructor::named
+      substitution: {T: dynamic}
+    staticType: null
+''');
+  }
+
+  test_genericClass_staticGetter() async {
+    await resolveTestCode(r'''
+class A<T> {
+  static T get foo {}
+}
+
+@A.foo
+void f() {}
+''');
+
+    _assertResolvedNodeText(findNode.annotation('@A'), r'''
+Annotation
+  element: self::@class::A::@getter::foo
+  name: PrefixedIdentifier
+    identifier: SimpleIdentifier
+      staticElement: self::@class::A::@getter::foo
+      staticType: null
+      token: foo
+    period: .
+    prefix: SimpleIdentifier
+      staticElement: self::@class::A
+      staticType: null
+      token: A
+    staticElement: self::@class::A::@getter::foo
+    staticType: null
+''');
+  }
+
+  test_genericClass_unnamedConstructor() async {
+    await assertNoErrorsInCode(r'''
+class A<T> {
+  const A();
+}
+
+@A()
+void f() {}
+''');
+
+    _assertResolvedNodeText(findNode.annotation('@A'), r'''
+Annotation
+  arguments: ArgumentList
+  element: ConstructorMember
+    base: self::@class::A::@constructor::•
+    substitution: {T: dynamic}
+  name: SimpleIdentifier
+    staticElement: self::@class::A
+    staticType: null
+    token: A
+''');
+  }
+
   test_onFieldFormal() async {
     await assertNoErrorsInCode(r'''
 class A {
@@ -55,7 +169,7 @@
   element: self::@class::A::@constructor::•
   name: SimpleIdentifier
     staticElement: self::@class::A
-    staticType: Type
+    staticType: null
     token: A
 ''');
   }
diff --git a/pkg/analyzer/test/src/summary/resynthesize_common.dart b/pkg/analyzer/test/src/summary/resynthesize_common.dart
index 863692f..a3cfc3c 100644
--- a/pkg/analyzer/test/src/summary/resynthesize_common.dart
+++ b/pkg/analyzer/test/src/summary/resynthesize_common.dart
@@ -8884,7 +8884,7 @@
         element: self::@class::C::@getter::foo
         name: SimpleIdentifier
           staticElement: self::@class::C::@getter::foo
-          staticType: int
+          staticType: null
           token: foo
 }
   metadata
@@ -8892,7 +8892,7 @@
       element: self::@getter::foo
       name: SimpleIdentifier
         staticElement: self::@getter::foo
-        staticType: int
+        staticType: null
         token: foo
   typeParameters
     T
@@ -8903,7 +8903,7 @@
           element: self::@getter::foo
           name: SimpleIdentifier
             staticElement: self::@getter::foo
-            staticType: int
+            staticType: null
             token: foo
 const int foo;
   constantInitializer
@@ -9121,7 +9121,7 @@
         element: self::@extension::E::@getter::foo
         name: SimpleIdentifier
           staticElement: self::@extension::E::@getter::foo
-          staticType: int
+          staticType: null
           token: foo
 }
   metadata
@@ -9129,7 +9129,7 @@
       element: self::@getter::foo
       name: SimpleIdentifier
         staticElement: self::@getter::foo
-        staticType: int
+        staticType: null
         token: foo
   typeParameters
     T
@@ -9140,7 +9140,7 @@
           element: self::@getter::foo
           name: SimpleIdentifier
             staticElement: self::@getter::foo
-            staticType: int
+            staticType: null
             token: foo
 const int foo;
   constantInitializer
@@ -9430,7 +9430,7 @@
         element: self::@mixin::M::@getter::foo
         name: SimpleIdentifier
           staticElement: self::@mixin::M::@getter::foo
-          staticType: int
+          staticType: null
           token: foo
 }
   metadata
@@ -9438,7 +9438,7 @@
       element: self::@getter::foo
       name: SimpleIdentifier
         staticElement: self::@getter::foo
-        staticType: int
+        staticType: null
         token: foo
   typeParameters
     T
@@ -9449,7 +9449,7 @@
           element: self::@getter::foo
           name: SimpleIdentifier
             staticElement: self::@getter::foo
-            staticType: int
+            staticType: null
             token: foo
 const int foo;
   constantInitializer
@@ -11789,7 +11789,7 @@
       element: <null>
       name: SimpleIdentifier
         staticElement: <null>
-        staticType: dynamic
+        staticType: null
         token: foo
 ''',
         withFullyResolvedAst: true);
@@ -11817,7 +11817,7 @@
       element: <null>
       name: SimpleIdentifier
         staticElement: <null>
-        staticType: dynamic
+        staticType: null
         token: v
 ''',
         withFullyResolvedAst: true);
diff --git a/pkg/dev_compiler/test/expression_compiler/expression_compiler_test.dart b/pkg/dev_compiler/test/expression_compiler/expression_compiler_test.dart
index f3e46aa..5636b5f 100644
--- a/pkg/dev_compiler/test/expression_compiler/expression_compiler_test.dart
+++ b/pkg/dev_compiler/test/expression_compiler/expression_compiler_test.dart
@@ -262,6 +262,7 @@
 
   group('Expression compiler tests in extension method:', () {
     const source = '''
+      // @dart = 2.9
       extension NumberParsing on String {
         int parseInt() {
           var ret = int.parse(this);
@@ -312,6 +313,7 @@
 
   group('Expression compiler tests in method:', () {
     const source = '''
+      // @dart = 2.9
       extension NumberParsing on String {
         int parseInt() {
           return int.parse(this);
@@ -565,6 +567,7 @@
 
   group('Expression compiler tests in method with no field access:', () {
     const source = '''
+      // @dart = 2.9
       extension NumberParsing on String {
         int parseInt() {
           return int.parse(this);
@@ -726,6 +729,7 @@
 
   group('Expression compiler tests in async method:', () {
     const source = '''
+      // @dart = 2.9
       class C {
         C(int this.field, int this._field);
 
@@ -786,6 +790,7 @@
 
   group('Expression compiler tests in global function:', () {
     const source = '''
+      // @dart = 2.9
       extension NumberParsing on String {
         int parseInt() {
           return int.parse(this);
@@ -1044,6 +1049,7 @@
 
   group('Expression compiler tests in closures:', () {
     const source = r'''
+      // @dart = 2.9
       int globalFunction() {
       int x = 15;
       var c = C(1, 2);
@@ -1108,6 +1114,7 @@
 
   group('Expression compiler tests in method with no type use', () {
     const source = '''
+      // @dart = 2.9
       abstract class Key {
         const factory Key(String value) = ValueKey;
         const Key.empty();
diff --git a/pkg/dev_compiler/test/expression_compiler/expression_compiler_worker_test.dart b/pkg/dev_compiler/test/expression_compiler/expression_compiler_worker_test.dart
index 1e649aa..65167c7 100644
--- a/pkg/dev_compiler/test/expression_compiler/expression_compiler_worker_test.dart
+++ b/pkg/dev_compiler/test/expression_compiler/expression_compiler_worker_test.dart
@@ -102,13 +102,14 @@
     File.fromUri(main)
       ..createSync(recursive: true)
       ..writeAsStringSync('''
+// @dart = 2.9
 import 'package:_testPackage/test_library.dart';
 
 var global = 0;
 
 void main() {
   var count = 0;
-  // line 7
+  // line 8
   print('Global is: \${++global}');
   print('Count is: \${++count}');
 
@@ -120,10 +121,11 @@
     File.fromUri(testLibrary)
       ..createSync()
       ..writeAsStringSync('''
+// @dart = 2.9
 import 'package:_testPackage/test_library2.dart';
 
 int testLibraryFunction(int formal) {
-  return formal; // line 4
+  return formal; // line 5
 }
 
 int callLibraryFunction2(int formal) {
@@ -139,8 +141,9 @@
     File.fromUri(testLibrary2)
       ..createSync()
       ..writeAsStringSync('''
+// @dart = 2.9
 int testLibraryFunction2(int formal) {
-  return formal; // line 2
+  return formal; // line 3
 }
 
 class C {
@@ -259,7 +262,7 @@
       requestController.add({
         'command': 'CompileExpression',
         'expression': 'formal',
-        'line': 4,
+        'line': 5,
         'column': 1,
         'jsModules': {},
         'jsScope': {'formal': 'formal'},
@@ -291,7 +294,7 @@
       requestController.add({
         'command': 'CompileExpression',
         'expression': 'count',
-        'line': 7,
+        'line': 8,
         'column': 1,
         'jsModules': {},
         'jsScope': {'count': 'count'},
@@ -324,7 +327,7 @@
       requestController.add({
         'command': 'CompileExpression',
         'expression': 'B().c().getNumber()',
-        'line': 7,
+        'line': 8,
         'column': 1,
         'jsModules': {},
         'jsScope': {},
@@ -445,7 +448,7 @@
       requestController.add({
         'command': 'CompileExpression',
         'expression': 'formal',
-        'line': 4,
+        'line': 5,
         'column': 1,
         'jsModules': {},
         'jsScope': {'formal': 'formal'},
@@ -477,7 +480,7 @@
       requestController.add({
         'command': 'CompileExpression',
         'expression': 'count',
-        'line': 7,
+        'line': 8,
         'column': 1,
         'jsModules': {},
         'jsScope': {'count': 'count'},
diff --git a/pkg/scrape/.gitignore b/pkg/scrape/.gitignore
new file mode 100644
index 0000000..37c2b44
--- /dev/null
+++ b/pkg/scrape/.gitignore
@@ -0,0 +1,6 @@
+# Files and directories created by pub
+.packages
+.dart_tool/
+
+# Remove the following pattern if you wish to check in your lock file
+pubspec.lock
diff --git a/pkg/scrape/README.md b/pkg/scrape/README.md
new file mode 100644
index 0000000..58b0cda
--- /dev/null
+++ b/pkg/scrape/README.md
@@ -0,0 +1,108 @@
+# Scrape
+
+The scrape package is sort of a micro-framework to make it easier to write
+little scripts that parse and traverse Dart code and gather statistics about the
+contents.
+
+For example, say you want to find out how many if statements in a body of Dart
+code contain else clauses. A script using this package to measure that is:
+
+```dart
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:scrape/scrape.dart';
+
+void main(List<String> arguments) {
+  Scrape()
+    ..addHistogram("If")
+    ..addVisitor(() => IfVisitor())
+    ..runCommandLine(arguments);
+}
+
+class IfVisitor extends ScrapeVisitor {
+  @override
+  void visitIfStatement(IfStatement node) {
+    if (node.elseStatement != null) {
+      record("If", "else");
+    } else {
+      record("If", "no else");
+    }
+    super.visitIfStatement(node);
+  }
+}
+```
+
+Run that script on the package itself:
+
+```
+$ dart example/if.dart .
+```
+
+And it prints out:
+
+```
+-- If (137 total) --
+    104 ( 75.912%): no else  =======================================
+     33 ( 24.088%): else     =============
+Took 262ms to scrape 1349 lines in 12 files.
+```
+
+So it looks like if statements without elses are about three times more common
+than ones with elses. We use data like this to inform how we design and evolve
+the language.
+
+## A scrape script
+
+I wanted to make scrape flexible enough to let you write whatever kinds of
+logic to analyze code. That meant that instead of scrape being a *tool you run*,
+it is more like a *library you consume*. This way, inside your script, you have
+access to the full Dart language. At the same time, I didn't want every script
+to have to copy/paste the same boring argument parsing and other code.
+
+The compromise between those is the Scrape class. It is a builder for an
+analysis over some code. It has a few methods you call on it to set up an
+analysis:
+
+*   `addHistogram()` registers a new named histogram. This is the main way you
+    count occurences of datapoints you care about in the code you are analyzing.
+    Each histogram is a named collection of datapoints. When the analysis
+    completes, scrape prints out each histogram, buckets the datapoints, and
+    shows how many of each datapoint occurred.
+
+    In the example above, we have one histogram named "If" and we count two
+    different datapoints, "no else", and "else".
+
+*   `addVisitor()` registers a callback that creates a visitor. This is the
+    main way you analyze code. When the analysis runs, scrape parses every
+    Dart file you specify. For each file and each registered visitor callback,
+    it invokes the callback to create a visitor and then runs that to walk over
+    the parsed code.
+
+    You call this passing in a callback that creates an instance of your own
+    visitor class, which should extend `ScrapeVisitor`.
+
+*   Then at the end call `runCommandLine()`, passing in your script's command
+    line arguments. This reads the file paths the user wants to analyze and
+    a few other command line options and flags that scrape automatically
+    supports. To learn more, call that with `--help`.
+
+## A visitor class
+
+The way your script analyzes code is through one or more custom subclasses of
+`ScrapeVisitor`. That base class itself extends the analyzer package's
+[`RecursiveAstVisitor`][visitor] class. It will walk over every single syntax
+tree node in the parsed Dart file and invoke visit methods specific to each one.
+You override the visit methods for the AST nodes you care about and put
+whatever logic you want in there to analyze the code.
+
+An important limitation of scrape is that it only *parses* Dart files. It does
+not to any static analysis, name resolution, or type checking. This makes it
+lightweight and fast to run (for example you can run it on a pub package
+without needing to download its dependencies), but significantly limits the
+kinds of analysis you can do.
+
+It's good for syntax and tolerable for things like API usage if you're willing
+to assume that certain names do refer to the API you think they do. If you look
+in the examples directory, you'll get a sense for what kinds of tasks scrape is
+well suited for.
+
+[visitor]: https://pub.dev/documentation/analyzer/0.40.0/dart_ast_visitor/RecursiveAstVisitor-class.html
diff --git a/pkg/scrape/analysis_options.yaml b/pkg/scrape/analysis_options.yaml
new file mode 100644
index 0000000..f6dcda64
--- /dev/null
+++ b/pkg/scrape/analysis_options.yaml
@@ -0,0 +1,5 @@
+include: package:pedantic/analysis_options.yaml
+analyzer:
+  strong-mode:
+    implicit-casts: false
+    implicit-dynamic: false
diff --git a/pkg/scrape/example/build_control_flow.dart b/pkg/scrape/example/build_control_flow.dart
new file mode 100644
index 0000000..bdd851a
--- /dev/null
+++ b/pkg/scrape/example/build_control_flow.dart
@@ -0,0 +1,119 @@
+// Copyright (c) 2020, 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:analyzer/dart/ast/ast.dart';
+
+import 'package:scrape/scrape.dart';
+
+/// Known cases where an "if" statement could instead be an "if" element inside
+/// a list or map literal. Usually this is an optional child widget in a list
+/// of children.
+final _knownCollection = {'flutter/examples/layers/widgets/styled_text.dart'};
+
+final _buildMethods = <String>[];
+
+void main(List<String> arguments) {
+  Scrape()
+    ..addHistogram('build methods')
+    ..addVisitor(() => ControlFlowVisitor())
+    ..runCommandLine(arguments);
+
+  _buildMethods.shuffle();
+  _buildMethods.take(100).forEach(print);
+}
+
+class ControlFlowVisitor extends ScrapeVisitor {
+  final List<String> _controlFlow = [];
+
+  @override
+  void beforeVisitBuildMethod(Declaration node) {
+    _controlFlow.clear();
+  }
+
+  @override
+  void afterVisitBuildMethod(Declaration node) {
+    if (_controlFlow.isNotEmpty) {
+      _buildMethods.add(nodeToString(node));
+
+      var hasIf = _controlFlow.any((s) => s.startsWith('if'));
+      var hasConditional = _controlFlow.any((s) => s.startsWith('conditional'));
+
+      if (hasIf && hasConditional) {
+        record('build methods', 'both');
+      } else if (hasIf) {
+        record('build methods', 'if');
+      } else {
+        record('build methods', 'conditional');
+      }
+    } else {
+      record('build methods', 'no control flow');
+    }
+  }
+
+  @override
+  void visitConditionalExpression(ConditionalExpression node) {
+    super.visitConditionalExpression(node);
+
+    if (!isInFlutterBuildMethod) return;
+
+    if (node.parent is NamedExpression) {
+      _controlFlow.add('conditional named arg');
+    } else if (node.parent is ArgumentList) {
+      _controlFlow.add('conditional positional arg');
+    } else if (node.parent is VariableDeclaration) {
+      _controlFlow.add('conditional variable');
+    } else if (node.parent is InterpolationExpression) {
+      _controlFlow.add('conditional interpolation');
+    } else {
+      _controlFlow.add('conditional');
+    }
+  }
+
+  @override
+  void visitIfStatement(IfStatement node) {
+    super.visitIfStatement(node);
+
+    if (!isInFlutterBuildMethod) return;
+
+    if (_isReturn(node.thenStatement) && _isReturn(node.elseStatement)) {
+      _controlFlow.add('if return');
+    } else if (_isAdd(node.thenStatement) && _isAdd(node.elseStatement)) {
+      _controlFlow.add('if add');
+    } else if (_knownCollection.contains(path)) {
+      _controlFlow.add('if collection');
+    } else {
+      _controlFlow.add('if');
+    }
+  }
+
+  bool _isReturn(Statement statement) {
+    // Ignore empty "else" clauses.
+    if (statement == null) return true;
+
+    if (statement is ReturnStatement) return true;
+
+    if (statement is Block && statement.statements.length == 1) {
+      return _isReturn(statement.statements.first);
+    }
+
+    return false;
+  }
+
+  bool _isAdd(Statement statement) {
+    // Ignore empty "else" clauses.
+    if (statement == null) return true;
+
+    if (statement is ExpressionStatement) {
+      var expr = statement.expression;
+      if (expr is MethodInvocation) {
+        if (expr.methodName.name == 'add' || expr.methodName.name == 'addAll') {
+          return true;
+        }
+      }
+    } else if (statement is Block && statement.statements.length == 1) {
+      return _isAdd(statement.statements.first);
+    }
+
+    return false;
+  }
+}
diff --git a/pkg/scrape/example/class_reuse.dart b/pkg/scrape/example/class_reuse.dart
new file mode 100644
index 0000000..8fb60c6
--- /dev/null
+++ b/pkg/scrape/example/class_reuse.dart
@@ -0,0 +1,46 @@
+// Copyright (c) 2020, 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:analyzer/dart/ast/ast.dart';
+import 'package:scrape/scrape.dart';
+
+void main(List<String> arguments) {
+  Scrape()
+    ..addHistogram('Declarations')
+    ..addHistogram('Uses')
+    ..addHistogram('Superclass names')
+    ..addHistogram('Superinterface names')
+    ..addHistogram('Mixin names')
+    ..addVisitor(() => ClassReuseVisitor())
+    ..runCommandLine(arguments);
+}
+
+class ClassReuseVisitor extends ScrapeVisitor {
+  @override
+  void visitClassDeclaration(ClassDeclaration node) {
+    if (node.isAbstract) {
+      record('Declarations', 'abstract class');
+    } else {
+      record('Declarations', 'class');
+    }
+
+    if (node.extendsClause != null) {
+      record('Uses', 'extend');
+      record('Superclass names', node.extendsClause.superclass.toString());
+    }
+
+    if (node.withClause != null) {
+      for (var mixin in node.withClause.mixinTypes) {
+        record('Uses', 'mixin');
+        record('Mixin names', mixin.toString());
+      }
+    }
+
+    if (node.implementsClause != null) {
+      for (var type in node.implementsClause.interfaces) {
+        record('Uses', 'implement');
+        record('Superinterface names', type.toString());
+      }
+    }
+  }
+}
diff --git a/pkg/scrape/example/control_flow.dart b/pkg/scrape/example/control_flow.dart
new file mode 100644
index 0000000..e8c9dacf
--- /dev/null
+++ b/pkg/scrape/example/control_flow.dart
@@ -0,0 +1,52 @@
+// Copyright (c) 2020, 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:analyzer/dart/ast/ast.dart';
+import 'package:scrape/scrape.dart';
+
+void main(List<String> arguments) {
+  Scrape()
+    ..addHistogram('Statements')
+    ..addVisitor(() => ControlFlowVisitor())
+    ..runCommandLine(arguments);
+}
+
+class ControlFlowVisitor extends ScrapeVisitor {
+  @override
+  void visitDoStatement(DoStatement node) {
+    record('Statements', 'do while');
+    super.visitDoStatement(node);
+  }
+
+  @override
+  void visitForStatement(ForStatement node) {
+    if (node.forLoopParts is ForEachParts) {
+      record('Statements', 'for in');
+    } else {
+      record('Statements', 'for ;;');
+    }
+    super.visitForStatement(node);
+  }
+
+  @override
+  void visitIfStatement(IfStatement node) {
+    if (node.elseStatement != null) {
+      record('Statements', 'if else');
+    } else {
+      record('Statements', 'if');
+    }
+    super.visitIfStatement(node);
+  }
+
+  @override
+  void visitSwitchStatement(SwitchStatement node) {
+    record('Statements', 'switch');
+    super.visitSwitchStatement(node);
+  }
+
+  @override
+  void visitWhileStatement(WhileStatement node) {
+    record('Statements', 'while');
+    super.visitWhileStatement(node);
+  }
+}
diff --git a/pkg/scrape/example/generic_classes.dart b/pkg/scrape/example/generic_classes.dart
new file mode 100644
index 0000000..09ecc32
--- /dev/null
+++ b/pkg/scrape/example/generic_classes.dart
@@ -0,0 +1,30 @@
+// Copyright (c) 2020, 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:analyzer/dart/ast/ast.dart';
+
+import 'package:scrape/scrape.dart';
+
+void main(List<String> arguments) {
+  Scrape()
+    ..addHistogram('Classes', order: SortOrder.numeric)
+    ..addVisitor(() => GenericClassVisitor())
+    ..runCommandLine(arguments);
+}
+
+class GenericClassVisitor extends ScrapeVisitor {
+  @override
+  void visitCompilationUnit(CompilationUnit node) {
+    super.visitCompilationUnit(node);
+  }
+
+  @override
+  void visitClassDeclaration(ClassDeclaration node) {
+    if (node.typeParameters == null) {
+      record('Classes', 0);
+    } else {
+      record('Classes', node.typeParameters.typeParameters.length);
+    }
+    super.visitClassDeclaration(node);
+  }
+}
diff --git a/pkg/scrape/example/if.dart b/pkg/scrape/example/if.dart
new file mode 100644
index 0000000..57573ff
--- /dev/null
+++ b/pkg/scrape/example/if.dart
@@ -0,0 +1,24 @@
+// Copyright (c) 2020, 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:analyzer/dart/ast/ast.dart';
+import 'package:scrape/scrape.dart';
+
+void main(List<String> arguments) {
+  Scrape()
+    ..addHistogram('If')
+    ..addVisitor(() => IfVisitor())
+    ..runCommandLine(arguments);
+}
+
+class IfVisitor extends ScrapeVisitor {
+  @override
+  void visitIfStatement(IfStatement node) {
+    if (node.elseStatement != null) {
+      record('If', 'else');
+    } else {
+      record('If', 'no else');
+    }
+    super.visitIfStatement(node);
+  }
+}
diff --git a/pkg/scrape/example/nesting.dart b/pkg/scrape/example/nesting.dart
new file mode 100644
index 0000000..6541d6d
--- /dev/null
+++ b/pkg/scrape/example/nesting.dart
@@ -0,0 +1,164 @@
+// Copyright (c) 2020, 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 'dart:math' as math;
+
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/token.dart';
+
+import 'package:scrape/scrape.dart';
+
+/// The paths to each build() method and its maximum nesting level.
+final buildMethods = <String, int>{};
+
+bool simplifyNames = false;
+
+void main(List<String> arguments) {
+  arguments = arguments.toList();
+  simplifyNames = arguments.remove('--simplify');
+  var allCode = arguments.remove('--all');
+
+  Scrape()
+    // The number of levels of nesting active when each argument appears.
+    ..addHistogram('Nesting depth')
+    // The number of levels of nesting active when each argument appears without
+    // counting lists.
+    ..addHistogram('Ignoring lists')
+    // The number of levels of "child" or "children" named parameter nesting
+    // when each argument appears.
+    ..addHistogram('Child nesting depth')
+    ..addHistogram('Argument names')
+    // Strings that describe the structure of each nested argument.
+    ..addHistogram('Argument nesting')
+    ..addVisitor(() => NestingVisitor(allCode: allCode))
+    ..runCommandLine(arguments);
+
+  var methods = buildMethods.keys.toList();
+  methods.sort((a, b) => buildMethods[b].compareTo(buildMethods[a]));
+  for (var method in methods) {
+    print('${buildMethods[method].toString().padLeft(3)}: $method');
+  }
+  print('${buildMethods.length} build() methods');
+}
+
+class NestingVisitor extends ScrapeVisitor {
+  final List<String> _stack = [];
+
+  final bool _allCode;
+  bool _pushed = false;
+  int _deepestNesting = 0;
+
+  NestingVisitor({bool allCode}) : _allCode = allCode ?? false;
+
+  @override
+  void beforeVisitBuildMethod(Declaration node) {
+    _deepestNesting = 0;
+  }
+
+  @override
+  void afterVisitBuildMethod(Declaration node) {
+    var startLine = lineInfo.getLocation(node.offset).lineNumber;
+    buildMethods['$path:$startLine'] = _deepestNesting;
+  }
+
+  @override
+  void visitArgumentList(ArgumentList node) {
+    // Only argument lists with trailing commas get indentation.
+    if (node.arguments.isNotEmpty &&
+        node.arguments.last.endToken.next.type == TokenType.COMMA) {
+      String name;
+      var parent = node.parent;
+      if (parent is MethodInvocation) {
+        name = parent.methodName.name;
+      } else if (parent is InstanceCreationExpression) {
+        name = parent.constructorName.toString();
+      } else if (parent is SuperConstructorInvocation) {
+        name = 'super.${parent.constructorName}';
+      } else {
+        name = '?(${parent.runtimeType})?';
+      }
+
+      if (simplifyNames) {
+        name = '';
+      }
+
+      for (var argument in node.arguments) {
+        var argName =
+            argument is NamedExpression ? argument.name.label.name : '';
+
+        if (_allCode || isInFlutterBuildMethod) {
+          record('Argument names', argName);
+        }
+
+        if (simplifyNames && argName != 'child' && argName != 'children') {
+          argName = '_';
+        }
+
+        _push('$name($argName:');
+        argument.accept(this);
+        _pop();
+      }
+    } else {
+      node.visitChildren(this);
+    }
+  }
+
+  @override
+  void visitBlock(Block node) {
+    var isFunction = node.parent is BlockFunctionBody;
+    if (!isFunction) _push('{');
+    node.visitChildren(this);
+    if (!isFunction) _pop();
+  }
+
+  @override
+  void visitExpressionFunctionBody(ExpressionFunctionBody node) {
+    _push('=>');
+    node.visitChildren(this);
+    _pop();
+  }
+
+  @override
+  void visitFunctionExpression(FunctionExpression node) {
+    var isDeclaration = node.parent is FunctionDeclaration;
+    if (!isDeclaration) _push('(){');
+    node.visitChildren(this);
+    if (!isDeclaration) _pop();
+  }
+
+  @override
+  void visitListLiteral(ListLiteral node) {
+    for (var element in node.elements) {
+      _push('[');
+      element.accept(this);
+      _pop();
+    }
+  }
+
+  @override
+  void visitSetOrMapLiteral(SetOrMapLiteral node) {
+    for (var element in node.elements) {
+      _push('{');
+      element.accept(this);
+      _pop();
+    }
+  }
+
+  void _push(String string) {
+    _stack.add(string);
+    _pushed = true;
+    _deepestNesting = math.max(_deepestNesting, _stack.length);
+  }
+
+  void _pop() {
+    if (_pushed && (_allCode || isInFlutterBuildMethod)) {
+      record('Argument nesting', _stack.join(' '));
+      record('Nesting depth', _stack.length);
+      record('Ignoring lists', _stack.where((s) => s != '[').length);
+      record('Child nesting depth',
+          _stack.where((s) => s.contains('child')).length);
+    }
+    _pushed = false;
+    _stack.removeLast();
+  }
+}
diff --git a/pkg/scrape/example/null_aware.dart b/pkg/scrape/example/null_aware.dart
new file mode 100644
index 0000000..392798e
--- /dev/null
+++ b/pkg/scrape/example/null_aware.dart
@@ -0,0 +1,307 @@
+// Copyright (c) 2020, 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:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/token.dart';
+
+import 'package:scrape/scrape.dart';
+
+void main(List<String> arguments) {
+  Scrape()
+    ..addHistogram('Null-aware types')
+    ..addHistogram('Null-aware chain lengths')
+    // Boolean contexts where null-aware operators are used.
+    ..addHistogram('Boolean contexts')
+    // Expressions used to convert a null-aware expression to a Boolean for use in
+    // a Boolean context.
+    ..addHistogram('Boolean conversions')
+    ..addVisitor(() => NullVisitor())
+    ..runCommandLine(arguments);
+}
+
+class NullVisitor extends ScrapeVisitor {
+  @override
+  void visitMethodInvocation(MethodInvocation node) {
+    if (node.operator != null &&
+        node.operator.type == TokenType.QUESTION_PERIOD) {
+      _nullAware(node);
+    }
+
+    super.visitMethodInvocation(node);
+  }
+
+  @override
+  void visitPropertyAccess(PropertyAccess node) {
+    if (node.operator.type == TokenType.QUESTION_PERIOD) {
+      _nullAware(node);
+    }
+
+    super.visitPropertyAccess(node);
+  }
+
+  void _nullAware(AstNode node) {
+    var parent = node.parent;
+
+    // Parentheses are purely syntactic.
+    if (parent is ParenthesizedExpression) parent = parent.parent;
+
+    // We want to treat a chain of null-aware operators as a single unit. We
+    // use the top-most node (the last method in the chain) as the "real" one
+    // because its parent is the context where the whole chain appears.
+    if (parent is PropertyAccess &&
+            parent.operator.type == TokenType.QUESTION_PERIOD &&
+            parent.target == node ||
+        parent is MethodInvocation &&
+            parent.operator != null &&
+            parent.operator.type == TokenType.QUESTION_PERIOD &&
+            parent.target == node) {
+      // This node is not the root of a null-aware chain, so skip it.
+      return;
+    }
+
+    // This node is the root of a null-aware chain. See how long the chain is.
+    var length = 0;
+    var chain = node;
+    while (true) {
+      if (chain is PropertyAccess &&
+          chain.operator.type == TokenType.QUESTION_PERIOD) {
+        chain = (chain as PropertyAccess).target;
+      } else if (chain is MethodInvocation &&
+          chain.operator != null &&
+          chain.operator.type == TokenType.QUESTION_PERIOD) {
+        chain = (chain as MethodInvocation).target;
+      } else {
+        break;
+      }
+
+      length++;
+    }
+
+    record('Null-aware chain lengths', length.toString());
+
+    void recordType(String label) {
+      record('Null-aware types', label);
+    }
+
+    // See if the expression is an if condition.
+    _checkCondition(node);
+
+    if (parent is ExpressionStatement) {
+      recordType("Expression statement 'foo?.bar();'");
+      return;
+    }
+
+    if (parent is ReturnStatement) {
+      recordType("Return statement 'return foo?.bar();'");
+      return;
+    }
+
+    if (parent is BinaryExpression &&
+        parent.operator.type == TokenType.BANG_EQ &&
+        parent.leftOperand == node &&
+        parent.rightOperand is BooleanLiteral &&
+        (parent.rightOperand as BooleanLiteral).value == true) {
+      recordType("Compare to true 'foo?.bar() != true'");
+      return;
+    }
+
+    if (parent is BinaryExpression &&
+        parent.operator.type == TokenType.EQ_EQ &&
+        parent.leftOperand == node &&
+        parent.rightOperand is BooleanLiteral &&
+        (parent.rightOperand as BooleanLiteral).value == true) {
+      recordType("Compare to true 'foo?.bar() == true'");
+      return;
+    }
+
+    if (parent is BinaryExpression &&
+        parent.operator.type == TokenType.BANG_EQ &&
+        parent.leftOperand == node &&
+        parent.rightOperand is BooleanLiteral &&
+        (parent.rightOperand as BooleanLiteral).value == false) {
+      recordType("Compare to false 'foo?.bar() != false'");
+      return;
+    }
+
+    if (parent is BinaryExpression &&
+        parent.operator.type == TokenType.EQ_EQ &&
+        parent.leftOperand == node &&
+        parent.rightOperand is BooleanLiteral &&
+        (parent.rightOperand as BooleanLiteral).value == false) {
+      recordType("Compare to false 'foo?.bar() == false'");
+      return;
+    }
+
+    if (parent is BinaryExpression &&
+        parent.operator.type == TokenType.BANG_EQ &&
+        parent.leftOperand == node &&
+        parent.rightOperand is NullLiteral) {
+      recordType("Compare to null 'foo?.bar() != null'");
+      return;
+    }
+
+    if (parent is BinaryExpression &&
+        parent.operator.type == TokenType.EQ_EQ &&
+        parent.leftOperand == node &&
+        parent.rightOperand is NullLiteral) {
+      recordType("Compare to null 'foo?.bar() == null'");
+      return;
+    }
+
+    if (parent is BinaryExpression && parent.operator.type == TokenType.EQ_EQ) {
+      recordType("Compare to other expression 'foo?.bar() == bang'");
+      return;
+    }
+
+    if (parent is BinaryExpression &&
+        parent.operator.type == TokenType.BANG_EQ) {
+      recordType("Compare to other expression 'foo?.bar() != bang'");
+      return;
+    }
+
+    if (parent is BinaryExpression &&
+        parent.operator.type == TokenType.QUESTION_QUESTION &&
+        parent.leftOperand == node) {
+      recordType("Coalesce 'foo?.bar() ?? baz'");
+      return;
+    }
+
+    if (parent is BinaryExpression &&
+        parent.operator.type == TokenType.QUESTION_QUESTION &&
+        parent.rightOperand == node) {
+      recordType("Reverse coalesce 'baz ?? foo?.bar()'");
+      return;
+    }
+
+    if (parent is ConditionalExpression && parent.condition == node) {
+      recordType(
+          "Condition in conditional expression 'foo?.bar() ? baz : bang");
+      return;
+    }
+
+    if (parent is ConditionalExpression) {
+      recordType("Then or else branch of conditional 'baz ? foo?.bar() : bang");
+      return;
+    }
+
+    if (parent is AsExpression && parent.expression == node) {
+      recordType("Cast expression 'foo?.bar as Baz'");
+      return;
+    }
+
+    if (parent is AssignmentExpression && parent.leftHandSide == node) {
+      recordType("Assign target 'foo?.bar ${parent.operator} baz'");
+      return;
+    }
+
+    if (parent is AssignmentExpression && parent.rightHandSide == node) {
+      recordType("Assign value 'baz = foo?.bar()'");
+      return;
+    }
+
+    if (parent is VariableDeclaration && parent.initializer == node) {
+      recordType("Variable initializer 'var baz = foo?.bar();'");
+      return;
+    }
+
+    if (parent is NamedExpression) {
+      recordType("Named argument 'fn(name: foo?.bar())'");
+      return;
+    }
+
+    if (parent is ArgumentList && parent.arguments.contains(node)) {
+      recordType("Positional argument 'fn(foo?.bar())'");
+      return;
+    }
+
+    if (parent is AwaitExpression) {
+      recordType("Await 'await foo?.bar()'");
+      return;
+    }
+
+    if (parent is MapLiteralEntry || parent is ListLiteral) {
+      recordType("Collection literal element '[foo?.bar()]'");
+      return;
+    }
+
+    if (parent is ExpressionFunctionBody) {
+      recordType("Member body 'member() => foo?.bar();'");
+      return;
+    }
+
+    if (parent is InterpolationExpression) {
+      recordType("String interpolation '\"blah \${foo?.bar()}\"'");
+      return;
+    }
+
+    if (parent is BinaryExpression) {
+      recordType('Uncategorized ${parent}');
+      return;
+    }
+
+    recordType('Uncategorized ${parent.runtimeType}');
+
+    // Find the surrounding statement containing the null-aware.
+    while (node is Expression) {
+      node = node.parent;
+    }
+
+    printNode(node);
+  }
+
+  void _checkCondition(AstNode node) {
+    String expression;
+
+    // Look at the expression that immediately wraps the null-aware to see if
+    // it deals with it somehow, like "foo?.bar ?? otherwise".
+    var parent = node.parent;
+    if (parent is ParenthesizedExpression) parent = parent.parent;
+
+    if (parent is BinaryExpression &&
+        (parent.operator.type == TokenType.EQ_EQ ||
+            parent.operator.type == TokenType.BANG_EQ ||
+            parent.operator.type == TokenType.QUESTION_QUESTION) &&
+        (parent.rightOperand is NullLiteral ||
+            parent.rightOperand is BooleanLiteral)) {
+      var binary = parent as BinaryExpression;
+      expression = 'foo?.bar ${binary.operator} ${binary.rightOperand}';
+
+      // This does handle it, so see the context where it appears.
+      node = parent as Expression;
+      if (node is ParenthesizedExpression) node = node.parent as Expression;
+      parent = node.parent;
+      if (parent is ParenthesizedExpression) parent = parent.parent;
+    }
+
+    String context;
+    if (parent is IfStatement && node == parent.condition) {
+      context = 'if';
+    } else if (parent is BinaryExpression &&
+        parent.operator.type == TokenType.AMPERSAND_AMPERSAND) {
+      context = '&&';
+    } else if (parent is BinaryExpression &&
+        parent.operator.type == TokenType.BAR_BAR) {
+      context = '||';
+    } else if (parent is WhileStatement && node == parent.condition) {
+      context = 'while';
+    } else if (parent is DoStatement && node == parent.condition) {
+      context = 'do';
+    } else if (parent is ForStatement &&
+        parent.forLoopParts is ForParts &&
+        node == (parent.forLoopParts as ForParts).condition) {
+      context = 'for';
+    } else if (parent is ConditionalExpression && node == parent.condition) {
+      context = '?:';
+    }
+
+    if (context != null) {
+      record('Boolean contexts', context);
+
+      if (expression != null) {
+        record('Boolean conversions', expression);
+      } else {
+        record('Boolean conversions', 'unknown: $node');
+      }
+    }
+  }
+}
diff --git a/pkg/scrape/example/spread_sizes.dart b/pkg/scrape/example/spread_sizes.dart
new file mode 100644
index 0000000..d1257e1
--- /dev/null
+++ b/pkg/scrape/example/spread_sizes.dart
@@ -0,0 +1,56 @@
+// Copyright (c) 2020, 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:analyzer/dart/ast/ast.dart';
+
+import 'package:scrape/scrape.dart';
+
+/// Looks at expressions that could likely be converted to spread operators and
+/// measures their length. "Likely" means calls to `addAll()` where the
+/// receiver is a list or map literal.
+void main(List<String> arguments) {
+  Scrape()
+    ..addHistogram('Arguments')
+    ..addHistogram('Lengths', order: SortOrder.numeric)
+    ..addVisitor(() => SpreadVisitor())
+    ..runCommandLine(arguments);
+}
+
+class SpreadVisitor extends ScrapeVisitor {
+  @override
+  void visitCascadeExpression(CascadeExpression node) {
+    for (var section in node.cascadeSections) {
+      if (section is MethodInvocation) {
+        _countCall(node, section.methodName, node.target, section.argumentList);
+      }
+    }
+
+    super.visitCascadeExpression(node);
+  }
+
+  @override
+  void visitMethodInvocation(MethodInvocation node) {
+    _countCall(node, node.methodName, node.target, node.argumentList);
+    super.visitMethodInvocation(node);
+  }
+
+  void _countCall(Expression node, SimpleIdentifier name, Expression target,
+      ArgumentList args) {
+    if (name.name != 'addAll') return;
+
+    // See if the target is a collection literal.
+    while (target is MethodInvocation) {
+      target = (target as MethodInvocation).target;
+    }
+
+    if (target is ListLiteral || target is SetOrMapLiteral) {
+      if (args.arguments.length == 1) {
+        var arg = args.arguments[0];
+        record('Arguments', arg.toString());
+        record('Lengths', arg.length);
+      }
+
+      printNode(node);
+    }
+  }
+}
diff --git a/pkg/scrape/lib/scrape.dart b/pkg/scrape/lib/scrape.dart
new file mode 100644
index 0000000..6359494
--- /dev/null
+++ b/pkg/scrape/lib/scrape.dart
@@ -0,0 +1,291 @@
+// Copyright (c) 2020, 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 'dart:io';
+import 'dart:math' as math;
+
+import 'package:analyzer/dart/analysis/features.dart';
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/source/line_info.dart';
+import 'package:analyzer/src/dart/scanner/reader.dart';
+import 'package:analyzer/src/dart/scanner/scanner.dart';
+import 'package:analyzer/src/generated/parser.dart';
+import 'package:analyzer/src/string_source.dart';
+import 'package:args/args.dart';
+import 'package:path/path.dart' as p;
+
+import 'src/error_listener.dart';
+import 'src/histogram.dart';
+import 'src/scrape_visitor.dart';
+
+export 'src/histogram.dart';
+export 'src/scrape_visitor.dart' hide bindVisitor;
+
+class Scrape {
+  final List<ScrapeVisitor Function()> _visitorFactories = [];
+
+  /// What percent of files should be processed.
+  int _percent;
+
+  /// Process package test files.
+  bool _includeTests = true;
+
+  /// Process Dart SDK language tests.
+  bool _includeLanguageTests = true;
+
+  /// Process Dart files generated from protobufs.
+  bool _includeProtobufs = false;
+
+  /// Whether every file should be printed before being processed.
+  bool _printFiles = true;
+
+  /// Whether parse errors should be printed.
+  bool _printErrors = true;
+
+  /// The number of files that have been processed.
+  int get scrapedFileCount => _scrapedFileCount;
+  int _scrapedFileCount = 0;
+
+  /// The number of lines of code that have been processed.
+  int get scrapedLineCount => _scrapedLineCount;
+  int _scrapedLineCount = 0;
+
+  final Map<String, Histogram> _histograms = {};
+
+  /// Whether we're in the middle of writing the running file count and need a
+  /// newline before any other output should be shown.
+  bool _needClearLine = false;
+
+  /// Register a new visitor factory.
+  ///
+  /// This function will be called for each scraped file and the resulting
+  /// [ScrapeVisitor] will traverse the parsed file's AST.
+  void addVisitor(ScrapeVisitor Function() createVisitor) {
+    _visitorFactories.add(createVisitor);
+  }
+
+  /// Defines a new histogram with [name] to collect occurrences.
+  ///
+  /// After the scrape completes, each defined histogram's collected counts
+  /// are shown, ordered by [order]. If [showBar] is not `false`, then shows an
+  /// ASCII bar chart for the counts.
+  ///
+  /// If [showAll] is `true`, then every item that occurred is shown. Otherwise,
+  /// only shows the first 100 items or occurrences that represent at least
+  /// 0.1% of the total, whichever is longer.
+  ///
+  /// If [minCount] is passed, then only shows items that occurred at least
+  /// that many times.
+  void addHistogram(String name,
+      {SortOrder order = SortOrder.descending,
+      bool showBar,
+      bool showAll,
+      int minCount}) {
+    _histograms.putIfAbsent(
+        name,
+        () => Histogram(
+            order: order,
+            showBar: showBar,
+            showAll: showAll,
+            minCount: minCount));
+  }
+
+  /// Add an occurrence of [item] to [histogram].
+  void record(String histogram, Object item) {
+    _histograms[histogram].add(item);
+  }
+
+  /// Run the scrape using the given set of command line arguments.
+  void runCommandLine(List<String> arguments) {
+    var parser = ArgParser(allowTrailingOptions: true);
+    parser.addOption('percent',
+        help: 'Only process a randomly selected percentage of files.');
+    parser.addFlag('tests',
+        defaultsTo: true, help: 'Process package test files.');
+    parser.addFlag('language-tests',
+        help: 'Process Dart SDK language test files.');
+    parser.addFlag('protobufs',
+        help: 'Process Dart files generated from protobufs.');
+    parser.addFlag('print-files',
+        defaultsTo: true, help: 'Print the path for each parsed file.');
+    parser.addFlag('print-errors',
+        defaultsTo: true, help: 'Print parse errors.');
+    parser.addFlag('help', negatable: false, help: 'Print help text.');
+
+    var results = parser.parse(arguments);
+
+    if (results['help'] as bool) {
+      var script = p.url.basename(Platform.script.toString());
+      print('Usage: $script [options] <paths...>');
+      print(parser.usage);
+      return;
+    }
+
+    _includeTests = results['tests'] as bool;
+    _includeLanguageTests = results['language-tests'] as bool;
+    _includeProtobufs = results['protobufs'] as bool;
+    _printFiles = results['print-files'] as bool;
+    _printErrors = results['print-errors'] as bool;
+
+    if (results.wasParsed('percent')) {
+      _percent = int.tryParse(results['percent'] as String);
+      if (_percent == null) {
+        print("--percent must be an integer, was '${results["percent"]}'.");
+        exit(1);
+      }
+    }
+
+    if (results.rest.isEmpty) {
+      print('Must pass at least one path to process.');
+      exit(1);
+    }
+
+    var watch = Stopwatch()..start();
+    for (var path in results.rest) {
+      _processPath(path);
+    }
+    watch.stop();
+
+    clearLine();
+    _histograms.forEach((name, histogram) {
+      histogram.printCounts(name);
+    });
+
+    var elapsed = _formatDuration(watch.elapsed);
+    var lines = _scrapedLineCount != 1 ? '$_scrapedLineCount lines' : '1 line';
+    var files = _scrapedFileCount != 1 ? '$_scrapedFileCount files' : '1 file';
+    print('Took $elapsed to scrape $lines in $files.');
+  }
+
+  /// Display [message], clearing the line if necessary.
+  void log(Object message) {
+    // TODO(rnystrom): Consider using cli_util package.
+    clearLine();
+    print(message);
+  }
+
+  /// Clear the current line if it needs it.
+  void clearLine() {
+    if (!_needClearLine) return;
+    stdout.write('\u001b[2K\r');
+    _needClearLine = false;
+  }
+
+  String _formatDuration(Duration duration) {
+    String pad(int width, int n) => n.toString().padLeft(width, '0');
+
+    if (duration.inMinutes >= 1) {
+      var minutes = duration.inMinutes;
+      var seconds = duration.inSeconds % 60;
+      var ms = duration.inMilliseconds % 1000;
+      return '$minutes:${pad(2, seconds)}.${pad(3, ms)}';
+    } else if (duration.inSeconds >= 1) {
+      return '${(duration.inMilliseconds / 1000).toStringAsFixed(3)}s';
+    } else {
+      return '${duration.inMilliseconds}ms';
+    }
+  }
+
+  void _processPath(String path) {
+    var random = math.Random();
+
+    if (File(path).existsSync()) {
+      _parseFile(File(path), path);
+      return;
+    }
+
+    for (var entry in Directory(path).listSync(recursive: true)) {
+      if (entry is! File) continue;
+
+      if (!entry.path.endsWith('.dart')) continue;
+
+      // For unknown reasons, some READMEs have a ".dart" extension. They aren't
+      // Dart files.
+      if (entry.path.endsWith('README.dart')) continue;
+
+      if (!_includeLanguageTests) {
+        if (entry.path.contains('/sdk/tests/')) continue;
+        if (entry.path.contains('/testcases/')) continue;
+        if (entry.path.contains('/sdk/runtime/tests/')) continue;
+        if (entry.path.contains('/linter/test/_data/')) continue;
+        if (entry.path.contains('/analyzer/test/')) continue;
+        if (entry.path.contains('/dev_compiler/test/')) continue;
+        if (entry.path.contains('/analyzer_cli/test/')) continue;
+        if (entry.path.contains('/analysis_server/test/')) continue;
+        if (entry.path.contains('/kernel/test/')) continue;
+      }
+
+      if (!_includeTests) {
+        if (entry.path.contains('/test/')) continue;
+        if (entry.path.endsWith('_test.dart')) continue;
+      }
+
+      // Don't care about cached packages.
+      if (entry.path.contains('sdk/third_party/pkg/')) continue;
+      if (entry.path.contains('sdk/third_party/pkg_tested/')) continue;
+      if (entry.path.contains('/.dart_tool/')) continue;
+
+      // Don't care about generated protobuf code.
+      if (!_includeProtobufs) {
+        if (entry.path.endsWith('.pb.dart')) continue;
+        if (entry.path.endsWith('.pbenum.dart')) continue;
+      }
+
+      if (_percent != null && random.nextInt(100) >= _percent) continue;
+
+      var relative = p.relative(entry.path, from: path);
+      _parseFile(entry as File, relative);
+    }
+  }
+
+  void _parseFile(File file, String shortPath) {
+    var source = file.readAsStringSync();
+
+    var errorListener = ErrorListener(this, _printErrors);
+
+    // Tokenize the source.
+    var reader = CharSequenceReader(source);
+    var stringSource = StringSource(source, file.path);
+    var scanner = Scanner(stringSource, reader, errorListener);
+    var startToken = scanner.tokenize();
+
+    // Parse it.
+    var parser = Parser(stringSource, errorListener,
+        featureSet: FeatureSet.latestLanguageVersion());
+    parser.enableOptionalNewAndConst = true;
+    parser.enableSetLiterals = true;
+
+    if (_printFiles) {
+      var line =
+          '[$_scrapedFileCount files, $_scrapedLineCount lines] ' '$shortPath';
+      if (Platform.isWindows) {
+        // No ANSI escape codes on Windows.
+        print(line);
+      } else {
+        // Overwrite the same line.
+        stdout.write('\u001b[2K\r'
+            '[$_scrapedFileCount files, $_scrapedLineCount lines] $shortPath');
+        _needClearLine = true;
+      }
+    }
+
+    AstNode node;
+    try {
+      node = parser.parseCompilationUnit(startToken);
+    } catch (error) {
+      print('Got exception parsing $shortPath:\n$error');
+      return;
+    }
+
+    var lineInfo = LineInfo(scanner.lineStarts);
+
+    _scrapedFileCount++;
+    _scrapedLineCount += lineInfo.lineCount;
+
+    for (var visitorFactory in _visitorFactories) {
+      var visitor = visitorFactory();
+      bindVisitor(visitor, this, shortPath, source, lineInfo);
+      node.accept(visitor);
+    }
+  }
+}
diff --git a/pkg/scrape/lib/src/error_listener.dart b/pkg/scrape/lib/src/error_listener.dart
new file mode 100644
index 0000000..69c9118
--- /dev/null
+++ b/pkg/scrape/lib/src/error_listener.dart
@@ -0,0 +1,21 @@
+// Copyright (c) 2020, 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:analyzer/error/error.dart';
+import 'package:analyzer/error/listener.dart';
+import 'package:scrape/scrape.dart';
+
+/// A simple [AnalysisErrorListener] that just collects the reported errors.
+class ErrorListener implements AnalysisErrorListener {
+  final Scrape _scrape;
+  final bool _printErrors;
+
+  ErrorListener(this._scrape, this._printErrors);
+
+  @override
+  void onError(AnalysisError error) {
+    if (_printErrors) {
+      _scrape.log(error);
+    }
+  }
+}
diff --git a/pkg/scrape/lib/src/histogram.dart b/pkg/scrape/lib/src/histogram.dart
new file mode 100644
index 0000000..12f2b9f
--- /dev/null
+++ b/pkg/scrape/lib/src/histogram.dart
@@ -0,0 +1,100 @@
+// Copyright (c) 2020, 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 'dart:math' as math;
+
+enum SortOrder {
+  /// From fewest occurrences to most.
+  ascending,
+
+  /// From most occurrences to fewest.
+  descending,
+
+  /// Lexicographically sorted by name.
+  alphabetical,
+
+  /// Keys are parsed as integers and sorted by value.
+  numeric,
+}
+
+/// Counts occurrences of strings and displays the results as a histogram.
+class Histogram {
+  final Map<Object, int> _counts = {};
+  final SortOrder _order;
+  final bool _showBar;
+  final bool _showAll;
+  final int _minCount;
+
+  int get totalCount => _counts.values.fold(0, (a, b) => a + b);
+
+  Histogram({SortOrder order, bool showBar, bool showAll, int minCount})
+      : _order = order ?? SortOrder.descending,
+        _showBar = showBar ?? true,
+        _showAll = showAll ?? false,
+        _minCount = minCount ?? 0;
+
+  void add(Object item) {
+    _counts.putIfAbsent(item, () => 0);
+    _counts[item]++;
+  }
+
+  void printCounts(String label) {
+    var total = totalCount;
+    print('');
+    print('-- $label ($total total) --');
+
+    var keys = _counts.keys.toList();
+    switch (_order) {
+      case SortOrder.ascending:
+        keys.sort((a, b) => _counts[a].compareTo(_counts[b]));
+        break;
+      case SortOrder.descending:
+        keys.sort((a, b) => _counts[b].compareTo(_counts[a]));
+        break;
+      case SortOrder.alphabetical:
+        keys.sort();
+        break;
+      case SortOrder.numeric:
+        // TODO(rnystrom): Using string keys but treating them as integers is
+        // kind of hokey. But it keeps the [ScrapeVisitor] API simpler.
+        keys.sort((a, b) => (a as int).compareTo(b as int));
+        break;
+    }
+
+    var longest = keys.fold<int>(
+        0, (length, key) => math.max(length, key.toString().length));
+    var barScale = 80 - 22 - longest;
+
+    var shown = 0;
+    var skipped = 0;
+    for (var object in keys) {
+      var count = _counts[object];
+      var countString = count.toString().padLeft(7);
+      var percent = 100 * count / total;
+      var percentString = percent.toStringAsFixed(3).padLeft(7);
+
+      if (_showAll || ((shown < 100 || percent >= 0.1) && count >= _minCount)) {
+        var line = '$countString ($percentString%): $object';
+        if (_showBar && barScale > 1) {
+          line = line.padRight(longest + 22);
+          line += '=' * (percent / 100 * barScale).ceil();
+        }
+        print(line);
+        shown++;
+      } else {
+        skipped++;
+      }
+    }
+
+    if (skipped > 0) print('And $skipped more less than 0.1%...');
+
+    // If we're counting numeric keys, show other statistics too.
+    if (_order == SortOrder.numeric && keys.isNotEmpty) {
+      var sum = keys.fold<int>(
+          0, (result, key) => result + (key as int) * _counts[key]);
+      var average = sum / total;
+      var median = _counts[keys[keys.length ~/ 2]];
+      print('Sum $sum, average ${average.toStringAsFixed(3)}, median $median');
+    }
+  }
+}
diff --git a/pkg/scrape/lib/src/scrape_visitor.dart b/pkg/scrape/lib/src/scrape_visitor.dart
new file mode 100644
index 0000000..0b8a405
--- /dev/null
+++ b/pkg/scrape/lib/src/scrape_visitor.dart
@@ -0,0 +1,139 @@
+// Copyright (c) 2020, 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:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/visitor.dart';
+import 'package:analyzer/source/line_info.dart';
+
+import '../scrape.dart';
+
+/// Wire up [visitor] to [scrape] the given [path] containing [source] with
+/// [info].
+///
+/// This is a top-level function instead of an instance method so that we can
+/// hide it and not export it from scrape's public API. Only [Scrape] itself
+/// should call this. We bind separately instead of passing these through the
+/// [ScrapeVisitor] constructor so that subclasses of [ScrapeVisitor] don't
+/// need to define a pass-through constructor.
+void bindVisitor(ScrapeVisitor visitor, Scrape scrape, String path,
+    String source, LineInfo info) {
+  assert(visitor._scrape == null, 'Should only bind once.');
+  visitor._scrape = scrape;
+  visitor._path = path;
+  visitor._source = source;
+  visitor.lineInfo = info;
+}
+
+/// Base Visitor class with some utility functionality.
+class ScrapeVisitor extends RecursiveAstVisitor<void> {
+  // These final-ish fields are initialized by [bindVisitor()].
+  Scrape _scrape;
+  String _path;
+  String _source;
+  LineInfo lineInfo;
+
+  /// How many levels deep the visitor is currently nested inside build methods.
+  int _inFlutterBuildMethods = 0;
+
+  String get path => _path;
+
+  // TODO(rnystrom): Remove this in favor of using surveyor for these kinds of
+  // analyses.
+  /// Whether the visitor is currently inside a Flutter "build" method,
+  /// either directly or nested inside some other function inside one.
+  ///
+  /// This is only an approximate guess. It assumes a method is a "build"-like
+  /// method if it returns "Widget", or has a parameter list that starts with
+  /// "BuildContext context".
+  bool get isInFlutterBuildMethod => _inFlutterBuildMethods > 0;
+
+  bool _isBuildMethod(TypeAnnotation returnType, SimpleIdentifier name,
+      FormalParameterList parameters) {
+    var parameterString = parameters.toString();
+
+    if (returnType.toString() == 'void') return false;
+    if (parameterString.startsWith('(BuildContext context')) return true;
+    if (returnType.toString() == 'Widget') return true;
+
+    return false;
+  }
+
+  /// Add an occurrence of [item] to [histogram].
+  void record(String histogram, Object item) {
+    _scrape.record(histogram, item);
+  }
+
+  /// Write [message] to stdout, clearing the current line if needed.
+  void log(Object message) {
+    _scrape.log(message);
+  }
+
+  /// Print a nice representation of [node].
+  void printNode(AstNode node) {
+    log(nodeToString(node));
+  }
+
+  /// Generate a nice string representation of [node] include file path and
+  /// line information.
+  String nodeToString(AstNode node) {
+    var startLine = lineInfo.getLocation(node.offset).lineNumber;
+    var endLine = lineInfo.getLocation(node.end).lineNumber;
+
+    startLine = startLine.clamp(0, lineInfo.lineCount - 1) as int;
+    endLine = endLine.clamp(0, lineInfo.lineCount - 1) as int;
+
+    var buffer = StringBuffer();
+    buffer.writeln('// $path:$startLine');
+    for (var line = startLine; line <= endLine; line++) {
+      // Note that getLocation() returns 1-based lines, but getOffsetOfLine()
+      // expects 0-based.
+      var offset = lineInfo.getOffsetOfLine(line - 1);
+      // -1 to not include the newline.
+      var end = lineInfo.getOffsetOfLine(line) - 1;
+
+      buffer.writeln(_source.substring(offset, end));
+    }
+
+    return buffer.toString();
+  }
+
+  /// Get the line number of the code at [offset].
+  int getLine(int offset) => lineInfo.getLocation(offset).lineNumber;
+
+  /// Override this to execute custom code before visiting a Flutter build
+  /// method.
+  void beforeVisitBuildMethod(Declaration node) {}
+
+  /// Override this to execute custom code after visiting a Flutter build
+  /// method.
+  void afterVisitBuildMethod(Declaration node) {}
+
+  @override
+  void visitMethodDeclaration(MethodDeclaration node) {
+    var isBuild = _isBuildMethod(node.returnType, node.name, node.parameters);
+    if (isBuild) _inFlutterBuildMethods++;
+
+    try {
+      if (isBuild) beforeVisitBuildMethod(node);
+      super.visitMethodDeclaration(node);
+      if (isBuild) afterVisitBuildMethod(node);
+    } finally {
+      if (isBuild) _inFlutterBuildMethods--;
+    }
+  }
+
+  @override
+  void visitFunctionDeclaration(FunctionDeclaration node) {
+    var isBuild = _isBuildMethod(
+        node.returnType, node.name, node.functionExpression.parameters);
+    if (isBuild) _inFlutterBuildMethods++;
+
+    try {
+      if (isBuild) beforeVisitBuildMethod(node);
+      super.visitFunctionDeclaration(node);
+      if (isBuild) afterVisitBuildMethod(node);
+    } finally {
+      if (isBuild) _inFlutterBuildMethods--;
+    }
+  }
+}
diff --git a/pkg/scrape/pubspec.yaml b/pkg/scrape/pubspec.yaml
new file mode 100644
index 0000000..a011644
--- /dev/null
+++ b/pkg/scrape/pubspec.yaml
@@ -0,0 +1,12 @@
+name: scrape
+description: Helper package for analyzing the syntax of Dart programs.
+# This package is not intended for consumption on pub.dev. DO NOT publish.
+publish_to: none
+environment:
+  sdk: ^2.10.0
+dependencies:
+  args: ^1.6.0
+  analyzer: ^0.40.4
+  path: ^1.7.0
+dev_dependencies:
+  pedantic: ^1.9.2
diff --git a/tools/VERSION b/tools/VERSION
index 0168c17..0838600 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 11
 PATCH 0
-PRERELEASE 236
+PRERELEASE 237
 PRERELEASE_PATCH 0
\ No newline at end of file