blob: c19d1e2ef35bd5a29c5811073bdfea5143f9c64f [file] [log] [blame]
godofredoca6db0e22020-05-18 17:44:39 -07001// Copyright 2020 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
godofredoca6db0e22020-05-18 17:44:39 -07005import 'dart:core';
godofredoca6db0e22020-05-18 17:44:39 -07006import 'dart:io';
godofredoca6db0e22020-05-18 17:44:39 -07007import 'package:path/path.dart' as path;
8import 'dart:io' as io_internals show exit;
9
10final bool hasColor = stdout.supportsAnsiEscapes;
11final String bold = hasColor ? '\x1B[1m' : ''; // used for shard titles
12final String red = hasColor ? '\x1B[31m' : ''; // used for errors
13final String reset = hasColor ? '\x1B[0m' : '';
14final String reverse = hasColor ? '\x1B[7m' : ''; // used for clocks
15
Casey Hillers8c85a6a2023-03-17 09:43:49 -070016Future<void> main() async {
godofredoca6db0e22020-05-18 17:44:39 -070017 print('$clock STARTING ANALYSIS');
18 try {
Casey Hillers8c85a6a2023-03-17 09:43:49 -070019 await run();
godofredoca6db0e22020-05-18 17:44:39 -070020 } on ExitException catch (error) {
21 error.apply();
22 }
23 print('$clock ${bold}Analysis successful.$reset');
24}
25
Casey Hillers8c85a6a2023-03-17 09:43:49 -070026Future<void> run() async {
Ricardo Amadorfa3d7c12022-11-03 13:15:05 -070027 final String cocoonPath = path.join(path.dirname(Platform.script.path), '..');
godofredoca6db0e22020-05-18 17:44:39 -070028 print('$clock Root path: $cocoonPath');
29 print('$clock Licenses...');
Casey Hillers8c85a6a2023-03-17 09:43:49 -070030 await verifyConsistentLicenses(cocoonPath);
godofredoca6db0e22020-05-18 17:44:39 -070031 await verifyNoMissingLicense(cocoonPath);
32}
33
34// TESTS
35String _generateLicense(String prefix) {
Nehal Patel34f808c2023-01-25 13:43:17 -080036 return '${prefix}Copyright (2014|2015|2016|2017|2018|2019|2020|2021|2022|2023) The Flutter Authors. All rights reserved.\n'
godofredoca6db0e22020-05-18 17:44:39 -070037 '${prefix}Use of this source code is governed by a BSD-style license that can be\n'
38 '${prefix}found in the LICENSE file.';
39}
40
Casey Hillers8c85a6a2023-03-17 09:43:49 -070041/// Ensure that LICENSES in Cocoon and its packages are consistent with each other.
42///
43/// Verifies that every LICENSE file in Cocoon matches cocoon/LICENSE.
44Future<void> verifyConsistentLicenses(String workingDirectory) async {
45 final String goldenLicensePath = '$workingDirectory/LICENSE';
46 final String goldenLicense = File(goldenLicensePath).readAsStringSync();
47 if (goldenLicense.isEmpty) {
48 throw Exception('No LICENSE was found at the root of Cocoon');
49 }
50
51 final List<String> badLicenses = <String>[];
52 for (final FileSystemEntity entity in Directory(workingDirectory).listSync(recursive: true)) {
53 final String cocoonPath = entity.path.split('/../').last;
54 if (cocoonPath.contains(RegExp('(\.git)|(\.dart_tool)|(\.plugin_symlinks)'))) {
55 continue;
56 }
57
58 if (path.basename(entity.path) == 'LICENSE') {
59 final String license = File(entity.path).readAsStringSync();
60 if (license != goldenLicense) {
61 badLicenses.add(cocoonPath);
62 }
63 }
64 }
65
66 if (badLicenses.isNotEmpty) {
67 exitWithError(
68 <String>['The following LICENSE files do not match the golden LICENSE at root:']..insertAll(1, badLicenses),
69 );
70 }
71}
72
Kate Lovett96770cc2020-07-17 12:57:13 -070073Future<void> verifyNoMissingLicense(String workingDirectory, {bool checkMinimums = true}) async {
Casey Hillersfddb3f52021-09-27 12:33:01 -070074 final int? overrideMinimumMatches = checkMinimums ? null : 0;
Kate Lovett96770cc2020-07-17 12:57:13 -070075 await _verifyNoMissingLicenseForExtension(
Ricardo Amadorfa3d7c12022-11-03 13:15:05 -070076 workingDirectory,
77 'dart',
78 overrideMinimumMatches ?? 2000,
79 _generateLicense('// '),
80 );
Kate Lovett96770cc2020-07-17 12:57:13 -070081 await _verifyNoMissingLicenseForExtension(
Ricardo Amadorfa3d7c12022-11-03 13:15:05 -070082 workingDirectory,
83 'java',
84 overrideMinimumMatches ?? 39,
85 _generateLicense('// '),
86 );
Kate Lovett96770cc2020-07-17 12:57:13 -070087 await _verifyNoMissingLicenseForExtension(
Ricardo Amadorfa3d7c12022-11-03 13:15:05 -070088 workingDirectory,
89 'h',
90 overrideMinimumMatches ?? 30,
91 _generateLicense('// '),
92 );
Kate Lovett96770cc2020-07-17 12:57:13 -070093 await _verifyNoMissingLicenseForExtension(
Ricardo Amadorfa3d7c12022-11-03 13:15:05 -070094 workingDirectory,
95 'm',
96 overrideMinimumMatches ?? 30,
97 _generateLicense('// '),
98 );
Kate Lovett96770cc2020-07-17 12:57:13 -070099 await _verifyNoMissingLicenseForExtension(
Ricardo Amadorfa3d7c12022-11-03 13:15:05 -0700100 workingDirectory,
101 'swift',
102 overrideMinimumMatches ?? 10,
103 _generateLicense('// '),
104 );
Kate Lovett96770cc2020-07-17 12:57:13 -0700105 await _verifyNoMissingLicenseForExtension(
Ricardo Amadorfa3d7c12022-11-03 13:15:05 -0700106 workingDirectory,
107 'gradle',
108 overrideMinimumMatches ?? 100,
109 _generateLicense('// '),
110 );
Kate Lovett96770cc2020-07-17 12:57:13 -0700111 await _verifyNoMissingLicenseForExtension(
Ricardo Amadorfa3d7c12022-11-03 13:15:05 -0700112 workingDirectory,
113 'gn',
114 overrideMinimumMatches ?? 0,
115 _generateLicense('# '),
116 );
Kate Lovett96770cc2020-07-17 12:57:13 -0700117 await _verifyNoMissingLicenseForExtension(
Ricardo Amadorfa3d7c12022-11-03 13:15:05 -0700118 workingDirectory,
119 'Dockerfile',
120 overrideMinimumMatches ?? 1,
121 _generateLicense('# '),
122 );
godofredoc7bcd3332022-03-22 22:15:01 -0700123 await _verifyNoMissingLicenseForExtension(
Ricardo Amadorfa3d7c12022-11-03 13:15:05 -0700124 workingDirectory,
125 'sh',
126 overrideMinimumMatches ?? 1,
127 '#!/bin/bash\n' + _generateLicense('# '),
128 );
Kate Lovett96770cc2020-07-17 12:57:13 -0700129 await _verifyNoMissingLicenseForExtension(
Ricardo Amadorfa3d7c12022-11-03 13:15:05 -0700130 workingDirectory,
131 'bat',
132 overrideMinimumMatches ?? 1,
133 _generateLicense(':: '),
134 );
Kate Lovett96770cc2020-07-17 12:57:13 -0700135 await _verifyNoMissingLicenseForExtension(
Ricardo Amadorfa3d7c12022-11-03 13:15:05 -0700136 workingDirectory,
137 'ps1',
138 overrideMinimumMatches ?? 1,
139 _generateLicense('# '),
140 );
Kate Lovett96770cc2020-07-17 12:57:13 -0700141 await _verifyNoMissingLicenseForExtension(
Ricardo Amadorfa3d7c12022-11-03 13:15:05 -0700142 workingDirectory,
143 'html',
144 overrideMinimumMatches ?? 1,
145 '<!-- ${_generateLicense('')} -->',
146 trailingBlank: false,
147 );
Kate Lovett96770cc2020-07-17 12:57:13 -0700148 await _verifyNoMissingLicenseForExtension(
Ricardo Amadorfa3d7c12022-11-03 13:15:05 -0700149 workingDirectory,
150 'xml',
151 overrideMinimumMatches ?? 1,
152 '<!-- ${_generateLicense('')} -->',
153 );
godofredoca6db0e22020-05-18 17:44:39 -0700154}
155
Kate Lovett96770cc2020-07-17 12:57:13 -0700156Future<void> _verifyNoMissingLicenseForExtension(
Ricardo Amadorfa3d7c12022-11-03 13:15:05 -0700157 String workingDirectory,
158 String extension,
159 int minimumMatches,
160 String license, {
161 bool trailingBlank = true,
162}) async {
godofredoca6db0e22020-05-18 17:44:39 -0700163 assert(!license.endsWith('\n'));
164 final String licensePattern = license + '\n' + (trailingBlank ? '\n' : '');
165 final List<String> errors = <String>[];
Kate Lovett96770cc2020-07-17 12:57:13 -0700166 for (final File file in _allFiles(workingDirectory, extension, minimumMatches: minimumMatches)) {
godofredoca6db0e22020-05-18 17:44:39 -0700167 final String contents = file.readAsStringSync().replaceAll('\r\n', '\n');
Kate Lovett96770cc2020-07-17 12:57:13 -0700168 if (contents.isEmpty) continue; // let's not go down the /bin/true rabbit hole
godofredoca6db0e22020-05-18 17:44:39 -0700169 if (!contents.startsWith(RegExp(licensePattern))) errors.add(file.path);
170 }
171 // Fail if any errors
172 if (errors.isNotEmpty) {
173 final String s = errors.length == 1 ? ' does' : 's do';
174 exitWithError(<String>[
175 '${bold}The following ${errors.length} file$s not have the right license header:$reset',
176 ...errors,
177 'The expected license header is:',
178 license,
179 if (trailingBlank) '...followed by a blank line.',
180 ]);
181 }
182}
183
Casey Hillersfddb3f52021-09-27 12:33:01 -0700184Iterable<File> _allFiles(String workingDirectory, String extension, {required int minimumMatches}) sync* {
185 assert(!extension.startsWith('.'), 'Extension argument should not start with a period.');
Kate Lovett96770cc2020-07-17 12:57:13 -0700186 final Set<FileSystemEntity> pending = <FileSystemEntity>{Directory(workingDirectory)};
godofredoca6db0e22020-05-18 17:44:39 -0700187 int matches = 0;
188 while (pending.isNotEmpty) {
189 final FileSystemEntity entity = pending.first;
190 pending.remove(entity);
191 if (path.extension(entity.path) == '.tmpl') continue;
192 if (entity is File) {
193 if (_isGeneratedPluginRegistrant(entity)) continue;
Greg Spencer39737752021-01-14 09:49:59 -0800194 if (path.basename(entity.path) == 'AppDelegate.h') continue;
Kate Lovett96770cc2020-07-17 12:57:13 -0700195 if (path.basename(entity.path) == 'flutter_export_environment.sh') continue;
godofredoca6db0e22020-05-18 17:44:39 -0700196 if (path.basename(entity.path) == 'gradlew.bat') continue;
godofredoca6db0e22020-05-18 17:44:39 -0700197 if (path.basename(entity.path) == 'Runner-Bridging-Header.h') continue;
198 if (path.basename(entity.path).endsWith('g.dart')) continue;
Casey Hillersc81dd662021-09-23 16:02:59 -0700199 if (path.basename(entity.path).endsWith('mocks.mocks.dart')) continue;
godofredoca6db0e22020-05-18 17:44:39 -0700200 if (path.basename(entity.path).endsWith('pb.dart')) continue;
Greg Spencer39737752021-01-14 09:49:59 -0800201 if (path.basename(entity.path).endsWith('pbenum.dart')) continue;
Casey Hillersde0c55b2021-03-25 14:36:30 -0700202 if (path.basename(entity.path).endsWith('pbjson.dart')) continue;
Casey Hillersd8b94242021-06-29 12:31:00 -0700203 if (path.basename(entity.path).endsWith('pbserver.dart')) continue;
Casey Hillersfddb3f52021-09-27 12:33:01 -0700204 if (path.extension(entity.path) == '.$extension') {
godofredoca6db0e22020-05-18 17:44:39 -0700205 matches += 1;
206 yield entity;
207 }
godofredoc7bcd3332022-03-22 22:15:01 -0700208 if (path.basename(entity.path) == 'Dockerfile' && extension == 'Dockerfile') {
209 matches += 1;
210 yield entity;
211 }
godofredoca6db0e22020-05-18 17:44:39 -0700212 } else if (entity is Directory) {
213 if (File(path.join(entity.path, '.dartignore')).existsSync()) continue;
214 if (path.basename(entity.path) == '.git') continue;
215 if (path.basename(entity.path) == '.gradle') continue;
216 if (path.basename(entity.path) == '.dart_tool') continue;
Greg Spencer2191ed12021-01-19 20:14:44 -0800217 if (_isPartOfAppTemplate(entity)) continue;
godofredoca6db0e22020-05-18 17:44:39 -0700218 pending.addAll(entity.listSync());
219 }
220 }
Ricardo Amadorfa3d7c12022-11-03 13:15:05 -0700221 assert(
222 matches >= minimumMatches,
223 'Expected to find at least $minimumMatches files with extension ".$extension" in "$workingDirectory", but only found $matches.',
224 );
godofredoca6db0e22020-05-18 17:44:39 -0700225}
226
Greg Spencer2191ed12021-01-19 20:14:44 -0800227bool _isPartOfAppTemplate(Directory directory) {
228 const Set<String> templateDirs = <String>{
229 'android',
230 'build',
231 'ios',
232 'linux',
233 'macos',
234 'web',
235 'windows',
236 };
237 // Project directories will have a metadata file in them.
238 if (File(path.join(directory.parent.path, '.metadata')).existsSync()) {
239 return templateDirs.contains(path.basename(directory.path));
240 }
241 return false;
242}
243
godofredoca6db0e22020-05-18 17:44:39 -0700244bool _isGeneratedPluginRegistrant(File file) {
Greg Spencer39737752021-01-14 09:49:59 -0800245 final String filename = path.basenameWithoutExtension(file.path);
246 return !path.split(file.path).contains('.pub-cache') &&
247 (filename == 'generated_plugin_registrant' || filename == 'GeneratedPluginRegistrant');
godofredoca6db0e22020-05-18 17:44:39 -0700248}
249
250void exitWithError(List<String> messages) {
Kate Lovett96770cc2020-07-17 12:57:13 -0700251 final String redLine = '$red━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━$reset';
godofredoca6db0e22020-05-18 17:44:39 -0700252 print(redLine);
253 messages.forEach(print);
254 print(redLine);
255 exit(1);
256}
257
258class ExitException implements Exception {
259 ExitException(this.exitCode);
260
261 final int exitCode;
262
263 void apply() {
264 io_internals.exit(exitCode);
265 }
266}
267
268String get clock {
269 final DateTime now = DateTime.now();
270 return '$reverse▌'
271 '${now.hour.toString().padLeft(2, "0")}:'
272 '${now.minute.toString().padLeft(2, "0")}:'
273 '${now.second.toString().padLeft(2, "0")}'
274 '▐$reset';
275}