blob: d015f26fc404c075a063b687b9e2f67f90d09126 [file] [edit]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. 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';
import 'package:analyzer/dart/analysis/utilities.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:package_config/package_config.dart';
import 'package:path/path.dart' as path;
import 'utils.dart';
const String _defaultVersionConstraint = 'any';
const String _evalWorkspaceName = 'dart_eval_workspace';
final RegExp _sdkVersionRegExp = RegExp(r'^(\d+\.\d+\.\d+)');
/// Manages resolution of package dependencies for `dart -e` evaluation.
abstract final class EvalPackageResolver {
/// Extracts non-core package names imported in the code snippet using the analyzer AST parser.
static Set<String> extractPackageImports(String code) {
final packages = <String>{};
final unit = parseString(
content: code,
throwIfDiagnostics: false,
).unit;
for (final directive in unit.directives) {
if (directive is ImportDirective) {
final uriString = directive.uri.stringValue;
if (uriString != null) {
final pkgName = Uri.tryParse(uriString)?.packageName;
if (pkgName != null) {
packages.add(pkgName);
}
}
}
}
return packages;
}
/// Merges package dependencies from auto-imports and `-P` / `--package-constraint` CLI flags.
static Map<String, String> collectDependencies(
String code, {
List<String>? packages,
}) {
final deps = <String, String>{};
// 1. Auto-detected package imports from code snippet
for (final pkg in extractPackageImports(code)) {
deps[pkg] = _defaultVersionConstraint;
}
// 2. CLI -P / --package-constraint flags (e.g. ["http:^1.2.0", "path"])
if (packages != null) {
for (final spec in packages) {
if (spec.trim().isEmpty) continue;
final parts = spec.split(':');
final pkg = parts[0].trim();
final version = parts.length > 1
? parts.sublist(1).join(':').trim()
: _defaultVersionConstraint;
if (pkg.isNotEmpty) {
deps[pkg] = version.isEmpty ? _defaultVersionConstraint : version;
}
}
}
return deps;
}
/// Resolves `.dart_tool/package_config.json` for the required dependencies.
///
/// Priority:
/// 1. Local project `.dart_tool/package_config.json` if present and contains all dependencies (only when no explicit package constraints are specified).
/// 2. Ephemeral `dart pub get` execution into a temporary directory.
static Future<String?> resolvePackageConfig(
String code, {
List<String>? packageConstraints,
String? localPackageConfig,
bool offline = false,
}) async {
final deps = collectDependencies(code, packages: packageConstraints);
if (deps.isEmpty) {
return localPackageConfig;
}
// 1. Check local package config
if (localPackageConfig != null) {
final configFile = File(localPackageConfig);
if (!configFile.existsSync()) {
throw StateError(
'Specified package configuration file does not exist: "$localPackageConfig"',
);
}
final PackageConfig config;
try {
config = await loadPackageConfig(configFile);
} catch (e) {
throw StateError(
'Failed to parse package configuration file "$localPackageConfig": $e',
);
}
// Only reuse local config when no explicit package constraints were passed.
if (packageConstraints == null || packageConstraints.isEmpty) {
final localPkgNames = {for (final p in config.packages) p.name};
if (deps.keys.every(localPkgNames.contains)) {
return localPackageConfig;
}
}
}
// 2. Ephemeral pub get in a temporary directory (no persistent caching)
final tempDir = Directory.systemTemp.createTempSync('dart_eval_');
final match = _sdkVersionRegExp.firstMatch(Platform.version);
if (match == null) {
throw StateError(
'Could not parse SDK version from Platform.version: "${Platform.version}"',
);
}
final sdkVersion = match.group(1)!;
final sortedDeps = deps.entries.toList()
..sort((a, b) => a.key.compareTo(b.key));
final pubspecBuffer = StringBuffer()
..writeln('name: $_evalWorkspaceName')
..writeln('environment:')
..writeln(' sdk: "^$sdkVersion"')
..writeln('dependencies:');
for (final entry in sortedDeps) {
final val = entry.value;
if (val.startsWith('{') || val.startsWith('[') || val.contains('\n')) {
pubspecBuffer.writeln(' ${entry.key}: $val');
} else {
pubspecBuffer.writeln(' ${entry.key}: "$val"');
}
}
File(
path.join(tempDir.path, 'pubspec.yaml'),
).writeAsStringSync(pubspecBuffer.toString());
final dartBin = Platform.resolvedExecutable;
final result = await Process.run(
dartBin,
[
'pub',
'get',
if (offline) '--offline',
],
workingDirectory: tempDir.path,
);
final generatedConfig = File(
path.join(tempDir.path, '.dart_tool', 'package_config.json'),
);
if (result.exitCode != 0 || !generatedConfig.existsSync()) {
final stderrOut = result.stderr.toString().trim();
final stdoutOut = result.stdout.toString().trim();
throw StateError(
'Failed to resolve package dependencies for evaluation snippet:\n\n'
'stdout:\n'
'$stdoutOut\n\n'
'stderr:\n'
'$stderrOut',
);
}
return generatedConfig.path;
}
}