Add script to scrape metadata annotation arguments.

Change-Id: Icbe5c84cfa5cb2fe376679b267452d6dcfe61ac8
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/206582
Reviewed-by: Jake Macdonald <jakemac@google.com>
Commit-Queue: Bob Nystrom <rnystrom@google.com>
diff --git a/pkg/scrape/example/annotation_arguments.dart b/pkg/scrape/example/annotation_arguments.dart
new file mode 100644
index 0000000..cf5da95
--- /dev/null
+++ b/pkg/scrape/example/annotation_arguments.dart
@@ -0,0 +1,81 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:scrape/scrape.dart';
+
+void main(List<String> arguments) {
+  Scrape()
+    ..addHistogram('Has argument list?')
+    ..addHistogram('Arguments', order: SortOrder.numeric)
+    ..addHistogram('Argument type')
+    ..addHistogram('Argument identifier')
+    ..addHistogram('Annotation')
+    ..addVisitor(() => AnnotationVisitor())
+    ..runCommandLine(arguments);
+}
+
+class AnnotationVisitor extends ScrapeVisitor {
+  @override
+  void visitAnnotation(Annotation node) {
+    record('Annotation', node.name.name);
+
+    var arguments = node.arguments;
+    if (arguments != null) {
+      record('Has argument list?', 'yes');
+      record('Arguments', arguments.arguments.length);
+      arguments.arguments.forEach(_recordArgument);
+    } else {
+      record('Has argument list?', 'no');
+    }
+
+    super.visitAnnotation(node);
+  }
+
+  void _recordArgument(AstNode? node) {
+    if (node is NamedExpression) {
+      _recordArgument(node.expression);
+    } else if (node is IfElement) {
+      _recordArgument(node.thenElement);
+      _recordArgument(node.elseElement);
+    } else if (node is ForElement) {
+      _recordArgument(node.body);
+    } else if (node is SpreadElement) {
+      _recordArgument(node.expression);
+    } else if (node is MapLiteralEntry) {
+      _recordArgument(node.key);
+      _recordArgument(node.value);
+    } else if (node is SimpleIdentifier || node is PrefixedIdentifier) {
+      record('Argument identifier', node.toString());
+      record('Argument type', 'identifier');
+    } else if (node is PrefixExpression) {
+      record('Argument type', 'unary operator');
+    } else if (node is BinaryExpression) {
+      record('Argument type', 'binary operator');
+    } else if (node is BooleanLiteral) {
+      record('Argument type', 'bool');
+    } else if (node is DoubleLiteral) {
+      record('Argument type', 'double');
+    } else if (node is IntegerLiteral) {
+      record('Argument type', 'int');
+    } else if (node is ListLiteral) {
+      record('Argument type', 'list');
+      node.elements.forEach(_recordArgument);
+    } else if (node is MethodInvocation) {
+      record('Argument type', 'method call');
+    } else if (node is NullLiteral) {
+      record('Argument type', 'null');
+    } else if (node is SetOrMapLiteral) {
+      record('Argument type', 'set or map');
+      node.elements.forEach(_recordArgument);
+    } else if (node is StringLiteral) {
+      record('Argument type', 'string');
+    } else if (node is SymbolLiteral) {
+      record('Argument type', 'symbol');
+    } else if (node == null) {
+      // Do nothing. Only happens for null else elements.
+    } else {
+      record('Argument type', node.runtimeType.toString());
+    }
+  }
+}
diff --git a/pkg/scrape/lib/src/histogram.dart b/pkg/scrape/lib/src/histogram.dart
index 74e35df..e27f137 100644
--- a/pkg/scrape/lib/src/histogram.dart
+++ b/pkg/scrape/lib/src/histogram.dart
@@ -86,14 +86,25 @@
       }
     }
 
-    if (skipped > 0) print('And $skipped more less than 0.1%...');
+    if (skipped > 0) print('And $skipped more...');
 
     // If we're counting numeric keys, show other statistics too.
     if (_order == SortOrder.numeric && keys.isNotEmpty) {
       var sum = keys.fold<int>(
           0, (result, key) => result + (key as int) * _counts[key]!);
       var average = sum / total;
-      var median = _counts[keys[keys.length ~/ 2]];
+
+      // Find the median key where half the total count is below it.
+      var count = 0;
+      var median = -1;
+      for (var key in keys) {
+        count += _counts[key]!;
+        if (count >= total ~/ 2) {
+          median = key as int;
+          break;
+        }
+      }
+
       print('Sum $sum, average ${average.toStringAsFixed(3)}, median $median');
     }
   }