Skip to content

Commit 7dc6d9a

Browse files
[tool] Only license-check checked-in files (#9905)
Instead of checking every file on the filesystem in the repo directory when checking licenses, only check the checked-in files. This avoids the problem of having many, many false positives when running locally if example apps have been built, since many builds create git-ignored files (generated files, downloaded Pods, etc.) that aren't our source, and so don't need to (and don't) pass the check that they contain our license block.
1 parent 9c6a5fe commit 7dc6d9a

File tree

2 files changed

+74
-24
lines changed

2 files changed

+74
-24
lines changed

script/tool/lib/src/license_check_command.dart

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'dart:io';
6+
57
import 'package:file/file.dart';
8+
import 'package:git/git.dart';
69
import 'package:path/path.dart' as p;
710

811
import 'common/core.dart';
912
import 'common/output_utils.dart';
1013
import 'common/package_command.dart';
1114

15+
const int _exitListFilesFailed = 3;
16+
1217
const Set<String> _codeFileExtensions = <String>{
1318
'.c',
1419
'.cc',
@@ -44,11 +49,6 @@ const Set<String> _ignoredFullBasenameList = <String>{
4449
'resource.h', // Generated by VS.
4550
};
4651

47-
// Path parts to ignore. Used to ignore entire subdirectories.
48-
const Set<String> _ignorePathPartList = <String>{
49-
'FlutterGeneratedPluginSwiftPackage', // Generated by Flutter tool.
50-
};
51-
5252
// Third-party packages where the code doesn't have file-level annotation, just
5353
// the package-level LICENSE file. Each entry must be a directory relative to
5454
// third_party/packages, as that is the only directory where this is allowed.
@@ -152,7 +152,7 @@ class LicenseCheckCommand extends PackageCommand {
152152
.map(
153153
(Directory dir) => '${dir.absolute.path}${platform.pathSeparator}');
154154

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

158158
final Iterable<File> codeFiles = allFiles.where((File file) =>
@@ -175,7 +175,7 @@ class LicenseCheckCommand extends PackageCommand {
175175
printError(
176176
'The following LICENSE files do not follow the expected format:');
177177
for (final File file in licenseFileFailures) {
178-
printError(' ${file.path}');
178+
printError(' ${_repoRelativePath(file)}');
179179
}
180180
printError('Please ensure that they use the exact format used in this '
181181
'repository".\n');
@@ -186,7 +186,7 @@ class LicenseCheckCommand extends PackageCommand {
186186
printError('The license block for these files is missing or incorrect:');
187187
for (final File file
188188
in codeFileFailures[_LicenseFailureType.incorrectFirstParty]!) {
189-
printError(' ${file.path}');
189+
printError(' ${_repoRelativePath(file)}');
190190
}
191191
printError(
192192
'If this third-party code, move it to a "third_party/" directory, '
@@ -200,7 +200,7 @@ class LicenseCheckCommand extends PackageCommand {
200200
'No recognized license was found for the following third-party files:');
201201
for (final File file
202202
in codeFileFailures[_LicenseFailureType.unknownThirdParty]!) {
203-
printError(' ${file.path}');
203+
printError(' ${_repoRelativePath(file)}');
204204
}
205205
print('Please check that they have a license at the top of the file. '
206206
'If they do, the license check needs to be updated to recognize '
@@ -242,7 +242,7 @@ class LicenseCheckCommand extends PackageCommand {
242242
};
243243

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

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

326-
final List<String> parts = path.split(file.path);
327-
if (parts.any(_ignorePathPartList.contains)) {
328-
return true;
329-
}
330-
331326
return false;
332327
}
333328

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

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

344351
// Returns the directories containing mapped submodules, if any.
345352
Future<Iterable<Directory>> _getSubmoduleDirectories() async {
@@ -358,6 +365,11 @@ class LicenseCheckCommand extends PackageCommand {
358365
}
359366
return submodulePaths;
360367
}
368+
369+
String _repoRelativePath(File file) {
370+
return p.posix.joinAll(path.split(
371+
path.relative(file.absolute.path, from: packagesDir.parent.path)));
372+
}
361373
}
362374

363375
enum _LicenseFailureType { incorrectFirstParty, unknownThirdParty }

script/tool/test/license_check_command_test.dart

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:file/file.dart';
77
import 'package:flutter_plugin_tools/src/common/core.dart';
88
import 'package:flutter_plugin_tools/src/license_check_command.dart';
99
import 'package:git/git.dart';
10+
import 'package:path/path.dart' as p;
1011
import 'package:platform/platform.dart';
1112
import 'package:test/test.dart';
1213

@@ -17,13 +18,14 @@ void main() {
1718
group('LicenseCheckCommand', () {
1819
late CommandRunner<void> runner;
1920
late Platform platform;
21+
late RecordingProcessRunner gitProcessRunner;
2022
late Directory packagesDir;
2123
late Directory root;
2224

2325
setUp(() {
2426
platform = MockPlatformWithSeparator();
2527
final GitDir gitDir;
26-
(:packagesDir, processRunner: _, gitProcessRunner: _, :gitDir) =
28+
(:packagesDir, processRunner: _, :gitProcessRunner, :gitDir) =
2729
configureBaseCommandMocks(platform: platform);
2830
root = packagesDir.parent;
2931

@@ -64,6 +66,22 @@ void main() {
6466
file.writeAsStringSync(lines.join(newline) + suffix + newline);
6567
}
6668

69+
/// Mocks `git ls-files` to return all files in `root`, simulating a
70+
/// repository where everything present is checked in.
71+
void mockGitFilesListWithAllFiles(Directory root) {
72+
final String fileList = root
73+
.listSync(recursive: true, followLinks: false)
74+
.whereType<File>()
75+
.map((File f) => p.posix
76+
.joinAll(p.split(p.relative(f.absolute.path, from: root.path))))
77+
.join('\n');
78+
79+
gitProcessRunner.mockProcessesForExecutable['git-ls-files'] =
80+
<FakeProcessInfo>[
81+
FakeProcessInfo(MockProcess(stdout: '$fileList\n')),
82+
];
83+
}
84+
6785
test('looks at only expected extensions', () async {
6886
final Map<String, bool> extensions = <String, bool>{
6987
'c': true,
@@ -88,6 +106,7 @@ void main() {
88106
for (final String fileExtension in extensions.keys) {
89107
root.childFile('$filenameBase.$fileExtension').createSync();
90108
}
109+
mockGitFilesListWithAllFiles(root);
91110

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

125145
final List<String> output =
126146
await runCapturingPrint(runner, <String>['license-check']);
@@ -157,7 +177,9 @@ void main() {
157177
}
158178
});
159179

160-
test('ignores FlutterGeneratedPluginSwiftPackage', () async {
180+
test('ignores files that are not checked in', () async {
181+
mockGitFilesListWithAllFiles(root);
182+
// Add files after creating the mock output from created files.
161183
final Directory packageDir = root
162184
.childDirectory('FlutterGeneratedPluginSwiftPackage')
163185
..createSync();
@@ -181,6 +203,7 @@ void main() {
181203
writeLicense(checked);
182204
final File notChecked = root.childFile('not_checked.md');
183205
notChecked.createSync();
206+
mockGitFilesListWithAllFiles(root);
184207

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

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

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

249275
Error? commandError;
250276
final List<String> output = await runCapturingPrint(
@@ -273,6 +299,7 @@ void main() {
273299
final File bad = root.childFile('bad.cc');
274300
bad.createSync();
275301
writeLicense(bad, copyright: '');
302+
mockGitFilesListWithAllFiles(root);
276303

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

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

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

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

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

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

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

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

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

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

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

537575
Error? commandError;
538576
final List<String> output = await runCapturingPrint(
@@ -563,7 +601,7 @@ void main() {
563601
final File checked = root.childFile('Package.swift');
564602
checked.createSync();
565603
writeLicense(checked, prefix: '// swift-tools-version: 5.9\n');
566-
604+
mockGitFilesListWithAllFiles(root);
567605
final List<String> output =
568606
await runCapturingPrint(runner, <String>['license-check']);
569607

0 commit comments

Comments
 (0)