blob: 99e271c733889aee46ade820792a784b8b26532f [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 'package:git/git.dart';
import 'package:platform/platform.dart';
import 'package:pubspec_parse/pubspec_parse.dart';
import 'package:yaml/yaml.dart';
import 'common/core.dart';
import 'common/package_looping_command.dart';
import 'common/process_runner.dart';
import 'common/repository_package.dart';
/// A command to enforce README conventions across the repository.
class ReadmeCheckCommand extends PackageLoopingCommand {
/// Creates an instance of the README check command.
ReadmeCheckCommand(
Directory packagesDir, {
ProcessRunner processRunner = const ProcessRunner(),
Platform platform = const LocalPlatform(),
GitDir? gitDir,
}) : super(
packagesDir,
processRunner: processRunner,
platform: platform,
gitDir: gitDir,
);
// Standardized capitalizations for platforms that a plugin can support.
static const Map<String, String> _standardPlatformNames = <String, String>{
'android': 'Android',
'ios': 'iOS',
'linux': 'Linux',
'macos': 'macOS',
'web': 'Web',
'windows': 'Windows',
};
@override
final String name = 'readme-check';
@override
final String description =
'Checks that READMEs follow repository conventions.';
@override
bool get hasLongOutput => false;
@override
Future<PackageResult> runForPackage(RepositoryPackage package) async {
final File readme = package.readmeFile;
if (!readme.existsSync()) {
return PackageResult.fail(<String>['Missing README.md']);
}
final List<String> errors = <String>[];
final Pubspec pubspec = package.parsePubspec();
final bool isPlugin = pubspec.flutter?['plugin'] != null;
if (isPlugin && (!package.isFederated || package.isAppFacing)) {
final String? error = _validateSupportedPlatforms(package, pubspec);
if (error != null) {
errors.add(error);
}
}
return errors.isEmpty
? PackageResult.success()
: PackageResult.fail(errors);
}
/// Validates that the plugin has a supported platforms table following the
/// expected format, returning an error string if any issues are found.
String? _validateSupportedPlatforms(
RepositoryPackage package, Pubspec pubspec) {
final List<String> contents = package.readmeFile.readAsLinesSync();
// Example table following expected format:
// | | Android | iOS | Web |
// |----------------|---------|----------|------------------------|
// | **Support** | SDK 21+ | iOS 10+* | [See `camera_web `][1] |
final int detailsLineNumber =
contents.indexWhere((String line) => line.startsWith('| **Support**'));
if (detailsLineNumber == -1) {
return 'No OS support table found';
}
final int osLineNumber = detailsLineNumber - 2;
if (osLineNumber < 0 || !contents[osLineNumber].startsWith('|')) {
return 'OS support table does not have the expected header format';
}
// Utility method to convert an iterable of strings to a case-insensitive
// sorted, comma-separated string of its elements.
String sortedListString(Iterable<String> entries) {
final List<String> entryList = entries.toList();
entryList.sort(
(String a, String b) => a.toLowerCase().compareTo(b.toLowerCase()));
return entryList.join(', ');
}
// Validate that the supported OS lists match.
final dynamic platformsEntry = pubspec.flutter!['plugin']!['platforms'];
if (platformsEntry == null) {
logWarning('Plugin not support any platforms');
return null;
}
final YamlMap platformSupportMaps = platformsEntry as YamlMap;
final Set<String> actuallySupportedPlatform =
platformSupportMaps.keys.toSet().cast<String>();
final Iterable<String> documentedPlatforms = contents[osLineNumber]
.split('|')
.map((String entry) => entry.trim())
.where((String entry) => entry.isNotEmpty);
final Set<String> documentedPlatformsLowercase =
documentedPlatforms.map((String entry) => entry.toLowerCase()).toSet();
if (actuallySupportedPlatform.length != documentedPlatforms.length ||
actuallySupportedPlatform
.intersection(documentedPlatformsLowercase)
.length !=
actuallySupportedPlatform.length) {
printError('''
${indentation}OS support table does not match supported platforms:
${indentation * 2}Actual: ${sortedListString(actuallySupportedPlatform)}
${indentation * 2}Documented: ${sortedListString(documentedPlatformsLowercase)}
''');
return 'Incorrect OS support table';
}
// Enforce a standard set of capitalizations for the OS headings.
final Iterable<String> incorrectCapitalizations = documentedPlatforms
.toSet()
.difference(_standardPlatformNames.values.toSet());
if (incorrectCapitalizations.isNotEmpty) {
final Iterable<String> expectedVersions = incorrectCapitalizations
.map((String name) => _standardPlatformNames[name.toLowerCase()]!);
printError('''
${indentation}Incorrect OS capitalization: ${sortedListString(incorrectCapitalizations)}
${indentation * 2}Please use standard capitalizations: ${sortedListString(expectedVersions)}
''');
return 'Incorrect OS support formatting';
}
// TODO(stuartmorgan): Add validation that the minimums in the table are
// consistent with what the current implementations require. See
// https://github.com/flutter/flutter/issues/84200
return null;
}
}