blob: 778bcdf8b2af68fb9e9cdd12afb4f33002391c2a [file] [log] [blame]
// 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:convert';
import 'package:analysis_server/lsp_protocol/protocol.dart'
show InitializeParams;
import 'package:analysis_server/src/session_logger/log_entry.dart';
import 'package:collection/collection.dart';
import 'package:language_server_protocol/json_parsing.dart';
/// A utility for normalizing paths in log entries.
class LogNormalizer {
/// A map from the path-to-be-replaced to the replacement string.
///
/// Paths are canonicalized to lowercase and the replacement regex is
/// case-insensitive.
final Map<String, String> _replacements =
CanonicalizedMap<String, String, String>((key) => key.toLowerCase());
/// A cached regex for all current replacements to allow them to occur in a
/// single pass.
RegExp? _replacementPattern;
/// Extracts paths and URIs from an LSP initialize request and adds
/// replacements for each.
void addLspWorkspaceReplacements(Message message) {
var params = message.params;
if (params == null ||
!InitializeParams.canParse(params, nullLspJsonReporter)) {
return;
}
var initializeParams = InitializeParams.fromJson(params);
if (initializeParams.rootPath case var rootPath?) {
addPathReplacement(rootPath, '{{rootPath}}');
}
if (initializeParams.rootUri case var rootUri?) {
if (rootUri.isScheme('file')) {
addUriReplacement(rootUri, '{{rootUri}}');
}
}
if (initializeParams.workspaceFolders case var workspaceFolders?) {
for (var i = 0; i < workspaceFolders.length; i++) {
var uri = workspaceFolders[i].uri;
if (uri.isScheme('file')) {
addUriReplacement(uri, '{{workspaceFolder-$i}}');
}
}
}
}
/// Adds a replacement for the given [inputPath] in both path and URI form, but raw
/// and JSON encoded.
void addPathReplacement(String inputPath, String replacement) {
if (inputPath.isEmpty) return;
var uri = Uri.file(inputPath);
var uriString = uri.toString();
// VS Code always encodes colons in the path part so we need to handle
// file:///c:/foo and
// file:///c%3A/foo
var uriStringWithEncodedColons = uri
.replace(path: uri.path.replaceAll(':', '%3A'))
.toString();
// All replacements must be in the map because we need to look up the values
// during replacement so if the regex matches variations, we need to be able
// to find them here.
_replacements[inputPath] = replacement;
_replacements[_jsonEncode(inputPath)] = replacement;
_replacements[uriString] = replacement;
_replacements[_jsonEncode(uriString)] = replacement;
_replacements[uriStringWithEncodedColons] = replacement;
_replacements[_jsonEncode(uriStringWithEncodedColons)] = replacement;
// Reset the cached pattern so it's built on the next call to normalize.
_replacementPattern = null;
}
/// Adds a replacement for the given [inputUri] in both path and URI form, but raw
/// and JSON encoded.
void addUriReplacement(Uri uri, String replacement) {
if (uri.isScheme('file')) {
addPathReplacement(uri.toFilePath(), replacement);
}
}
/// Returns the given [json] string after replacing any known paths with
/// placeholders.
String normalize(String json) {
if (_replacements.isEmpty) return json;
// Build a single regex that matches any of the replacements.
var replacementPattern = _replacementPattern ??= RegExp(
_replacements.keys.map(RegExp.escape).join('|'),
caseSensitive: false,
);
// Replace with the respective value from the replacement map.
return json.replaceAllMapped(
replacementPattern,
(match) => _replacements[match[0]]!,
);
}
/// Returns a JSON-encoded version of [inputString] without the surrounding
/// quotes.
String _jsonEncode(String inputString) {
var encoded = json.encode(inputString);
return encoded.substring(1, encoded.length - 1);
}
}