blob: 19c719c40fd10f72676a251fb9f1aa4fe724b6a0 [file] [log] [blame]
// 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.
// ignore_for_file: implementation_imports
import 'dart:io';
import 'dart:math' as math;
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/src/dart/scanner/reader.dart';
import 'package:analyzer/src/dart/scanner/scanner.dart';
import 'package:analyzer/src/generated/parser.dart';
import 'package:analyzer/src/string_source.dart';
import 'package:args/args.dart';
import 'package:path/path.dart' as p;
import 'src/error_listener.dart';
import 'src/histogram.dart';
import 'src/scrape_visitor.dart';
export 'src/histogram.dart';
export 'src/scrape_visitor.dart' hide bindVisitor;
class Scrape {
final List<ScrapeVisitor Function()> _visitorFactories = [];
/// What percent of files should be processed.
int? _percent;
/// Process package test files.
bool _includeTests = true;
/// Process Dart SDK language tests.
bool _includeLanguageTests = true;
/// Process Dart files generated from protobufs.
bool _includeProtobufs = false;
/// Whether every file should be printed before being processed.
bool _printFiles = true;
/// Whether parse errors should be printed.
bool _printErrors = true;
/// The number of files that have been processed.
int get scrapedFileCount => _scrapedFileCount;
int _scrapedFileCount = 0;
/// The number of lines of code that have been processed.
int get scrapedLineCount => _scrapedLineCount;
int _scrapedLineCount = 0;
/// The number of files that could not be parsed.
int get errorFileCount => _errorFileCount;
int _errorFileCount = 0;
final Map<String, Histogram> _histograms = {};
/// Whether we're in the middle of writing the running file count and need a
/// newline before any other output should be shown.
bool _needClearLine = false;
/// Register a new visitor factory.
///
/// This function will be called for each scraped file and the resulting
/// [ScrapeVisitor] will traverse the parsed file's AST.
void addVisitor(ScrapeVisitor Function() createVisitor) {
_visitorFactories.add(createVisitor);
}
/// Defines a new histogram with [name] to collect occurrences.
///
/// After the scrape completes, each defined histogram's collected counts
/// are shown, ordered by [order]. If [showBar] is not `false`, then shows an
/// ASCII bar chart for the counts.
///
/// If [showAll] is `true`, then every item that occurred is shown. Otherwise,
/// only shows the first 100 items or occurrences that represent at least
/// 0.1% of the total, whichever is longer.
///
/// If [minCount] is passed, then only shows items that occurred at least
/// that many times.
void addHistogram(String name,
{SortOrder order = SortOrder.descending,
bool? showBar,
bool? showAll,
int? minCount}) {
_histograms.putIfAbsent(
name,
() => Histogram(
order: order,
showBar: showBar,
showAll: showAll,
minCount: minCount));
}
/// Add an occurrence of [item] to [histogram].
void record(String histogram, Object item) {
_histograms[histogram]!.add(item);
}
/// Run the scrape using the given set of command line arguments.
void runCommandLine(List<String> arguments) {
var parser = ArgParser(allowTrailingOptions: true);
parser.addOption('percent',
help: 'Only process a randomly selected percentage of files.');
parser.addFlag('tests',
defaultsTo: true, help: 'Process package test files.');
parser.addFlag('language-tests',
help: 'Process Dart SDK language test files.');
parser.addFlag('protobufs',
help: 'Process Dart files generated from protobufs.');
parser.addFlag('print-files',
defaultsTo: true, help: 'Print the path for each parsed file.');
parser.addFlag('print-errors',
defaultsTo: true, help: 'Print parse errors.');
parser.addFlag('help', negatable: false, help: 'Print help text.');
var results = parser.parse(arguments);
if (results['help'] as bool) {
var script = p.url.basename(Platform.script.toString());
print('Usage: $script [options] <paths...>');
print(parser.usage);
return;
}
_includeTests = results['tests'] as bool;
_includeLanguageTests = results['language-tests'] as bool;
_includeProtobufs = results['protobufs'] as bool;
_printFiles = results['print-files'] as bool;
_printErrors = results['print-errors'] as bool;
if (results.wasParsed('percent')) {
_percent = int.tryParse(results['percent'] as String);
if (_percent == null) {
print("--percent must be an integer, was '${results["percent"]}'.");
exit(1);
}
}
if (results.rest.isEmpty) {
print('Must pass at least one path to process.');
exit(1);
}
var watch = Stopwatch()..start();
for (var path in results.rest) {
_processPath(path);
}
watch.stop();
clearLine();
_histograms.forEach((name, histogram) {
histogram.printCounts(name);
});
String count(int n, String unit) {
if (n == 1) return '1 $unit';
return '$n ${unit}s';
}
var elapsed = _formatDuration(watch.elapsed);
var lines = count(_scrapedLineCount, 'line');
var files = count(_scrapedFileCount, 'file');
var message = 'Took $elapsed to scrape $lines in $files.';
if (_errorFileCount > 0) {
var errors = count(_errorFileCount, 'file');
message += ' ($errors could not be parsed.)';
}
print(message);
}
/// 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('/_fe_analyzer_shared/test/')) continue;
if (entry.path.contains('/analysis_server/test/')) continue;
if (entry.path.contains('/analyzer/test/')) continue;
if (entry.path.contains('/analyzer_cli/test/')) continue;
if (entry.path.contains('/compiler/test/')) continue;
if (entry.path.contains('/dart/runtime/observatory/tests/')) continue;
if (entry.path.contains('/dart/runtime/observatory_2/tests/')) continue;
if (entry.path.contains('/dart/runtime/tests/')) continue;
if (entry.path.contains('/dart/tests/')) continue;
if (entry.path.contains('/dev_compiler/test/')) continue;
if (entry.path.contains('/front_end/parser_testcases/')) continue;
if (entry.path.contains('/front_end/test/')) continue;
if (entry.path.contains('/kernel/test/')) continue;
if (entry.path.contains('/linter/test/_data/')) continue;
if (entry.path.contains('/testcases/')) 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('/.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, relative);
}
}
void _parseFile(File file, String shortPath) {
var source = file.readAsStringSync();
var errorListener = ErrorListener(this, _printErrors);
var featureSet = FeatureSet.latestLanguageVersion();
// Tokenize the source.
var reader = CharSequenceReader(source);
var stringSource = StringSource(source, file.path);
var scanner = Scanner(stringSource, reader, errorListener);
scanner.configureFeatures(
featureSet: featureSet, featureSetForOverriding: featureSet);
var startToken = scanner.tokenize();
var lineInfo = LineInfo(scanner.lineStarts);
// Parse it.
var parser = Parser(stringSource, errorListener,
featureSet: featureSet, lineInfo: lineInfo);
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;
}
// Don't process files with syntax errors.
if (errorListener.hadError) {
_errorFileCount++;
return;
}
_scrapedFileCount++;
_scrapedLineCount += lineInfo.lineCount;
for (var visitorFactory in _visitorFactories) {
var visitor = visitorFactory();
bindVisitor(visitor, this, shortPath, source, lineInfo);
node.accept(visitor);
}
}
}