Add a tool to detect orphaned Dart files in "test/".

Change-Id: I20322067f2cb65c858adfbfef5f785322b8a7a6c
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/198060
Auto-Submit: Bob Nystrom <rnystrom@google.com>
Reviewed-by: Nate Bosch <nbosch@google.com>
Commit-Queue: Nate Bosch <nbosch@google.com>
diff --git a/pkg/test_runner/pubspec.yaml b/pkg/test_runner/pubspec.yaml
index 9fad3c1..506b9b2 100644
--- a/pkg/test_runner/pubspec.yaml
+++ b/pkg/test_runner/pubspec.yaml
@@ -20,6 +20,8 @@
   status_file:
     path: ../status_file
 dev_dependencies:
+  analyzer:
+    path: ../../pkg/analyzer
   expect:
     path: ../expect
   file: any
@@ -27,6 +29,8 @@
 dependency_overrides:
   # Other packages in the dependency graph have normal hosted dependencies on
   # this, so just override it to force the local one.
+  analyzer:
+    path: ../../pkg/analyzer
   args:
     path: ../../third_party/pkg/args
   file:
diff --git a/pkg/test_runner/tool/orphan_files.dart b/pkg/test_runner/tool/orphan_files.dart
new file mode 100644
index 0000000..8d7e8f6
--- /dev/null
+++ b/pkg/test_runner/tool/orphan_files.dart
@@ -0,0 +1,92 @@
+// Copyright (c) 2019, 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.
+
+/// Looks for ".dart" files in "tests/" that appear to be orphaned. That means
+/// they don't end in "_test.dart" so aren't run as tests by the test_runner,
+/// but they also don't appear to be referenced by any other tests.
+///
+/// Usually this means that someone accidentally left off the "_test" and the
+/// file is supposed to be a test but is silently getting ignored.
+import 'dart:io';
+
+import 'package:analyzer/dart/analysis/analysis_context.dart';
+import 'package:analyzer/dart/analysis/context_builder.dart';
+import 'package:analyzer/dart/analysis/context_locator.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:analyzer/dart/ast/ast.dart';
+
+import 'package:test_runner/src/path.dart';
+
+AnalysisContext _analysisContext;
+
+void main(List<String> arguments) {
+  _initAnalysisContext();
+
+  var suites = Directory('tests').listSync();
+  suites.sort((a, b) => a.path.compareTo(b.path));
+
+  for (var entry in suites) {
+    // Skip the co19 tests since they don't use '_test.dart'.
+    if (entry is Directory && !entry.path.contains('co19')) {
+      _checkTestDirectory(entry);
+    }
+  }
+}
+
+void _initAnalysisContext() {
+  var roots = ContextLocator().locateRoots(includedPaths: ['test']);
+  if (roots.length != 1) {
+    throw StateError('Expected to find exactly one context root, got $roots');
+  }
+
+  _analysisContext = ContextBuilder().createContext(contextRoot: roots[0]);
+}
+
+void _checkTestDirectory(Directory directory) {
+  print('-- ${directory.path} --');
+  var paths = directory
+      .listSync(recursive: true)
+      .map((entry) => entry.path)
+      .where((path) => path.endsWith('.dart'))
+      .toList();
+  paths.sort();
+
+  // Collect the set of all files that are known to be referred to by others.
+  print('Finding referenced files...');
+  var importedPaths = <String>{};
+  for (var path in paths) {
+    _parseReferences(importedPaths, path);
+  }
+
+  // Find the ".dart" files that don't end in "_test.dart" but also aren't used
+  // by another library. Those should probably be tests.
+  var hasOrphan = false;
+  for (var path in paths) {
+    if (!path.endsWith('_test.dart') && !importedPaths.contains(path)) {
+      print('Suspected orphan: $path');
+      hasOrphan = true;
+    }
+  }
+
+  if (!hasOrphan) print('No orphans :)');
+}
+
+void _parseReferences(Set<String> importedPaths, String filePath) {
+  var absolute = Path(filePath).absolute.toNativePath();
+  var parseResult = _analysisContext.currentSession.getParsedUnit2(absolute);
+  var unit = (parseResult as ParsedUnitResult).unit;
+
+  void add(String importPath) {
+    if (importPath.startsWith('dart:')) return;
+
+    var resolved = Uri.file(filePath).resolve(importPath).path;
+    importedPaths.add(resolved);
+  }
+
+  for (var directive in unit.directives) {
+    if (directive is UriBasedDirective) {
+      add(directive.uri.stringValue);
+    }
+  }
+}