| // 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(); |
| } |