| // 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); | |
| } | |
| } |