Skip to content

Commit cdf5b1d

Browse files
authored
Pub dependencies project validator (#106895)
1 parent da8070d commit cdf5b1d

File tree

7 files changed

+275
-8
lines changed

7 files changed

+275
-8
lines changed

packages/flutter_tools/lib/executable.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ List<FlutterCommand> generateCommands({
142142
logger: globals.logger,
143143
terminal: globals.terminal,
144144
artifacts: globals.artifacts!,
145+
// new ProjectValidators should be added here for the --suggestions to run
145146
allProjectValidators: <ProjectValidator>[],
146147
),
147148
AssembleCommand(verboseHelp: verboseHelp, buildSystem: globals.buildSystem),

packages/flutter_tools/lib/src/commands/analyze.dart

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,14 @@ class AnalyzeCommand extends FlutterCommand {
138138
}
139139
if (workingDirectory == null) {
140140
final Set<String> items = findDirectories(argResults!, _fileSystem);
141-
if (items.isEmpty || items.length > 1) {
142-
throwToolExit('The suggestions flags needs one directory path');
141+
if (items.isEmpty) { // user did not specify any path
142+
directoryPath = _fileSystem.currentDirectory.path;
143+
_logger.printTrace('Showing suggestions for current directory: $directoryPath');
144+
} else if (items.length > 1) { // if the user sends more than one path
145+
throwToolExit('The suggestions flag can process only one directory path');
146+
} else {
147+
directoryPath = items.first;
143148
}
144-
directoryPath = items.first;
145149
} else {
146150
directoryPath = workingDirectory!.path;
147151
}
@@ -150,6 +154,7 @@ class AnalyzeCommand extends FlutterCommand {
150154
logger: _logger,
151155
allProjectValidators: _allProjectValidators,
152156
userPath: directoryPath,
157+
processManager: _processManager,
153158
).run();
154159
} else if (boolArgDeprecated('watch')) {
155160
await AnalyzeContinuously(

packages/flutter_tools/lib/src/commands/validate_project.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
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 'package:process/process.dart';
6+
57
import '../base/file_system.dart';
68
import '../base/logger.dart';
79
import '../project.dart';
@@ -15,14 +17,16 @@ class ValidateProject {
1517
required this.logger,
1618
required this.allProjectValidators,
1719
required this.userPath,
18-
this.verbose = false
20+
required this.processManager,
21+
this.verbose = false,
1922
});
2023

2124
final FileSystem fileSystem;
2225
final Logger logger;
2326
final bool verbose;
2427
final String userPath;
2528
final List<ProjectValidator> allProjectValidators;
29+
final ProcessManager processManager;
2630

2731
Future<FlutterCommandResult> run() async {
2832
final Directory workingDirectory = userPath.isEmpty ? fileSystem.currentDirectory : fileSystem.directory(userPath);
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright 2014 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+
5+
import 'dart:collection';
6+
7+
/// Parsing the output of "dart pub deps --json"
8+
///
9+
/// expected structure: {"name": "package name", "source": "hosted", "dependencies": [...]}
10+
class DartDependencyPackage {
11+
DartDependencyPackage({
12+
required this.name,
13+
required this.version,
14+
required this.source,
15+
required this.dependencies,
16+
});
17+
18+
factory DartDependencyPackage.fromHashMap(dynamic packageInfo) {
19+
String name = '';
20+
String version = '';
21+
String source = '';
22+
List<dynamic> dependencies = <dynamic>[];
23+
24+
if (packageInfo is LinkedHashMap) {
25+
final LinkedHashMap<String, dynamic> info = packageInfo as LinkedHashMap<String, dynamic>;
26+
if (info.containsKey('name')) {
27+
name = info['name'] as String;
28+
}
29+
if (info.containsKey('version')) {
30+
version = info['version'] as String;
31+
}
32+
if (info.containsKey('source')) {
33+
source = info['source'] as String;
34+
}
35+
if (info.containsKey('dependencies')) {
36+
dependencies = info['dependencies'] as List<dynamic>;
37+
}
38+
}
39+
return DartDependencyPackage(
40+
name: name,
41+
version: version,
42+
source: source,
43+
dependencies: dependencies.map((dynamic e) => e.toString()).toList(),
44+
);
45+
}
46+
47+
final String name;
48+
final String version;
49+
final String source;
50+
final List<String> dependencies;
51+
52+
}
53+
54+
class DartPubJson {
55+
DartPubJson(this._json);
56+
final LinkedHashMap<String, dynamic> _json;
57+
final List<DartDependencyPackage> _packages = <DartDependencyPackage>[];
58+
59+
List<DartDependencyPackage> get packages {
60+
if (_packages.isNotEmpty) {
61+
return _packages;
62+
}
63+
if (_json.containsKey('packages')) {
64+
final List<dynamic> packagesInfo = _json['packages'] as List<dynamic>;
65+
for (final dynamic info in packagesInfo) {
66+
_packages.add(DartDependencyPackage.fromHashMap(info));
67+
}
68+
}
69+
return _packages;
70+
}
71+
}

packages/flutter_tools/lib/src/project_validator.dart

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,23 @@
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:collection';
6+
7+
import 'package:process/process.dart';
8+
9+
import 'base/io.dart';
10+
import 'convert.dart';
11+
import 'dart_pub_json_formatter.dart';
512
import 'flutter_manifest.dart';
613
import 'project.dart';
714
import 'project_validator_result.dart';
815

916
abstract class ProjectValidator {
17+
const ProjectValidator();
1018
String get title;
1119
bool supportsProject(FlutterProject project);
1220
/// Can return more than one result in case a file/command have a lot of info to share to the user
1321
Future<List<ProjectValidatorResult>> start(FlutterProject project);
14-
/// new ProjectValidators should be added here for the ValidateProjectCommand to run
15-
static List <ProjectValidator> allProjectValidators = <ProjectValidator>[
16-
GeneralInfoProjectValidator(),
17-
];
1822
}
1923

2024
/// Validator run for all platforms that extract information from the pubspec.yaml.
@@ -109,3 +113,77 @@ class GeneralInfoProjectValidator extends ProjectValidator{
109113
@override
110114
String get title => 'General Info';
111115
}
116+
117+
class PubDependenciesProjectValidator extends ProjectValidator {
118+
const PubDependenciesProjectValidator(this._processManager);
119+
final ProcessManager _processManager;
120+
121+
@override
122+
Future<List<ProjectValidatorResult>> start(FlutterProject project) async {
123+
const String name = 'Dart dependencies';
124+
final ProcessResult processResult = await _processManager.run(<String>['dart', 'pub', 'deps', '--json']);
125+
if (processResult.stdout is! String) {
126+
return <ProjectValidatorResult>[
127+
_createProjectValidatorError(name, 'Command dart pub deps --json failed')
128+
];
129+
}
130+
131+
final LinkedHashMap<String, dynamic> jsonResult;
132+
final List<ProjectValidatorResult> result = <ProjectValidatorResult>[];
133+
try {
134+
jsonResult = json.decode(
135+
processResult.stdout.toString()
136+
) as LinkedHashMap<String, dynamic>;
137+
} on FormatException{
138+
result.add(_createProjectValidatorError(name, processResult.stderr.toString()));
139+
return result;
140+
}
141+
142+
final DartPubJson dartPubJson = DartPubJson(jsonResult);
143+
final List <String> dependencies = <String>[];
144+
145+
// Information retrieved from the pubspec.lock file if a dependency comes from
146+
// the hosted url https://pub.dartlang.org we ignore it or if the package
147+
// is the current directory being analyzed (root).
148+
final Set<String> hostedDependencies = <String>{'hosted', 'root'};
149+
150+
for (final DartDependencyPackage package in dartPubJson.packages) {
151+
if (!hostedDependencies.contains(package.source)) {
152+
dependencies.addAll(package.dependencies);
153+
}
154+
}
155+
156+
if (dependencies.isNotEmpty) {
157+
final String verb = dependencies.length == 1 ? 'is' : 'are';
158+
result.add(
159+
ProjectValidatorResult(
160+
name: name,
161+
value: '${dependencies.join(', ')} $verb not hosted',
162+
status: StatusProjectValidator.warning,
163+
)
164+
);
165+
} else {
166+
result.add(
167+
const ProjectValidatorResult(
168+
name: name,
169+
value: 'All pub dependencies are hosted on https://pub.dartlang.org',
170+
status: StatusProjectValidator.success,
171+
)
172+
);
173+
}
174+
175+
return result;
176+
}
177+
178+
@override
179+
bool supportsProject(FlutterProject project) {
180+
return true;
181+
}
182+
183+
@override
184+
String get title => 'Pub dependencies';
185+
186+
ProjectValidatorResult _createProjectValidatorError(String name, String value) {
187+
return ProjectValidatorResult(name: name, value: value, status: StatusProjectValidator.error);
188+
}
189+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright 2014 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+
5+
import 'package:file/memory.dart';
6+
import 'package:flutter_tools/src/base/file_system.dart';
7+
import 'package:flutter_tools/src/project.dart';
8+
import 'package:flutter_tools/src/project_validator.dart';
9+
import 'package:flutter_tools/src/project_validator_result.dart';
10+
11+
import '../src/common.dart';
12+
import '../src/context.dart';
13+
14+
void main() {
15+
late FileSystem fileSystem;
16+
17+
group('PubDependenciesProjectValidator', () {
18+
19+
setUp(() {
20+
fileSystem = MemoryFileSystem.test();
21+
});
22+
23+
testWithoutContext('success when all dependencies are hosted', () async {
24+
final ProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
25+
const FakeCommand(
26+
command: <String>['dart', 'pub', 'deps', '--json'],
27+
stdout: '{"packages": [{"dependencies": ["abc"], "source": "hosted"}]}',
28+
),
29+
]);
30+
final PubDependenciesProjectValidator validator = PubDependenciesProjectValidator(processManager);
31+
32+
final List<ProjectValidatorResult> result = await validator.start(
33+
FlutterProject.fromDirectoryTest(fileSystem.currentDirectory)
34+
);
35+
const String expected = 'All pub dependencies are hosted on https://pub.dartlang.org';
36+
expect(result.length, 1);
37+
expect(result[0].value, expected);
38+
expect(result[0].status, StatusProjectValidator.success);
39+
});
40+
41+
testWithoutContext('error when command dart pub deps fails', () async {
42+
final ProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
43+
const FakeCommand(
44+
command: <String>['dart', 'pub', 'deps', '--json'],
45+
stderr: 'command fail',
46+
),
47+
]);
48+
final PubDependenciesProjectValidator validator = PubDependenciesProjectValidator(processManager);
49+
50+
final List<ProjectValidatorResult> result = await validator.start(
51+
FlutterProject.fromDirectoryTest(fileSystem.currentDirectory)
52+
);
53+
const String expected = 'command fail';
54+
expect(result.length, 1);
55+
expect(result[0].value, expected);
56+
expect(result[0].status, StatusProjectValidator.error);
57+
});
58+
59+
testWithoutContext('warning on dependencies not hosted', () async {
60+
final ProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
61+
const FakeCommand(
62+
command: <String>['dart', 'pub', 'deps', '--json'],
63+
stdout: '{"packages": [{"dependencies": ["dep1", "dep2"], "source": "other"}]}',
64+
),
65+
]);
66+
final PubDependenciesProjectValidator validator = PubDependenciesProjectValidator(processManager);
67+
68+
final List<ProjectValidatorResult> result = await validator.start(
69+
FlutterProject.fromDirectoryTest(fileSystem.currentDirectory)
70+
);
71+
const String expected = 'dep1, dep2 are not hosted';
72+
expect(result.length, 1);
73+
expect(result[0].value, expected);
74+
expect(result[0].status, StatusProjectValidator.warning);
75+
});
76+
});
77+
}

