blob: 0ae7ee8f0699997567417f55b3533218eac1f9db [file] [log] [blame] [edit]
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. 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';
import 'package:args/args.dart';
import 'package:native_test_helpers/native_test_helpers.dart';
void main(List<String> args) {
final stopwatch = Stopwatch()..start();
final parser = ArgParser()
..addFlag(
'set-exit-if-changed',
negatable: false,
help: 'Return a non-zero exit code if any files were changed.',
);
final argResults = parser.parse(args);
final setExitIfChanged = argResults['set-exit-if-changed'] as bool;
final counts = Counts();
final errors = <String>[];
final hooksPackageRoot = findPackageRoot('hooks');
for (final package in ['hooks', 'code_assets', 'data_assets']) {
final packageRoot = hooksPackageRoot.resolve('../$package/');
final files = Directory.fromUri(packageRoot)
.listSync(recursive: true)
.whereType<File>()
.where((e) => e.path.endsWith('.dart') || e.path.endsWith('.md'));
for (final file in files) {
updateSnippetsInFile(file, counts, errors);
}
}
stopwatch.stop();
final duration = stopwatch.elapsedMilliseconds / 1000.0;
print(
'Processed ${counts.processed} files (${counts.changed} changed) in '
'${duration.toStringAsFixed(2)} seconds.',
);
if (errors.isNotEmpty) {
for (final error in errors) {
print(error);
}
print('See pkgs/hooks/CONTRIBUTING.md for details.');
exit(1);
}
if (setExitIfChanged && counts.changed > 0) {
exit(1);
}
}
void updateSnippetsInFile(File file, Counts counts, List<String> errors) {
final oldContent = file.readAsStringSync();
var newContent = oldContent;
final oldContentNormalized = oldContent.replaceAll('\r\n', '\n');
newContent = updateSnippets(oldContentNormalized, file.uri, errors);
final newContentNormalized = newContent.replaceAll('\r\n', '\n');
if (newContentNormalized != oldContentNormalized) {
file.writeAsStringSync(newContent);
print('Updated snippets in ${file.uri} (content changed)');
counts.changed++;
}
counts.processed++;
}
class Counts {
int processed = 0;
int changed = 0;
}
String updateSnippets(String oldContent, Uri fileUri, List<String> errors) {
var newContent = oldContent;
final markers = RegExp(
r'^([ \t]*/*[ ]?)```(\w*)',
multiLine: true,
).allMatches(oldContent);
if (markers.length % 2 != 0) {
final lastMarker = markers.last;
final line = oldContent.substring(0, lastMarker.start).split('\n').length;
errors.add('Error: Unmatched ``` in $fileUri at line $line.');
}
for (var i = markers.length - 2; i >= 0; i -= 2) {
final startMatch = markers.elementAt(i);
final endMatch = markers.elementAt(i + 1);
final prefix = startMatch.group(1)!;
final prefixWithoutSpace = prefix.endsWith(' ')
? prefix.substring(0, prefix.length - 1)
: prefix;
final endPrefix = endMatch.group(1)!;
if (prefix != endPrefix) {
final line = oldContent.substring(0, startMatch.start).split('\n').length;
errors.add(
'Error: Mismatched prefixes for code block in $fileUri at line $line. '
'Start prefix: "$prefix", end prefix: "$endPrefix"',
);
continue;
}
final contentStart = startMatch.end + 1;
final contentEnd = endMatch.start;
final content = oldContent.substring(contentStart, contentEnd);
final contentLines = content.split('\n');
var prefixesConsistent = true;
for (var j = 0; j < contentLines.length; j++) {
final line = contentLines[j];
if (line.trim().isEmpty) continue;
// Allow for empty lines at the end of the content.
if (j == contentLines.length - 1 && line.isEmpty) continue;
if (!line.startsWith(prefixWithoutSpace)) {
final lineNumber =
oldContent.substring(0, startMatch.end).split('\n').length + j;
errors.add(
'Error: Inconsistent prefix in code block in $fileUri on line '
'$lineNumber. Expected prefix "$prefixWithoutSpace".',
);
prefixesConsistent = false;
break;
}
}
if (!prefixesConsistent) continue;
final language = startMatch.group(2)!;
if (language != 'dart') continue;
final blockStartIndex = startMatch.start;
final lastLineOfContentBefore =
oldContent.substring(0, blockStartIndex).split('\n').length - 2;
final lineBeforeText = oldContent.split('\n')[lastLineOfContentBefore];
final fileLineMatch = RegExp(
r'^(.*?)<!-- (?:file://./(\S+)|(no-source-file)) -->\s*$',
).firstMatch(lineBeforeText);
if (fileLineMatch == null) {
final line = lastLineOfContentBefore + 1;
errors.add(
'Error: Did not find <!-- file://./... --> comment in $fileUri at line $line. ',
);
continue;
}
final fileLinePrefix = fileLineMatch.group(1)!;
if (fileLinePrefix.trim() != prefix.trim()) {
final line = oldContent.substring(0, blockStartIndex).split('\n').length;
errors.add(
'Error: Mismatched prefix for file metadata comment at $fileUri:$line. '
'Expected "$prefix", found "$fileLinePrefix"',
);
continue;
}
final filePath = fileLineMatch.group(2);
final noSourceFile = fileLineMatch.group(3) != null;
if (noSourceFile || filePath == null) continue;
final snippetUri = fileUri.resolve(filePath);
final snippetFile = File.fromUri(snippetUri);
if (!snippetFile.existsSync()) {
errors.add('Error: Snippet file not found: $snippetUri.');
continue;
}
final snippetContent = snippetFile.readAsStringSync().replaceAll(
'\r\n',
'\n',
);
var newSnippetText = snippetContent;
final anchorRegex = RegExp(
r'// snippet-start\n([\s\S]*?)// snippet-end',
multiLine: true,
);
final extractedMatch = anchorRegex.firstMatch(snippetContent);
if (extractedMatch != null) {
newSnippetText = extractedMatch.group(1)!;
}
final copyrightRegex = RegExp(r'''
// Copyright \(c\) [0-9]*, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
''');
final copyrightHeader = copyrightRegex.firstMatch(newSnippetText);
if (copyrightHeader != null) {
newSnippetText = newSnippetText.replaceFirst(
copyrightHeader.group(0)!,
'',
);
}
newSnippetText = newSnippetText.trim();
final newContentForBlock = newSnippetText
.split('\n')
.map((l) => l.isEmpty ? prefixWithoutSpace : '$prefix$l')
.join('\n');
final trailingNewline = content.endsWith('\n') ? '\n' : '';
newContent = newContent.replaceRange(
contentStart,
contentEnd,
'$newContentForBlock$trailingNewline',
);
}
return newContent;
}