diff --git a/bin/dependency_validator.dart b/bin/dependency_validator.dart index bc762f8..b6e92fe 100644 --- a/bin/dependency_validator.dart +++ b/bin/dependency_validator.dart @@ -12,13 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -import 'dart:io' show exit, stderr, stdout; +import 'dart:io' show Directory, exit, stderr, stdout; import 'package:args/args.dart'; import 'package:dependency_validator/src/dependency_validator.dart'; import 'package:io/io.dart'; import 'package:logging/logging.dart'; +const String pathArg = 'path'; +const String autoFixArg = 'auto-fix'; const String helpArg = 'help'; const String verboseArg = 'verbose'; const String helpMessage = @@ -36,6 +38,16 @@ usage:'''; /// Parses the command-line arguments final ArgParser argParser = ArgParser() + ..addOption( + pathArg, + abbr: 'p', + help: 'Specify package path', + ) + ..addFlag( + autoFixArg, + abbr: 'a', + help: 'Auto fix issues', + ) ..addFlag( helpArg, abbr: 'h', @@ -78,5 +90,10 @@ void main(List args) async { Logger.root.level = Level.ALL; } - await run(); + final path = argResults[pathArg] as String?; + if (path != null) { + Directory.current = path; + } + + await run(shouldAutoFix: argResults[autoFixArg]); } diff --git a/lib/src/auto_fix.dart b/lib/src/auto_fix.dart new file mode 100644 index 0000000..64e73d5 --- /dev/null +++ b/lib/src/auto_fix.dart @@ -0,0 +1,130 @@ +import 'package:dependency_validator/src/utils.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; + +class AutoFix { + final Pubspec pubspec; + + final _pubRemoveNames = []; + final _pubAdds = []; + + AutoFix(this.pubspec); + + void handleMissingDependencies(Set deps) { + _pubAdds.add(PubAddCommand(packageAndConstraints: deps.toList(), dev: false)); + } + + void handleMissingDevDependencies(Set deps) { + _pubAdds.add(PubAddCommand(packageAndConstraints: deps.toList(), dev: true)); + } + + void handleOverPromotedDependencies(Set deps) { + _pubRemoveNames.addAll(deps); + _pubAdds.addAll(_parsePubAddListByPubspec(deps, dev: true)); + } + + void handleUnderPromotedDependencies(Set deps) { + _pubRemoveNames.addAll(deps); + _pubAdds.addAll(_parsePubAddListByPubspec(deps, dev: false)); + } + + void handleUnusedDependencies(Set deps) { + _pubRemoveNames.addAll(deps); + } + + List _parsePubAddListByPubspec(Set deps, {required bool dev}) { + return deps.map((dep) => _parsePubAddByPubspec(dep, dev: dev)).where((e) => e != null).map((e) => e!).toList(); + } + + PubAddCommand? _parsePubAddByPubspec(String name, {required bool dev}) { + final dependency = pubspec.dependencies[name] ?? pubspec.devDependencies[name]; + if (dependency == null) { + logger.warning('WARN: cannot find dependency name=$name'); + return null; + } + + if (dependency is HostedDependency) { + final constraint = dependency.version.toString(); + return PubAddCommand(packageAndConstraints: ['$name:$constraint'], dev: dev); + } + + if (dependency is PathDependency) { + return PubAddCommand( + packageAndConstraints: [name], + dev: dev, + extraArgs: '--path ${dependency.path}', + ); + } + + if (dependency is GitDependency) { + var extraArgs = '--git-url ${dependency.url} '; + if (dependency.ref != null) extraArgs += '--git-ref ${dependency.ref} '; + if (dependency.path != null) extraArgs += '--git-path ${dependency.path} '; + + return PubAddCommand( + packageAndConstraints: [name], + dev: dev, + extraArgs: extraArgs, + ); + } + + logger.warning('WARN: do not know type of dependency ' + 'name=$name dependency=$dependency type=${dependency.runtimeType}'); + return null; + } + + String compile() { + // final mergedPubAdds = PubAddCommand.merge(_pubAdds); + return [ + if (_pubRemoveNames.isNotEmpty) 'dart pub remove ' + _pubRemoveNames.join(' '), + ..._pubAdds.map((e) => e.compile()), + ].join('; '); + } +} + +class PubAddCommand { + final bool dev; + final List packageAndConstraints; + final String? extraArgs; + + PubAddCommand({ + required this.packageAndConstraints, + required this.dev, + this.extraArgs, + }); + + String compile() { + var ans = 'dart pub add '; + if (dev) ans += '--dev '; + ans += packageAndConstraints.join(' ') + ' '; + ans += extraArgs ?? ''; + return ans; + } + + // static List merge(List commands) { + // final simpleAdd = []; + // final simpleAddDev = []; + // final others = []; + // + // for (final command in commands) { + // if (command.extraArgs == null) { + // (command.dev ? simpleAddDev : simpleAdd).add(command); + // } else { + // others.add(command); + // } + // } + // + // return [ + // if (simpleAdd.isNotEmpty) + // PubAddCommand( + // packageAndConstraints: simpleAdd.expand((c) => c.packageAndConstraints).toList(), + // dev: false, + // ), + // if (simpleAddDev.isNotEmpty) + // PubAddCommand( + // packageAndConstraints: simpleAddDev.expand((c) => c.packageAndConstraints).toList(), + // dev: true, + // ), + // ...others, + // ]; + // } +} diff --git a/lib/src/dependency_validator.dart b/lib/src/dependency_validator.dart index 4655cd3..fd2ac2a 100644 --- a/lib/src/dependency_validator.dart +++ b/lib/src/dependency_validator.dart @@ -15,6 +15,7 @@ import 'dart:io'; import 'package:build_config/build_config.dart'; +import 'package:dependency_validator/src/auto_fix.dart'; import 'package:glob/glob.dart'; import 'package:io/ansi.dart'; import 'package:logging/logging.dart'; @@ -27,7 +28,7 @@ import 'pubspec_config.dart'; import 'utils.dart'; /// Check for missing, under-promoted, over-promoted, and unused dependencies. -Future run() async { +Future run({required bool shouldAutoFix}) async { if (!File('pubspec.yaml').existsSync()) { logger.shout(red.wrap('pubspec.yaml not found')); exit(1); @@ -76,6 +77,8 @@ Future run() async { final pubspec = Pubspec.parse(pubspecFile.readAsStringSync(), sourceUrl: pubspecFile.uri); + final autoFix = AutoFix(pubspec); + logger.info('Validating dependencies for ${pubspec.name}...'); if (!config.allowPins) { @@ -200,6 +203,7 @@ Future run() async { 'These packages are used in lib/ but are not dependencies:', missingDependencies, ); + autoFix.handleMissingDependencies(missingDependencies); exitCode = 1; } @@ -222,6 +226,7 @@ Future run() async { 'These packages are used outside lib/ but are not dev_dependencies:', missingDevDependencies, ); + autoFix.handleMissingDevDependencies(missingDevDependencies); exitCode = 1; } @@ -242,6 +247,7 @@ Future run() async { 'These packages are only used outside lib/ and should be downgraded to dev_dependencies:', overPromotedDependencies, ); + autoFix.handleOverPromotedDependencies(overPromotedDependencies); exitCode = 1; } @@ -258,6 +264,7 @@ Future run() async { 'These packages are used in lib/ and should be promoted to actual dependencies:', underPromotedDependencies, ); + autoFix.handleUnderPromotedDependencies(underPromotedDependencies); exitCode = 1; } @@ -343,9 +350,27 @@ Future run() async { 'These packages may be unused, or you may be using assets from these packages:', unusedDependencies, ); + autoFix.handleUnusedDependencies(unusedDependencies); exitCode = 1; } + final autoFixCommand = autoFix.compile(); + if (autoFixCommand.isNotEmpty) { + logger.info('Suggestion for auto fix: ${autoFixCommand}'); + } + + if (shouldAutoFix && autoFixCommand.isNotEmpty) { + logger.info('Start autofix...'); + final process = await Process.start('/bin/sh', ['-xc', autoFixCommand]); + process.stdout.pipe(stdout); + process.stderr.pipe(stderr); + final processExitCode = await process.exitCode; + if (processExitCode != 0) throw Exception('process exit with exitCode=$processExitCode'); + logger.info('End autofix.'); + + exitCode = 0; + } + if (exitCode == 0) { logger.info(green.wrap('✓ No dependency issues found!')); }