flutter_flutter/ci/bin/lint.dart
Greg Spencer 2218513712
Clean up the CI scripts and make it so that they can be run from anywhere (#20538)
This cleans up the ci scripts so that they can be run from an arbitrary directory, and so that they don't have any bash lint issues, and are more explicit about which dart/pub/dartanalyzer executable they run.

I also fixed the format script to take a "--fix" argument that will fix all of the formatting issues found, including trailing whitespace and gn files.

I added a warning to the license script about untracked/ignored files in the fluttter repo because those so often trip up the license script.

I added missing license information to the ci scripts too.

There's now a bit of boilerplate at the beginning of each script (the follow_links function) in order to reliably find the actual location of the script: I'd put it into a common file, except that that code would be needed to reliably find the common location too, so I needed to duplicate it. It's the same boilerplate as what is used in the flutter/flutter repo for the flutter and dart scripts.

I deleted the ci/test.sh script, since it seems to be obsolete (the test it tries to run doesn't exist anywhere).
2020-08-17 14:03:01 -07:00

261 lines
9.6 KiB
Dart

// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Runs clang-tidy on files with changes.
//
// usage:
// dart lint.dart <path to compile_commands.json> <path to git repository> [clang-tidy checks]
//
// User environment variable FLUTTER_LINT_ALL to run on all files.
import 'dart:async' show Completer;
import 'dart:convert' show jsonDecode, utf8, LineSplitter;
import 'dart:io' show File, exit, Directory, FileSystemEntity, Platform, stderr;
import 'package:args/args.dart';
import 'package:path/path.dart' as path;
import 'package:process_runner/process_runner.dart';
String _linterOutputHeader = '''
┌──────────────────────────┐
│ Engine Clang Tidy Linter │
└──────────────────────────┘
The following errors have been reported by the Engine Clang Tidy Linter. For
more information on addressing these issues please see:
https://github.com/flutter/flutter/wiki/Engine-Clang-Tidy-Linter
''';
class Command {
Directory directory = Directory('');
String command = '';
File file = File('');
}
Command parseCommand(Map<String, dynamic> map) {
final Directory dir = Directory(map['directory'] as String).absolute;
return Command()
..directory = dir
..command = map['command'] as String
..file = File(path.normalize(path.join(dir.path, map['file'] as String)));
}
String calcTidyArgs(Command command) {
String result = command.command;
result = result.replaceAll(RegExp(r'\S*clang/bin/clang'), '');
result = result.replaceAll(RegExp(r'-MF \S*'), '');
return result;
}
String calcTidyPath(Command command) {
final RegExp regex = RegExp(r'\S*clang/bin/clang');
return regex
.stringMatch(command.command)
?.replaceAll('clang/bin/clang', 'clang/bin/clang-tidy') ??
'';
}
bool isNonEmptyString(String str) => str.isNotEmpty;
bool containsAny(File file, Iterable<File> queries) {
return queries.where((File query) => path.equals(query.path, file.path)).isNotEmpty;
}
/// Returns a list of all non-deleted files which differ from the nearest
/// merge-base with `master`. If it can't find a fork point, uses the default
/// merge-base.
Future<List<File>> getListOfChangedFiles(Directory repoPath) async {
final ProcessRunner processRunner = ProcessRunner(defaultWorkingDirectory: repoPath);
final ProcessRunnerResult fetchResult = await processRunner.runProcess(
<String>['git', 'fetch', 'upstream', 'master'],
failOk: true,
);
if (fetchResult.exitCode != 0) {
await processRunner.runProcess(<String>['git', 'fetch', 'origin', 'master']);
}
final Set<String> result = <String>{};
ProcessRunnerResult mergeBaseResult = await processRunner.runProcess(
<String>['git', 'merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'],
failOk: true);
if (mergeBaseResult.exitCode != 0) {
if (verbose) {
stderr.writeln("Didn't find a fork point, falling back to default merge base.");
}
mergeBaseResult = await processRunner
.runProcess(<String>['git', 'merge-base', 'FETCH_HEAD', 'HEAD'], failOk: false);
}
final String mergeBase = mergeBaseResult.stdout.trim();
final ProcessRunnerResult masterResult = await processRunner
.runProcess(<String>['git', 'diff', '--name-only', '--diff-filter=ACMRT', mergeBase]);
result.addAll(masterResult.stdout.split('\n').where(isNonEmptyString));
return result.map<File>((String filePath) => File(path.join(repoPath.path, filePath))).toList();
}
Future<List<File>> dirContents(Directory dir) {
final List<File> files = <File>[];
final Completer<List<File>> completer = Completer<List<File>>();
final Stream<FileSystemEntity> lister = dir.list(recursive: true);
lister.listen((FileSystemEntity file) => file is File ? files.add(file) : null,
onError: (Object e) => completer.completeError(e), onDone: () => completer.complete(files));
return completer.future;
}
File buildFileAsRepoFile(String buildFile, Directory repoPath) {
// Removes the "../../flutter" from the build files to make it relative to the flutter
// dir.
final String relativeBuildFile = path.joinAll(path.split(buildFile).sublist(3));
final File result = File(path.join(repoPath.absolute.path, relativeBuildFile));
print('Build file: $buildFile => ${result.path}');
return result;
}
Future<String> shouldIgnoreFile(File file) async {
if (path.split(file.path).contains('third_party')) {
return 'third_party';
} else {
final RegExp exp = RegExp(r'//\s*FLUTTER_NOLINT');
await for (String line
in file.openRead().transform(utf8.decoder).transform(const LineSplitter())) {
if (exp.hasMatch(line)) {
return 'FLUTTER_NOLINT';
} else if (line.isNotEmpty && line[0] != '\n' && line[0] != '/') {
// Quick out once we find a line that isn't empty or a comment. The
// FLUTTER_NOLINT must show up before the first real code.
return '';
}
}
return '';
}
}
void _usage(ArgParser parser, {int exitCode = 1}) {
stderr.writeln('lint.dart [--help] [--lint-all] [--verbose] [--diff-branch]');
stderr.writeln(parser.usage);
exit(exitCode);
}
bool verbose = false;
void main(List<String> arguments) async {
final ArgParser parser = ArgParser();
parser.addFlag('help', help: 'Print help.');
parser.addFlag('lint-all',
help: 'lint all of the sources, regardless of FLUTTER_NOLINT.', defaultsTo: false);
parser.addFlag('verbose', help: 'Print verbose output.', defaultsTo: verbose);
parser.addOption('repo', help: 'Use the given path as the repo path');
parser.addOption('compile-commands',
help: 'Use the given path as the source of compile_commands.json. This '
'file is created by running tools/gn');
parser.addOption('checks',
help: 'Perform the given checks on the code. Defaults to the empty '
'string, indicating all checks should be performed.',
defaultsTo: '');
final ArgResults options = parser.parse(arguments);
verbose = options['verbose'] as bool;
if (options['help'] as bool) {
_usage(parser, exitCode: 0);
}
if (!options.wasParsed('compile-commands')) {
stderr.writeln('ERROR: The --compile-commands argument is requried.');
_usage(parser);
}
if (!options.wasParsed('repo')) {
stderr.writeln('ERROR: The --repo argument is requried.');
_usage(parser);
}
final File buildCommandsPath = File(options['compile-commands'] as String);
if (!buildCommandsPath.existsSync()) {
stderr.writeln("ERROR: Build commands path ${buildCommandsPath.absolute.path} doesn't exist.");
_usage(parser);
}
final Directory repoPath = Directory(options['repo'] as String);
if (!repoPath.existsSync()) {
stderr.writeln("ERROR: Repo path ${repoPath.absolute.path} doesn't exist.");
_usage(parser);
}
print(_linterOutputHeader);
final String checksArg = options.wasParsed('checks') ? options['checks'] as String : '';
final String checks = checksArg.isNotEmpty ? '--checks=$checksArg' : '--config=';
final bool lintAll =
Platform.environment['FLUTTER_LINT_ALL'] != null || options['lint-all'] as bool;
final List<File> changedFiles =
lintAll ? await dirContents(repoPath) : await getListOfChangedFiles(repoPath);
if (verbose) {
print('Checking lint in repo at $repoPath.');
if (checksArg.isNotEmpty) {
print('Checking for specific checks: $checks.');
}
if (lintAll) {
print('Checking all ${changedFiles.length} files the repo dir.');
} else {
print('Dectected ${changedFiles.length} files that have changed');
}
}
final List<dynamic> buildCommandMaps =
jsonDecode(await buildCommandsPath.readAsString()) as List<dynamic>;
final List<Command> buildCommands = buildCommandMaps
.map<Command>((dynamic x) => parseCommand(x as Map<String, dynamic>))
.toList();
final Command firstCommand = buildCommands[0];
final String tidyPath = calcTidyPath(firstCommand);
assert(tidyPath.isNotEmpty);
final List<Command> changedFileBuildCommands =
buildCommands.where((Command x) => containsAny(x.file, changedFiles)).toList();
if (changedFileBuildCommands.isEmpty) {
print('No changed files that have build commands associated with them '
'were found.');
exit(0);
}
if (verbose) {
print('Found ${changedFileBuildCommands.length} files that have build '
'commands associated with them and can be lint checked.');
}
int exitCode = 0;
final List<WorkerJob> jobs = <WorkerJob>[];
for (Command command in changedFileBuildCommands) {
final String relativePath = path.relative(command.file.path, from: repoPath.parent.path);
final String ignoreReason = await shouldIgnoreFile(command.file);
if (ignoreReason.isEmpty) {
final String tidyArgs = calcTidyArgs(command);
final List<String> args = <String>[command.file.path, checks, '--'];
args.addAll(tidyArgs?.split(' ') ?? <String>[]);
print('🔶 linting $relativePath');
jobs.add(WorkerJob(
<String>[tidyPath, ...args],
workingDirectory: command.directory,
name: 'clang-tidy on ${command.file.path}',
));
} else {
print('🔷 ignoring $relativePath ($ignoreReason)');
}
}
final ProcessPool pool = ProcessPool();
await for (final WorkerJob job in pool.startWorkers(jobs)) {
if (job.result?.stdout.isEmpty ?? true) {
continue;
}
print('❌ Failures for ${job.name}:');
print(job.result.stdout);
exitCode = 1;
}
print('\n');
if (exitCode == 0) {
print('No lint problems found.');
}
exit(exitCode);
}