1// Copyright 2017 The Chromium 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 5import 'dart:async'; 6import 'dart:convert'; 7import 'dart:io'; 8 9import 'package:path/path.dart' as path; 10import 'package:meta/meta.dart'; 11 12import 'run_command.dart'; 13 14final String flutterRoot = path.dirname(path.dirname(path.dirname(path.fromUri(Platform.script)))); 15final String flutter = path.join(flutterRoot, 'bin', Platform.isWindows ? 'flutter.bat' : 'flutter'); 16final String dart = path.join(flutterRoot, 'bin', 'cache', 'dart-sdk', 'bin', Platform.isWindows ? 'dart.exe' : 'dart'); 17final String pub = path.join(flutterRoot, 'bin', 'cache', 'dart-sdk', 'bin', Platform.isWindows ? 'pub.bat' : 'pub'); 18final String pubCache = path.join(flutterRoot, '.pub-cache'); 19 20/// When you call this, you can pass additional arguments to pass custom 21/// arguments to flutter analyze. For example, you might want to call this 22/// script with the parameter --dart-sdk to use custom dart sdk. 23/// 24/// For example: 25/// bin/cache/dart-sdk/bin/dart dev/bots/analyze.dart --dart-sdk=/tmp/dart-sdk 26Future<void> main(List<String> args) async { 27 bool assertsEnabled = false; 28 assert(() { assertsEnabled = true; return true; }()); 29 if (!assertsEnabled) { 30 print('The analyze.dart script must be run with --enable-asserts.'); 31 exit(1); 32 } 33 await _verifyNoMissingLicense(flutterRoot); 34 await _verifyNoTestImports(flutterRoot); 35 await _verifyNoTestPackageImports(flutterRoot); 36 await _verifyGeneratedPluginRegistrants(flutterRoot); 37 await _verifyNoBadImportsInFlutter(flutterRoot); 38 await _verifyNoBadImportsInFlutterTools(flutterRoot); 39 await _verifyInternationalizations(); 40 41 { 42 // Analyze all the Dart code in the repo. 43 await _runFlutterAnalyze(flutterRoot, options: <String>[ 44 '--flutter-repo', 45 ...args, 46 ]); 47 } 48 49 // Ensure that all package dependencies are in sync. 50 await runCommand(flutter, <String>['update-packages', '--verify-only'], 51 workingDirectory: flutterRoot, 52 ); 53 54 // Analyze all the sample code in the repo 55 await runCommand(dart, 56 <String>[path.join(flutterRoot, 'dev', 'bots', 'analyze-sample-code.dart')], 57 workingDirectory: flutterRoot, 58 ); 59 60 // Try with the --watch analyzer, to make sure it returns success also. 61 // The --benchmark argument exits after one run. 62 { 63 await _runFlutterAnalyze(flutterRoot, options: <String>[ 64 '--flutter-repo', 65 '--watch', 66 '--benchmark', 67 ...args, 68 ]); 69 } 70 71 await _checkForTrailingSpaces(); 72 73 // Try analysis against a big version of the gallery; generate into a temporary directory. 74 final Directory outDir = Directory.systemTemp.createTempSync('flutter_mega_gallery.'); 75 76 try { 77 await runCommand(dart, 78 <String>[ 79 path.join(flutterRoot, 'dev', 'tools', 'mega_gallery.dart'), 80 '--out', 81 outDir.path, 82 ], 83 workingDirectory: flutterRoot, 84 ); 85 { 86 await _runFlutterAnalyze(outDir.path, options: <String>[ 87 '--watch', 88 '--benchmark', 89 ...args, 90 ]); 91 } 92 } finally { 93 outDir.deleteSync(recursive: true); 94 } 95 96 print('${bold}DONE: Analysis successful.$reset'); 97} 98 99Future<void> _verifyInternationalizations() async { 100 final EvalResult materialGenResult = await _evalCommand( 101 dart, 102 <String>[ 103 path.join('dev', 'tools', 'localization', 'gen_localizations.dart'), 104 '--material', 105 ], 106 workingDirectory: flutterRoot, 107 ); 108 final EvalResult cupertinoGenResult = await _evalCommand( 109 dart, 110 <String>[ 111 path.join('dev', 'tools', 'localization', 'gen_localizations.dart'), 112 '--cupertino', 113 ], 114 workingDirectory: flutterRoot, 115 ); 116 117 final String materialLocalizationsFile = path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n', 'generated_material_localizations.dart'); 118 final String cupertinoLocalizationsFile = path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n', 'generated_cupertino_localizations.dart'); 119 final String expectedMaterialResult = await File(materialLocalizationsFile).readAsString(); 120 final String expectedCupertinoResult = await File(cupertinoLocalizationsFile).readAsString(); 121 122 if (materialGenResult.stdout.trim() != expectedMaterialResult.trim()) { 123 stderr 124 ..writeln('<<<<<<< $materialLocalizationsFile') 125 ..writeln(expectedMaterialResult.trim()) 126 ..writeln('=======') 127 ..writeln(materialGenResult.stdout.trim()) 128 ..writeln('>>>>>>> gen_localizations') 129 ..writeln('The contents of $materialLocalizationsFile are different from that produced by gen_localizations.') 130 ..writeln() 131 ..writeln('Did you forget to run gen_localizations.dart after updating a .arb file?'); 132 exit(1); 133 } 134 if (cupertinoGenResult.stdout.trim() != expectedCupertinoResult.trim()) { 135 stderr 136 ..writeln('<<<<<<< $cupertinoLocalizationsFile') 137 ..writeln(expectedCupertinoResult.trim()) 138 ..writeln('=======') 139 ..writeln(cupertinoGenResult.stdout.trim()) 140 ..writeln('>>>>>>> gen_localizations') 141 ..writeln('The contents of $cupertinoLocalizationsFile are different from that produced by gen_localizations.') 142 ..writeln() 143 ..writeln('Did you forget to run gen_localizations.dart after updating a .arb file?'); 144 exit(1); 145 } 146} 147 148Future<String> _getCommitRange() async { 149 // Using --fork-point is more conservative, and will result in the correct 150 // fork point, but when running locally, it may return nothing. Git is 151 // guaranteed to return a (reasonable, but maybe not optimal) result when not 152 // using --fork-point, so we fall back to that if we can't get a definitive 153 // fork point. See "git merge-base" documentation for more info. 154 EvalResult result = await _evalCommand( 155 'git', 156 <String>['merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'], 157 workingDirectory: flutterRoot, 158 allowNonZeroExit: true, 159 ); 160 if (result.exitCode != 0) { 161 result = await _evalCommand( 162 'git', 163 <String>['merge-base', 'FETCH_HEAD', 'HEAD'], 164 workingDirectory: flutterRoot, 165 ); 166 } 167 return result.stdout.trim(); 168} 169 170 171Future<void> _checkForTrailingSpaces() async { 172 if (!Platform.isWindows) { 173 final String commitRange = Platform.environment.containsKey('TEST_COMMIT_RANGE') 174 ? Platform.environment['TEST_COMMIT_RANGE'] 175 : await _getCommitRange(); 176 final List<String> fileTypes = <String>[ 177 '*.dart', '*.cxx', '*.cpp', '*.cc', '*.c', '*.C', '*.h', '*.java', '*.mm', '*.m', '*.yml', 178 ]; 179 final EvalResult changedFilesResult = await _evalCommand( 180 'git', <String>['diff', '-U0', '--no-color', '--name-only', commitRange, '--'] + fileTypes, 181 workingDirectory: flutterRoot, 182 ); 183 if (changedFilesResult.stdout == null || changedFilesResult.stdout.trim().isEmpty) { 184 print('No files found that need to be checked for trailing whitespace.'); 185 return; 186 } 187 // Only include files that actually exist, so that we don't try and grep for 188 // nonexistent files, which can occur when files are deleted or moved. 189 final List<String> changedFiles = changedFilesResult.stdout.split('\n').where((String filename) { 190 return File(filename).existsSync(); 191 }).toList(); 192 if (changedFiles.isNotEmpty) { 193 await runCommand('grep', 194 <String>[ 195 '--line-number', 196 '--extended-regexp', 197 r'[[:blank:]]$', 198 ] + changedFiles, 199 workingDirectory: flutterRoot, 200 failureMessage: '${red}Whitespace detected at the end of source code lines.$reset\nPlease remove:', 201 expectNonZeroExit: true, // Just means a non-zero exit code is expected. 202 expectedExitCode: 1, // Indicates that zero lines were found. 203 ); 204 } 205 } 206} 207 208class EvalResult { 209 EvalResult({ 210 this.stdout, 211 this.stderr, 212 this.exitCode = 0, 213 }); 214 215 final String stdout; 216 final String stderr; 217 final int exitCode; 218} 219 220Future<EvalResult> _evalCommand(String executable, List<String> arguments, { 221 @required String workingDirectory, 222 Map<String, String> environment, 223 bool skip = false, 224 bool allowNonZeroExit = false, 225}) async { 226 final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}'; 227 final String relativeWorkingDir = path.relative(workingDirectory); 228 if (skip) { 229 printProgress('SKIPPING', relativeWorkingDir, commandDescription); 230 return null; 231 } 232 printProgress('RUNNING', relativeWorkingDir, commandDescription); 233 234 final DateTime start = DateTime.now(); 235 final Process process = await Process.start(executable, arguments, 236 workingDirectory: workingDirectory, 237 environment: environment, 238 ); 239 240 final Future<List<List<int>>> savedStdout = process.stdout.toList(); 241 final Future<List<List<int>>> savedStderr = process.stderr.toList(); 242 final int exitCode = await process.exitCode; 243 final EvalResult result = EvalResult( 244 stdout: utf8.decode((await savedStdout).expand<int>((List<int> ints) => ints).toList()), 245 stderr: utf8.decode((await savedStderr).expand<int>((List<int> ints) => ints).toList()), 246 exitCode: exitCode, 247 ); 248 249 print('$clock ELAPSED TIME: $bold${elapsedTime(start)}$reset for $commandDescription in $relativeWorkingDir: '); 250 251 if (exitCode != 0 && !allowNonZeroExit) { 252 stderr.write(result.stderr); 253 print( 254 '$redLine\n' 255 '${bold}ERROR:$red Last command exited with $exitCode.$reset\n' 256 '${bold}Command:$red $commandDescription$reset\n' 257 '${bold}Relative working directory:$red $relativeWorkingDir$reset\n' 258 '$redLine' 259 ); 260 exit(1); 261 } 262 263 return result; 264} 265 266Future<void> _runFlutterAnalyze(String workingDirectory, { 267 List<String> options = const <String>[], 268}) { 269 return runCommand( 270 flutter, 271 <String>['analyze', '--dartdocs', ...options], 272 workingDirectory: workingDirectory, 273 ); 274} 275 276Future<void> _verifyNoTestPackageImports(String workingDirectory) async { 277 // TODO(ianh): Remove this whole test once https://github.com/dart-lang/matcher/issues/98 is fixed. 278 final List<String> shims = <String>[]; 279 final List<String> errors = Directory(workingDirectory) 280 .listSync(recursive: true) 281 .where((FileSystemEntity entity) { 282 return entity is File && entity.path.endsWith('.dart'); 283 }) 284 .map<String>((FileSystemEntity entity) { 285 final File file = entity; 286 final String name = Uri.file(path.relative(file.path, 287 from: workingDirectory)).toFilePath(windows: false); 288 if (name.startsWith('bin/cache') || 289 name == 'dev/bots/test.dart' || 290 name.startsWith('.pub-cache')) 291 return null; 292 final String data = file.readAsStringSync(); 293 if (data.contains("import 'package:test/test.dart'")) { 294 if (data.contains("// Defines a 'package:test' shim.")) { 295 shims.add(' $name'); 296 if (!data.contains('https://github.com/dart-lang/matcher/issues/98')) 297 return ' $name: Shims must link to the isInstanceOf issue.'; 298 if (data.contains("import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;") && 299 data.contains("export 'package:test/test.dart' hide TypeMatcher, isInstanceOf;")) 300 return null; 301 return ' $name: Shim seems to be missing the expected import/export lines.'; 302 } 303 final int count = 'package:test'.allMatches(data).length; 304 if (path.split(file.path).contains('test_driver') || 305 name.startsWith('dev/missing_dependency_tests/') || 306 name.startsWith('dev/automated_tests/') || 307 name.startsWith('dev/snippets/') || 308 name.startsWith('packages/flutter/test/engine/') || 309 name.startsWith('examples/layers/test/smoketests/raw/') || 310 name.startsWith('examples/layers/test/smoketests/rendering/') || 311 name.startsWith('examples/flutter_gallery/test/calculator')) { 312 // We only exempt driver tests, some of our special trivial tests. 313 // Driver tests aren't typically expected to use TypeMatcher and company. 314 // The trivial tests don't typically do anything at all and it would be 315 // a pain to have to give them a shim. 316 if (!data.contains("import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;")) 317 return ' $name: test does not hide TypeMatcher and isInstanceOf from package:test; consider using a shim instead.'; 318 assert(count > 0); 319 if (count == 1) 320 return null; 321 return ' $name: uses \'package:test\' $count times.'; 322 } 323 if (name.startsWith('packages/flutter_test/')) { 324 // flutter_test has deep ties to package:test 325 return null; 326 } 327 if (data.contains("import 'package:test/test.dart' as test_package;") || 328 data.contains("import 'package:test/test.dart' as test_package show ")) { 329 if (count == 1) 330 return null; 331 } 332 return ' $name: uses \'package:test\' directly'; 333 } 334 return null; 335 }) 336 .where((String line) => line != null) 337 .toList() 338 ..sort(); 339 340 // Fail if any errors 341 if (errors.isNotEmpty) { 342 print('$redLine'); 343 final String s1 = errors.length == 1 ? 's' : ''; 344 final String s2 = errors.length == 1 ? '' : 's'; 345 print('${bold}The following file$s2 use$s1 \'package:test\' incorrectly:$reset'); 346 print(errors.join('\n')); 347 print('Rather than depending on \'package:test\' directly, use one of the shims:'); 348 print(shims.join('\n')); 349 print('This insulates us from breaking changes in \'package:test\'.'); 350 print('$redLine\n'); 351 exit(1); 352 } 353} 354 355Future<void> _verifyNoBadImportsInFlutter(String workingDirectory) async { 356 final List<String> errors = <String>[]; 357 final String libPath = path.join(workingDirectory, 'packages', 'flutter', 'lib'); 358 final String srcPath = path.join(workingDirectory, 'packages', 'flutter', 'lib', 'src'); 359 // Verify there's one libPath/*.dart for each srcPath/*/. 360 final List<String> packages = Directory(libPath).listSync() 361 .where((FileSystemEntity entity) => entity is File && path.extension(entity.path) == '.dart') 362 .map<String>((FileSystemEntity entity) => path.basenameWithoutExtension(entity.path)) 363 .toList()..sort(); 364 final List<String> directories = Directory(srcPath).listSync() 365 .whereType<Directory>() 366 .map<String>((Directory entity) => path.basename(entity.path)) 367 .toList()..sort(); 368 if (!_matches<String>(packages, directories)) { 369 errors.add( 370 'flutter/lib/*.dart does not match flutter/lib/src/*/:\n' 371 'These are the exported packages:\n' + 372 packages.map<String>((String path) => ' lib/$path.dart').join('\n') + 373 'These are the directories:\n' + 374 directories.map<String>((String path) => ' lib/src/$path/').join('\n') 375 ); 376 } 377 // Verify that the imports are well-ordered. 378 final Map<String, Set<String>> dependencyMap = <String, Set<String>>{}; 379 for (String directory in directories) { 380 dependencyMap[directory] = _findFlutterDependencies(path.join(srcPath, directory), errors, checkForMeta: directory != 'foundation'); 381 } 382 assert(dependencyMap['material'].contains('widgets') && 383 dependencyMap['widgets'].contains('rendering') && 384 dependencyMap['rendering'].contains('painting')); // to make sure we're convinced _findFlutterDependencies is finding some 385 for (String package in dependencyMap.keys) { 386 if (dependencyMap[package].contains(package)) { 387 errors.add( 388 'One of the files in the $yellow$package$reset package imports that package recursively.' 389 ); 390 } 391 } 392 for (String package in dependencyMap.keys) { 393 final List<String> loop = _deepSearch<String>(dependencyMap, package); 394 if (loop != null) { 395 errors.add( 396 '${yellow}Dependency loop:$reset ' + 397 loop.join(' depends on ') 398 ); 399 } 400 } 401 // Fail if any errors 402 if (errors.isNotEmpty) { 403 print('$redLine'); 404 if (errors.length == 1) { 405 print('${bold}An error was detected when looking at import dependencies within the Flutter package:$reset\n'); 406 } else { 407 print('${bold}Multiple errors were detected when looking at import dependencies within the Flutter package:$reset\n'); 408 } 409 print(errors.join('\n\n')); 410 print('$redLine\n'); 411 exit(1); 412 } 413} 414 415bool _matches<T>(List<T> a, List<T> b) { 416 assert(a != null); 417 assert(b != null); 418 if (a.length != b.length) 419 return false; 420 for (int index = 0; index < a.length; index += 1) { 421 if (a[index] != b[index]) 422 return false; 423 } 424 return true; 425} 426 427final RegExp _importPattern = RegExp(r'''^\s*import (['"])package:flutter/([^.]+)\.dart\1'''); 428final RegExp _importMetaPattern = RegExp(r'''^\s*import (['"])package:meta/meta\.dart\1'''); 429 430Set<String> _findFlutterDependencies(String srcPath, List<String> errors, { bool checkForMeta = false }) { 431 return Directory(srcPath).listSync(recursive: true).where((FileSystemEntity entity) { 432 return entity is File && path.extension(entity.path) == '.dart'; 433 }).map<Set<String>>((FileSystemEntity entity) { 434 final Set<String> result = <String>{}; 435 final File file = entity; 436 for (String line in file.readAsLinesSync()) { 437 Match match = _importPattern.firstMatch(line); 438 if (match != null) 439 result.add(match.group(2)); 440 if (checkForMeta) { 441 match = _importMetaPattern.firstMatch(line); 442 if (match != null) { 443 errors.add( 444 '${file.path}\nThis package imports the ${yellow}meta$reset package.\n' 445 'You should instead import the "foundation.dart" library.' 446 ); 447 } 448 } 449 } 450 return result; 451 }).reduce((Set<String> value, Set<String> element) { 452 value ??= <String>{}; 453 value.addAll(element); 454 return value; 455 }); 456} 457 458List<T> _deepSearch<T>(Map<T, Set<T>> map, T start, [ Set<T> seen ]) { 459 for (T key in map[start]) { 460 if (key == start) 461 continue; // we catch these separately 462 if (seen != null && seen.contains(key)) 463 return <T>[start, key]; 464 final List<T> result = _deepSearch<T>( 465 map, 466 key, 467 <T>{ 468 if (seen == null) start else ...seen, 469 key, 470 }, 471 ); 472 if (result != null) { 473 result.insert(0, start); 474 // Only report the shortest chains. 475 // For example a->b->a, rather than c->a->b->a. 476 // Since we visit every node, we know the shortest chains are those 477 // that start and end on the loop. 478 if (result.first == result.last) 479 return result; 480 } 481 } 482 return null; 483} 484 485Future<void> _verifyNoBadImportsInFlutterTools(String workingDirectory) async { 486 final List<String> errors = <String>[]; 487 for (FileSystemEntity entity in Directory(path.join(workingDirectory, 'packages', 'flutter_tools', 'lib')) 488 .listSync(recursive: true) 489 .where((FileSystemEntity entity) => entity is File && path.extension(entity.path) == '.dart')) { 490 final File file = entity; 491 if (file.readAsStringSync().contains('package:flutter_tools/')) { 492 errors.add('$yellow${file.path}$reset imports flutter_tools.'); 493 } 494 } 495 // Fail if any errors 496 if (errors.isNotEmpty) { 497 print('$redLine'); 498 if (errors.length == 1) { 499 print('${bold}An error was detected when looking at import dependencies within the flutter_tools package:$reset\n'); 500 } else { 501 print('${bold}Multiple errors were detected when looking at import dependencies within the flutter_tools package:$reset\n'); 502 } 503 print(errors.join('\n\n')); 504 print('$redLine\n'); 505 exit(1); 506 } 507} 508 509Future<void> _verifyNoMissingLicense(String workingDirectory) async { 510 final List<String> errors = <String>[]; 511 for (FileSystemEntity entity in Directory(path.join(workingDirectory, 'packages')) 512 .listSync(recursive: true) 513 .where((FileSystemEntity entity) => entity is File && path.extension(entity.path) == '.dart')) { 514 final File file = entity; 515 bool hasLicense = false; 516 final List<String> lines = file.readAsLinesSync(); 517 if (lines.isNotEmpty) 518 hasLicense = lines.first.startsWith(RegExp(r'// Copyright \d{4}')); 519 if (!hasLicense) 520 errors.add(file.path); 521 } 522 // Fail if any errors 523 if (errors.isNotEmpty) { 524 print('$redLine'); 525 final String s = errors.length == 1 ? '' : 's'; 526 print('${bold}License headers cannot be found at the beginning of the following file$s.$reset\n'); 527 print(errors.join('\n')); 528 print('$redLine\n'); 529 exit(1); 530 } 531} 532 533final RegExp _testImportPattern = RegExp(r'''import (['"])([^'"]+_test\.dart)\1'''); 534const Set<String> _exemptTestImports = <String>{ 535 'package:flutter_test/flutter_test.dart', 536 'hit_test.dart', 537 'package:test_api/src/backend/live_test.dart', 538}; 539 540Future<void> _verifyNoTestImports(String workingDirectory) async { 541 final List<String> errors = <String>[]; 542 assert("// foo\nimport 'binding_test.dart' as binding;\n'".contains(_testImportPattern)); 543 for (FileSystemEntity entity in Directory(path.join(workingDirectory, 'packages')) 544 .listSync(recursive: true) 545 .where((FileSystemEntity entity) => entity is File && path.extension(entity.path) == '.dart')) { 546 final File file = entity; 547 for (String line in file.readAsLinesSync()) { 548 final Match match = _testImportPattern.firstMatch(line); 549 if (match != null && !_exemptTestImports.contains(match.group(2))) 550 errors.add(file.path); 551 } 552 } 553 // Fail if any errors 554 if (errors.isNotEmpty) { 555 print('$redLine'); 556 final String s = errors.length == 1 ? '' : 's'; 557 print('${bold}The following file$s import a test directly. Test utilities should be in their own file.$reset\n'); 558 print(errors.join('\n')); 559 print('$redLine\n'); 560 exit(1); 561 } 562} 563 564Future<void> _verifyGeneratedPluginRegistrants(String flutterRoot) async { 565 final Directory flutterRootDir = Directory(flutterRoot); 566 567 final Map<String, List<File>> packageToRegistrants = <String, List<File>>{}; 568 569 for (FileSystemEntity entity in flutterRootDir.listSync(recursive: true)) { 570 if (entity is! File) 571 continue; 572 if (_isGeneratedPluginRegistrant(entity)) { 573 final String package = _getPackageFor(entity, flutterRootDir); 574 final List<File> registrants = packageToRegistrants.putIfAbsent(package, () => <File>[]); 575 registrants.add(entity); 576 } 577 } 578 579 final Set<String> outOfDate = <String>{}; 580 581 for (String package in packageToRegistrants.keys) { 582 final Map<File, String> fileToContent = <File, String>{}; 583 for (File f in packageToRegistrants[package]) { 584 fileToContent[f] = f.readAsStringSync(); 585 } 586 await runCommand(flutter, <String>['inject-plugins'], 587 workingDirectory: package, 588 outputMode: OutputMode.discard, 589 ); 590 for (File registrant in fileToContent.keys) { 591 if (registrant.readAsStringSync() != fileToContent[registrant]) { 592 outOfDate.add(registrant.path); 593 } 594 } 595 } 596 597 if (outOfDate.isNotEmpty) { 598 print('$redLine'); 599 print('${bold}The following GeneratedPluginRegistrants are out of date:$reset'); 600 for (String registrant in outOfDate) { 601 print(' - $registrant'); 602 } 603 print('\nRun "flutter inject-plugins" in the package that\'s out of date.'); 604 print('$redLine'); 605 exit(1); 606 } 607} 608 609String _getPackageFor(File entity, Directory flutterRootDir) { 610 for (Directory dir = entity.parent; dir != flutterRootDir; dir = dir.parent) { 611 if (File(path.join(dir.path, 'pubspec.yaml')).existsSync()) { 612 return dir.path; 613 } 614 } 615 throw ArgumentError('$entity is not within a dart package.'); 616} 617 618bool _isGeneratedPluginRegistrant(File file) { 619 final String filename = path.basename(file.path); 620 return !file.path.contains('.pub-cache') 621 && (filename == 'GeneratedPluginRegistrant.java' || 622 filename == 'GeneratedPluginRegistrant.h' || 623 filename == 'GeneratedPluginRegistrant.m'); 624} 625