|
| 1 | +// Copyright 2013 The Flutter Authors. All rights reserved. |
| 2 | +// Use of this source code is governed by a BSD-style license that can be |
| 3 | +// found in the LICENSE file. |
| 4 | + |
| 5 | +import 'package:file/file.dart'; |
| 6 | + |
| 7 | +import 'common/core.dart'; |
| 8 | +import 'common/output_utils.dart'; |
| 9 | +import 'common/package_looping_command.dart'; |
| 10 | +import 'common/repository_package.dart'; |
| 11 | + |
| 12 | +const int _exitBadTableEntry = 3; |
| 13 | +const int _exitUnknownPackageEntry = 4; |
| 14 | + |
| 15 | +/// A command to verify repository-level metadata about packages, such as |
| 16 | +/// repo README and CODEOWNERS entries. |
| 17 | +class RepoPackageInfoCheckCommand extends PackageLoopingCommand { |
| 18 | + /// Creates Dependabot check command instance. |
| 19 | + RepoPackageInfoCheckCommand(super.packagesDir, {super.gitDir}); |
| 20 | + |
| 21 | + late Directory _repoRoot; |
| 22 | + |
| 23 | + /// Data from the root README.md table of packages. |
| 24 | + final Map<String, List<String>> _readmeTableEntries = |
| 25 | + <String, List<String>>{}; |
| 26 | + |
| 27 | + /// Packages with entries in CODEOWNERS. |
| 28 | + final List<String> _ownedPackages = <String>[]; |
| 29 | + |
| 30 | + @override |
| 31 | + final String name = 'repo-package-info-check'; |
| 32 | + |
| 33 | + @override |
| 34 | + List<String> get aliases => <String>['check-repo-package-info']; |
| 35 | + |
| 36 | + @override |
| 37 | + final String description = |
| 38 | + 'Checks that all packages are listed correctly in the repo README.'; |
| 39 | + |
| 40 | + @override |
| 41 | + final bool hasLongOutput = false; |
| 42 | + |
| 43 | + @override |
| 44 | + Future<void> initializeRun() async { |
| 45 | + _repoRoot = packagesDir.fileSystem.directory((await gitDir).path); |
| 46 | + |
| 47 | + // Extract all of the README.md table entries. |
| 48 | + final RegExp namePattern = RegExp(r'\[(.*?)\]\('); |
| 49 | + for (final String line |
| 50 | + in _repoRoot.childFile('README.md').readAsLinesSync()) { |
| 51 | + // Find all the table entries, skipping the header. |
| 52 | + if (line.startsWith('|') && |
| 53 | + !line.startsWith('| Package') && |
| 54 | + !line.startsWith('|-')) { |
| 55 | + final List<String> cells = line |
| 56 | + .split('|') |
| 57 | + .map((String s) => s.trim()) |
| 58 | + .where((String s) => s.isNotEmpty) |
| 59 | + .toList(); |
| 60 | + // Extract the name, removing any markdown escaping. |
| 61 | + final String? name = |
| 62 | + namePattern.firstMatch(cells[0])?.group(1)?.replaceAll(r'\_', '_'); |
| 63 | + if (name == null) { |
| 64 | + printError('Unexpected README table line:\n $line'); |
| 65 | + throw ToolExit(_exitBadTableEntry); |
| 66 | + } |
| 67 | + _readmeTableEntries[name] = cells; |
| 68 | + |
| 69 | + if (!(packagesDir.childDirectory(name).existsSync() || |
| 70 | + thirdPartyPackagesDir.childDirectory(name).existsSync())) { |
| 71 | + printError('Unknown package "$name" in root README.md table.'); |
| 72 | + throw ToolExit(_exitUnknownPackageEntry); |
| 73 | + } |
| 74 | + } |
| 75 | + } |
| 76 | + |
| 77 | + // Extract all of the CODEOWNERS package entries. |
| 78 | + final RegExp packageOwnershipPattern = |
| 79 | + RegExp(r'^((?:third_party/)?packages/(?:[^/]*/)?([^/]*))/\*\*'); |
| 80 | + for (final String line |
| 81 | + in _repoRoot.childFile('CODEOWNERS').readAsLinesSync()) { |
| 82 | + final RegExpMatch? match = packageOwnershipPattern.firstMatch(line); |
| 83 | + if (match == null) { |
| 84 | + continue; |
| 85 | + } |
| 86 | + final String path = match.group(1)!; |
| 87 | + final String name = match.group(2)!; |
| 88 | + if (!_repoRoot.childDirectory(path).existsSync()) { |
| 89 | + printError('Unknown directory "$path" in CODEOWNERS'); |
| 90 | + throw ToolExit(_exitUnknownPackageEntry); |
| 91 | + } |
| 92 | + _ownedPackages.add(name); |
| 93 | + } |
| 94 | + } |
| 95 | + |
| 96 | + @override |
| 97 | + Future<PackageResult> runForPackage(RepositoryPackage package) async { |
| 98 | + final String packageName = package.directory.basename; |
| 99 | + final List<String> errors = <String>[]; |
| 100 | + |
| 101 | + // All packages should have an owner. |
| 102 | + // Platform interface packages are considered to be owned by the app-facing |
| 103 | + // package owner. |
| 104 | + if (!(_ownedPackages.contains(packageName) || |
| 105 | + package.isPlatformInterface && |
| 106 | + _ownedPackages.contains(package.directory.parent.basename))) { |
| 107 | + printError('${indentation}Missing CODEOWNERS entry.'); |
| 108 | + errors.add('Missing CODEOWNERS entry'); |
| 109 | + } |
| 110 | + |
| 111 | + // Any published package should be in the README table. |
| 112 | + // For federated plugins, only the app-facing package is listed. |
| 113 | + if (package.isPublishable() && |
| 114 | + (!package.isFederated || package.isAppFacing)) { |
| 115 | + final List<String>? cells = _readmeTableEntries[packageName]; |
| 116 | + |
| 117 | + if (cells == null) { |
| 118 | + printError('${indentation}Missing repo root README.md table entry'); |
| 119 | + errors.add('Missing repo root README.md table entry'); |
| 120 | + } else { |
| 121 | + // Extract the two parts of a "[label](link)" .md link. |
| 122 | + final RegExp mdLinkPattern = RegExp(r'^\[(.*)\]\((.*)\)$'); |
| 123 | + // Possible link targets. |
| 124 | + for (final String cell in cells) { |
| 125 | + final RegExpMatch? match = mdLinkPattern.firstMatch(cell); |
| 126 | + if (match == null) { |
| 127 | + printError( |
| 128 | + '${indentation}Invalid repo root README.md table entry: "$cell"'); |
| 129 | + errors.add('Invalid root README.md table entry'); |
| 130 | + } else { |
| 131 | + final String encodedIssueTag = |
| 132 | + Uri.encodeComponent(_issueTagForPackage(packageName)); |
| 133 | + final String encodedPRTag = |
| 134 | + Uri.encodeComponent(_prTagForPackage(packageName)); |
| 135 | + final String anchor = match.group(1)!; |
| 136 | + final String target = match.group(2)!; |
| 137 | + |
| 138 | + // The anchor should be one of: |
| 139 | + // - The package name (optionally with any underscores escaped) |
| 140 | + // - An image with a name-based link |
| 141 | + // - An image with a tag-based link |
| 142 | + final RegExp packageLink = |
| 143 | + RegExp(r'^!\[.*\]\(https://img.shields.io/pub/.*/' |
| 144 | + '$packageName' |
| 145 | + r'(?:\.svg)?\)$'); |
| 146 | + final RegExp issueTagLink = RegExp( |
| 147 | + r'^!\[.*\]\(https://img.shields.io/github/issues/flutter/flutter/' |
| 148 | + '$encodedIssueTag' |
| 149 | + r'\?label=\)$'); |
| 150 | + final RegExp prTagLink = RegExp( |
| 151 | + r'^!\[.*\]\(https://img.shields.io/github/issues-pr/flutter/packages/' |
| 152 | + '$encodedPRTag' |
| 153 | + r'\?label=\)$'); |
| 154 | + if (!(anchor == packageName || |
| 155 | + anchor == packageName.replaceAll('_', r'\_') || |
| 156 | + packageLink.hasMatch(anchor) || |
| 157 | + issueTagLink.hasMatch(anchor) || |
| 158 | + prTagLink.hasMatch(anchor))) { |
| 159 | + printError( |
| 160 | + '${indentation}Incorrect anchor in root README.md table: "$anchor"'); |
| 161 | + errors.add('Incorrect anchor in root README.md table'); |
| 162 | + } |
| 163 | + |
| 164 | + // The link should be one of: |
| 165 | + // - a relative link to the in-repo package |
| 166 | + // - a pub.dev link to the package |
| 167 | + // - a github label link to the package's label |
| 168 | + final RegExp pubDevLink = |
| 169 | + RegExp('^https://pub.dev/packages/$packageName(?:/score)?\$'); |
| 170 | + final RegExp gitHubIssueLink = RegExp( |
| 171 | + '^https://github.com/flutter/flutter/labels/$encodedIssueTag\$'); |
| 172 | + final RegExp gitHubPRLink = RegExp( |
| 173 | + '^https://github.com/flutter/packages/labels/$encodedPRTag\$'); |
| 174 | + if (!(target == './packages/$packageName/' || |
| 175 | + target == './third_party/packages/$packageName/' || |
| 176 | + pubDevLink.hasMatch(target) || |
| 177 | + gitHubIssueLink.hasMatch(target) || |
| 178 | + gitHubPRLink.hasMatch(target))) { |
| 179 | + printError( |
| 180 | + '${indentation}Incorrect link in root README.md table: "$target"'); |
| 181 | + errors.add('Incorrect link in root README.md table'); |
| 182 | + } |
| 183 | + } |
| 184 | + } |
| 185 | + } |
| 186 | + } |
| 187 | + |
| 188 | + return errors.isEmpty |
| 189 | + ? PackageResult.success() |
| 190 | + : PackageResult.fail(errors); |
| 191 | + } |
| 192 | + |
| 193 | + String _prTagForPackage(String packageName) => 'p: $packageName'; |
| 194 | + |
| 195 | + String _issueTagForPackage(String packageName) { |
| 196 | + switch (packageName) { |
| 197 | + case 'google_maps_flutter': |
| 198 | + return 'p: maps'; |
| 199 | + case 'webview_flutter': |
| 200 | + return 'p: webview'; |
| 201 | + default: |
| 202 | + return 'p: $packageName'; |
| 203 | + } |
| 204 | + } |
| 205 | +} |
0 commit comments