analyzer: Tidy up Spelunker API

Work towards https://github.com/dart-lang/sdk/issues/55660

Spelunker will move to the analyzer_testing package, so we must first
clean it up.

Spelunker had two subclasses, but their only differentiation was the
calculation of a getter, which could be a final field. So I remove the
subclasses, move the "File"-based one to the spelunk utility script,
and change the Spelunker constructor to take in a source string.
Additionally:

* Make all of the fields private.
* Remove isDartFileName and isPubspecFileName. These calculations can
  be inlined into their call sites and make use of the `file_paths`
  library.

Change-Id: I4b97ed03a2845440dabd3a9ab0aa3b3ba4af5959
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/428083
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Samuel Rawlins <srawlins@google.com>
diff --git a/pkg/analyzer/lib/src/lint/io.dart b/pkg/analyzer/lib/src/lint/io.dart
index 6886b02..84ff615 100644
--- a/pkg/analyzer/lib/src/lint/io.dart
+++ b/pkg/analyzer/lib/src/lint/io.dart
@@ -4,21 +4,11 @@
 
 import 'dart:io';
 
-import 'package:analyzer/src/lint/util.dart';
-import 'package:path/path.dart' as p;
-
 /// A shared sink for standard error reporting.
 StringSink errorSink = stderr;
 
 /// A shared sink for standard out reporting.
 StringSink outSink = stdout;
 
-/// Returns `true` if this [entry] is a Dart file.
-bool isDartFile(FileSystemEntity entry) => isDartFileName(entry.path);
-
-/// Returns `true` if this [entry] is a pubspec file.
-bool isPubspecFile(FileSystemEntity entry) =>
-    isPubspecFileName(p.basename(entry.path));
-
 /// Synchronously read the contents of the file at the given [path] as a string.
 String readFile(String path) => File(path).readAsStringSync();
diff --git a/pkg/analyzer/lib/src/lint/util.dart b/pkg/analyzer/lib/src/lint/util.dart
index 0025ebe..ccef175 100644
--- a/pkg/analyzer/lib/src/lint/util.dart
+++ b/pkg/analyzer/lib/src/lint/util.dart
@@ -9,71 +9,20 @@
 import 'package:analyzer/dart/ast/ast.dart';
 import 'package:analyzer/dart/ast/token.dart';
 import 'package:analyzer/dart/ast/visitor.dart';
-import 'package:path/path.dart' as path;
 
