diff --git a/.cirrus.yml b/.cirrus.yml index 6f37dabf0c634..1a6720e9a3836 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -146,3 +146,7 @@ task: build_script: | cd $ENGINE_PATH/src/flutter ./ci/build.sh + - name: lint_test + lint_script: | + cd $ENGINE_PATH/src + ./flutter/ci/lint.sh diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000000000..fe6d3fcfd359f --- /dev/null +++ b/.clang-tidy @@ -0,0 +1 @@ +Checks: 'google-*' diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index be2534491f587..fff517be9de01 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -10,6 +10,7 @@ LIBRARY: tonic LIBRARY: txt ORIGIN: ../../../flutter/LICENSE TYPE: LicenseType.bsd +FILE: ../../../flutter/.clang-tidy FILE: ../../../flutter/DEPS FILE: ../../../flutter/assets/asset_manager.cc FILE: ../../../flutter/assets/asset_manager.h diff --git a/ci/lint.dart b/ci/lint.dart new file mode 100644 index 0000000000000..adff165eb4a98 --- /dev/null +++ b/ci/lint.dart @@ -0,0 +1,127 @@ +/// Runs clang-tidy on files with changes. +/// +/// usage: +/// dart lint.dart [clang-tidy checks] +/// +/// User environment variable FLUTTER_LINT_ALL to run on all files. + +import 'dart:io' + show + File, + Process, + ProcessResult, + exit, + Directory, + FileSystemEntity, + Platform; +import 'dart:convert' show jsonDecode, utf8; +import 'dart:async' show Completer; + +class Command { + String directory; + String command; + String file; +} + +Command parseCommand(Map map) { + return Command() + ..directory = map['directory'] + ..command = map['command'] + ..file = map['file']; +} + +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.length > 0; + +bool containsAny(String str, List queries) { + for (String query in queries) { + if (str.contains(query)) { + return true; + } + } + return false; +} + +/// Returns a list of all files with current changes or differ from `master`. +List getListOfChangedFiles(String repoPath) { + final Set result = Set(); + final ProcessResult diffResult = Process.runSync( + 'git', ['diff', '--name-only'], + workingDirectory: repoPath); + final ProcessResult diffCachedResult = Process.runSync( + 'git', ['diff', '--cached', '--name-only'], + workingDirectory: repoPath); + + final ProcessResult mergeBaseResult = Process.runSync( + 'git', ['merge-base', 'master', 'HEAD'], + workingDirectory: repoPath); + final String mergeBase = mergeBaseResult.stdout.trim(); + final ProcessResult masterResult = Process.runSync( + 'git', ['diff', '--name-only', mergeBase], + workingDirectory: repoPath); + result.addAll(diffResult.stdout.split('\n').where(isNonEmptyString)); + result.addAll(diffCachedResult.stdout.split('\n').where(isNonEmptyString)); + result.addAll(masterResult.stdout.split('\n').where(isNonEmptyString)); + return result.toList(); +} + +Future> dirContents(String repoPath) { + Directory dir = Directory(repoPath); + var files = []; + var completer = new Completer>(); + var lister = dir.list(recursive: true); + lister.listen((FileSystemEntity file) => files.add(file.path), + // should also register onError + onDone: () => completer.complete(files)); + return completer.future; +} + +void main(List arguments) async { + final String buildCommandsPath = arguments[0]; + final String repoPath = arguments[1]; + final String checks = + arguments.length >= 3 ? '--checks=${arguments[2]}' : '--config='; + final List changedFiles = + Platform.environment['FLUTTER_LINT_ALL'] != null + ? await dirContents(repoPath) + : getListOfChangedFiles(repoPath); + + final List buildCommandMaps = + jsonDecode(await new File(buildCommandsPath).readAsString()); + final List buildCommands = + buildCommandMaps.map((x) => parseCommand(x)).toList(); + final Command firstCommand = buildCommands[0]; + final String tidyPath = calcTidyPath(firstCommand); + final List changedFileBuildCommands = + buildCommands.where((x) => containsAny(x.file, changedFiles)).toList(); + + int exitCode = 0; + //TODO(aaclarke): Coalesce this into one call using the `-p` arguement. + for (Command command in changedFileBuildCommands) { + final String tidyArgs = calcTidyArgs(command); + final List args = [command.file, checks, '--']; + args.addAll(tidyArgs.split(' ')); + print('# linting ${command.file}'); + final Process process = await Process.start(tidyPath, args, + workingDirectory: command.directory, runInShell: false); + process.stdout.transform(utf8.decoder).listen((data) { + print(data); + exitCode = 1; + }); + await process.exitCode; + } + exit(exitCode); +} diff --git a/ci/lint.sh b/ci/lint.sh new file mode 100755 index 0000000000000..023d98292ae30 --- /dev/null +++ b/ci/lint.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e + +COMPILE_COMMANDS="out/compile_commands.json" +if [ ! -f $COMPILE_COMMANDS ]; then + ./flutter/tools/gn +fi + +dart flutter/ci/lint.dart $COMPILE_COMMANDS flutter/