blob: 5369f6eede0ef5fb5a1e70b8fd77ff6f4c036a28 [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 'package:file/file.dart';
import 'common/core.dart';
import 'common/output_utils.dart';
import 'common/package_looping_command.dart';
import 'common/repository_package.dart';
const int _exitBadTableEntry = 3;
const int _exitUnknownPackageEntry = 4;
/// A command to verify repository-level metadata about packages, such as
/// repo README and CODEOWNERS entries.
class RepoPackageInfoCheckCommand extends PackageLoopingCommand {
/// Creates Dependabot check command instance.
RepoPackageInfoCheckCommand(super.packagesDir, {super.gitDir});
late Directory _repoRoot;
/// Data from the root README.md table of packages.
final Map<String, List<String>> _readmeTableEntries =
<String, List<String>>{};
/// Packages with entries in CODEOWNERS.
final List<String> _ownedPackages = <String>[];
@override
final String name = 'repo-package-info-check';
@override
List<String> get aliases => <String>['check-repo-package-info'];
@override
final String description =
'Checks that all packages are listed correctly in the repo README.';
@override
final bool hasLongOutput = false;
@override
Future<void> initializeRun() async {
_repoRoot = packagesDir.fileSystem.directory((await gitDir).path);
// Extract all of the README.md table entries.
final RegExp namePattern = RegExp(r'\[(.*?)\]\(');
for (final String line
in _repoRoot.childFile('README.md').readAsLinesSync()) {
// Find all the table entries, skipping the header.
if (line.startsWith('|') &&
!line.startsWith('| Package') &&
!line.startsWith('|-')) {
final List<String> cells = line
.split('|')
.map((String s) => s.trim())
.where((String s) => s.isNotEmpty)
.toList();
// Extract the name, removing any markdown escaping.
final String? name =
namePattern.firstMatch(cells[0])?.group(1)?.replaceAll(r'\_', '_');
if (name == null) {
printError('Unexpected README table line:\n $line');
throw ToolExit(_exitBadTableEntry);
}
_readmeTableEntries[name] = cells;
if (!(packagesDir.childDirectory(name).existsSync() ||
thirdPartyPackagesDir.childDirectory(name).existsSync())) {
printError('Unknown package "$name" in root README.md table.');
throw ToolExit(_exitUnknownPackageEntry);
}
}
}
// Extract all of the CODEOWNERS package entries.
final RegExp packageOwnershipPattern =
RegExp(r'^((?:third_party/)?packages/(?:[^/]*/)?([^/]*))/\*\*');
for (final String line
in _repoRoot.childFile('CODEOWNERS').readAsLinesSync()) {
final RegExpMatch? match = packageOwnershipPattern.firstMatch(line);
if (match == null) {
continue;
}
final String path = match.group(1)!;
final String name = match.group(2)!;
if (!_repoRoot.childDirectory(path).existsSync()) {
printError('Unknown directory "$path" in CODEOWNERS');
throw ToolExit(_exitUnknownPackageEntry);
}
_ownedPackages.add(name);
}
}
@override
Future<PackageResult> runForPackage(RepositoryPackage package) async {
final String packageName = package.directory.basename;
final List<String> errors = <String>[];
// All packages should have an owner.
// Platform interface packages are considered to be owned by the app-facing
// package owner.
if (!(_ownedPackages.contains(packageName) ||
package.isPlatformInterface &&
_ownedPackages.contains(package.directory.parent.basename))) {
printError('${indentation}Missing CODEOWNERS entry.');
errors.add('Missing CODEOWNERS entry');
}
// Any published package should be in the README table.
// For federated plugins, only the app-facing package is listed.
if (package.isPublishable() &&
(!package.isFederated || package.isAppFacing)) {
final List<String>? cells = _readmeTableEntries[packageName];
if (cells == null) {
printError('${indentation}Missing repo root README.md table entry');
errors.add('Missing repo root README.md table entry');
} else {
// Extract the two parts of a "[label](link)" .md link.
final RegExp mdLinkPattern = RegExp(r'^\[(.*)\]\((.*)\)$');
// Possible link targets.
for (final String cell in cells) {
final RegExpMatch? match = mdLinkPattern.firstMatch(cell);
if (match == null) {
printError(
'${indentation}Invalid repo root README.md table entry: "$cell"');
errors.add('Invalid root README.md table entry');
} else {
final String encodedIssueTag =
Uri.encodeComponent(_issueTagForPackage(packageName));
final String encodedPRTag =
Uri.encodeComponent(_prTagForPackage(packageName));
final String anchor = match.group(1)!;
final String target = match.group(2)!;
// The anchor should be one of:
// - The package name (optionally with any underscores escaped)
// - An image with a name-based link
// - An image with a tag-based link
final RegExp packageLink =
RegExp(r'^!\[.*\]\(https://img.shields.io/pub/.*/'
'$packageName'
r'(?:\.svg)?\)$');
final RegExp issueTagLink = RegExp(
r'^!\[.*\]\(https://img.shields.io/github/issues/flutter/flutter/'
'$encodedIssueTag'
r'\?label=\)$');
final RegExp prTagLink = RegExp(
r'^!\[.*\]\(https://img.shields.io/github/issues-pr/flutter/packages/'
'$encodedPRTag'
r'\?label=\)$');
if (!(anchor == packageName ||
anchor == packageName.replaceAll('_', r'\_') ||
packageLink.hasMatch(anchor) ||
issueTagLink.hasMatch(anchor) ||
prTagLink.hasMatch(anchor))) {
printError(
'${indentation}Incorrect anchor in root README.md table: "$anchor"');
errors.add('Incorrect anchor in root README.md table');
}
// The link should be one of:
// - a relative link to the in-repo package
// - a pub.dev link to the package
// - a github label link to the package's label
final RegExp pubDevLink =
RegExp('^https://pub.dev/packages/$packageName(?:/score)?\$');
final RegExp gitHubIssueLink = RegExp(
'^https://github.com/flutter/flutter/labels/$encodedIssueTag\$');
final RegExp gitHubPRLink = RegExp(
'^https://github.com/flutter/packages/labels/$encodedPRTag\$');
if (!(target == './packages/$packageName/' ||
target == './third_party/packages/$packageName/' ||
pubDevLink.hasMatch(target) ||
gitHubIssueLink.hasMatch(target) ||
gitHubPRLink.hasMatch(target))) {
printError(
'${indentation}Incorrect link in root README.md table: "$target"');
errors.add('Incorrect link in root README.md table');
}
}
}
}
}
return errors.isEmpty
? PackageResult.success()
: PackageResult.fail(errors);
}
String _prTagForPackage(String packageName) => 'p: $packageName';
String _issueTagForPackage(String packageName) {
switch (packageName) {
case 'google_maps_flutter':
return 'p: maps';
case 'webview_flutter':
return 'p: webview';
default:
return 'p: $packageName';
}
}
}