Файли

278 рядки
12 KiB
Rust
Неформатований Постійне посилання Анотація Історія

Цей файл містить неоднозначні символи Юнікоду
Цей файл містить символи Юнікоду, які можна сплутати з іншими символами. Якщо ви вважаєте, що це зроблено навмисно, можете сміливо ігнорувати це попередження. Щоб показати їх, скористайтеся кнопкою Escape.
//! Virtual documents: read-only contents a language server provides for a
//! URI (library classes from `jdt://`, disk classes from `file://`, or
//! `workspace/textDocumentContent`).
use percent_encoding::{AsciiSet, CONTROLS, percent_decode_str, utf8_percent_encode};
const URI_ESCAPES: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'"')
.add(b'<')
.add(b'>')
.add(b'[')
.add(b']')
.add(b'\\')
.add(b'^')
.add(b'`')
.add(b'{')
.add(b'|')
.add(b'}');
/// csharp-ls uses .NET Uri.ToString(), which can leave spaces and Unicode unescaped.
/// Normalize only document URI fields, before lsp-types' strict URI deserialization.
pub(crate) fn normalize_csharp_uris(value: &mut serde_json::Value) {
match value {
serde_json::Value::Array(values) => values.iter_mut().for_each(normalize_csharp_uris),
serde_json::Value::Object(fields) => {
for (key, value) in fields {
// Opaque payloads must survive resolve/execute round trips unchanged.
if matches!(key.as_str(), "data" | "arguments") {
continue;
}
if matches!(key.as_str(), "uri" | "targetUri")
&& let Some(uri) = value.as_str().filter(|s| s.starts_with("csharp:"))
{
*value = utf8_percent_encode(uri, URI_ESCAPES).to_string().into();
} else {
normalize_csharp_uris(value);
}
}
}
_ => {}
}
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct CSharpMetadata {
pub source: String,
pub assembly_name: Option<String>,
}
/// csharp-ls serves both decompiled and source-generated documents through metadata.
pub(crate) fn is_csharp_decompiled(uri: &str) -> bool {
if scheme(uri) != "csharp" {
return false;
}
let segs = segments(uri);
// The kind follows the project file, not a directory that happens to have this name.
if let Some(kind) = segs.windows(2).find_map(|s| {
(s[0].ends_with(".csproj") && matches!(s[1].as_str(), "decompiled" | "generated")).then_some(s[1].as_str())
}) {
return kind == "decompiled";
}
// Older csharp-ls versions used csharp:/metadata/... for decompiled symbols.
segs.first().is_some_and(|s| s == "metadata")
}
/// Recognize decompiler headers, without treating disassembler stubs as Java source.
pub fn is_decompiled(text: &str) -> bool {
let head: String = text.chars().take(600).collect();
head.lines()
.take(12)
.take_while(|line| {
let line = line.trim();
line.is_empty() || line.starts_with("//") || line.starts_with("/*") || line.starts_with('*')
})
.any(|line| {
let line = line.trim().trim_start_matches('/').trim_start_matches('*').trim().to_ascii_lowercase();
[
"source code is unavailable, and was generated by the fernflower decompiler.",
"source code is decompiled from a .class file using fernflower decompiler",
"decompiled with cfr",
"decompiled with procyon",
]
.iter()
.any(|marker| line.starts_with(marker))
})
}
/// `scheme` of a URI (`jdt` for `jdt://…`).
pub fn scheme(uri: &str) -> &str {
uri.split_once(':').map(|(s, _)| s).unwrap_or("")
}
/// The URI's path segments (without query / fragment), percent-decoded.
fn segments(uri: &str) -> Vec<String> {
let rest = uri.split_once(':').map(|(_, r)| r).unwrap_or(uri);
let rest = rest.split(['?', '#']).next().unwrap_or(rest);
let rest = rest.strip_prefix("//").unwrap_or(rest);
rest.split('/').filter(|s| !s.is_empty()).map(|s| percent_decode_str(s).decode_utf8_lossy().into_owned()).collect()
}
/// Editor tab title: the last path segment; a class file whose real source
/// was found is shown as `.java` (IDEA: `String.java` vs `String.class`).
pub fn title(uri: &str, decompiled: bool) -> String {
let mut name = segments(uri).pop().unwrap_or_else(|| uri.to_owned());
// Recent jdtls versions use a .java path even for decompiled class handles.
if decompiled
&& scheme(uri) == "jdt"
&& name.ends_with(".java")
&& uri.split_once('?').is_some_and(|(_, q)| percent_decode_str(q).decode_utf8_lossy().contains(".class"))
{
name.truncate(name.len() - 5);
name.push_str(".class");
}
match name.strip_suffix(".class") {
Some(stem) if !decompiled && scheme(uri) == "jdt" => format!("{stem}.java"),
_ => name,
}
}
/// Where the document comes from, e.g. the jar path of a `jdt://` class.
pub fn description(uri: &str) -> Option<String> {
if scheme(uri) == "jar" {
return crate::library_source::parse(uri).map(|e| e.jar.to_string_lossy().into_owned());
}
if scheme(uri) != "jdt" {
return None;
}
let segs = segments(uri);
// jdt://contents/<jar or module>/<package>/<Class.class>?=<project>/<escaped jar path><package(Class.class
let container = segs.get(1).cloned();
let query = uri.split_once('?').map(|(_, q)| q).unwrap_or("");
let query = percent_decode_str(query).decode_utf8_lossy();
let jar = query
.split_once('/')
.map(|(_, rest)| rest.replace('\\', ""))
.and_then(|rest| rest.split(['<', '(', '`', '=']).next().map(str::to_owned))
.filter(|p| p.ends_with(".jar") || p.ends_with(".jmod"));
let package = segs.get(2).filter(|_| segs.len() >= 4).cloned();
match (jar, container) {
(Some(jar), _) if jar.ends_with("jrt-fs.jar") => segs.get(1).cloned(),
(Some(jar), _) => Some(jar),
(None, Some(c)) => Some(match package {
Some(p) => format!("{c} › {p}"),
None => c,
}),
(None, None) => None,
}
}
/// Check both the package and actual archive, so duplicate type names never pick another jar.
pub(crate) fn matches_class(uri: &str, jar: &std::path::Path, package: &str, name: &str) -> bool {
let segs = segments(uri);
scheme(uri) == "jdt"
&& (package.is_empty() && segs.len() == 3 || segs.get(2).is_some_and(|s| s == package))
&& segs.last().is_some_and(|s| s == &format!("{name}.class") || s == &format!("{name}.java"))
&& description(uri).is_some_and(|p| {
let path = std::path::Path::new(&p);
path == jar
|| dunce::canonicalize(path).ok().zip(dunce::canonicalize(jar).ok()).is_some_and(|(a, b)| a == b)
})
}
#[cfg(test)]
mod tests {
use super::*;
const JDT: &str = "jdt://contents/spring-boot-4.1.1.jar/org.springframework.boot/SpringApplication.class?=spring-demo/%5C/Users%5C/me%5C/.m2%5C/repository%5C/spring-boot-4.1.1.jar%3Corg.springframework.boot%28SpringApplication.class";
#[test]
fn csharp_metadata_titles_and_kinds() {
for (uri, expected) in [
("csharp:/D:/My%20Game/Game.csproj/decompiled/UnityEngine.MonoBehaviour.cs", true),
("csharp:/home/me/%E9%A1%B9%E7%9B%AE/App.csproj/decompiled/System.String.cs", true),
("csharp:/metadata/projects/App/assemblies/System.Runtime/symbols/System.String.cs", true),
("csharp:/work/App.csproj/generated/Generator/Player.g.cs", false),
("csharp:/work/decompiled/App.csproj/generated/Generator/Player.g.cs", false),
("csharp:/metadata/App.csproj/generated/Player.g.cs", false),
("csharp:/work/App.csproj/generated/decompiled/Player.g.cs", false),
("file:///work/App.csproj/decompiled/Player.cs", false),
] {
assert_eq!(is_csharp_decompiled(uri), expected, "{uri}");
}
assert_eq!(
title("csharp:/D:/My%20Game/Game.csproj/decompiled/UnityEngine.MonoBehaviour.cs", true),
"UnityEngine.MonoBehaviour.cs"
);
assert_eq!(title("csharp:/home/me/App.csproj/decompiled/Example.%E7%B1%BB.cs", true), "Example.类.cs");
}
#[test]
fn csharp_uri_encoding_preserves_escapes_and_other_response_fields() {
let uri = "csharp:/D:/My Game/项目 [1]/App%23%25.csproj/decompiled/Example.C%601.cs";
let encoded = "csharp:/D:/My%20Game/%E9%A1%B9%E7%9B%AE%20%5B1%5D/App%23%25.csproj/decompiled/Example.C%601.cs";
let mut response = serde_json::json!([
{ "uri": uri, "source": uri },
{ "location": { "targetUri": uri } },
{ "uri": encoded },
{ "uri": "file:///D:/project/App.cs" },
{ "uri": "jdt://contents/a.jar/p/C.class?=p/%5C/a.jar" },
{ "data": { "uri": uri }, "command": { "arguments": [{ "targetUri": uri }] } }
]);
let original = response.clone();
normalize_csharp_uris(&mut response);
assert_eq!(response[0]["uri"], encoded);
assert_eq!(response[0]["source"], uri);
assert_eq!(response[1]["location"]["targetUri"], encoded);
assert_eq!(response.as_array().unwrap()[2..], original.as_array().unwrap()[2..]);
assert!(encoded.parse::<lsp_types::Uri>().is_ok());
}
#[test]
fn jdt_titles() {
assert_eq!(title(JDT, true), "SpringApplication.class");
assert_eq!(title(JDT, false), "SpringApplication.java");
assert_eq!(title("untitled:foo/bar.txt", false), "bar.txt");
assert_eq!(scheme(JDT), "jdt");
}
#[test]
fn jdt_description() {
assert!(matches_class(
JDT,
std::path::Path::new("/Users/me/.m2/repository/spring-boot-4.1.1.jar"),
"org.springframework.boot",
"SpringApplication"
));
assert!(!matches_class(
JDT,
std::path::Path::new("/elsewhere/spring-boot-4.1.1.jar"),
"org.springframework.boot",
"SpringApplication"
));
assert!(!matches_class(
JDT,
std::path::Path::new("/Users/me/.m2/repository/spring-boot-4.1.1.jar"),
"other.package",
"SpringApplication"
));
assert!(matches_class(
"jdt://contents/a.jar//C.java?=p/%5C/m2%5C/a.jar%3C%28C.class",
std::path::Path::new("/m2/a.jar"),
"",
"C"
));
assert_eq!(description(JDT).as_deref(), Some("/Users/me/.m2/repository/spring-boot-4.1.1.jar"));
assert_eq!(
description(
"jdt://contents/java.base/java.lang/String.class?=p/%5C/jdk%5C/lib%5C/jrt-fs.jar%60java.base=x"
)
.as_deref(),
Some("java.base")
);
assert_eq!(description("jdt://contents/a.jar/p/C.class").as_deref(), Some("a.jar › p"));
assert_eq!(description("foo://x/y"), None);
}
#[test]
fn detects_generated_code() {
assert!(is_decompiled(
"// Source code is decompiled from a .class file using FernFlower decompiler (from Intellij IDEA).\npackage a;"
));
assert!(!is_decompiled("package a;\n// generated by the fernflower decompiler"));
assert!(!is_decompiled("// Documentation about code generated by the Fernflower decompiler.\npackage a;"));
let recent = "jdt://contents/a.jar/p/C.java?=p/a.jar%3Cp%28C.class";
assert_eq!(title(recent, true), "C.class");
assert_eq!(title(recent, false), "C.java");
assert!(is_decompiled(
"// Source code is unavailable, and was generated by the Fernflower decompiler.\npackage a;"
));
assert!(!is_decompiled(
"// Failed to get sources. Instead, stub sources have been generated by the disassembler."
));
assert!(!is_decompiled("/*\n * Copyright 2012-2024 the original author or authors.\n */\npackage a;"));
}
}