Enable header_guard_check (and --fix) in ci/format (flutter/engine#50102)

Closes https://github.com/flutter/flutter/issues/133415.

This includes a performance optimization for the runner itself, where
`--include=...`s are used as a base to check, versus checking _every_
file and seeing if it exists in a list. This works better with what is
expected in `ci/bin/format.dart` without any contract change.

Also added a few more functional tests of the system.
This commit is contained in:
Matan Lurey 2024-01-26 16:32:14 -08:00 committed by GitHub
parent 7d955bfd7c
commit a50711f4f4
5 changed files with 316 additions and 37 deletions

View File

@ -39,11 +39,13 @@ enum MessageType {
}
enum FormatCheck {
clang,
gn,
java,
python,
whitespace,
header,
// Run clang after the header check.
clang,
}
FormatCheck nameToFormatCheck(String name) {
@ -58,6 +60,8 @@ FormatCheck nameToFormatCheck(String name) {
return FormatCheck.python;
case 'whitespace':
return FormatCheck.whitespace;
case 'header':
return FormatCheck.header;
default:
throw FormattingException('Unknown FormatCheck type $name');
}
@ -75,6 +79,8 @@ String formatCheckToName(FormatCheck check) {
return 'Python';
case FormatCheck.whitespace:
return 'Trailing whitespace';
case FormatCheck.header:
return 'Header guards';
}
}
@ -167,6 +173,14 @@ abstract class FormatChecker {
allFiles: allFiles,
messageCallback: messageCallback,
);
case FormatCheck.header:
return HeaderFormatChecker(
processManager: processManager,
baseGitRef: baseGitRef,
repoDir: repoDir,
allFiles: allFiles,
messageCallback: messageCallback,
);
}
}
@ -944,6 +958,94 @@ class WhitespaceFormatChecker extends FormatChecker {
}
}
final class HeaderFormatChecker extends FormatChecker {
HeaderFormatChecker({
required super.baseGitRef,
required super.repoDir,
super.processManager,
super.allFiles,
super.messageCallback,
});
// $ENGINE/third_party/dart/tools/sdks/dart-sdk/bin/dart
late final String _dartBin = path.join(
repoDir.absolute.parent.path,
'third_party',
'dart',
'tools',
'sdks',
'dart-sdk',
'bin',
'dart',
);
// $ENGINE/src/flutter/tools/bin/main.dart
late final String _headerGuardCheckBin = path.join(
repoDir.absolute.path,
'tools',
'header_guard_check',
'bin',
'main.dart',
);
@override
Future<bool> checkFormatting() async {
final List<String> include = <String>[];
if (!allFiles) {
include.addAll(await getFileList(<String>[
'*.h',
]));
if (include.isEmpty) {
message('No header files with changes, skipping header guard check.');
return true;
}
}
final List<String> args = <String>[
_dartBin,
_headerGuardCheckBin,
...include.map((String f) => '--include=$f'),
];
// TIP: --exclude is encoded into the tool itself.
// see tools/header_guard_check/lib/header_guard_check.dart
final ProcessRunnerResult result = await _processRunner.runProcess(args);
if (result.exitCode != 0) {
error('Header check failed. The following files have incorrect header guards:');
message(result.stdout);
return false;
}
return true;
}
@override
Future<bool> fixFormatting() async {
final List<String> include = <String>[];
if (!allFiles) {
include.addAll(await getFileList(<String>[
'*.h',
]));
if (include.isEmpty) {
message('No header files with changes, skipping header guard fix.');
return true;
}
}
final List<String> args = <String>[
_dartBin,
_headerGuardCheckBin,
'--fix',
...include.map((String f) => '--include=$f'),
];
// TIP: --exclude is encoded into the tool itself.
// see tools/header_guard_check/lib/header_guard_check.dart
final ProcessRunnerResult result = await _processRunner.runProcess(args);
if (result.exitCode != 0) {
error('Header check fix failed:');
message(result.stdout);
return false;
}
return true;
}
}
Future<String> _getDiffBaseRevision(ProcessManager processManager, Directory repoDir) async {
final ProcessRunner processRunner = ProcessRunner(
defaultWorkingDirectory: repoDir,

View File

@ -34,6 +34,21 @@ final FileContentPair pythonContentPair = FileContentPair(
final FileContentPair whitespaceContentPair = FileContentPair(
'int main() {\n return 0; \n}\n',
'int main() {\n return 0;\n}\n');
final FileContentPair headerContentPair = FileContentPair(
<String>[
'#ifndef FOO_H_',
'#define FOO_H_',
'',
'#endif // FOO_H_',
].join('\n'),
<String>[
'#ifndef FLUTTER_FORMAT_TEST_H_',
'#define FLUTTER_FORMAT_TEST_H_',
'',
'#endif // FLUTTER_FORMAT_TEST_H_',
'',
].join('\n'),
);
class TestFileFixture {
TestFileFixture(this.type) {
@ -62,6 +77,10 @@ class TestFileFixture {
final io.File whitespaceFile = io.File('${repoDir.path}/format_test.c');
whitespaceFile.writeAsStringSync(whitespaceContentPair.original);
files.add(whitespaceFile);
case target.FormatCheck.header:
final io.File headerFile = io.File('${repoDir.path}/format_test.h');
headerFile.writeAsStringSync(headerContentPair.original);
files.add(headerFile);
}
}
@ -118,6 +137,11 @@ class TestFileFixture {
content,
whitespaceContentPair.formatted,
);
case target.FormatCheck.header:
return FileContentPair(
content,
headerContentPair.formatted,
);
}
});
}
@ -210,4 +234,21 @@ void main() {
fixture.gitRemove();
}
});
test('Can fix header guard formatting errors', () {
final TestFileFixture fixture = TestFileFixture(target.FormatCheck.header);
try {
fixture.gitAdd();
io.Process.runSync(
formatterPath, <String>['--check', 'header', '--fix'],
workingDirectory: repoDir.path,
);
final Iterable<FileContentPair> files = fixture.getFileContents();
for (final FileContentPair pair in files) {
expect(pair.original, equals(pair.formatted));
}
} finally {
fixture.gitRemove();
}
});
}

