| // 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:package_config/package_config.dart'; | |
| import 'package:path/path.dart' as path; | |
| import 'package:yaml/yaml.dart'; | |
| typedef PubspecTelemetry = ({ | |
| Set<String> publicDependencies, | |
| bool hasFlutterSdk, | |
| String? environmentSdk, | |
| }); | |
| /// Locates `pubspec.yaml` in [dir] or an ancestor directory. | |
| File? findPubspecFile(Directory dir) => _findFile(dir, 'pubspec.yaml'); | |
| /// Statelessly scans for a `pubspec.yaml` in [dir] or its parent hierarchy | |
| /// and extracts direct public dependencies, applying strict privacy | |
| /// verification against `.dart_tool/package_config.json`. | |
| /// | |
| /// Note: This will not collect information in a Bazel workspace because Bazel | |
| /// does not generate a `.dart_tool/package_config.json` file. | |
| /// | |
| /// Returns `null` if no pubspec is found, if `.dart_tool/package_config.json` | |
| /// is missing, or if any file system or parsing error occurs. | |
| PubspecTelemetry? scanPubspecTelemetry( | |
| Directory dir, { | |
| Map<String, String>? environment, | |
| }) { | |
| try { | |
| final pubspecFile = findPubspecFile(dir); | |
| if (pubspecFile == null) return null; | |
| // In Dart 3.5+ Pub Workspaces, package_config.json may be in the current | |
| // package or in an ancestor workspace root. | |
| final packageConfigFile = _findFile( | |
| pubspecFile.parent, | |
| path.join('.dart_tool', 'package_config.json'), | |
| ); | |
| if (packageConfigFile == null) return null; | |
| final hostedPublicPackages = _extractHostedPublicPackages( | |
| packageConfigFile, | |
| environment ?? Platform.environment, | |
| ); | |
| if (hostedPublicPackages == null) return null; | |
| final doc = loadYaml(pubspecFile.readAsStringSync()); | |
| if (doc is! YamlMap) return null; | |
| final publicDeps = <String>{}; | |
| bool hasFlutterSdk = false; | |
| String? environmentSdk; | |
| // Check environment for flutter sdk and sdk constraint | |
| if (doc['environment'] case YamlMap environment) { | |
| if (environment.containsKey('flutter')) { | |
| hasFlutterSdk = true; | |
| } | |
| if (environment['sdk'] case final Object sdk) { | |
| final rawSdk = sdk.toString().trim(); | |
| if (rawSdk.isNotEmpty) { | |
| environmentSdk = rawSdk.length > 100 | |
| ? rawSdk.substring(0, 100) | |
| : rawSdk; | |
| } | |
| } | |
| } | |
| void processDependencies(Object? deps) { | |
| if (deps case YamlMap map) { | |
| for (final MapEntry(:key, :value) in map.entries) { | |
| if (key is! String) continue; | |
| if (key == 'flutter') { | |
| if (value case {'sdk': 'flutter'}) { | |
| hasFlutterSdk = true; | |
| continue; | |
| } | |
| } | |
| if (_isCandidatePublicDependency(value) && | |
| hostedPublicPackages.contains(key)) { | |
| publicDeps.add(key); | |
| } | |
| } | |
| } | |
| } | |
| processDependencies(doc['dependencies']); | |
| processDependencies(doc['dev_dependencies']); | |
| return ( | |
| publicDependencies: publicDeps, | |
| hasFlutterSdk: hasFlutterSdk, | |
| environmentSdk: environmentSdk, | |
| ); | |
| } catch (_) { | |
| return null; | |
| } | |
| } | |
| File? _findFile(Directory dir, String relativePath) { | |
| var current = dir; | |
| while (true) { | |
| final file = File(path.join(current.path, relativePath)); | |
| if (file.existsSync()) return file; | |
| final parent = current.parent; | |
| if (parent.path == current.path) { | |
| return null; // Root reached | |
| } | |
| current = parent; | |
| } | |
| } | |
| Set<String>? _extractHostedPublicPackages( | |
| File packageConfigFile, | |
| Map<String, String> environment, | |
| ) { | |
| try { | |
| final content = packageConfigFile.readAsStringSync(); | |
| final config = PackageConfig.parseString(content, packageConfigFile.uri); | |
| final hosted = <String>{}; | |
| for (final package in config.packages) { | |
| if (_isPublicHostedUri(package.root, environment)) { | |
| hosted.add(package.name); | |
| } | |
| } | |
| return hosted; | |
| } catch (_) { | |
| return null; | |
| } | |
| } | |
| bool _isPublicHostedUri(Uri resolvedUri, Map<String, String> environment) { | |
| final segments = resolvedUri.pathSegments; | |
| final hostedIndex = segments.indexOf('hosted'); | |
| if (hostedIndex <= 0 || hostedIndex + 1 >= segments.length) { | |
| return false; | |
| } | |
| final host = segments[hostedIndex + 1]; | |
| if (host != 'pub.dev' && host != 'pub.dartlang.org') { | |
| return false; | |
| } | |
| // 1. Check if PUB_CACHE environment variable is set and matches. | |
| if (environment['PUB_CACHE'] case final String envCache | |
| when envCache.isNotEmpty) { | |
| final cacheUri = Directory(envCache).absolute.uri; | |
| if (resolvedUri.toString().startsWith(cacheUri.toString())) { | |
| return true; | |
| } | |
| } | |
| // 2. Check standard default pub cache directory names: | |
| // - POSIX default: ~/.pub-cache/hosted/pub.dev/... | |
| // - Windows default: %LOCALAPPDATA%\Pub\Cache\hosted\pub.dev\... | |
| final parent = segments[hostedIndex - 1]; | |
| return parent == '.pub-cache' || parent == 'Cache' || parent == 'pub-cache'; | |
| } | |
| bool _isCandidatePublicDependency(Object? value) => switch (value) { | |
| String() || null => true, | |
| {'hosted': String url} || | |
| {'hosted': {'url': String url}} => _isPublicHostedUrl(url), | |
| {'version': _} => value is YamlMap && value.length == 1, | |
| YamlMap map => map.isEmpty, | |
| _ => false, | |
| }; | |
| bool _isPublicHostedUrl(String url) => switch (Uri.tryParse(url)?.host) { | |
| 'pub.dev' || 'pub.dartlang.org' => true, | |
| _ => false, | |
| }; |