| // Copyright 2014 The Flutter 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:io' show Platform; |
| |
| import 'package:file/file.dart'; |
| import 'package:path/path.dart' as path; |
| |
| /// A typedef that contains a set of [File]s which import Cupertino, |
| /// and a set of [File]s which import Material. |
| typedef CrossImportingFiles = ({Set<File> cupertinoImports, Set<File> materialImports}); |
| |
| /// A library that is checked for cross imports. |
| abstract interface class CrossImportCheckedLibrary { |
| /// Get the error message that is used to assert in [getImportError], |
| /// that an import error is expected. |
| /// |
| /// This error message should describe why an import is not allowed. |
| /// |
| /// For example: "Only Material is allowed to import Material" |
| String get cannotImportMessage; |
| |
| /// Get the list of known cross imports for this [CrossImportCheckedLibrary]. |
| Set<String> get knownCrossImports; |
| |
| /// The short name of the library. |
| /// |
| /// For example `packages/flutter/test/widgets` or `examples/api/foo`. |
| String get libraryName; |
| |
| /// The message that instructs how to remove now-fixed cross imports for this library, |
| /// in the relevant cross imports checker. |
| /// |
| /// For example: |
| /// "However, they now need to be removed from the\n cross imports list in the script /dev/bots/check_tests_cross_imports.dart." |
| String get removeCrossImportsInstructionMessage; |
| |
| /// Returns whether this library may contain the given [import]. |
| bool canImport(LibraryCrossImportStatementType import); |
| |
| /// Get an error message that describes that [filesCount] files |
| /// have a disallowed import of [importedLibraryName]. |
| /// |
| /// This message is used as preamble when listing the import errors emitted by a cross imports checker. |
| /// |
| /// For example: |
| /// "The following $filesCount files have a disallowed import of $importedLibraryName. Refactor them or move them to $importedLibraryName." |
| String getDisallowedImportMessage(String importedLibraryName, int filesCount); |
| } |
| |
| /// An enum that defines the possible cross import statements for libraries, |
| /// which are of special interest for a cross imports checker. |
| enum LibraryCrossImportStatementType { |
| /// A cross import of the Material library. |
| material('Material', "import 'package:flutter/material.dart'"), |
| |
| /// A cross import of the Cupertino library. |
| cupertino('Cupertino', "import 'package:flutter/cupertino.dart'"); |
| |
| const LibraryCrossImportStatementType(this.readableName, this.importString); |
| |
| /// The readable name for the library that is being cross imported. |
| /// |
| /// For example `Material` or `Cupertino`. |
| final String readableName; |
| |
| /// The import statement string for a cross import of this type. |
| /// |
| /// This string is a valid Dart import statement, |
| /// for example `import 'package:flutter/material.dart'`. |
| /// |
| /// This import statement does not include a trailing semicolon, |
| /// as there may be a `show` keyword following the import statement in the affected library. |
| final String importString; |
| } |
| |
| /// Returns the [Set] of paths in [knownPaths] that are not in [files]. |
| /// |
| /// Each file is expected to have a path that includes [prefix]. |
| Set<String> differencePaths(Set<String> knownPaths, Set<File> files, {required Pattern prefix}) { |
| final Set<String> testPaths = files.map((File file) { |
| final int index = file.absolute.path.indexOf(prefix); |
| if (index < 0) { |
| throw ArgumentError('All files must include $prefix in their path.', 'files'); |
| } |
| |
| return file.absolute.path.substring(index).replaceAll(Platform.pathSeparator, '/'); |
| }).toSet(); |
| |
| return knownPaths.difference(testPaths); |
| } |
| |
| /// Get the [Map] of files, per [CrossImportCheckedLibrary], |
| /// of the files that have cross imports on Material and Cupertino. |
| Map<CrossImportCheckedLibrary, CrossImportingFiles> getCrossImports( |
| Map<CrossImportCheckedLibrary, Set<File>> libraries, |
| ) { |
| final Map<CrossImportCheckedLibrary, CrossImportingFiles> crossImports = {}; |
| |
| for (final MapEntry<CrossImportCheckedLibrary, Set<File>> entry in libraries.entries) { |
| final Set<File> cupertinoImports = {}; |
| final Set<File> materialImports = {}; |
| |
| for (final File file in entry.value) { |
| final String contents = file.readAsStringSync(); |
| |
| for (final LibraryCrossImportStatementType importStatement |
| in LibraryCrossImportStatementType.values) { |
| switch (importStatement) { |
| case .cupertino: |
| if (!entry.key.canImport(importStatement) && |
| contents.contains(importStatement.importString)) { |
| cupertinoImports.add(file); |
| } |
| case .material: |
| if (!entry.key.canImport(importStatement) && |
| contents.contains(importStatement.importString)) { |
| materialImports.add(file); |
| } |
| } |
| } |
| } |
| |
| crossImports[entry.key] = ( |
| cupertinoImports: cupertinoImports, |
| materialImports: materialImports, |
| ); |
| } |
| |
| return crossImports; |
| } |
| |
| /// Returns the error message for the given [fixedPaths] |
| /// that no longer have a cross import. |
| /// |
| /// The [library] must not be the Material library, |
| /// because Material is allowed to cross-import. |
| String getFixedImportError(Set<String> fixedPaths, CrossImportCheckedLibrary library) { |
| assert(fixedPaths.isNotEmpty); |
| final buffer = StringBuffer( |
| 'Huzzah! The following files in ${library.libraryName} no longer contain cross imports!\n', |
| ); |
| for (final path in fixedPaths) { |
| buffer.writeln(' $path'); |
| } |
| buffer.write(library.removeCrossImportsInstructionMessage); |
| |
| return buffer.toString().trimRight(); |
| } |
| |
| /// Returns the import error for the [files] in [checkedLibrary] which contain the given [importStatement]. |
| String getImportError({ |
| required Set<File> files, |
| required Directory flutterRoot, |
| required CrossImportCheckedLibrary checkedLibrary, |
| required LibraryCrossImportStatementType importStatement, |
| }) { |
| assert(!checkedLibrary.canImport(importStatement), checkedLibrary.cannotImportMessage); |
| |
| final String importedLibraryName = importStatement.readableName; |
| final buffer = StringBuffer( |
| checkedLibrary.getDisallowedImportMessage(importedLibraryName, files.length), |
| ); |
| for (final file in files) { |
| buffer.writeln( |
| ' ${getRelativePath(file, flutterRoot: flutterRoot).replaceAll(Platform.pathSeparator, '/')}', |
| ); |
| } |
| return buffer.toString().trimRight(); |
| } |
| |
| /// Returns the [file]'s relative path, relative to [flutterRoot]. |
| String getRelativePath(File file, {required Directory flutterRoot}) { |
| return path.relative(file.absolute.path, from: flutterRoot.absolute.path); |
| } |
| |
| /// Returns the [Set] of files that are not in [knownPaths]. |
| /// |
| /// Each file is expected to have a path that includes [prefix]. |
| Set<File> getUnknowns(Set<String> knownPaths, Set<File> files, {required Pattern prefix}) { |
| return files.where((File file) { |
| final int index = file.absolute.path.indexOf(prefix); |
| |
| if (index < 0) { |
| throw ArgumentError('All files must include $prefix in their path.', 'files'); |
| } |
| |
| final String comparablePath = file.absolute.path |
| .substring(index) |
| .replaceAll(Platform.pathSeparator, '/'); |
| |
| return !knownPaths.contains(comparablePath); |
| }).toSet(); |
| } |