packages/flutter_tools/test/integration.shard/analyze_suggestions_integration_test.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,36 @@ void main() {
5454

5555
expect(loggerTest.statusText, contains(expected));
5656
});
57+
58+
testUsingContext('PubDependenciesProjectValidator success ', () async {
59+
final BufferLogger loggerTest = BufferLogger.test();
60+
final AnalyzeCommand command = AnalyzeCommand(
61+
artifacts: globals.artifacts!,
62+
fileSystem: fileSystem,
63+
logger: loggerTest,
64+
platform: globals.platform,
65+
terminal: globals.terminal,
66+
processManager: globals.processManager,
67+
allProjectValidators: <ProjectValidator>[
68+
PubDependenciesProjectValidator(globals.processManager),
69+
],
70+
);
71+
final CommandRunner<void> runner = createTestCommandRunner(command);
72+
73+
await runner.run(<String>[
74+
'analyze',
75+
'--no-pub',
76+
'--no-current-package',
77+
'--suggestions',
78+
'../../dev/integration_tests/flutter_gallery',
79+
]);
80+
81+
const String expected = '\n'
82+
'┌────────────────────────────────────────────────────────────────────────────────────┐\n'
83+
'│ Pub dependencies │\n'
84+
'│ [✓] Dart dependencies: All pub dependencies are hosted on https://pub.dartlang.org │\n'
85+
'└────────────────────────────────────────────────────────────────────────────────────┘\n';
86+
expect(loggerTest.statusText, contains(expected));
87+
});
5788
});
5889
}

0 commit comments

Comments
 (0)