blob: 494d6421f9b14348f86d0e1c5a4cb06e48bad360 [file] [log] [blame]
// 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),
);
}
static ({
int start,
int end,
String line,
}) _getLine(SourceFile sourceFile, int index) {
final int start = sourceFile.getOffset(index);
int end = index == sourceFile.lines - 1
? sourceFile.length
: sourceFile.getOffset(index + 1) - 1;
String line = sourceFile.getText(start, end);
// On Windows, it's common for files to have CRLF line endings, and for
// developers to use git's `core.autocrlf` setting to convert them to LF
// line endings.
//
// However, our scripts expect LF line endings, so we need to remove the
// CR characters from the line endings when computing the line so that
// properly formatted files are not considered malformed.
if (line.isNotEmpty && sourceFile.getText(end - 1, end) == '\r') {
end--;
line = line.substring(0, line.length - 1);
}
return (
start: start,
end: end,
line: line,
);
}
/// 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, :int end, :String line) = _getLine(sourceFile, i);
// 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, :int end, :String line) = _getLine(sourceFile, i);
// 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, :int end, :String line) = _getLine(sourceFile, i);
// 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);
}
}