blob: 4e0e5931d3eee95e96afe2b98122a6ca4b18f165 [file] [log] [blame]
// Copyright 2017 The Chromium 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:async';
import 'package:file/file.dart';
import 'package:path/path.dart' as p;
import 'common.dart';
const Set<String> _codeFileExtensions = <String>{
'.c',
'.cc',
'.cpp',
'.dart',
'.h',
'.html',
'.java',
'.m',
'.mm',
'.swift',
'.sh',
};
// Basenames without extensions of files to ignore.
const Set<String> _ignoreBasenameList = <String>{
'flutter_export_environment',
'GeneratedPluginRegistrant',
'generated_plugin_registrant',
};
// File suffixes that otherwise match _codeFileExtensions to ignore.
const Set<String> _ignoreSuffixList = <String>{
'.g.dart', // Generated API code.
'.mocks.dart', // Generated by Mockito.
};
// Full basenames of files to ignore.
const Set<String> _ignoredFullBasenameList = <String>{
'resource.h', // Generated by VS.
};
// Copyright and license regexes.
//
// These are intentionally very simple, since almost all source in this
// repository should be using the same license text, comment style, etc., so
// they shouldn't need to be very flexible. Complexity can be added as-needed
// on a case-by-case basis.
final RegExp _copyrightRegex =
RegExp(r'^(?://|#|<!--) Copyright \d+,? ([^.]+)', multiLine: true);
// Non-Flutter code. When adding license regexes here, include the copyright
// info to ensure that any new additions are flagged for added scrutiny in
// review.
// -----
// Third-party code used in url_launcher_web.
final RegExp _workivaLicenseRegex = RegExp(
r'^// Copyright 2017 Workiva Inc..*'
'^// Licensed under the Apache License, Version 2.0',
multiLine: true,
dotAll: true);
// TODO(stuartmorgan): Replace this with a single string once all the copyrights
// are standardized.
final List<String> _firstPartyAuthors = <String>[
'The Chromium Authors',
'the Chromium project authors',
'The Flutter Authors',
'the Flutter project authors',
];
/// Validates that code files have copyright and license blocks.
class LicenseCheckCommand extends PluginCommand {
/// Creates a new license check command for [packagesDir].
LicenseCheckCommand(
Directory packagesDir,
FileSystem fileSystem, {
Print print = print,
}) : _print = print,
super(packagesDir, fileSystem);
final Print _print;
@override
final String name = 'license-check';
@override
final String description =
'Ensures that all code files have copyright/license blocks.';
@override
Future<Null> run() async {
Iterable<File> codeFiles = (await _getAllFiles()).where((File file) =>
_codeFileExtensions.contains(p.extension(file.path)) &&
!_shouldIgnoreFile(file));
bool succeeded = await _checkLicenses(codeFiles);
if (!succeeded) {
throw ToolExit(1);
}
}
// Creates the expected license block (without copyright) for first-party
// code.
String _generateLicense(String comment, {String suffix = ''}) {
return '${comment}Use of this source code is governed by a BSD-style license that can be\n'
'${comment}found in the LICENSE file.$suffix\n';
}
// Checks all license blocks for [codeFiles], returning false if any of them
// fail validation.
Future<bool> _checkLicenses(Iterable<File> codeFiles) async {
final List<File> filesWithoutDetectedCopyright = <File>[];
final List<File> filesWithoutDetectedLicense = <File>[];
final List<File> misplacedThirdPartyFiles = <File>[];
// Most code file types in the repository use '//' comments.
final String defaultBsdLicenseBlock = _generateLicense('// ');
// A few file types have a different comment structure.
final Map<String, String> bsdLicenseBlockByExtension = <String, String>{
'.sh': _generateLicense('# '),
'.html': _generateLicense('', suffix: ' -->'),
};
for (final File file in codeFiles) {
_print('Checking ${file.path}');
final String content = await file.readAsString();
final RegExpMatch copyright = _copyrightRegex.firstMatch(content);
if (copyright == null) {
filesWithoutDetectedCopyright.add(file);
continue;
}
final String author = copyright.group(1);
if (!_firstPartyAuthors.contains(author) &&
!p.split(file.path).contains('third_party')) {
misplacedThirdPartyFiles.add(file);
}
final String bsdLicense =
bsdLicenseBlockByExtension[p.extension(file.path)] ??
defaultBsdLicenseBlock;
if (!content.contains(bsdLicense) &&
!_workivaLicenseRegex.hasMatch(content)) {
filesWithoutDetectedLicense.add(file);
}
}
_print('\n\n');
// Sort by path for more usable output.
final pathCompare = (File a, File b) => a.path.compareTo(b.path);
filesWithoutDetectedCopyright.sort(pathCompare);
filesWithoutDetectedLicense.sort(pathCompare);
misplacedThirdPartyFiles.sort(pathCompare);
if (filesWithoutDetectedCopyright.isNotEmpty) {
_print('No copyright line was found for the following files:');
for (final File file in filesWithoutDetectedCopyright) {
_print(' ${file.path}');
}
_print('Please check that they have a copyright and license block. '
'If they do, the license check may need to be updated to recognize its '
'format.\n');
}
if (filesWithoutDetectedLicense.isNotEmpty) {
_print('No recognized license was found for the following files:');
for (final File file in filesWithoutDetectedLicense) {
_print(' ${file.path}');
}
_print('Please check that they have a license at the top of the file. '
'If they do, the license check may need to be updated to recognize '
'either the license or the specific format of the license '
'block.\n');
}
if (misplacedThirdPartyFiles.isNotEmpty) {
_print('The following files do not have a recognized first-party author '
'but are not in a "third_party/" directory:');
for (final File file in misplacedThirdPartyFiles) {
_print(' ${file.path}');
}
_print('Please move these files to "third_party/".\n');
}
bool succeeded = filesWithoutDetectedCopyright.isEmpty &&
filesWithoutDetectedLicense.isEmpty &&
misplacedThirdPartyFiles.isEmpty;
if (succeeded) {
_print('All files passed validation!');
}
return succeeded;
}
bool _shouldIgnoreFile(File file) {
final String path = file.path;
return _ignoreBasenameList.contains(p.basenameWithoutExtension(path)) ||
_ignoreSuffixList.any((String suffix) =>
path.endsWith(suffix) ||
_ignoredFullBasenameList.contains(p.basename(path)));
}
Future<List<File>> _getAllFiles() => packagesDir.parent
.list(recursive: true, followLinks: false)
.where((FileSystemEntity entity) => entity is File)
.map((FileSystemEntity file) => file as File)
.toList();
}