| // Copyright (c) 2025, 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' show Directory, exit; | |
| import 'package:analysis_server/src/session_logger/log_normalizer.dart'; | |
| import 'package:analyzer/file_system/file_system.dart'; | |
| import 'package:analyzer/file_system/physical_file_system.dart'; | |
| import 'package:args/args.dart'; | |
| import 'package:cli_util/cli_util.dart'; | |
| import 'package:package_config/package_config.dart'; | |
| import '../performance/project_generator/project_generator.dart' | |
| show ContextRoot, getContextRoots; | |
| import 'log.dart'; | |
| Future<void> main(List<String> args) async { | |
| var parsed = argParser.parse(args); | |
| if (parsed.flag('help')) { | |
| print(argParser.usage); | |
| return; | |
| } | |
| var resourceProvider = PhysicalResourceProvider.INSTANCE; | |
| var inputFile = resourceProvider.getFile( | |
| Uri.base.resolve(parsed.option('input')!).toFilePath(), | |
| ); | |
| if (!inputFile.exists) { | |
| print('Input file ${inputFile.path} does not exist'); | |
| exit(1); | |
| } | |
| var outputFile = resourceProvider.getFile( | |
| Uri.base.resolve(parsed.option('output')!).toFilePath(), | |
| ); | |
| var rootDirPath = parsed.option('root-dir'); | |
| if (rootDirPath == null) { | |
| print('Root directory not specified'); | |
| exit(1); | |
| } | |
| var rootDir = resourceProvider.getFolder(rootDirPath); | |
| if (!rootDir.exists) { | |
| print('Root directory $rootDirPath does not exist'); | |
| exit(1); | |
| } | |
| List<ContextRoot> contextRoots; | |
| var packageConfigPath = parsed.option('package-config'); | |
| if (packageConfigPath != null) { | |
| var packageConfigFile = resourceProvider.getFile(packageConfigPath); | |
| if (!packageConfigFile.exists) { | |
| print('Package config file $packageConfigPath does not exist'); | |
| exit(1); | |
| } | |
| var packageConfig = PackageConfig.parseBytes( | |
| packageConfigFile.readAsBytesSync(), | |
| packageConfigFile.toUri(), | |
| ); | |
| contextRoots = [ContextRoot(Directory(rootDirPath), packageConfig)]; | |
| } else { | |
| contextRoots = await getContextRoots(rootDirPath); | |
| } | |
| print('normalizing log at ${inputFile.path}'); | |
| var normalized = normalizeLog(inputFile, contextRoots); | |
| outputFile.writeAsStringSync(normalized); | |
| print('wrote normalized log to ${outputFile.path}'); | |
| var absFileMatches = normalized.allMatches('"file:///'); | |
| if (absFileMatches.isNotEmpty) { | |
| print('found ${absFileMatches.length} absolute file paths remaining:'); | |
| } | |
| for (var match in absFileMatches.take(5)) { | |
| print('- ${match[0]}'); | |
| } | |
| } | |
| final argParser = ArgParser() | |
| ..addOption( | |
| 'input', | |
| abbr: 'i', | |
| help: 'The path to the input log to be normalized', | |
| mandatory: true, | |
| ) | |
| ..addOption( | |
| 'output', | |
| abbr: 'o', | |
| help: 'The path output the normalized log to', | |
| mandatory: true, | |
| ) | |
| ..addOption( | |
| 'root-dir', | |
| abbr: 'r', | |
| help: 'The path to the root directory for normalizing package paths', | |
| mandatory: true, | |
| ) | |
| ..addOption( | |
| 'package-config', | |
| abbr: 'p', | |
| help: | |
| 'The path to the package config file, if specified, will be used ' | |
| 'instead of inferring it from the workspace directories.', | |
| ) | |
| ..addFlag('help', abbr: 'h', help: 'Prints the usage text'); | |
| /// Reads an [input] log file, and attempts to normalize it so that it can work | |
| /// across multiple environments. | |
| /// | |
| /// Specifically, this: | |
| /// - Replaces all workspace folder paths with `{{workspaceFolder-[i]}}` | |
| /// placeholders. | |
| /// - Replaces the Dart SDK root with `{{dartSdkRoot}}`. | |
| /// - Replaces all package roots with `{{package-root:[package-name]}}`. | |
| /// | |
| /// Returns the new file contents after normalization. | |
| // | |
| // TODO(somebody): Support legacy protocol. | |
| // | |
| // TODO(somebody): Don't take a package config, instead infer them from the | |
| // workspace directories. | |
| String normalizeLog(File input, List<ContextRoot> contextRoots) { | |
| var normalizer = LogNormalizer(); | |
| var content = input.readAsStringSync(); | |
| var original = Log.fromString(content); | |
| var initializeMessage = original.entries.firstWhere( | |
| (log) => log.isMessage && log.message.isInitializeRequest, | |
| ); | |
| // Add all the workspace + root paths from the initialize message. | |
| normalizer.addLspWorkspaceReplacements(initializeMessage.message); | |
| // Additionally add SDKs and packages. | |
| if (sdkPath case var path?) { | |
| normalizer.addReplacementsForPath(path, 'dartSdkRoot'); | |
| } | |
| // TODO(somebody): replace {{flutterSdkRoot}} with the flutter SDK path | |
| for (var i = 0; i < contextRoots.length; i++) { | |
| for (var package in contextRoots[i].packageConfig.packages) { | |
| normalizer.addReplacementsForUri( | |
| package.root, | |
| 'context-$i:package-root:${package.name}', | |
| ); | |
| } | |
| } | |
| // Normalize each entry in the log. | |
| return '${original.entries.map((entry) => normalizer.normalize(entry.map)).join('\n')}\n'; | |
| } |