mirror of
https://github.com/denoland/deno.git
synced 2024-12-23 15:49:44 -05:00
84733d90c7
Ref #8643
621 lines
19 KiB
Rust
621 lines
19 KiB
Rust
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
|
|
|
|
use super::analysis::get_lint_references;
|
|
use super::analysis::references_to_diagnostics;
|
|
use super::analysis::ResolvedDependency;
|
|
use super::language_server;
|
|
use super::tsc;
|
|
|
|
use crate::diagnostics;
|
|
use crate::media_type::MediaType;
|
|
use crate::tokio_util::create_basic_runtime;
|
|
|
|
use deno_core::error::anyhow;
|
|
use deno_core::error::AnyError;
|
|
use deno_core::serde_json;
|
|
use deno_core::serde_json::json;
|
|
use deno_core::ModuleSpecifier;
|
|
use log::error;
|
|
use lspower::lsp;
|
|
use lspower::Client;
|
|
use std::collections::HashMap;
|
|
use std::collections::HashSet;
|
|
use std::mem;
|
|
use std::sync::Arc;
|
|
use std::sync::Mutex;
|
|
use std::thread;
|
|
use tokio::sync::mpsc;
|
|
use tokio::sync::oneshot;
|
|
use tokio::time::sleep;
|
|
use tokio::time::Duration;
|
|
use tokio::time::Instant;
|
|
|
|
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
|
pub enum DiagnosticSource {
|
|
Deno,
|
|
Lint,
|
|
TypeScript,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum DiagnosticRequest {
|
|
Get(
|
|
ModuleSpecifier,
|
|
DiagnosticSource,
|
|
oneshot::Sender<Vec<lsp::Diagnostic>>,
|
|
),
|
|
Invalidate(ModuleSpecifier),
|
|
Update,
|
|
}
|
|
|
|
/// Given a client and a diagnostics collection, publish the appropriate changes
|
|
/// to the client.
|
|
async fn publish_diagnostics(
|
|
client: &Client,
|
|
collection: Arc<Mutex<DiagnosticCollection>>,
|
|
snapshot: &language_server::StateSnapshot,
|
|
) {
|
|
let mut collection = collection.lock().unwrap();
|
|
let maybe_changes = collection.take_changes();
|
|
if let Some(diagnostic_changes) = maybe_changes {
|
|
for specifier in diagnostic_changes {
|
|
// TODO(@kitsonk) not totally happy with the way we collect and store
|
|
// different types of diagnostics and offer them up to the client, we
|
|
// do need to send "empty" vectors though when a particular feature is
|
|
// disabled, otherwise the client will not clear down previous
|
|
// diagnostics
|
|
let mut diagnostics: Vec<lsp::Diagnostic> =
|
|
if snapshot.config.workspace_settings.lint {
|
|
collection
|
|
.diagnostics_for(&specifier, &DiagnosticSource::Lint)
|
|
.cloned()
|
|
.collect()
|
|
} else {
|
|
vec![]
|
|
};
|
|
if snapshot.config.specifier_enabled(&specifier) {
|
|
diagnostics.extend(
|
|
collection
|
|
.diagnostics_for(&specifier, &DiagnosticSource::TypeScript)
|
|
.cloned(),
|
|
);
|
|
diagnostics.extend(
|
|
collection
|
|
.diagnostics_for(&specifier, &DiagnosticSource::Deno)
|
|
.cloned(),
|
|
);
|
|
}
|
|
let uri = specifier.clone();
|
|
let version = snapshot.documents.version(&specifier);
|
|
client.publish_diagnostics(uri, diagnostics, version).await;
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn update_diagnostics(
|
|
client: &Client,
|
|
collection: Arc<Mutex<DiagnosticCollection>>,
|
|
snapshot: &language_server::StateSnapshot,
|
|
ts_server: &tsc::TsServer,
|
|
) {
|
|
let mark = snapshot.performance.mark("update_diagnostics");
|
|
let lint_enabled = snapshot.config.workspace_settings.lint;
|
|
|
|
let lint = async {
|
|
let collection = collection.clone();
|
|
if lint_enabled {
|
|
let mark = snapshot.performance.mark("update_diagnostics_lint");
|
|
let diagnostics =
|
|
generate_lint_diagnostics(snapshot.clone(), collection.clone()).await;
|
|
{
|
|
let mut collection = collection.lock().unwrap();
|
|
for (specifier, version, diagnostics) in diagnostics {
|
|
collection.set(
|
|
specifier,
|
|
DiagnosticSource::Lint,
|
|
version,
|
|
diagnostics,
|
|
);
|
|
}
|
|
}
|
|
publish_diagnostics(client, collection, snapshot).await;
|
|
snapshot.performance.measure(mark);
|
|
};
|
|
};
|
|
|
|
let ts = async {
|
|
let collection = collection.clone();
|
|
let mark = snapshot.performance.mark("update_diagnostics_ts");
|
|
let diagnostics =
|
|
generate_ts_diagnostics(snapshot.clone(), collection.clone(), ts_server)
|
|
.await
|
|
.map_err(|err| {
|
|
error!("Error generating TypeScript diagnostics: {}", err);
|
|
err
|
|
})
|
|
.unwrap_or_default();
|
|
{
|
|
let mut collection = collection.lock().unwrap();
|
|
for (specifier, version, diagnostics) in diagnostics {
|
|
collection.set(
|
|
specifier,
|
|
DiagnosticSource::TypeScript,
|
|
version,
|
|
diagnostics,
|
|
);
|
|
}
|
|
}
|
|
publish_diagnostics(client, collection, snapshot).await;
|
|
snapshot.performance.measure(mark);
|
|
};
|
|
|
|
let deps = async {
|
|
let collection = collection.clone();
|
|
let mark = snapshot.performance.mark("update_diagnostics_deps");
|
|
let diagnostics =
|
|
generate_dependency_diagnostics(snapshot.clone(), collection.clone())
|
|
.await
|
|
.map_err(|err| {
|
|
error!("Error generating dependency diagnostics: {}", err);
|
|
err
|
|
})
|
|
.unwrap_or_default();
|
|
{
|
|
let mut collection = collection.lock().unwrap();
|
|
for (specifier, version, diagnostics) in diagnostics {
|
|
collection.set(specifier, DiagnosticSource::Deno, version, diagnostics);
|
|
}
|
|
}
|
|
publish_diagnostics(client, collection, snapshot).await;
|
|
snapshot.performance.measure(mark);
|
|
};
|
|
|
|
tokio::join!(lint, ts, deps);
|
|
|
|
snapshot.performance.measure(mark);
|
|
}
|
|
|
|
/// A server which calculates diagnostics in its own thread and publishes them
|
|
/// to an LSP client.
|
|
#[derive(Debug)]
|
|
pub(crate) struct DiagnosticsServer(
|
|
Option<mpsc::UnboundedSender<DiagnosticRequest>>,
|
|
);
|
|
|
|
impl DiagnosticsServer {
|
|
pub(crate) fn new() -> Self {
|
|
Self(None)
|
|
}
|
|
|
|
pub(crate) fn start(
|
|
&mut self,
|
|
language_server: Arc<tokio::sync::Mutex<language_server::Inner>>,
|
|
client: Client,
|
|
ts_server: Arc<tsc::TsServer>,
|
|
) {
|
|
let (tx, mut rx) = mpsc::unbounded_channel::<DiagnosticRequest>();
|
|
self.0 = Some(tx);
|
|
|
|
let _join_handle = thread::spawn(move || {
|
|
let runtime = create_basic_runtime();
|
|
let collection = Arc::new(Mutex::new(DiagnosticCollection::default()));
|
|
|
|
runtime.block_on(async {
|
|
// Debounce timer delay. 150ms between keystrokes is about 45 WPM, so we
|
|
// want something that is longer than that, but not too long to
|
|
// introduce detectable UI delay; 200ms is a decent compromise.
|
|
const DELAY: Duration = Duration::from_millis(200);
|
|
// If the debounce timer isn't active, it will be set to expire "never",
|
|
// which is actually just 1 year in the future.
|
|
const NEVER: Duration = Duration::from_secs(365 * 24 * 60 * 60);
|
|
|
|
// A flag that is set whenever something has changed that requires the
|
|
// diagnostics collection to be updated.
|
|
let mut dirty = false;
|
|
|
|
let debounce_timer = sleep(NEVER);
|
|
tokio::pin!(debounce_timer);
|
|
|
|
loop {
|
|
// "race" the next message off the rx queue or the debounce timer.
|
|
// The debounce timer gets reset every time a message comes off the
|
|
// queue. When the debounce timer expires, a snapshot of the most
|
|
// up-to-date state is used to produce diagnostics.
|
|
tokio::select! {
|
|
maybe_request = rx.recv() => {
|
|
use DiagnosticRequest::*;
|
|
match maybe_request {
|
|
None => break, // Request channel closed.
|
|
Some(Get(specifier, source, tx)) => {
|
|
let diagnostics = collection
|
|
.lock()
|
|
.unwrap()
|
|
.diagnostics_for(&specifier, &source)
|
|
.cloned()
|
|
.collect();
|
|
// If this fails, the requestor disappeared; not a problem.
|
|
let _ = tx.send(diagnostics);
|
|
}
|
|
Some(Invalidate(specifier)) => {
|
|
collection.lock().unwrap().invalidate(&specifier);
|
|
}
|
|
Some(Update) => {
|
|
dirty = true;
|
|
debounce_timer.as_mut().reset(Instant::now() + DELAY);
|
|
}
|
|
}
|
|
}
|
|
_ = debounce_timer.as_mut(), if dirty => {
|
|
dirty = false;
|
|
debounce_timer.as_mut().reset(Instant::now() + NEVER);
|
|
|
|
let snapshot = language_server.lock().await.snapshot();
|
|
update_diagnostics(
|
|
&client,
|
|
collection.clone(),
|
|
&snapshot,
|
|
&ts_server
|
|
).await;
|
|
}
|
|
}
|
|
}
|
|
})
|
|
});
|
|
}
|
|
|
|
pub async fn get(
|
|
&self,
|
|
specifier: ModuleSpecifier,
|
|
source: DiagnosticSource,
|
|
) -> Result<Vec<lsp::Diagnostic>, AnyError> {
|
|
let (tx, rx) = oneshot::channel::<Vec<lsp::Diagnostic>>();
|
|
if let Some(self_tx) = &self.0 {
|
|
self_tx.send(DiagnosticRequest::Get(specifier, source, tx))?;
|
|
rx.await.map_err(|err| err.into())
|
|
} else {
|
|
Err(anyhow!("diagnostic server not started"))
|
|
}
|
|
}
|
|
|
|
pub fn invalidate(&self, specifier: ModuleSpecifier) -> Result<(), AnyError> {
|
|
if let Some(tx) = &self.0 {
|
|
tx.send(DiagnosticRequest::Invalidate(specifier))
|
|
.map_err(|err| err.into())
|
|
} else {
|
|
Err(anyhow!("diagnostic server not started"))
|
|
}
|
|
}
|
|
|
|
pub fn update(&self) -> Result<(), AnyError> {
|
|
if let Some(tx) = &self.0 {
|
|
tx.send(DiagnosticRequest::Update).map_err(|err| err.into())
|
|
} else {
|
|
Err(anyhow!("diagnostic server not started"))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone)]
|
|
struct DiagnosticCollection {
|
|
map: HashMap<(ModuleSpecifier, DiagnosticSource), Vec<lsp::Diagnostic>>,
|
|
versions: HashMap<ModuleSpecifier, HashMap<DiagnosticSource, i32>>,
|
|
changes: HashSet<ModuleSpecifier>,
|
|
}
|
|
|
|
impl DiagnosticCollection {
|
|
pub fn set(
|
|
&mut self,
|
|
specifier: ModuleSpecifier,
|
|
source: DiagnosticSource,
|
|
version: Option<i32>,
|
|
diagnostics: Vec<lsp::Diagnostic>,
|
|
) {
|
|
self
|
|
.map
|
|
.insert((specifier.clone(), source.clone()), diagnostics);
|
|
if let Some(version) = version {
|
|
let source_versions = self.versions.entry(specifier.clone()).or_default();
|
|
source_versions.insert(source, version);
|
|
}
|
|
self.changes.insert(specifier);
|
|
}
|
|
|
|
pub fn diagnostics_for(
|
|
&self,
|
|
specifier: &ModuleSpecifier,
|
|
source: &DiagnosticSource,
|
|
) -> impl Iterator<Item = &lsp::Diagnostic> {
|
|
self
|
|
.map
|
|
.get(&(specifier.clone(), source.clone()))
|
|
.into_iter()
|
|
.flatten()
|
|
}
|
|
|
|
pub fn get_version(
|
|
&self,
|
|
specifier: &ModuleSpecifier,
|
|
source: &DiagnosticSource,
|
|
) -> Option<i32> {
|
|
if let Some(source_versions) = self.versions.get(specifier) {
|
|
source_versions.get(source).cloned()
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub fn invalidate(&mut self, specifier: &ModuleSpecifier) {
|
|
self.versions.remove(specifier);
|
|
}
|
|
|
|
pub fn take_changes(&mut self) -> Option<HashSet<ModuleSpecifier>> {
|
|
if self.changes.is_empty() {
|
|
return None;
|
|
}
|
|
Some(mem::take(&mut self.changes))
|
|
}
|
|
}
|
|
|
|
pub type DiagnosticVec =
|
|
Vec<(ModuleSpecifier, Option<i32>, Vec<lsp::Diagnostic>)>;
|
|
|
|
async fn generate_lint_diagnostics(
|
|
state_snapshot: language_server::StateSnapshot,
|
|
collection: Arc<Mutex<DiagnosticCollection>>,
|
|
) -> DiagnosticVec {
|
|
tokio::task::spawn_blocking(move || {
|
|
let mut diagnostic_list = Vec::new();
|
|
for specifier in state_snapshot.documents.open_specifiers() {
|
|
let version = state_snapshot.documents.version(specifier);
|
|
let current_version = collection
|
|
.lock()
|
|
.unwrap()
|
|
.get_version(specifier, &DiagnosticSource::Lint);
|
|
if version != current_version {
|
|
let media_type = MediaType::from(specifier);
|
|
if let Ok(Some(source_code)) =
|
|
state_snapshot.documents.content(specifier)
|
|
{
|
|
if let Ok(references) =
|
|
get_lint_references(specifier, &media_type, &source_code)
|
|
{
|
|
if !references.is_empty() {
|
|
diagnostic_list.push((
|
|
specifier.clone(),
|
|
version,
|
|
references_to_diagnostics(references),
|
|
));
|
|
} else {
|
|
diagnostic_list.push((specifier.clone(), version, Vec::new()));
|
|
}
|
|
}
|
|
} else {
|
|
error!("Missing file contents for: {}", specifier);
|
|
}
|
|
}
|
|
}
|
|
|
|
diagnostic_list
|
|
})
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
impl<'a> From<&'a diagnostics::DiagnosticCategory> for lsp::DiagnosticSeverity {
|
|
fn from(category: &'a diagnostics::DiagnosticCategory) -> Self {
|
|
match category {
|
|
diagnostics::DiagnosticCategory::Error => lsp::DiagnosticSeverity::Error,
|
|
diagnostics::DiagnosticCategory::Warning => {
|
|
lsp::DiagnosticSeverity::Warning
|
|
}
|
|
diagnostics::DiagnosticCategory::Suggestion => {
|
|
lsp::DiagnosticSeverity::Hint
|
|
}
|
|
diagnostics::DiagnosticCategory::Message => {
|
|
lsp::DiagnosticSeverity::Information
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> From<&'a diagnostics::Position> for lsp::Position {
|
|
fn from(pos: &'a diagnostics::Position) -> Self {
|
|
Self {
|
|
line: pos.line as u32,
|
|
character: pos.character as u32,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn to_lsp_range(
|
|
start: &diagnostics::Position,
|
|
end: &diagnostics::Position,
|
|
) -> lsp::Range {
|
|
lsp::Range {
|
|
start: start.into(),
|
|
end: end.into(),
|
|
}
|
|
}
|
|
|
|
type TsDiagnostics = HashMap<String, Vec<diagnostics::Diagnostic>>;
|
|
|
|
fn get_diagnostic_message(diagnostic: &diagnostics::Diagnostic) -> String {
|
|
if let Some(message) = diagnostic.message_text.clone() {
|
|
message
|
|
} else if let Some(message_chain) = diagnostic.message_chain.clone() {
|
|
message_chain.format_message(0)
|
|
} else {
|
|
"[missing message]".to_string()
|
|
}
|
|
}
|
|
|
|
fn to_lsp_related_information(
|
|
related_information: &Option<Vec<diagnostics::Diagnostic>>,
|
|
) -> Option<Vec<lsp::DiagnosticRelatedInformation>> {
|
|
related_information.as_ref().map(|related| {
|
|
related
|
|
.iter()
|
|
.filter_map(|ri| {
|
|
if let (Some(source), Some(start), Some(end)) =
|
|
(&ri.source, &ri.start, &ri.end)
|
|
{
|
|
let uri = lsp::Url::parse(&source).unwrap();
|
|
Some(lsp::DiagnosticRelatedInformation {
|
|
location: lsp::Location {
|
|
uri,
|
|
range: to_lsp_range(start, end),
|
|
},
|
|
message: get_diagnostic_message(&ri),
|
|
})
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect()
|
|
})
|
|
}
|
|
|
|
fn ts_json_to_diagnostics(
|
|
diagnostics: &[diagnostics::Diagnostic],
|
|
) -> Vec<lsp::Diagnostic> {
|
|
diagnostics
|
|
.iter()
|
|
.filter_map(|d| {
|
|
if let (Some(start), Some(end)) = (&d.start, &d.end) {
|
|
Some(lsp::Diagnostic {
|
|
range: to_lsp_range(start, end),
|
|
severity: Some((&d.category).into()),
|
|
code: Some(lsp::NumberOrString::Number(d.code as i32)),
|
|
code_description: None,
|
|
source: Some("deno-ts".to_string()),
|
|
message: get_diagnostic_message(d),
|
|
related_information: to_lsp_related_information(
|
|
&d.related_information,
|
|
),
|
|
tags: match d.code {
|
|
// These are codes that indicate the variable is unused.
|
|
2695 | 6133 | 6138 | 6192 | 6196 | 6198 | 6199 | 7027 | 7028 => {
|
|
Some(vec![lsp::DiagnosticTag::Unnecessary])
|
|
}
|
|
_ => None,
|
|
},
|
|
data: None,
|
|
})
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
async fn generate_ts_diagnostics(
|
|
state_snapshot: language_server::StateSnapshot,
|
|
collection: Arc<Mutex<DiagnosticCollection>>,
|
|
ts_server: &tsc::TsServer,
|
|
) -> Result<DiagnosticVec, AnyError> {
|
|
let mut diagnostics = Vec::new();
|
|
let mut specifiers = Vec::new();
|
|
{
|
|
let collection = collection.lock().unwrap();
|
|
for specifier in state_snapshot.documents.open_specifiers() {
|
|
if state_snapshot.config.specifier_enabled(specifier) {
|
|
let version = state_snapshot.documents.version(specifier);
|
|
let current_version =
|
|
collection.get_version(specifier, &DiagnosticSource::TypeScript);
|
|
if version != current_version {
|
|
specifiers.push(specifier.clone());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !specifiers.is_empty() {
|
|
let req = tsc::RequestMethod::GetDiagnostics(specifiers);
|
|
let res = ts_server.request(state_snapshot.clone(), req).await?;
|
|
let ts_diagnostic_map: TsDiagnostics = serde_json::from_value(res)?;
|
|
for (specifier_str, ts_diagnostics) in ts_diagnostic_map.iter() {
|
|
let specifier = deno_core::resolve_url(specifier_str)?;
|
|
let version = state_snapshot.documents.version(&specifier);
|
|
diagnostics.push((
|
|
specifier,
|
|
version,
|
|
ts_json_to_diagnostics(ts_diagnostics),
|
|
));
|
|
}
|
|
}
|
|
Ok(diagnostics)
|
|
}
|
|
|
|
async fn generate_dependency_diagnostics(
|
|
mut state_snapshot: language_server::StateSnapshot,
|
|
collection: Arc<Mutex<DiagnosticCollection>>,
|
|
) -> Result<DiagnosticVec, AnyError> {
|
|
tokio::task::spawn_blocking(move || {
|
|
let mut diagnostics = Vec::new();
|
|
|
|
let sources = &mut state_snapshot.sources;
|
|
for specifier in state_snapshot.documents.open_specifiers() {
|
|
if !state_snapshot.config.specifier_enabled(specifier) {
|
|
continue;
|
|
}
|
|
let version = state_snapshot.documents.version(specifier);
|
|
let current_version = collection.lock().unwrap().get_version(specifier, &DiagnosticSource::Deno);
|
|
if version != current_version {
|
|
let mut diagnostic_list = Vec::new();
|
|
if let Some(dependencies) = state_snapshot.documents.dependencies(specifier) {
|
|
for (_, dependency) in dependencies.iter() {
|
|
if let (Some(code), Some(range)) = (
|
|
&dependency.maybe_code,
|
|
&dependency.maybe_code_specifier_range,
|
|
) {
|
|
match code.clone() {
|
|
ResolvedDependency::Err(dependency_err) => {
|
|
diagnostic_list.push(lsp::Diagnostic {
|
|
range: *range,
|
|
severity: Some(lsp::DiagnosticSeverity::Error),
|
|
code: Some(dependency_err.as_code()),
|
|
code_description: None,
|
|
source: Some("deno".to_string()),
|
|
message: format!("{}", dependency_err),
|
|
related_information: None,
|
|
tags: None,
|
|
data: None,
|
|
})
|
|
}
|
|
ResolvedDependency::Resolved(specifier) => {
|
|
if !(state_snapshot.documents.contains_key(&specifier) || sources.contains_key(&specifier)) {
|
|
let scheme = specifier.scheme();
|
|
let (code, message) = if scheme == "file" {
|
|
(Some(lsp::NumberOrString::String("no-local".to_string())), format!("Unable to load a local module: \"{}\".\n Please check the file path.", specifier))
|
|
} else if scheme == "data" {
|
|
(Some(lsp::NumberOrString::String("no-cache-data".to_string())), "Uncached data URL.".to_string())
|
|
} else {
|
|
(Some(lsp::NumberOrString::String("no-cache".to_string())), format!("Unable to load the remote module: \"{}\".", specifier))
|
|
};
|
|
diagnostic_list.push(lsp::Diagnostic {
|
|
range: *range,
|
|
severity: Some(lsp::DiagnosticSeverity::Error),
|
|
code,
|
|
code_description: None,
|
|
source: Some("deno".to_string()),
|
|
message,
|
|
related_information: None,
|
|
tags: None,
|
|
data: Some(json!({
|
|
"specifier": specifier
|
|
})),
|
|
})
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
diagnostics.push((specifier.clone(), version, diagnostic_list))
|
|
}
|
|
}
|
|
|
|
Ok(diagnostics)
|
|
})
|
|
.await
|
|
.unwrap()
|
|
}
|