diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart index 9ea76318cff..3d69b03b6ea 100644 --- a/packages/flutter_tools/lib/executable.dart +++ b/packages/flutter_tools/lib/executable.dart @@ -142,6 +142,7 @@ List generateCommands({ logger: globals.logger, terminal: globals.terminal, artifacts: globals.artifacts!, + // new ProjectValidators should be added here for the --suggestions to run allProjectValidators: [], ), AssembleCommand(verboseHelp: verboseHelp, buildSystem: globals.buildSystem), diff --git a/packages/flutter_tools/lib/src/commands/analyze.dart b/packages/flutter_tools/lib/src/commands/analyze.dart index 62c109d44dc..e63d2a42793 100644 --- a/packages/flutter_tools/lib/src/commands/analyze.dart +++ b/packages/flutter_tools/lib/src/commands/analyze.dart @@ -138,10 +138,14 @@ class AnalyzeCommand extends FlutterCommand { } if (workingDirectory == null) { final Set items = findDirectories(argResults!, _fileSystem); - if (items.isEmpty || items.length > 1) { - throwToolExit('The suggestions flags needs one directory path'); + if (items.isEmpty) { // user did not specify any path + directoryPath = _fileSystem.currentDirectory.path; + _logger.printTrace('Showing suggestions for current directory: $directoryPath'); + } else if (items.length > 1) { // if the user sends more than one path + throwToolExit('The suggestions flag can process only one directory path'); + } else { + directoryPath = items.first; } - directoryPath = items.first; } else { directoryPath = workingDirectory!.path; } @@ -150,6 +154,7 @@ class AnalyzeCommand extends FlutterCommand { logger: _logger, allProjectValidators: _allProjectValidators, userPath: directoryPath, + processManager: _processManager, ).run(); } else if (boolArgDeprecated('watch')) { await AnalyzeContinuously( diff --git a/packages/flutter_tools/lib/src/commands/validate_project.dart b/packages/flutter_tools/lib/src/commands/validate_project.dart index 9117f7cab47..b8ff3c37aa5 100644 --- a/packages/flutter_tools/lib/src/commands/validate_project.dart +++ b/packages/flutter_tools/lib/src/commands/validate_project.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:process/process.dart'; + import '../base/file_system.dart'; import '../base/logger.dart'; import '../project.dart'; @@ -15,7 +17,8 @@ class ValidateProject { required this.logger, required this.allProjectValidators, required this.userPath, - this.verbose = false + required this.processManager, + this.verbose = false, }); final FileSystem fileSystem; @@ -23,6 +26,7 @@ class ValidateProject { final bool verbose; final String userPath; final List allProjectValidators; + final ProcessManager processManager; Future run() async { final Directory workingDirectory = userPath.isEmpty ? fileSystem.currentDirectory : fileSystem.directory(userPath); diff --git a/packages/flutter_tools/lib/src/dart_pub_json_formatter.dart b/packages/flutter_tools/lib/src/dart_pub_json_formatter.dart new file mode 100644 index 00000000000..156df4804dd --- /dev/null +++ b/packages/flutter_tools/lib/src/dart_pub_json_formatter.dart @@ -0,0 +1,71 @@ +// 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:collection'; + +/// Parsing the output of "dart pub deps --json" +/// +/// expected structure: {"name": "package name", "source": "hosted", "dependencies": [...]} +class DartDependencyPackage { + DartDependencyPackage({ + required this.name, + required this.version, + required this.source, + required this.dependencies, + }); + + factory DartDependencyPackage.fromHashMap(dynamic packageInfo) { + String name = ''; + String version = ''; + String source = ''; + List dependencies = []; + + if (packageInfo is LinkedHashMap) { + final LinkedHashMap info = packageInfo as LinkedHashMap; + if (info.containsKey('name')) { + name = info['name'] as String; + } + if (info.containsKey('version')) { + version = info['version'] as String; + } + if (info.containsKey('source')) { + source = info['source'] as String; + } + if (info.containsKey('dependencies')) { + dependencies = info['dependencies'] as List; + } + } + return DartDependencyPackage( + name: name, + version: version, + source: source, + dependencies: dependencies.map((dynamic e) => e.toString()).toList(), + ); + } + + final String name; + final String version; + final String source; + final List dependencies; + +} + +class DartPubJson { + DartPubJson(this._json); + final LinkedHashMap _json; + final List _packages = []; + + List get packages { + if (_packages.isNotEmpty) { + return _packages; + } + if (_json.containsKey('packages')) { + final List packagesInfo = _json['packages'] as List; + for (final dynamic info in packagesInfo) { + _packages.add(DartDependencyPackage.fromHashMap(info)); + } + } + return _packages; + } +} diff --git a/packages/flutter_tools/lib/src/project_validator.dart b/packages/flutter_tools/lib/src/project_validator.dart index a2abcc47d87..595e77c2693 100644 --- a/packages/flutter_tools/lib/src/project_validator.dart +++ b/packages/flutter_tools/lib/src/project_validator.dart @@ -2,19 +2,23 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:collection'; + +import 'package:process/process.dart'; + +import 'base/io.dart'; +import 'convert.dart'; +import 'dart_pub_json_formatter.dart'; import 'flutter_manifest.dart'; import 'project.dart'; import 'project_validator_result.dart'; abstract class ProjectValidator { + const ProjectValidator(); String get title; bool supportsProject(FlutterProject project); /// Can return more than one result in case a file/command have a lot of info to share to the user Future> start(FlutterProject project); - /// new ProjectValidators should be added here for the ValidateProjectCommand to run - static List allProjectValidators = [ - GeneralInfoProjectValidator(), - ]; } /// Validator run for all platforms that extract information from the pubspec.yaml. @@ -109,3 +113,77 @@ class GeneralInfoProjectValidator extends ProjectValidator{ @override String get title => 'General Info'; } + +class PubDependenciesProjectValidator extends ProjectValidator { + const PubDependenciesProjectValidator(this._processManager); + final ProcessManager _processManager; + + @override + Future> start(FlutterProject project) async { + const String name = 'Dart dependencies'; + final ProcessResult processResult = await _processManager.run(['dart', 'pub', 'deps', '--json']); + if (processResult.stdout is! String) { + return [ + _createProjectValidatorError(name, 'Command dart pub deps --json failed') + ]; + } + + final LinkedHashMap jsonResult; + final List result = []; + try { + jsonResult = json.decode( + processResult.stdout.toString() + ) as LinkedHashMap; + } on FormatException{ + result.add(_createProjectValidatorError(name, processResult.stderr.toString())); + return result; + } + + final DartPubJson dartPubJson = DartPubJson(jsonResult); + final List dependencies = []; + + // Information retrieved from the pubspec.lock file if a dependency comes from + // the hosted url https://pub.dartlang.org we ignore it or if the package + // is the current directory being analyzed (root). + final Set hostedDependencies = {'hosted', 'root'}; + + for (final DartDependencyPackage package in dartPubJson.packages) { + if (!hostedDependencies.contains(package.source)) { + dependencies.addAll(package.dependencies); + } + } + + if (dependencies.isNotEmpty) { + final String verb = dependencies.length == 1 ? 'is' : 'are'; + result.add( + ProjectValidatorResult( + name: name, + value: '${dependencies.join(', ')} $verb not hosted', + status: StatusProjectValidator.warning, + ) + ); + } else { + result.add( + const ProjectValidatorResult( + name: name, + value: 'All pub dependencies are hosted on https://pub.dartlang.org', + status: StatusProjectValidator.success, + ) + ); + } + + return result; + } + + @override + bool supportsProject(FlutterProject project) { + return true; + } + + @override + String get title => 'Pub dependencies'; + + ProjectValidatorResult _createProjectValidatorError(String name, String value) { + return ProjectValidatorResult(name: name, value: value, status: StatusProjectValidator.error); + } +} diff --git a/packages/flutter_tools/test/general.shard/pub_dependencies_project_validator_test.dart b/packages/flutter_tools/test/general.shard/pub_dependencies_project_validator_test.dart new file mode 100644 index 00000000000..7591604f5df --- /dev/null +++ b/packages/flutter_tools/test/general.shard/pub_dependencies_project_validator_test.dart @@ -0,0 +1,77 @@ +// 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 'package:file/memory.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/project.dart'; +import 'package:flutter_tools/src/project_validator.dart'; +import 'package:flutter_tools/src/project_validator_result.dart'; + +import '../src/common.dart'; +import '../src/context.dart'; + +void main() { + late FileSystem fileSystem; + + group('PubDependenciesProjectValidator', () { + + setUp(() { + fileSystem = MemoryFileSystem.test(); + }); + + testWithoutContext('success when all dependencies are hosted', () async { + final ProcessManager processManager = FakeProcessManager.list([ + const FakeCommand( + command: ['dart', 'pub', 'deps', '--json'], + stdout: '{"packages": [{"dependencies": ["abc"], "source": "hosted"}]}', + ), + ]); + final PubDependenciesProjectValidator validator = PubDependenciesProjectValidator(processManager); + + final List result = await validator.start( + FlutterProject.fromDirectoryTest(fileSystem.currentDirectory) + ); + const String expected = 'All pub dependencies are hosted on https://pub.dartlang.org'; + expect(result.length, 1); + expect(result[0].value, expected); + expect(result[0].status, StatusProjectValidator.success); + }); + + testWithoutContext('error when command dart pub deps fails', () async { + final ProcessManager processManager = FakeProcessManager.list([ + const FakeCommand( + command: ['dart', 'pub', 'deps', '--json'], + stderr: 'command fail', + ), + ]); + final PubDependenciesProjectValidator validator = PubDependenciesProjectValidator(processManager); + + final List result = await validator.start( + FlutterProject.fromDirectoryTest(fileSystem.currentDirectory) + ); + const String expected = 'command fail'; + expect(result.length, 1); + expect(result[0].value, expected); + expect(result[0].status, StatusProjectValidator.error); + }); + + testWithoutContext('warning on dependencies not hosted', () async { + final ProcessManager processManager = FakeProcessManager.list([ + const FakeCommand( + command: ['dart', 'pub', 'deps', '--json'], + stdout: '{"packages": [{"dependencies": ["dep1", "dep2"], "source": "other"}]}', + ), + ]); + final PubDependenciesProjectValidator validator = PubDependenciesProjectValidator(processManager); + + final List result = await validator.start( + FlutterProject.fromDirectoryTest(fileSystem.currentDirectory) + ); + const String expected = 'dep1, dep2 are not hosted'; + expect(result.length, 1); + expect(result[0].value, expected); + expect(result[0].status, StatusProjectValidator.warning); + }); + }); +} diff --git a/packages/flutter_tools/test/integration.shard/analyze_suggestions_integration_test.dart b/packages/flutter_tools/test/integration.shard/analyze_suggestions_integration_test.dart index 52cbb17691e..cae469f2447 100644 --- a/packages/flutter_tools/test/integration.shard/analyze_suggestions_integration_test.dart +++ b/packages/flutter_tools/test/integration.shard/analyze_suggestions_integration_test.dart @@ -54,5 +54,36 @@ void main() { expect(loggerTest.statusText, contains(expected)); }); + + testUsingContext('PubDependenciesProjectValidator success ', () async { + final BufferLogger loggerTest = BufferLogger.test(); + final AnalyzeCommand command = AnalyzeCommand( + artifacts: globals.artifacts!, + fileSystem: fileSystem, + logger: loggerTest, + platform: globals.platform, + terminal: globals.terminal, + processManager: globals.processManager, + allProjectValidators: [ + PubDependenciesProjectValidator(globals.processManager), + ], + ); + final CommandRunner runner = createTestCommandRunner(command); + + await runner.run([ + 'analyze', + '--no-pub', + '--no-current-package', + '--suggestions', + '../../dev/integration_tests/flutter_gallery', + ]); + + const String expected = '\n' + '┌────────────────────────────────────────────────────────────────────────────────────┐\n' + '│ Pub dependencies │\n' + '│ [✓] Dart dependencies: All pub dependencies are hosted on https://pub.dartlang.org │\n' + '└────────────────────────────────────────────────────────────────────────────────────┘\n'; + expect(loggerTest.statusText, contains(expected)); + }); }); }