-final _pubspec = RegExp(r'^[_]?pubspec\.yaml$');
+final class Spelunker {
+  final StringSink _sink;
+  final FeatureSet _featureSet;
+  final String _source;
 
-/// Create a library name prefix based on [libraryPath], [projectRoot] and
-/// current [packageName].
-String createLibraryNamePrefix({
-  required String libraryPath,
-  String? projectRoot,
-  String? packageName,
-}) {
-  // Use the posix context to canonicalize separators (`\`).
-  var libraryDirectory = path.posix.dirname(libraryPath);
-  var relativePath = path.posix.relative(libraryDirectory, from: projectRoot);
-  // Drop 'lib/'.
-  var segments = path.split(relativePath);
-  if (segments[0] == 'lib') {
-    relativePath = path.posix.joinAll(segments.sublist(1));
-  }
-  // Replace separators.
-  relativePath = relativePath.replaceAll('/', '.');
-  // Add separator if needed.
-  if (relativePath.isNotEmpty) {
-    relativePath = '.$relativePath';
-  }
-
-  return '$packageName$relativePath';
-}
-
-/// Returns `true` if this [fileName] is a Dart file.
-bool isDartFileName(String fileName) => fileName.endsWith('.dart');
-
-/// Returns `true` if this [fileName] is a Pubspec file.
-bool isPubspecFileName(String fileName) => _pubspec.hasMatch(fileName);
-
-class FileSpelunker extends _AbstractSpelunker {
-  final String path;
-  FileSpelunker(this.path, {super.sink, super.featureSet});
-  @override
-  String getSource() => File(path).readAsStringSync();
-}
-
-class StringSpelunker extends _AbstractSpelunker {
-  final String source;
-  StringSpelunker(this.source, {super.sink, super.featureSet});
-  @override
-  String getSource() => source;
-}
-
-abstract class _AbstractSpelunker {
-  final StringSink sink;
-  FeatureSet featureSet;
-
-  _AbstractSpelunker({StringSink? sink, FeatureSet? featureSet})
-    : sink = sink ?? stdout,
-      featureSet = featureSet ?? FeatureSet.latestLanguageVersion();
-
-  String getSource();
+  Spelunker(this._source, {StringSink? sink, FeatureSet? featureSet})
+    : _sink = sink ?? stdout,
+      _featureSet = featureSet ?? FeatureSet.latestLanguageVersion();
 
   void spelunk() {
-    var contents = getSource();
+    var parseResult = parseString(content: _source, featureSet: _featureSet);
 
-    var parseResult = parseString(content: contents, featureSet: featureSet);
-
-    var visitor = _SourceVisitor(sink);
+    var visitor = _SourceVisitor(_sink);
     parseResult.unit.accept(visitor);
   }
 }
@@ -85,8 +34,7 @@
 
   _SourceVisitor(this.sink);
 
-  String asString(AstNode node) =>
-      '${typeInfo(node.runtimeType)} [${node.toString()}]';
+  String asString(AstNode node) => '${typeInfo(node.runtimeType)} [$node]';
 
   List<CommentToken> getPrecedingComments(Token token) {
     var comments = <CommentToken>[];
diff --git a/pkg/linter/lib/src/test_utilities/test_linter.dart b/pkg/linter/lib/src/test_utilities/test_linter.dart
index 8c453a6..8be4f5c 100644
--- a/pkg/linter/lib/src/test_utilities/test_linter.dart
+++ b/pkg/linter/lib/src/test_utilities/test_linter.dart
@@ -11,9 +11,9 @@
 import 'package:analyzer/source/file_source.dart';
 import 'package:analyzer/source/source.dart';
 // ignore: implementation_imports
-import 'package:analyzer/src/lint/io.dart';
-// ignore: implementation_imports
 import 'package:analyzer/src/lint/pub.dart';
+// ignore: implementation_imports
+import 'package:analyzer/src/util/file_paths.dart' as file_paths;
 import 'package:meta/meta.dart';
 import 'package:path/path.dart' as path;
 
@@ -39,10 +39,11 @@
           resourceProvider ?? file_system.PhysicalResourceProvider.INSTANCE;
 
   Future<List<DiagnosticInfo>> lintFiles(List<File> files) async {
-    var errors = <DiagnosticInfo>[];
     var lintDriver = LintDriver(options, _resourceProvider);
-    errors.addAll(await lintDriver.analyze(files.where(isDartFile)));
-    for (var file in files.where(isPubspecFile)) {
+    var errors = await lintDriver.analyze(
+      files.where((f) => f.path.endsWith('.dart')),
+    );
+    for (var file in files.where(_isPubspecFile)) {
       lintPubspecSource(
         contents: file.readAsStringSync(),
         sourcePath: _resourceProvider.pathContext.normalize(file.absolute.path),
@@ -77,4 +78,8 @@
 
   @override
   void onError(Diagnostic error) => errors.add(error);
+
+  /// Returns whether this [entry] is a pubspec file.
+  bool _isPubspecFile(FileSystemEntity entry) =>
+      path.basename(entry.path) == file_paths.pubspecYaml;
 }
diff --git a/pkg/linter/test/rule_test_support.dart b/pkg/linter/test/rule_test_support.dart
index 0e818de..09e1ba4 100644
--- a/pkg/linter/test/rule_test_support.dart
+++ b/pkg/linter/test/rule_test_support.dart
@@ -446,7 +446,7 @@
         try {
           var astSink = StringBuffer();
 
-          StringSpelunker(
+          Spelunker(
             result.unit.toSource(),
             sink: astSink,
             featureSet: result.unit.featureSet,
diff --git a/pkg/linter/tool/benchmark.dart b/pkg/linter/tool/benchmark.dart
index b18f886..b9bf93f 100644
--- a/pkg/linter/tool/benchmark.dart
+++ b/pkg/linter/tool/benchmark.dart
@@ -9,7 +9,7 @@
 import 'package:analyzer/src/lint/config.dart';
 import 'package:analyzer/src/lint/io.dart';
 import 'package:analyzer/src/lint/registry.dart';
-import 'package:analyzer/src/lint/util.dart';
+import 'package:analyzer/src/util/file_paths.dart' as file_paths;
 import 'package:args/args.dart';
 import 'package:linter/src/analyzer.dart';
 import 'package:linter/src/extensions.dart';
@@ -266,7 +266,7 @@
 
   /// Whether this path is a Dart file or a Pubspec file.
   bool get isLintable =>
-      isDartFileName(this) || isPubspecFileName(path.basename(this));
+      endsWith('.dart') || path.basename(this) == file_paths.pubspecYaml;
 }
 
 extension on StringSink {
diff --git a/pkg/linter/tool/spelunk.dart b/pkg/linter/tool/spelunk.dart
index 0d20bd0..6b6ff15 100644
--- a/pkg/linter/tool/spelunk.dart
+++ b/pkg/linter/tool/spelunk.dart
@@ -2,7 +2,9 @@
 // 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/src/lint/util.dart' show FileSpelunker;
+import 'dart:io';
+
+import 'package:analyzer/src/lint/util.dart' show Spelunker;
 import 'package:args/args.dart';
 
 /// AST Spelunker
@@ -11,6 +13,7 @@
 
   var options = parser.parse(args);
   for (var path in options.rest) {
-    FileSpelunker(path).spelunk();
+    var source = File(path).readAsStringSync();
+    Spelunker(source).spelunk();
   }
 }