From 691d07575bea3c9c17e2d3a34792de5be58a1622 Mon Sep 17 00:00:00 2001 From: Brian Wilkerson Date: Mon, 19 Oct 2020 21:44:27 +0000 Subject: [PATCH 1/4] Add tests for flutter-specific uses of data-driven fixes This also adds a fix for a bug found by the tests. There are a couple of failing tests, but they're failing because the support required for them hasn't been implemented yet, not because of a bug. Change-Id: Ia24dffc02aac0c4b61c9c7d8e2d744211f5b4bea Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/168401 Reviewed-by: Konstantin Shcheglov Reviewed-by: Phil Quitslund Commit-Queue: Brian Wilkerson --- .../fix/data_driven/value_generator.dart | 10 + .../data_driven/flutter_use_case_test.dart | 1835 +++++++++++++++++ .../correction/fix/data_driven/test_all.dart | 2 + 3 files changed, 1847 insertions(+) create mode 100644 pkg/analysis_server/test/src/services/correction/fix/data_driven/flutter_use_case_test.dart 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 1214e8b16846d..3bfb6394be6e3 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 @@ class ArgumentExpression extends ValueGenerator { 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 0000000000000..c26ea182617a4 --- /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 + 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 + 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 + 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 + 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 + 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 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 + 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 + 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 + 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 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 + 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 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 + 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 + 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 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 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 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 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 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 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 + test_widgets_BuildContext_ancestorInheritedElementForWidgetOfExactType_deprecated() async { + setPackageContent(''' +class BuildContext { + @deprecated + void ancestorInheritedElementForWidgetOfExactType(Type t) {} + void getElementForInheritedWidgetOfExactType() {} +} +'''); + 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(); +} +'''); + } + + Future + test_widgets_BuildContext_ancestorInheritedElementForWidgetOfExactType_removed() async { + setPackageContent(''' +class BuildContext { + void getElementForInheritedWidgetOfExactType() {} +} +'''); + 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(); +} +'''); + } + + Future + test_widgets_BuildContext_ancestorWidgetOfExactType_deprecated() async { + setPackageContent(''' +class BuildContext { + @deprecated + void ancestorWidgetOfExactType(Type t) {} + void findAncestorWidgetOfExactType() {} +} +'''); + 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(); +} +'''); + } + + Future + test_widgets_BuildContext_ancestorWidgetOfExactType_removed() async { + setPackageContent(''' +class BuildContext { + void findAncestorWidgetOfExactType() {} +} +'''); + 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(); +} +'''); + } + + Future 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 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 + test_widgets_BuildContext_inheritFromWidgetOfExactType_deprecated() async { + setPackageContent(''' +class BuildContext { + @deprecated + void inheritFromWidgetOfExactType(Type t) {} + void dependOnInheritedWidgetOfExactType() {} +} +'''); + 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(); +} +'''); + } + + Future + test_widgets_BuildContext_inheritFromWidgetOfExactType_removed() async { + setPackageContent(''' +class BuildContext { + void dependOnInheritedWidgetOfExactType() {} +} +'''); + 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(); +} +'''); + } + + Future + test_widgets_Element_ancestorInheritedElementForWidgetOfExactType_deprecated() async { + setPackageContent(''' +class Element { + @deprecated + void ancestorInheritedElementForWidgetOfExactType(Type t) {} + void getElementForInheritedWidgetOfExactType() {} +} +'''); + 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(); +} +'''); + } + + Future + test_widgets_Element_ancestorInheritedElementForWidgetOfExactType_removed() async { + setPackageContent(''' +class Element { + void getElementForInheritedWidgetOfExactType() {} +} +'''); + 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(); +} +'''); + } + + Future + test_widgets_Element_ancestorWidgetOfExactType_deprecated() async { + setPackageContent(''' +class Element { + @deprecated + void ancestorWidgetOfExactType(Type t) {} + void findAncestorWidgetOfExactType() {} +} +'''); + 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(); +} +'''); + } + + Future test_widgets_Element_ancestorWidgetOfExactType_removed() async { + setPackageContent(''' +class Element { + void findAncestorWidgetOfExactType() {} +} +'''); + 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(); +} +'''); + } + + Future 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 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 + test_widgets_Element_inheritFromWidgetOfExactType_deprecated() async { + setPackageContent(''' +class Element { + @deprecated + void inheritFromWidgetOfExactType(Type t) {} + void dependOnInheritedWidgetOfExactType() {} +} +'''); + 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(); +} +'''); + } + + Future + test_widgets_Element_inheritFromWidgetOfExactType_removed() async { + setPackageContent(''' +class Element { + void dependOnInheritedWidgetOfExactType() {} +} +'''); + 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(); +} +'''); + } + + Future + 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 + 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 + 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 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 + 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 + 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 + 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 + 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 bd85ebe85cf7d..5f2419e2289cd 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 'add_type_parameter_test.dart' as add_type_parameter_change; 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 @@ void main() { code_template.main(); diagnostics.main(); end_to_end.main(); + flutter_use_case.main(); modify_parameters.main(); rename_change.main(); transform_set_manager.main(); From 891a8a1aa3ee27332953abe53467f43f2674c2af Mon Sep 17 00:00:00 2001 From: Robert Nystrom Date: Mon, 19 Oct 2020 22:24:28 +0000 Subject: [PATCH 2/4] Add new "scrape" package. The README explains, it but, in short, this is a little library I harvested from the scripts I wrote during ui-as-code. It makes it easier to write scripts to gather bits of statistical data from Dart syntax. Change-Id: Ia33797f582c4d4f9f966b7f6dc7b58ca2414601e Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/166788 Commit-Queue: Bob Nystrom Reviewed-by: Phil Quitslund Auto-Submit: Bob Nystrom --- pkg/scrape/.gitignore | 6 + pkg/scrape/README.md | 108 ++++++++ pkg/scrape/analysis_options.yaml | 5 + pkg/scrape/example/build_control_flow.dart | 119 ++++++++ pkg/scrape/example/class_reuse.dart | 46 +++ pkg/scrape/example/control_flow.dart | 52 ++++ pkg/scrape/example/generic_classes.dart | 30 ++ pkg/scrape/example/if.dart | 24 ++ pkg/scrape/example/nesting.dart | 164 +++++++++++ pkg/scrape/example/null_aware.dart | 307 +++++++++++++++++++++ pkg/scrape/example/spread_sizes.dart | 56 ++++ pkg/scrape/lib/scrape.dart | 291 +++++++++++++++++++ pkg/scrape/lib/src/error_listener.dart | 21 ++ pkg/scrape/lib/src/histogram.dart | 100 +++++++ pkg/scrape/lib/src/scrape_visitor.dart | 139 ++++++++++ pkg/scrape/pubspec.yaml | 12 + 16 files changed, 1480 insertions(+) create mode 100644 pkg/scrape/.gitignore create mode 100644 pkg/scrape/README.md create mode 100644 pkg/scrape/analysis_options.yaml create mode 100644 pkg/scrape/example/build_control_flow.dart create mode 100644 pkg/scrape/example/class_reuse.dart create mode 100644 pkg/scrape/example/control_flow.dart create mode 100644 pkg/scrape/example/generic_classes.dart create mode 100644 pkg/scrape/example/if.dart create mode 100644 pkg/scrape/example/nesting.dart create mode 100644 pkg/scrape/example/null_aware.dart create mode 100644 pkg/scrape/example/spread_sizes.dart create mode 100644 pkg/scrape/lib/scrape.dart create mode 100644 pkg/scrape/lib/src/error_listener.dart create mode 100644 pkg/scrape/lib/src/histogram.dart create mode 100644 pkg/scrape/lib/src/scrape_visitor.dart create mode 100644 pkg/scrape/pubspec.yaml diff --git a/pkg/scrape/.gitignore b/pkg/scrape/.gitignore new file mode 100644 index 0000000000000..37c2b4481ab22 --- /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 0000000000000..58b0cda98fbe6 --- /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 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 0000000000000..f6dcda64cc4b9 --- /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 0000000000000..bdd851a951bbc --- /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 = []; + +void main(List arguments) { + Scrape() + ..addHistogram('build methods') + ..addVisitor(() => ControlFlowVisitor()) + ..runCommandLine(arguments); + + _buildMethods.shuffle(); + _buildMethods.take(100).forEach(print); +} + +class ControlFlowVisitor extends ScrapeVisitor { + final List _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 0000000000000..8fb60c65bb6e8 --- /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 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 0000000000000..e8c9dacf5e2c2 --- /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 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 0000000000000..09ecc3248a387 --- /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 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 0000000000000..57573ffe90177 --- /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 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 0000000000000..6541d6d64caf9 --- /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 = {}; + +bool simplifyNames = false; + +void main(List 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 _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 0000000000000..392798e40022e --- /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 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 0000000000000..d1257e1a9b903 --- /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 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 0000000000000..635949479e7af --- /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 _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 _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 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] '); + 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 0000000000000..69c91180eaf33 --- /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 0000000000000..12f2b9fef206f --- /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 _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( + 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( + 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 0000000000000..0b8a40518999a --- /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 { + // 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 0000000000000..a0116443030dc --- /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 From 92704cf8e4386e7bfb6b8ac5d368f12876d94efe Mon Sep 17 00:00:00 2001 From: Konstantin Shcheglov Date: Mon, 19 Oct 2020 23:25:48 +0000 Subject: [PATCH 3/4] Don't set types for class/constructor/getter identifiers of annotations. They are not expressions, and so don't have types. Actually setting a type for a constructor of a generic class breaks summary serialization because `T Function()` type of the constructor `class A { const A.named()l }` is not valid - `T` is not in scope. So, this crashed the linker. Change-Id: I25a6e5eb6902faa479e4dcb683ef3936af07b5b9 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/168420 Reviewed-by: Samuel Rawlins Reviewed-by: Brian Wilkerson Commit-Queue: Konstantin Shcheglov --- pkg/analyzer/CHANGELOG.md | 2 + .../dart/resolver/annotation_resolver.dart | 19 --- .../dart/analysis/driver_resolution_test.dart | 24 ++-- .../src/dart/resolution/metadata_test.dart | 116 +++++++++++++++++- .../test/src/summary/resynthesize_common.dart | 22 ++-- 5 files changed, 140 insertions(+), 43 deletions(-) diff --git a/pkg/analyzer/CHANGELOG.md b/pkg/analyzer/CHANGELOG.md index 13a6e340cbb6d..e67947ef79079 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 92fef68bfac32..bd112a863ad1d 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 @@ class AnnotationResolver { 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 @@ class AnnotationResolver { 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 @@ class AnnotationResolver { [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 7f16523477949..e955fb1886bbb 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 @@ void topLevelFunction() {} 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 @@ const a = 1; SimpleIdentifier aRef = annotation.name; assertElement(aRef, findElement.topGet('a')); - assertType(aRef, 'int'); + assertTypeNull(aRef); } test_annotation_onDirective_import() async { @@ -222,7 +222,7 @@ const a = 1; SimpleIdentifier aRef = annotation.name; assertElement(aRef, findElement.topGet('a')); - assertType(aRef, 'int'); + assertTypeNull(aRef); } test_annotation_onDirective_library() async { @@ -242,7 +242,7 @@ const a = 1; SimpleIdentifier aRef = annotation.name; assertElement(aRef, findElement.topGet('a')); - assertType(aRef, 'int'); + assertTypeNull(aRef); } test_annotation_onDirective_part() async { @@ -265,7 +265,7 @@ const a = 1; SimpleIdentifier aRef = annotation.name; assertElement(aRef, findElement.topGet('a')); - assertType(aRef, 'int'); + assertTypeNull(aRef); } test_annotation_onDirective_partOf() async { @@ -288,7 +288,7 @@ const a = 1; SimpleIdentifier aRef = annotation.name; assertElement(aRef, findElement.topGet('a')); - assertType(aRef, 'int'); + assertTypeNull(aRef); } test_annotation_onFormalParameter_redirectingFactory() async { @@ -310,7 +310,7 @@ const c = 2; SimpleIdentifier ref = annotation.name; assertElement(ref, getter); - assertType(ref, 'int'); + assertTypeNull(ref); } { @@ -386,7 +386,7 @@ class C { 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 @@ class A { 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 @@ class A { 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 @@ void main() { 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 0e2a55e33be1e..c325cf2c6c366 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 @@ main() { @reflectiveTest class MetadataResolutionTest extends PubPackageResolutionTest { + test_genericClass_instanceGetter() async { + await resolveTestCode(r''' +class A { + 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 { + 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 { + 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 { + 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 @@ Annotation 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 863692fce1ea3..a3cfc3cc645b2 100644 --- a/pkg/analyzer/test/src/summary/resynthesize_common.dart +++ b/pkg/analyzer/test/src/summary/resynthesize_common.dart @@ -8884,7 +8884,7 @@ class C { 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 @@ class C { element: self::@getter::foo name: SimpleIdentifier staticElement: self::@getter::foo - staticType: int + staticType: null token: foo typeParameters T @@ -8903,7 +8903,7 @@ class C { element: self::@getter::foo name: SimpleIdentifier staticElement: self::@getter::foo - staticType: int + staticType: null token: foo const int foo; constantInitializer @@ -9121,7 +9121,7 @@ extension E on int { 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 @@ extension E on int { element: self::@getter::foo name: SimpleIdentifier staticElement: self::@getter::foo - staticType: int + staticType: null token: foo typeParameters T @@ -9140,7 +9140,7 @@ extension E on int { element: self::@getter::foo name: SimpleIdentifier staticElement: self::@getter::foo - staticType: int + staticType: null token: foo const int foo; constantInitializer @@ -9430,7 +9430,7 @@ mixin M on Object { 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 @@ mixin M on Object { element: self::@getter::foo name: SimpleIdentifier staticElement: self::@getter::foo - staticType: int + staticType: null token: foo typeParameters T @@ -9449,7 +9449,7 @@ mixin M on Object { element: self::@getter::foo name: SimpleIdentifier staticElement: self::@getter::foo - staticType: int + staticType: null token: foo const int foo; constantInitializer @@ -11789,7 +11789,7 @@ class C { element: name: SimpleIdentifier staticElement: - staticType: dynamic + staticType: null token: foo ''', withFullyResolvedAst: true); @@ -11817,7 +11817,7 @@ class C { element: name: SimpleIdentifier staticElement: - staticType: dynamic + staticType: null token: v ''', withFullyResolvedAst: true); From f3962367a5e1bda1f77b2102557c14833862e558 Mon Sep 17 00:00:00 2001 From: Nicholas Shahan Date: Tue, 20 Oct 2020 00:48:08 +0000 Subject: [PATCH 4/4] [ddc] Add language version to test files Avoids breaks that appear when enabling the `nnbd` experiment by default as seen in: https://dart-review.googlesource.com/c/sdk/+/166790 Some of these synthetic files are created as packages but the expression compiler wasn't designed to handle that information. In the future we should support language versioning better in the expression compiler or setup similar tests for opted in code since in reality the language versioning might be handled by the dev build system. Fixes: https://github.com/dart-lang/sdk/issues/43843 Change-Id: Icfac40d1c9b47e75fb92d7672700e066ac372267 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/168441 Reviewed-by: Anna Gringauze Commit-Queue: Nicholas Shahan --- .../expression_compiler_test.dart | 7 +++++++ .../expression_compiler_worker_test.dart | 19 +++++++++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) 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 f3e46aa78735f..5636b5f119f31 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 @@ void main() { 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 @@ void main() { 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 @@ void main() { 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 @@ void main() { 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 @@ void main() { 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 @@ void main() { 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 @@ void main() { 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 1e649aa9afb5f..65167c7e83cd0 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 @@ _testPackage:lib/ 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 @@ void main() { 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 @@ class B { 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 @@ void main() async { requestController.add({ 'command': 'CompileExpression', 'expression': 'formal', - 'line': 4, + 'line': 5, 'column': 1, 'jsModules': {}, 'jsScope': {'formal': 'formal'}, @@ -291,7 +294,7 @@ void main() async { requestController.add({ 'command': 'CompileExpression', 'expression': 'count', - 'line': 7, + 'line': 8, 'column': 1, 'jsModules': {}, 'jsScope': {'count': 'count'}, @@ -324,7 +327,7 @@ void main() async { requestController.add({ 'command': 'CompileExpression', 'expression': 'B().c().getNumber()', - 'line': 7, + 'line': 8, 'column': 1, 'jsModules': {}, 'jsScope': {}, @@ -445,7 +448,7 @@ void main() async { requestController.add({ 'command': 'CompileExpression', 'expression': 'formal', - 'line': 4, + 'line': 5, 'column': 1, 'jsModules': {}, 'jsScope': {'formal': 'formal'}, @@ -477,7 +480,7 @@ void main() async { requestController.add({ 'command': 'CompileExpression', 'expression': 'count', - 'line': 7, + 'line': 8, 'column': 1, 'jsModules': {}, 'jsScope': {'count': 'count'},