Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 31 additions & 21 deletions script/tool/lib/src/license_check_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:io';

import 'package:file/file.dart';
import 'package:git/git.dart';
import 'package:path/path.dart' as p;

import 'common/core.dart';
import 'common/output_utils.dart';
import 'common/package_command.dart';

const int _exitListFilesFailed = 3;

const Set<String> _codeFileExtensions = <String>{
'.c',
'.cc',
Expand Down Expand Up @@ -43,11 +48,6 @@ const Set<String> _ignoredFullBasenameList = <String>{
'resource.h', // Generated by VS.
};

// Path parts to ignore. Used to ignore entire subdirectories.
const Set<String> _ignorePathPartList = <String>{
'FlutterGeneratedPluginSwiftPackage', // Generated by Flutter tool.
};

// Third-party packages where the code doesn't have file-level annotation, just
// the package-level LICENSE file. Each entry must be a directory relative to
// third_party/packages, as that is the only directory where this is allowed.
Expand Down Expand Up @@ -151,7 +151,7 @@ class LicenseCheckCommand extends PackageCommand {
.map(
(Directory dir) => '${dir.absolute.path}${platform.pathSeparator}');

final Iterable<File> allFiles = (await _getAllFiles()).where(
final Iterable<File> allFiles = (await _getAllCheckedInFiles()).where(
(File file) => !submodulePaths.any(file.absolute.path.startsWith));

final Iterable<File> codeFiles = allFiles.where((File file) =>
Expand All @@ -174,7 +174,7 @@ class LicenseCheckCommand extends PackageCommand {
printError(
'The following LICENSE files do not follow the expected format:');
for (final File file in licenseFileFailures) {
printError(' ${file.path}');
printError(' ${_repoRelativePath(file)}');
}
printError('Please ensure that they use the exact format used in this '
'repository".\n');
Expand All @@ -185,7 +185,7 @@ class LicenseCheckCommand extends PackageCommand {
printError('The license block for these files is missing or incorrect:');
for (final File file
in codeFileFailures[_LicenseFailureType.incorrectFirstParty]!) {
printError(' ${file.path}');
printError(' ${_repoRelativePath(file)}');
}
printError(
'If this third-party code, move it to a "third_party/" directory, '
Expand All @@ -199,7 +199,7 @@ class LicenseCheckCommand extends PackageCommand {
'No recognized license was found for the following third-party files:');
for (final File file
in codeFileFailures[_LicenseFailureType.unknownThirdParty]!) {
printError(' ${file.path}');
printError(' ${_repoRelativePath(file)}');
}
print('Please check that they have a license at the top of the file. '
'If they do, the license check needs to be updated to recognize '
Expand Down Expand Up @@ -241,7 +241,7 @@ class LicenseCheckCommand extends PackageCommand {
};

for (final File file in codeFiles) {
print('Checking ${file.path}');
print('Checking ${_repoRelativePath(file)}');
// Some third-party directories have code that doesn't annotate each file,
// so for those check the LICENSE file instead. This is done even though
// it's redundant to re-check it for each file because it ensures that we
Expand Down Expand Up @@ -297,7 +297,7 @@ class LicenseCheckCommand extends PackageCommand {
final List<File> incorrectLicenseFiles = <File>[];

for (final File file in files) {
print('Checking ${file.path}');
print('Checking ${_repoRelativePath(file)}');
// On Windows, git may auto-convert line endings on checkout; this should
// still pass since they will be converted back on commit.
final String contents = file.readAsStringSync().replaceAll('\r\n', '\n');
Expand All @@ -322,23 +322,29 @@ class LicenseCheckCommand extends PackageCommand {
return true;
}

final List<String> parts = path.split(file.path);
if (parts.any(_ignorePathPartList.contains)) {
return true;
}

return false;
}

bool _isThirdParty(File file) {
return path.split(file.path).contains('third_party');
}

Future<List<File>> _getAllFiles() => packagesDir.parent
.list(recursive: true, followLinks: false)
.where((FileSystemEntity entity) => entity is File)
.map((FileSystemEntity file) => file as File)
.toList();
Future<Iterable<File>> _getAllCheckedInFiles() async {
final GitDir git = await gitDir;
final ProcessResult result =
await git.runCommand(<String>['ls-files'], throwOnError: false);
if (result.exitCode != 0) {
printError('Unable to get list of files under source control');
throw ToolExit(_exitListFilesFailed);
}
final Directory repoRoot = packagesDir.parent;
return (result.stdout as String)
.trim()
.split('\n')
.map((String path) => repoRoot.childFile(path))
// Filter out symbolic links to avoid checking files multiple times.
.where((File f) => !repoRoot.fileSystem.isLinkSync(f.path));
}

// Returns the directories containing mapped submodules, if any.
Future<Iterable<Directory>> _getSubmoduleDirectories() async {
Expand All @@ -357,6 +363,10 @@ class LicenseCheckCommand extends PackageCommand {
}
return submodulePaths;
}

String _repoRelativePath(File file) {
return p.relative(file.absolute.path, from: packagesDir.parent.path);
}
}

enum _LicenseFailureType { incorrectFirstParty, unknownThirdParty }
44 changes: 41 additions & 3 deletions script/tool/test/license_check_command_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:file/file.dart';
import 'package:flutter_plugin_tools/src/common/core.dart';
import 'package:flutter_plugin_tools/src/license_check_command.dart';
import 'package:git/git.dart';
import 'package:path/path.dart' as p;
import 'package:platform/platform.dart';
import 'package:test/test.dart';

Expand All @@ -17,13 +18,14 @@ void main() {
group('LicenseCheckCommand', () {
late CommandRunner<void> runner;
late Platform platform;
late RecordingProcessRunner gitProcessRunner;
late Directory packagesDir;
late Directory root;

setUp(() {
platform = MockPlatformWithSeparator();
final GitDir gitDir;
(:packagesDir, processRunner: _, gitProcessRunner: _, :gitDir) =
(:packagesDir, processRunner: _, :gitProcessRunner, :gitDir) =
configureBaseCommandMocks(platform: platform);
root = packagesDir.parent;

Expand Down Expand Up @@ -64,6 +66,22 @@ void main() {
file.writeAsStringSync(lines.join(newline) + suffix + newline);
}

/// Mocks `git ls-files` to return all files in `root`, simulating a
/// repository where everything present is checked in.
void mockGitFilesListWithAllFiles(Directory root) {
final String fileList = root
.listSync(recursive: true, followLinks: false)
.whereType<File>()
.map((File f) => p.posix
.joinAll(p.split(p.relative(f.absolute.path, from: root.path))))
.join('\n');

gitProcessRunner.mockProcessesForExecutable['git-ls-files'] =
<FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: '$fileList\n')),
];
}

test('looks at only expected extensions', () async {
final Map<String, bool> extensions = <String, bool>{
'c': true,
Expand All @@ -88,6 +106,7 @@ void main() {
for (final String fileExtension in extensions.keys) {
root.childFile('$filenameBase.$fileExtension').createSync();
}
mockGitFilesListWithAllFiles(root);

final List<String> output = await runCapturingPrint(
runner, <String>['license-check'], errorHandler: (Error e) {
Expand Down Expand Up @@ -121,6 +140,7 @@ void main() {
for (final String name in ignoredFiles) {
root.childFile(name).createSync();
}
mockGitFilesListWithAllFiles(root);

final List<String> output =
await runCapturingPrint(runner, <String>['license-check']);
Expand Down Expand Up @@ -157,7 +177,9 @@ void main() {
}
});

test('ignores FlutterGeneratedPluginSwiftPackage', () async {
test('ignores files that are not checked in', () async {
mockGitFilesListWithAllFiles(root);
// Add files after creating the mock output from created files.
final Directory packageDir = root
.childDirectory('FlutterGeneratedPluginSwiftPackage')
..createSync();
Expand All @@ -181,6 +203,7 @@ void main() {
writeLicense(checked);
final File notChecked = root.childFile('not_checked.md');
notChecked.createSync();
mockGitFilesListWithAllFiles(root);

final List<String> output =
await runCapturingPrint(runner, <String>['license-check']);
Expand All @@ -198,6 +221,7 @@ void main() {
final File checked = root.childFile('checked.cc');
checked.createSync();
writeLicense(checked, useCrlf: true);
mockGitFilesListWithAllFiles(root);

final List<String> output =
await runCapturingPrint(runner, <String>['license-check']);
Expand All @@ -221,6 +245,7 @@ void main() {
final File fileC = root.childFile('file_c.html');
fileC.createSync();
writeLicense(fileC, comment: '', prefix: '<!-- ', suffix: ' -->');
mockGitFilesListWithAllFiles(root);

final List<String> output =
await runCapturingPrint(runner, <String>['license-check']);
Expand All @@ -245,6 +270,7 @@ void main() {
writeLicense(goodB);
root.childFile('bad.cc').createSync();
root.childFile('bad.h').createSync();
mockGitFilesListWithAllFiles(root);

Error? commandError;
final List<String> output = await runCapturingPrint(
Expand Down Expand Up @@ -273,6 +299,7 @@ void main() {
final File bad = root.childFile('bad.cc');
bad.createSync();
writeLicense(bad, copyright: '');
mockGitFilesListWithAllFiles(root);

Error? commandError;
final List<String> output = await runCapturingPrint(
Expand Down Expand Up @@ -300,6 +327,7 @@ void main() {
final File bad = root.childFile('bad.cc');
bad.createSync();
writeLicense(bad, license: <String>[]);
mockGitFilesListWithAllFiles(root);

Error? commandError;
final List<String> output = await runCapturingPrint(
Expand All @@ -325,6 +353,7 @@ void main() {
final File thirdPartyFile = root.childFile('third_party.cc');
thirdPartyFile.createSync();
writeLicense(thirdPartyFile, copyright: 'Copyright 2017 Someone Else');
mockGitFilesListWithAllFiles(root);

Error? commandError;
final List<String> output = await runCapturingPrint(
Expand Down Expand Up @@ -359,6 +388,7 @@ void main() {
'Licensed under the Apache License, Version 2.0 (the "License");',
'you may not use this file except in compliance with the License.'
]);
mockGitFilesListWithAllFiles(root);

final List<String> output =
await runCapturingPrint(runner, <String>['license-check']);
Expand All @@ -381,6 +411,7 @@ void main() {
.childFile('first_party.cc');
firstPartyFileInThirdParty.createSync(recursive: true);
writeLicense(firstPartyFileInThirdParty);
mockGitFilesListWithAllFiles(root);

final List<String> output =
await runCapturingPrint(runner, <String>['license-check']);
Expand All @@ -404,6 +435,7 @@ void main() {
'This program is free software: you can redistribute it and/or modify',
'it under the terms of the GNU General Public License',
]);
mockGitFilesListWithAllFiles(root);

Error? commandError;
final List<String> output = await runCapturingPrint(
Expand Down Expand Up @@ -439,6 +471,7 @@ void main() {
'you may not use this file except in compliance with the License.'
],
);
mockGitFilesListWithAllFiles(root);

Error? commandError;
final List<String> output = await runCapturingPrint(
Expand All @@ -464,6 +497,7 @@ void main() {
final File license = root.childFile('LICENSE');
license.createSync();
license.writeAsStringSync(_correctLicenseFileText);
mockGitFilesListWithAllFiles(root);

final List<String> output =
await runCapturingPrint(runner, <String>['license-check']);
Expand All @@ -482,6 +516,7 @@ void main() {
license.createSync();
license
.writeAsStringSync(_correctLicenseFileText.replaceAll('\n', '\r\n'));
mockGitFilesListWithAllFiles(root);

final List<String> output =
await runCapturingPrint(runner, <String>['license-check']);
Expand All @@ -500,6 +535,7 @@ void main() {
final File license = root.childFile('LICENSE');
license.createSync();
license.writeAsStringSync(_incorrectLicenseFileText);
mockGitFilesListWithAllFiles(root);

Error? commandError;
final List<String> output = await runCapturingPrint(
Expand All @@ -516,6 +552,7 @@ void main() {
root.childDirectory('third_party').childFile('LICENSE');
license.createSync(recursive: true);
license.writeAsStringSync(_incorrectLicenseFileText);
mockGitFilesListWithAllFiles(root);

final List<String> output =
await runCapturingPrint(runner, <String>['license-check']);
Expand All @@ -533,6 +570,7 @@ void main() {
final File license = root.childFile('LICENSE');
license.createSync();
license.writeAsStringSync(_incorrectLicenseFileText);
mockGitFilesListWithAllFiles(root);

Error? commandError;
final List<String> output = await runCapturingPrint(
Expand Down Expand Up @@ -563,7 +601,7 @@ void main() {
final File checked = root.childFile('Package.swift');
checked.createSync();
writeLicense(checked, prefix: '// swift-tools-version: 5.9\n');

mockGitFilesListWithAllFiles(root);
final List<String> output =
await runCapturingPrint(runner, <String>['license-check']);

Expand Down