View File

@ -22,9 +22,6 @@ If the header file does not follow this pattern, the tool will print an error
message and exit with a non-zero exit code. For more information about why we
use this pattern, see [the Google C++ style guide](https://google.github.io/styleguide/cppguide.html#The__define_Guard).
> [!IMPORTANT]
> This is a prototype tool and is not yet integrated into the engine's CI.
## Automatic fixes
The tool can automatically fix header files that do not follow the pattern:

View File

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:collection';
import 'dart:io' as io;
import 'package:args/args.dart';
@ -15,12 +16,14 @@ import 'src/header_file.dart';
@immutable
final class HeaderGuardCheck {
/// Creates a new header guard checker.
const HeaderGuardCheck({
HeaderGuardCheck({
required this.source,
required this.exclude,
this.include = const <String>[],
this.fix = false,
});
StringSink? stdOut,
StringSink? stdErr,
}) : _stdOut = stdOut ?? io.stdout, _stdErr = stdErr ?? io.stderr;
/// Parses the command line arguments and creates a new header guard checker.
factory HeaderGuardCheck.fromCommandLine(List<String> arguments) {
@ -45,14 +48,20 @@ final class HeaderGuardCheck {
/// Path directories to exclude from the check.
final List<String> exclude;
/// Stdout.
final StringSink _stdOut;
/// Stderr.
final StringSink _stdErr;
/// Runs the header guard check.
Future<int> run() async {
final List<HeaderFile> badFiles = _checkFiles(_findIncludedHeaderFiles()).toList();
if (badFiles.isNotEmpty) {
io.stdout.writeln('The following ${badFiles.length} files have invalid header guards:');
_stdOut.writeln('The following ${badFiles.length} files have invalid header guards:');
for (final HeaderFile headerFile in badFiles) {
io.stdout.writeln(' ${headerFile.path}');
_stdOut.writeln(' ${headerFile.path}');
}
// If we're fixing, fix the files.
@ -61,7 +70,7 @@ final class HeaderGuardCheck {
headerFile.fix(engineRoot: source.flutterDir.path);
}
io.stdout.writeln('Fixed ${badFiles.length} files.');
_stdOut.writeln('Fixed ${badFiles.length} files.');
return 0;
}
@ -72,32 +81,33 @@ final class HeaderGuardCheck {
}
Iterable<io.File> _findIncludedHeaderFiles() sync* {
final io.Directory dir = source.flutterDir;
for (final io.FileSystemEntity entity in dir.listSync(recursive: true)) {
if (entity is! io.File) {
continue;
}
if (!entity.path.endsWith('.h')) {
continue;
}
if (!_isIncluded(entity.path) || _isExcluded(entity.path)) {
continue;
}
yield entity;
final Queue<String> queue = Queue<String>();
final Set<String> yielded = <String>{};
if (include.isEmpty) {
queue.add(source.flutterDir.path);
} else {
queue.addAll(include);
}
}
bool _isIncluded(String path) {
for (final String includePath in include) {
final String relativePath = p.relative(includePath, from: source.flutterDir.path);
if (p.isWithin(relativePath, path) || p.equals(relativePath, path)) {
return true;
while (queue.isNotEmpty) {
final String path = queue.removeFirst();
if (path.endsWith('.h')) {
if (!_isExcluded(path) && yielded.add(path)) {
yield io.File(path);
}
} else if (io.FileSystemEntity.isDirectorySync(path)) {
if (_isExcluded(path)) {
continue;
}
final io.Directory directory = io.Directory(path);
for (final io.FileSystemEntity entity in directory.listSync(recursive: true)) {
if (entity is io.File && entity.path.endsWith('.h')) {
queue.add(entity.path);
}
}
} else {
// Neither a header file nor a directory that might contain header files.
}
}
return include.isEmpty;
}
bool _isExcluded(String path) {
@ -114,27 +124,32 @@ final class HeaderGuardCheck {
for (final io.File header in headers) {
final HeaderFile headerFile = HeaderFile.parse(header.path);
if (headerFile.pragmaOnce != null) {
io.stderr.writeln(headerFile.pragmaOnce!.message('Unexpected #pragma once'));
_stdErr.writeln(headerFile.pragmaOnce!.message('Unexpected #pragma once'));
yield headerFile;
continue;
}
if (headerFile.guard == null) {
io.stderr.writeln('Missing header guard in ${headerFile.path}');
_stdErr.writeln('Missing header guard in ${headerFile.path}');
yield headerFile;
continue;
}
final String expectedGuard = headerFile.computeExpectedName(engineRoot: source.flutterDir.path);
if (headerFile.guard!.ifndefValue != expectedGuard) {
io.stderr.writeln(headerFile.guard!.ifndefSpan!.message('Expected #ifndef $expectedGuard'));
_stdErr.writeln(headerFile.guard!.ifndefSpan!.message('Expected #ifndef $expectedGuard'));
yield headerFile;
continue;
}
if (headerFile.guard!.defineValue != expectedGuard) {
io.stderr.writeln(headerFile.guard!.defineSpan!.message('Expected #define $expectedGuard'));
_stdErr.writeln(headerFile.guard!.defineSpan!.message('Expected #define $expectedGuard'));
yield headerFile;
continue;
}
if (headerFile.guard!.endifValue != expectedGuard) {
io.stderr.writeln(headerFile.guard!.endifSpan!.message('Expected #endif // $expectedGuard'));
_stdErr.writeln(headerFile.guard!.endifSpan!.message('Expected #endif // $expectedGuard'));
yield headerFile;
continue;
}
}
}

View File

@ -0,0 +1,124 @@
// 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.
import 'dart:io' as io;
import 'package:engine_repo_tools/engine_repo_tools.dart';
import 'package:header_guard_check/header_guard_check.dart';
import 'package:litetest/litetest.dart';
import 'package:path/path.dart' as p;
Future<int> main(List<String> args) async {
void withTestRepository(String path, void Function(io.Directory) fn) {
// Create a temporary directory and delete it when we're done.
final io.Directory tempDir = io.Directory.systemTemp.createTempSync('header_guard_check_test');
final io.Directory repoDir = io.Directory(p.join(tempDir.path, path));
repoDir.createSync(recursive: true);
try {
fn(repoDir);
} finally {
tempDir.deleteSync(recursive: true);
}
}
group('HeaderGuardCheck', () {
test('by default checks all files', () {
withTestRepository('engine/src', (io.Directory repoDir) {
final io.Directory flutterDir = io.Directory(p.join(repoDir.path, 'flutter'));
flutterDir.createSync(recursive: true);
final io.File file1 = io.File(p.join(flutterDir.path, 'foo.h'));
file1.createSync(recursive: true);
final io.File file2 = io.File(p.join(flutterDir.path, 'bar.h'));
file2.createSync(recursive: true);
final io.File file3 = io.File(p.join(flutterDir.path, 'baz.h'));
file3.createSync(recursive: true);
final StringBuffer stdOut = StringBuffer();
final StringBuffer stdErr = StringBuffer();
final HeaderGuardCheck check = HeaderGuardCheck(
source: Engine.fromSrcPath(repoDir.path),
exclude: const <String>[],
stdOut: stdOut,
stdErr: stdErr,
);
expect(check.run(), completion(1));
expect(stdOut.toString(), contains('foo.h'));
expect(stdOut.toString(), contains('bar.h'));
expect(stdOut.toString(), contains('baz.h'));
});
});
test('if --include is provided, checks specific files', () {
withTestRepository('engine/src', (io.Directory repoDir) {
final io.Directory flutterDir = io.Directory(p.join(repoDir.path, 'flutter'));
flutterDir.createSync(recursive: true);
final io.File file1 = io.File(p.join(flutterDir.path, 'foo.h'));
file1.createSync(recursive: true);
final io.File file2 = io.File(p.join(flutterDir.path, 'bar.h'));
file2.createSync(recursive: true);
final io.File file3 = io.File(p.join(flutterDir.path, 'baz.h'));
file3.createSync(recursive: true);
final StringBuffer stdOut = StringBuffer();
final StringBuffer stdErr = StringBuffer();
final HeaderGuardCheck check = HeaderGuardCheck(
source: Engine.fromSrcPath(repoDir.path),
include: <String>[file1.path, file3.path],
exclude: const <String>[],
stdOut: stdOut,
stdErr: stdErr,
);
expect(check.run(), completion(1));
expect(stdOut.toString(), contains('foo.h'));
expect(stdOut.toString(), contains('baz.h'));
// TODO(matanlurey): https://github.com/flutter/flutter/issues/133569).
if (stdOut.toString().contains('bar.h')) {
// There is no not(contains(...)) matcher.
fail('bar.h should not be checked. Output: $stdOut');
}
});
});
test('if --include is provided, checks specific directories', () {
withTestRepository('engine/src', (io.Directory repoDir) {
final io.Directory flutterDir = io.Directory(p.join(repoDir.path, 'flutter'));
flutterDir.createSync(recursive: true);
// Create a sub-directory called "impeller".
final io.Directory impellerDir = io.Directory(p.join(flutterDir.path, 'impeller'));
impellerDir.createSync(recursive: true);
// Create one file in both the root and in impeller.
final io.File file1 = io.File(p.join(flutterDir.path, 'foo.h'));
file1.createSync(recursive: true);
final io.File file2 = io.File(p.join(impellerDir.path, 'bar.h'));
file2.createSync(recursive: true);
final StringBuffer stdOut = StringBuffer();
final StringBuffer stdErr = StringBuffer();
final HeaderGuardCheck check = HeaderGuardCheck(
source: Engine.fromSrcPath(repoDir.path),
include: <String>[impellerDir.path],
exclude: const <String>[],
stdOut: stdOut,
stdErr: stdErr,
);
expect(check.run(), completion(1));
expect(stdOut.toString(), contains('bar.h'));
// TODO(matanlurey): https://github.com/flutter/flutter/issues/133569).
if (stdOut.toString().contains('foo.h')) {
// There is no not(contains(...)) matcher.
fail('foo.h should not be checked. Output: $stdOut');
}
});
});
});
return 0;
}