diff --git a/testing/run_tests.py b/testing/run_tests.py index 2517c091123ce..bc5fa72269889 100755 --- a/testing/run_tests.py +++ b/testing/run_tests.py @@ -1001,6 +1001,7 @@ def build_dart_host_test_list(build_dir): ], ), (os.path.join('flutter', 'tools', 'githooks'), []), + (os.path.join('flutter', 'tools', 'header_guard_check'), []), (os.path.join('flutter', 'tools', 'pkg', 'engine_build_configs'), []), (os.path.join('flutter', 'tools', 'pkg', 'engine_repo_tools'), []), (os.path.join('flutter', 'tools', 'pkg', 'git_repo_tools'), []), diff --git a/tools/header_guard_check/README.md b/tools/header_guard_check/README.md new file mode 100644 index 0000000000000..20ec093564f0e --- /dev/null +++ b/tools/header_guard_check/README.md @@ -0,0 +1,46 @@ +# header_guard_check + +A tool to check that C++ header guards are used consistently in the engine. + +```shell +# Assuming you are in the `flutter` root of the engine repo. +dart ./tools/header_guard_check/bin/main.dart +``` + +The tool checks _all_ header files for the following pattern: + +```h +// path/to/file.h + +#ifndef PATH_TO_FILE_H_ +#define PATH_TO_FILE_H_ +... +#endif // PATH_TO_FILE_H_ +``` + +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: + +```shell +dart ./tools/header_guard_check/bin/main.dart --fix +``` + +## Advanced usage + +### Restricting the files to check + +By default, the tool checks all header files in the engine. You can restrict the +files to check by passing a relative (to the `engine/src/flutter` root) paths to +include: + +```shell +dart ./tools/header_guard_check/bin/main.dart --include impeller +``` diff --git a/tools/header_guard_check/bin/main.dart b/tools/header_guard_check/bin/main.dart new file mode 100644 index 0000000000000..014b51c2d839d --- /dev/null +++ b/tools/header_guard_check/bin/main.dart @@ -0,0 +1,15 @@ +// 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:header_guard_check/header_guard_check.dart'; + +Future main(List arguments) async { + final int result = await HeaderGuardCheck.fromCommandLine(arguments).run(); + if (result != 0) { + io.exit(result); + } + return result; +} diff --git a/tools/header_guard_check/lib/header_guard_check.dart b/tools/header_guard_check/lib/header_guard_check.dart new file mode 100644 index 0000000000000..f7880c7fe562d --- /dev/null +++ b/tools/header_guard_check/lib/header_guard_check.dart @@ -0,0 +1,175 @@ +// 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:args/args.dart'; +import 'package:engine_repo_tools/engine_repo_tools.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as p; + +import 'src/header_file.dart'; + +/// Checks C++ header files for header guards. +@immutable +final class HeaderGuardCheck { + /// Creates a new header guard checker. + const HeaderGuardCheck({ + required this.source, + required this.exclude, + this.include = const [], + this.fix = false, + }); + + /// Parses the command line arguments and creates a new header guard checker. + factory HeaderGuardCheck.fromCommandLine(List arguments) { + final ArgResults argResults = _parser.parse(arguments); + return HeaderGuardCheck( + source: Engine.fromSrcPath(argResults['root'] as String), + include: argResults['include'] as List, + exclude: argResults['exclude'] as List, + fix: argResults['fix'] as bool, + ); + } + + /// Engine source root. + final Engine source; + + /// Whether to automatically fix most header guards. + final bool fix; + + /// Path directories to include in the check. + final List include; + + /// Path directories to exclude from the check. + final List exclude; + + /// Runs the header guard check. + Future run() async { + final List badFiles = _checkFiles(_findIncludedHeaderFiles()).toList(); + + if (badFiles.isNotEmpty) { + io.stdout.writeln('The following ${badFiles.length} files have invalid header guards:'); + for (final HeaderFile headerFile in badFiles) { + io.stdout.writeln(' ${headerFile.path}'); + } + + // If we're fixing, fix the files. + if (fix) { + for (final HeaderFile headerFile in badFiles) { + headerFile.fix(engineRoot: source.flutterDir.path); + } + + io.stdout.writeln('Fixed ${badFiles.length} files.'); + return 0; + } + + return 1; + } + + return 0; + } + + Iterable _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; + } + } + + 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; + } + } + return include.isEmpty; + } + + bool _isExcluded(String path) { + for (final String excludePath in exclude) { + final String relativePath = p.relative(excludePath, from: source.flutterDir.path); + if (p.isWithin(relativePath, path) || p.equals(relativePath, path)) { + return true; + } + } + return false; + } + + Iterable _checkFiles(Iterable headers) sync* { + 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')); + yield headerFile; + } + + if (headerFile.guard == null) { + io.stderr.writeln('Missing header guard in ${headerFile.path}'); + yield headerFile; + } + + final String expectedGuard = headerFile.computeExpectedName(engineRoot: source.flutterDir.path); + if (headerFile.guard!.ifndefValue != expectedGuard) { + io.stderr.writeln(headerFile.guard!.ifndefSpan!.message('Expected #ifndef $expectedGuard')); + yield headerFile; + } + if (headerFile.guard!.defineValue != expectedGuard) { + io.stderr.writeln(headerFile.guard!.defineSpan!.message('Expected #define $expectedGuard')); + yield headerFile; + } + if (headerFile.guard!.endifValue != expectedGuard) { + io.stderr.writeln(headerFile.guard!.endifSpan!.message('Expected #endif // $expectedGuard')); + yield headerFile; + } + } + } +} + +final Engine? _engine = Engine.tryFindWithin(p.dirname(p.fromUri(io.Platform.script))); + +final ArgParser _parser = ArgParser() + ..addFlag( + 'fix', + help: 'Automatically fixes most header guards.', + ) + ..addOption( + 'root', + abbr: 'r', + help: 'Path to the engine source root.', + valueHelp: 'path/to/engine/src', + defaultsTo: _engine?.srcDir.path, + ) + ..addMultiOption( + 'include', + abbr: 'i', + help: 'Paths to include in the check.', + valueHelp: 'path/to/dir/or/file (relative to the engine root)', + defaultsTo: [], + ) + ..addMultiOption( + 'exclude', + abbr: 'e', + help: 'Paths to exclude from the check.', + valueHelp: 'path/to/dir/or/file (relative to the engine root)', + defaultsTo: _engine != null ? [ + 'build', + 'impeller/compiler/code_gen_template.h', + 'prebuilts', + 'third_party', + ] : null, + ); diff --git a/tools/header_guard_check/lib/src/header_file.dart b/tools/header_guard_check/lib/src/header_file.dart new file mode 100644 index 0000000000000..9ab0b6318b015 --- /dev/null +++ b/tools/header_guard_check/lib/src/header_file.dart @@ -0,0 +1,326 @@ +// 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:meta/meta.dart'; +import 'package:path/path.dart' as p; +import 'package:source_span/source_span.dart'; + +/// Represents a C++ header file, i.e. a file on disk that ends in `.h`. +@immutable +final class HeaderFile { + /// Creates a new header file from the given [path]. + const HeaderFile.from( + this.path, { + required this.guard, + required this.pragmaOnce, + }); + + /// Parses the given [path] as a header file. + /// + /// Throws an [ArgumentError] if the file does not exist. + factory HeaderFile.parse(String path) { + final io.File file = io.File(path); + if (!file.existsSync()) { + throw ArgumentError.value(path, 'path', 'File does not exist.'); + } + + final String contents = file.readAsStringSync(); + final SourceFile sourceFile = SourceFile.fromString(contents, url: p.toUri(path)); + return HeaderFile.from( + path, + guard: _parseGuard(sourceFile), + pragmaOnce: _parsePragmaOnce(sourceFile), + ); + } + + /// Parses the header guard of the given [sourceFile]. + static HeaderGuardSpans? _parseGuard(SourceFile sourceFile) { + SourceSpan? ifndefSpan; + SourceSpan? defineSpan; + SourceSpan? endifSpan; + + // Iterate over the lines in the file. + for (int i = 0; i < sourceFile.lines; i++) { + final int start = sourceFile.getOffset(i); + final int end = i == sourceFile.lines - 1 + ? sourceFile.length + : sourceFile.getOffset(i + 1) - 1; + final String line = sourceFile.getText(start, end); + + // Check if the line is a header guard directive. + if (line.startsWith('#ifndef')) { + ifndefSpan = sourceFile.span(start, end); + } else if (line.startsWith('#define')) { + // If we find a define preceding an ifndef, it is not a header guard. + if (ifndefSpan == null) { + continue; + } + defineSpan = sourceFile.span(start, end); + break; + } + } + + // If we found no header guard, return null. + if (ifndefSpan == null) { + return null; + } + + // Now iterate backwards to find the (last) #endif directive. + for (int i = sourceFile.lines - 1; i > 0; i--) { + final int start = sourceFile.getOffset(i); + final int end = i == sourceFile.lines - 1 + ? sourceFile.length + : sourceFile.getOffset(i + 1) - 1; + final String line = sourceFile.getText(start, end); + + // Check if the line is a header guard directive. + if (line.startsWith('#endif')) { + endifSpan = sourceFile.span(start, end); + break; + } + } + + return HeaderGuardSpans( + ifndefSpan: ifndefSpan, + defineSpan: defineSpan, + endifSpan: endifSpan, + ); + } + + /// Parses the `#pragma once` directive of the given [sourceFile]. + static SourceSpan? _parsePragmaOnce(SourceFile sourceFile) { + // Iterate over the lines in the file. + for (int i = 0; i < sourceFile.lines; i++) { + final int start = sourceFile.getOffset(i); + final int end = i == sourceFile.lines - 1 + ? sourceFile.length + : sourceFile.getOffset(i + 1) - 1; + final String line = sourceFile.getText(start, end); + + // Check if the line is a header guard directive. + if (line.startsWith('#pragma once')) { + return sourceFile.span(start, end); + } + } + + return null; + } + + /// Path to the file on disk. + final String path; + + /// The header guard span, if any. + /// + /// This is `null` if the file does not have a header guard. + final HeaderGuardSpans? guard; + + /// The `#pragma once` directive, if any. + /// + /// This is `null` if the file does not have a `#pragma once` directive. + final SourceSpan? pragmaOnce; + + static final RegExp _nonAlphaNumeric = RegExp(r'[^a-zA-Z0-9]'); + + /// Returns the expected header guard for this file, relative to [engineRoot]. + /// + /// For example, if the file is `foo/bar/baz.h`, this will return `FLUTTER_FOO_BAR_BAZ_H_`. + String computeExpectedName({required String engineRoot}) { + final String relativePath = p.relative(path, from: engineRoot); + final String underscoredRelativePath = p.withoutExtension(relativePath).replaceAll(_nonAlphaNumeric, '_'); + return 'FLUTTER_${underscoredRelativePath.toUpperCase()}_H_'; + } + + /// Updates the file at [path] to have the expected header guard. + /// + /// Returns `true` if the file was modified, `false` otherwise. + bool fix({required String engineRoot}) { + final String expectedGuard = computeExpectedName(engineRoot: engineRoot); + + // Check if the file already has a valid header guard. + if (guard != null) { + if (guard!.ifndefValue == expectedGuard && + guard!.defineValue == expectedGuard && + guard!.endifValue == expectedGuard) { + return false; + } + } + + // Get the contents of the file. + final String oldContents = io.File(path).readAsStringSync(); + + // If we're using pragma once, replace it with an ifndef/define, and + // append an endif and a newline at the end of the file. + if (pragmaOnce != null) { + // Append the endif and newline. + String newContents = '$oldContents\n#endif // $expectedGuard\n'; + + // Replace the span with the ifndef/define. + newContents = newContents.replaceRange( + pragmaOnce!.start.offset, + pragmaOnce!.end.offset, + '#ifndef $expectedGuard\n' + '#define $expectedGuard' + ); + + // Write the new contents to the file. + io.File(path).writeAsStringSync(newContents); + return true; + } + + // If we're not using pragma once, replace the header guard with the + // expected header guard. + if (guard != null) { + + // Replace endif: + String newContents = oldContents.replaceRange( + guard!.endifSpan!.start.offset, + guard!.endifSpan!.end.offset, + '#endif // $expectedGuard' + ); + + // Replace define: + newContents = newContents.replaceRange( + guard!.defineSpan!.start.offset, + guard!.defineSpan!.end.offset, + '#define $expectedGuard' + ); + + // Replace ifndef: + newContents = newContents.replaceRange( + guard!.ifndefSpan!.start.offset, + guard!.ifndefSpan!.end.offset, + '#ifndef $expectedGuard' + ); + + // Write the new contents to the file. + io.File(path).writeAsStringSync('$newContents\n'); + return true; + } + + // If we're missing a guard entirely, add one. The rules are: + // 1. Add a newline, #endif at the end of the file. + // 2. Add a newline, #ifndef, #define after the first non-comment line. + String newContents = oldContents; + newContents += '\n#endif // $expectedGuard\n'; + newContents = newContents.replaceFirst( + RegExp(r'^(?!//)', multiLine: true), + '\n#ifndef $expectedGuard\n' + '#define $expectedGuard\n' + ); + + // Write the new contents to the file. + io.File(path).writeAsStringSync(newContents); + return true; + } + + @override + bool operator ==(Object other) { + return other is HeaderFile && + path == other.path && + guard == other.guard && + pragmaOnce == other.pragmaOnce; + } + + @override + int get hashCode => Object.hash(path, guard, pragmaOnce); + + @override + String toString() { + return 'HeaderFile(\n' + ' path: $path\n' + ' guard: $guard\n' + ' pragmaOnce: $pragmaOnce\n' + ')'; + } +} + +/// Source elements that are part of a header guard. +@immutable +final class HeaderGuardSpans { + /// Collects the source spans of the header guard directives. + const HeaderGuardSpans({ + required this.ifndefSpan, + required this.defineSpan, + required this.endifSpan, + }); + + /// Location of the `#ifndef` directive. + final SourceSpan? ifndefSpan; + + /// Location of the `#define` directive. + final SourceSpan? defineSpan; + + /// Location of the `#endif` directive. + final SourceSpan? endifSpan; + + @override + bool operator ==(Object other) { + return other is HeaderGuardSpans && + ifndefSpan == other.ifndefSpan && + defineSpan == other.defineSpan && + endifSpan == other.endifSpan; + } + + @override + int get hashCode => Object.hash(ifndefSpan, defineSpan, endifSpan); + + @override + String toString() { + return 'HeaderGuardSpans(\n' + ' #ifndef: $ifndefSpan\n' + ' #define: $defineSpan\n' + ' #endif: $endifSpan\n' + ')'; + } + + /// Returns the value of the `#ifndef` directive. + /// + /// For example, `#ifndef FOO_H_`, this will return `FOO_H_`. + /// + /// If the span is not a valid `#ifndef` directive, `null` is returned. + String? get ifndefValue { + final String? value = ifndefSpan?.text; + if (value == null) { + return null; + } + if (!value.startsWith('#ifndef ')) { + return null; + } + return value.substring('#ifndef '.length); + } + + /// Returns the value of the `#define` directive. + /// + /// For example, `#define FOO_H_`, this will return `FOO_H_`. + /// + /// If the span is not a valid `#define` directive, `null` is returned. + String? get defineValue { + final String? value = defineSpan?.text; + if (value == null) { + return null; + } + if (!value.startsWith('#define ')) { + return null; + } + return value.substring('#define '.length); + } + + /// Returns the value of the `#endif` directive. + /// + /// For example, `#endif // FOO_H_`, this will return `FOO_H_`. + /// + /// If the span is not a valid `#endif` directive, `null` is returned. + String? get endifValue { + final String? value = endifSpan?.text; + if (value == null) { + return null; + } + if (!value.startsWith('#endif // ')) { + return null; + } + return value.substring('#endif // '.length); + } +} diff --git a/tools/header_guard_check/pubspec.yaml b/tools/header_guard_check/pubspec.yaml new file mode 100644 index 0000000000000..d775e403853e2 --- /dev/null +++ b/tools/header_guard_check/pubspec.yaml @@ -0,0 +1,70 @@ +# 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. + +name: header_guard_check +publish_to: none + +# Do not add any dependencies that require more than what is provided in +# //third_party/dart/pkg or //third_party/dart/third_party/pkg. +# In particular, package:test is not usable here. + +# If you do add packages here, make sure you can run `pub get --offline`, and +# check the .packages and .package_config to make sure all the paths are +# relative to this directory into //third_party/dart + +environment: + sdk: '>=3.2.0-0 <4.0.0' + +dependencies: + args: any + git_repo_tools: any + engine_repo_tools: any + meta: any + path: any + source_span: any + +dev_dependencies: + async_helper: any + expect: any + litetest: any + process_fakes: any + smith: any + +dependency_overrides: + args: + path: ../../../third_party/dart/third_party/pkg/args + async: + path: ../../../third_party/dart/third_party/pkg/async + async_helper: + path: ../../../third_party/dart/pkg/async_helper + collection: + path: ../../../third_party/dart/third_party/pkg/collection + engine_repo_tools: + path: ../pkg/engine_repo_tools + expect: + path: ../../../third_party/dart/pkg/expect + file: + path: ../../../third_party/dart/third_party/pkg/file/packages/file + git_repo_tools: + path: ../pkg/git_repo_tools + litetest: + path: ../../testing/litetest + meta: + path: ../../../third_party/dart/pkg/meta + path: + path: ../../../third_party/dart/third_party/pkg/path + platform: + path: ../../third_party/pkg/platform + process: + path: ../../third_party/pkg/process + process_fakes: + path: ../pkg/process_fakes + process_runner: + path: ../../third_party/pkg/process_runner + smith: + path: ../../../third_party/dart/pkg/smith + source_span: + path: ../../../third_party/dart/third_party/pkg/source_span + term_glyph: + path: ../../../third_party/dart/third_party/pkg/term_glyph diff --git a/tools/header_guard_check/test/header_file_test.dart b/tools/header_guard_check/test/header_file_test.dart new file mode 100644 index 0000000000000..4342ba3ed40b5 --- /dev/null +++ b/tools/header_guard_check/test/header_file_test.dart @@ -0,0 +1,332 @@ +// 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:header_guard_check/src/header_file.dart'; +import 'package:litetest/litetest.dart'; +import 'package:path/path.dart' as p; +import 'package:source_span/source_span.dart'; + +Future main(List args) async { + void withTestFile(String path, String contents, void Function(io.File) fn) { + // Create a temporary file and delete it when we're done. + final io.Directory tempDir = io.Directory.systemTemp.createTempSync('header_guard_check_test'); + final io.File file = io.File(p.join(tempDir.path, path)); + file.writeAsStringSync(contents); + try { + fn(file); + } finally { + tempDir.deleteSync(recursive: true); + } + } + + group('HeaderGuardSpans', () { + test('parses #ifndef', () { + const String input = '#ifndef FOO_H_'; + final HeaderGuardSpans guard = HeaderGuardSpans( + ifndefSpan: SourceSpanWithContext( + SourceLocation(0), + SourceLocation(input.length), + input, + input, + ), + defineSpan: null, + endifSpan: null, + ); + expect(guard.ifndefValue, 'FOO_H_'); + }); + + test('ignores #ifndef if omitted', () { + const HeaderGuardSpans guard = HeaderGuardSpans( + ifndefSpan: null, + defineSpan: null, + endifSpan: null, + ); + expect(guard.ifndefValue, isNull); + }); + + test('ignores #ifndef if invalid', () { + const String input = '#oops FOO_H_'; + final HeaderGuardSpans guard = HeaderGuardSpans( + ifndefSpan: SourceSpanWithContext( + SourceLocation(0), + SourceLocation(input.length), + input, + input, + ), + defineSpan: null, + endifSpan: null, + ); + expect(guard.ifndefValue, isNull); + }); + + test('parses #define', () { + const String input = '#define FOO_H_'; + final HeaderGuardSpans guard = HeaderGuardSpans( + ifndefSpan: null, + defineSpan: SourceSpanWithContext( + SourceLocation(0), + SourceLocation(input.length), + input, + input, + ), + endifSpan: null, + ); + expect(guard.defineValue, 'FOO_H_'); + }); + + test('ignores #define if omitted', () { + const HeaderGuardSpans guard = HeaderGuardSpans( + ifndefSpan: null, + defineSpan: null, + endifSpan: null, + ); + expect(guard.defineValue, isNull); + }); + + test('ignores #define if invalid', () { + const String input = '#oops FOO_H_'; + final HeaderGuardSpans guard = HeaderGuardSpans( + ifndefSpan: null, + defineSpan: SourceSpanWithContext( + SourceLocation(0), + SourceLocation(input.length), + input, + input, + ), + endifSpan: null, + ); + expect(guard.defineValue, isNull); + }); + + test('parses #endif', () { + const String input = '#endif // FOO_H_'; + final HeaderGuardSpans guard = HeaderGuardSpans( + ifndefSpan: null, + defineSpan: null, + endifSpan: SourceSpanWithContext( + SourceLocation(0), + SourceLocation(input.length), + input, + input, + ), + ); + expect(guard.endifValue, 'FOO_H_'); + }); + + test('ignores #endif if omitted', () { + const HeaderGuardSpans guard = HeaderGuardSpans( + ifndefSpan: null, + defineSpan: null, + endifSpan: null, + ); + expect(guard.endifValue, isNull); + }); + + test('ignores #endif if invalid', () { + const String input = '#oops // FOO_H_'; + final HeaderGuardSpans guard = HeaderGuardSpans( + ifndefSpan: null, + defineSpan: null, + endifSpan: SourceSpanWithContext( + SourceLocation(0), + SourceLocation(input.length), + input, + input, + ), + ); + expect(guard.endifValue, isNull); + }); + }); + + group('HeaderFile', () { + test('produces a valid header guard name from various file names', () { + // All of these should produce the name `FOO_BAR_BAZ_H_`. + const List inputs = [ + 'foo_bar_baz.h', + 'foo-bar-baz.h', + 'foo_bar-baz.h', + 'foo-bar_baz.h', + 'foo+bar+baz.h', + ]; + for (final String input in inputs) { + final HeaderFile headerFile = HeaderFile.from( + input, + guard: null, + pragmaOnce: null, + ); + expect(headerFile.computeExpectedName(engineRoot: ''), endsWith('FOO_BAR_BAZ_H_')); + } + }); + + test('parses a header file with a valid guard', () { + final String input = [ + '#ifndef FOO_H_', + '#define FOO_H_', + '', + '#endif // FOO_H_', + ].join('\n'); + withTestFile('foo.h', input, (io.File file) { + final HeaderFile headerFile = HeaderFile.parse(file.path); + expect(headerFile.guard!.ifndefValue, 'FOO_H_'); + expect(headerFile.guard!.defineValue, 'FOO_H_'); + expect(headerFile.guard!.endifValue, 'FOO_H_'); + }); + }); + + test('parses a header file with an invalid #endif', () { + final String input = [ + '#ifndef FOO_H_', + '#define FOO_H_', + '', + // No comment after the #endif. + '#endif', + ].join('\n'); + withTestFile('foo.h', input, (io.File file) { + final HeaderFile headerFile = HeaderFile.parse(file.path); + expect(headerFile.guard!.ifndefValue, 'FOO_H_'); + expect(headerFile.guard!.defineValue, 'FOO_H_'); + expect(headerFile.guard!.endifValue, isNull); + }); + }); + + test('parses a header file with a missing #define', () { + final String input = [ + '#ifndef FOO_H_', + // No #define. + '', + '#endif // FOO_H_', + ].join('\n'); + withTestFile('foo.h', input, (io.File file) { + final HeaderFile headerFile = HeaderFile.parse(file.path); + expect(headerFile.guard!.ifndefValue, 'FOO_H_'); + expect(headerFile.guard!.defineValue, isNull); + expect(headerFile.guard!.endifValue, 'FOO_H_'); + }); + }); + + test('parses a header file with a missing #ifndef', () { + final String input = [ + // No #ifndef. + '#define FOO_H_', + '', + '#endif // FOO_H_', + ].join('\n'); + withTestFile('foo.h', input, (io.File file) { + final HeaderFile headerFile = HeaderFile.parse(file.path); + expect(headerFile.guard, isNull); + }); + }); + + test('parses a header file with a #pragma once', () { + final String input = [ + '#pragma once', + '', + ].join('\n'); + withTestFile('foo.h', input, (io.File file) { + final HeaderFile headerFile = HeaderFile.parse(file.path); + expect(headerFile.pragmaOnce, isNotNull); + }); + }); + + test('fixes a file that uses #pragma once', () { + final String input = [ + '#pragma once', + '', + '// ...', + ].join('\n'); + withTestFile('foo.h', input, (io.File file) { + final HeaderFile headerFile = HeaderFile.parse(file.path); + expect(headerFile.fix(engineRoot: p.dirname(file.path)), isTrue); + expect(file.readAsStringSync(), [ + '#ifndef FLUTTER_FOO_H_', + '#define FLUTTER_FOO_H_', + '', + '// ...', + '#endif // FLUTTER_FOO_H_', + '', + ].join('\n')); + }); + }); + + test('fixes a file with an incorrect header guard', () { + final String input = [ + '#ifndef FOO_H_', + '#define FOO_H_', + '', + '#endif // FOO_H_', + ].join('\n'); + withTestFile('foo.h', input, (io.File file) { + final HeaderFile headerFile = HeaderFile.parse(file.path); + expect(headerFile.fix(engineRoot: p.dirname(file.path)), isTrue); + expect(file.readAsStringSync(), [ + '#ifndef FLUTTER_FOO_H_', + '#define FLUTTER_FOO_H_', + '', + '#endif // FLUTTER_FOO_H_', + '', + ].join('\n')); + }); + }); + + test('fixes a file with no header guard', () { + final String input = [ + '// 1.', + '// 2.', + '// 3.', + '', + "#import 'flutter/shell/platform/darwin/Flutter.h'", + '', + '@protocl Flutter', + '', + '@end', + '', + ].join('\n'); + withTestFile('foo.h', input, (io.File file) { + final HeaderFile headerFile = HeaderFile.parse(file.path); + expect(headerFile.fix(engineRoot: p.dirname(file.path)), isTrue); + expect(file.readAsStringSync(), [ + '// 1.', + '// 2.', + '// 3.', + '', + '#ifndef FLUTTER_FOO_H_', + '#define FLUTTER_FOO_H_', + '', + "#import 'flutter/shell/platform/darwin/Flutter.h'", + '', + '@protocl Flutter', + '', + '@end', + '', + '#endif // FLUTTER_FOO_H_', + '', + ].join('\n')); + }); + }); + + test('does not touch a file with an existing guard and another #define', () { + final String input = [ + '// 1.', + '// 2.', + '// 3.', + '', + '#define FML_USED_ON_EMBEDDER', + '', + '#ifndef FLUTTER_FOO_H_', + '#define FLUTTER_FOO_H_', + '', + '#endif // FLUTTER_FOO_H_', + '', + ].join('\n'); + withTestFile('foo.h', input, (io.File file) { + final HeaderFile headerFile = HeaderFile.parse(file.path); + expect(headerFile.fix(engineRoot: p.dirname(file.path)), isFalse); + }); + }); + }); + + return 0; +} diff --git a/tools/pub_get_offline.py b/tools/pub_get_offline.py index 931db7319b8f9..f5e41ba3245e1 100644 --- a/tools/pub_get_offline.py +++ b/tools/pub_get_offline.py @@ -40,6 +40,7 @@ os.path.join(ENGINE_DIR, 'tools', 'const_finder'), os.path.join(ENGINE_DIR, 'tools', 'gen_web_locale_keymap'), os.path.join(ENGINE_DIR, 'tools', 'githooks'), + os.path.join(ENGINE_DIR, 'tools', 'header_guard_check'), os.path.join(ENGINE_DIR, 'tools', 'licenses'), os.path.join(ENGINE_DIR, 'tools', 'path_ops', 'dart'), os.path.join(ENGINE_DIR, 'tools', 'pkg', 'engine_build_configs'),