mirror of
https://github.com/denoland/deno.git
synced 2024-11-22 15:06:54 -05:00
1741 lines
55 KiB
Rust
1741 lines
55 KiB
Rust
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
|
|
|
use super::analysis;
|
|
use super::cache;
|
|
use super::client::Client;
|
|
use super::config::ConfigSnapshot;
|
|
use super::documents;
|
|
use super::documents::Document;
|
|
use super::documents::DocumentsFilter;
|
|
use super::language_server;
|
|
use super::language_server::StateSnapshot;
|
|
use super::performance::Performance;
|
|
use super::tsc;
|
|
use super::tsc::TsServer;
|
|
use super::urls::LspClientUrl;
|
|
use super::urls::LspUrlMap;
|
|
|
|
use crate::args::LintOptions;
|
|
use crate::graph_util;
|
|
use crate::graph_util::enhanced_resolution_error_message;
|
|
use crate::lsp::lsp_custom::DiagnosticBatchNotificationParams;
|
|
use crate::tools::lint::get_configured_rules;
|
|
|
|
use deno_ast::MediaType;
|
|
use deno_core::anyhow::anyhow;
|
|
use deno_core::error::AnyError;
|
|
use deno_core::resolve_url;
|
|
use deno_core::serde::Deserialize;
|
|
use deno_core::serde_json;
|
|
use deno_core::serde_json::json;
|
|
use deno_core::task::spawn;
|
|
use deno_core::task::spawn_blocking;
|
|
use deno_core::task::JoinHandle;
|
|
use deno_core::ModuleSpecifier;
|
|
use deno_graph::Resolution;
|
|
use deno_graph::ResolutionError;
|
|
use deno_graph::SpecifierError;
|
|
use deno_lint::rules::LintRule;
|
|
use deno_runtime::deno_node;
|
|
use deno_runtime::tokio_util::create_basic_runtime;
|
|
use deno_semver::npm::NpmPackageReqReference;
|
|
use deno_semver::package::PackageReq;
|
|
use log::error;
|
|
use std::collections::HashMap;
|
|
use std::collections::HashSet;
|
|
use std::sync::atomic::AtomicUsize;
|
|
use std::sync::Arc;
|
|
use std::thread;
|
|
use tokio::sync::mpsc;
|
|
use tokio::sync::Mutex;
|
|
use tokio::time::Duration;
|
|
use tokio_util::sync::CancellationToken;
|
|
use tower_lsp::lsp_types as lsp;
|
|
|
|
#[derive(Debug)]
|
|
pub struct DiagnosticServerUpdateMessage {
|
|
pub snapshot: Arc<StateSnapshot>,
|
|
pub config: Arc<ConfigSnapshot>,
|
|
pub lint_options: LintOptions,
|
|
pub url_map: LspUrlMap,
|
|
}
|
|
|
|
struct DiagnosticRecord {
|
|
pub specifier: ModuleSpecifier,
|
|
pub versioned: VersionedDiagnostics,
|
|
}
|
|
|
|
#[derive(Clone, Default, Debug)]
|
|
struct VersionedDiagnostics {
|
|
pub version: Option<i32>,
|
|
pub diagnostics: Vec<lsp::Diagnostic>,
|
|
}
|
|
|
|
type DiagnosticVec = Vec<DiagnosticRecord>;
|
|
|
|
#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)]
|
|
pub enum DiagnosticSource {
|
|
Deno,
|
|
Lint,
|
|
Ts,
|
|
}
|
|
|
|
impl DiagnosticSource {
|
|
pub fn as_lsp_source(&self) -> &'static str {
|
|
match self {
|
|
Self::Deno => "deno",
|
|
Self::Lint => "deno-lint",
|
|
Self::Ts => "deno-ts",
|
|
}
|
|
}
|
|
}
|
|
|
|
type DiagnosticsBySource = HashMap<DiagnosticSource, VersionedDiagnostics>;
|
|
|
|
#[derive(Debug)]
|
|
struct DiagnosticsPublisher {
|
|
client: Client,
|
|
diagnostics_by_specifier:
|
|
Mutex<HashMap<ModuleSpecifier, DiagnosticsBySource>>,
|
|
}
|
|
|
|
impl DiagnosticsPublisher {
|
|
pub fn new(client: Client) -> Self {
|
|
Self {
|
|
client,
|
|
diagnostics_by_specifier: Default::default(),
|
|
}
|
|
}
|
|
|
|
pub async fn publish(
|
|
&self,
|
|
source: DiagnosticSource,
|
|
diagnostics: DiagnosticVec,
|
|
url_map: &LspUrlMap,
|
|
token: &CancellationToken,
|
|
) -> usize {
|
|
let mut diagnostics_by_specifier =
|
|
self.diagnostics_by_specifier.lock().await;
|
|
let mut seen_specifiers = HashSet::with_capacity(diagnostics.len());
|
|
let mut messages_sent = 0;
|
|
|
|
for record in diagnostics {
|
|
if token.is_cancelled() {
|
|
return messages_sent;
|
|
}
|
|
|
|
seen_specifiers.insert(record.specifier.clone());
|
|
|
|
let diagnostics_by_source = diagnostics_by_specifier
|
|
.entry(record.specifier.clone())
|
|
.or_default();
|
|
let version = record.versioned.version;
|
|
let source_diagnostics = diagnostics_by_source.entry(source).or_default();
|
|
*source_diagnostics = record.versioned;
|
|
|
|
// DO NOT filter these by version. We want to display even out
|
|
// of date diagnostics in order to prevent flickering. The user's
|
|
// lsp client will eventually catch up.
|
|
let all_specifier_diagnostics = diagnostics_by_source
|
|
.values()
|
|
.flat_map(|d| &d.diagnostics)
|
|
.cloned()
|
|
.collect::<Vec<_>>();
|
|
|
|
self
|
|
.client
|
|
.when_outside_lsp_lock()
|
|
.publish_diagnostics(
|
|
url_map
|
|
.normalize_specifier(&record.specifier)
|
|
.unwrap_or(LspClientUrl::new(record.specifier)),
|
|
all_specifier_diagnostics,
|
|
version,
|
|
)
|
|
.await;
|
|
messages_sent += 1;
|
|
}
|
|
|
|
// now check all the specifiers to clean up any ones with old diagnostics
|
|
let mut specifiers_to_remove = Vec::new();
|
|
for (specifier, diagnostics_by_source) in
|
|
diagnostics_by_specifier.iter_mut()
|
|
{
|
|
if seen_specifiers.contains(specifier) {
|
|
continue;
|
|
}
|
|
if token.is_cancelled() {
|
|
break;
|
|
}
|
|
let maybe_removed_value = diagnostics_by_source.remove(&source);
|
|
if diagnostics_by_source.is_empty() {
|
|
specifiers_to_remove.push(specifier.clone());
|
|
if let Some(removed_value) = maybe_removed_value {
|
|
// clear out any diagnostics for this specifier
|
|
self
|
|
.client
|
|
.when_outside_lsp_lock()
|
|
.publish_diagnostics(
|
|
url_map
|
|
.normalize_specifier(specifier)
|
|
.unwrap_or_else(|_| LspClientUrl::new(specifier.clone())),
|
|
Vec::new(),
|
|
removed_value.version,
|
|
)
|
|
.await;
|
|
messages_sent += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// clean up specifiers with no diagnostics
|
|
for specifier in specifiers_to_remove {
|
|
diagnostics_by_specifier.remove(&specifier);
|
|
}
|
|
|
|
messages_sent
|
|
}
|
|
|
|
pub async fn clear(&self) {
|
|
let mut all_diagnostics = self.diagnostics_by_specifier.lock().await;
|
|
all_diagnostics.clear();
|
|
}
|
|
}
|
|
|
|
type DiagnosticMap = HashMap<ModuleSpecifier, VersionedDiagnostics>;
|
|
|
|
#[derive(Clone, Default, Debug)]
|
|
struct TsDiagnosticsStore(Arc<deno_core::parking_lot::Mutex<DiagnosticMap>>);
|
|
|
|
impl TsDiagnosticsStore {
|
|
pub fn get(
|
|
&self,
|
|
specifier: &ModuleSpecifier,
|
|
document_version: Option<i32>,
|
|
) -> Vec<lsp::Diagnostic> {
|
|
let ts_diagnostics = self.0.lock();
|
|
if let Some(versioned) = ts_diagnostics.get(specifier) {
|
|
// only get the diagnostics if they're up to date
|
|
if document_version == versioned.version {
|
|
return versioned.diagnostics.clone();
|
|
}
|
|
}
|
|
Vec::new()
|
|
}
|
|
|
|
pub fn invalidate(&self, specifiers: &[ModuleSpecifier]) {
|
|
let mut ts_diagnostics = self.0.lock();
|
|
for specifier in specifiers {
|
|
ts_diagnostics.remove(specifier);
|
|
}
|
|
}
|
|
|
|
pub fn invalidate_all(&self) {
|
|
self.0.lock().clear();
|
|
}
|
|
|
|
fn update(&self, diagnostics: &DiagnosticVec) {
|
|
let mut stored_ts_diagnostics = self.0.lock();
|
|
*stored_ts_diagnostics = diagnostics
|
|
.iter()
|
|
.map(|record| (record.specifier.clone(), record.versioned.clone()))
|
|
.collect();
|
|
}
|
|
}
|
|
|
|
pub fn should_send_diagnostic_batch_index_notifications() -> bool {
|
|
crate::args::has_flag_env_var(
|
|
"DENO_DONT_USE_INTERNAL_LSP_DIAGNOSTIC_SYNC_FLAG",
|
|
)
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct DiagnosticBatchCounter(Option<Arc<AtomicUsize>>);
|
|
|
|
impl Default for DiagnosticBatchCounter {
|
|
fn default() -> Self {
|
|
if should_send_diagnostic_batch_index_notifications() {
|
|
Self(Some(Default::default()))
|
|
} else {
|
|
Self(None)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl DiagnosticBatchCounter {
|
|
pub fn inc(&self) -> Option<usize> {
|
|
self
|
|
.0
|
|
.as_ref()
|
|
.map(|value| value.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1)
|
|
}
|
|
|
|
pub fn get(&self) -> Option<usize> {
|
|
self
|
|
.0
|
|
.as_ref()
|
|
.map(|value| value.load(std::sync::atomic::Ordering::SeqCst))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum ChannelMessage {
|
|
Update(ChannelUpdateMessage),
|
|
Clear,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct ChannelUpdateMessage {
|
|
message: DiagnosticServerUpdateMessage,
|
|
batch_index: Option<usize>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct DiagnosticsServer {
|
|
channel: Option<mpsc::UnboundedSender<ChannelMessage>>,
|
|
ts_diagnostics: TsDiagnosticsStore,
|
|
client: Client,
|
|
performance: Arc<Performance>,
|
|
ts_server: Arc<TsServer>,
|
|
batch_counter: DiagnosticBatchCounter,
|
|
}
|
|
|
|
impl DiagnosticsServer {
|
|
pub fn new(
|
|
client: Client,
|
|
performance: Arc<Performance>,
|
|
ts_server: Arc<TsServer>,
|
|
) -> Self {
|
|
DiagnosticsServer {
|
|
channel: Default::default(),
|
|
ts_diagnostics: Default::default(),
|
|
client,
|
|
performance,
|
|
ts_server,
|
|
batch_counter: Default::default(),
|
|
}
|
|
}
|
|
|
|
pub fn get_ts_diagnostics(
|
|
&self,
|
|
specifier: &ModuleSpecifier,
|
|
document_version: Option<i32>,
|
|
) -> Vec<lsp::Diagnostic> {
|
|
self.ts_diagnostics.get(specifier, document_version)
|
|
}
|
|
|
|
pub fn invalidate(&self, specifiers: &[ModuleSpecifier]) {
|
|
self.ts_diagnostics.invalidate(specifiers);
|
|
}
|
|
|
|
pub fn invalidate_all(&self) {
|
|
self.ts_diagnostics.invalidate_all();
|
|
if let Some(tx) = &self.channel {
|
|
let _ = tx.send(ChannelMessage::Clear);
|
|
}
|
|
}
|
|
|
|
#[allow(unused_must_use)]
|
|
pub fn start(&mut self) {
|
|
let (tx, mut rx) = mpsc::unbounded_channel::<ChannelMessage>();
|
|
self.channel = Some(tx);
|
|
let client = self.client.clone();
|
|
let performance = self.performance.clone();
|
|
let ts_diagnostics_store = self.ts_diagnostics.clone();
|
|
let ts_server = self.ts_server.clone();
|
|
|
|
let _join_handle = thread::spawn(move || {
|
|
let runtime = create_basic_runtime();
|
|
|
|
runtime.block_on(async {
|
|
let mut token = CancellationToken::new();
|
|
let mut ts_handle: Option<JoinHandle<()>> = None;
|
|
let mut lint_handle: Option<JoinHandle<()>> = None;
|
|
let mut deps_handle: Option<JoinHandle<()>> = None;
|
|
let diagnostics_publisher =
|
|
Arc::new(DiagnosticsPublisher::new(client.clone()));
|
|
|
|
loop {
|
|
match rx.recv().await {
|
|
// channel has closed
|
|
None => break,
|
|
Some(message) => {
|
|
let message = match message {
|
|
ChannelMessage::Update(message) => message,
|
|
ChannelMessage::Clear => {
|
|
token.cancel();
|
|
token = CancellationToken::new();
|
|
diagnostics_publisher.clear().await;
|
|
continue;
|
|
}
|
|
};
|
|
let ChannelUpdateMessage {
|
|
message:
|
|
DiagnosticServerUpdateMessage {
|
|
snapshot,
|
|
config,
|
|
lint_options,
|
|
url_map,
|
|
},
|
|
batch_index,
|
|
} = message;
|
|
let url_map = Arc::new(url_map);
|
|
|
|
// cancel the previous run
|
|
token.cancel();
|
|
token = CancellationToken::new();
|
|
|
|
let previous_ts_handle = ts_handle.take();
|
|
ts_handle = Some(spawn({
|
|
let performance = performance.clone();
|
|
let diagnostics_publisher = diagnostics_publisher.clone();
|
|
let ts_server = ts_server.clone();
|
|
let token = token.clone();
|
|
let ts_diagnostics_store = ts_diagnostics_store.clone();
|
|
let snapshot = snapshot.clone();
|
|
let config = config.clone();
|
|
let url_map = url_map.clone();
|
|
async move {
|
|
if let Some(previous_handle) = previous_ts_handle {
|
|
// Wait on the previous run to complete in order to prevent
|
|
// multiple threads queueing up a lot of tsc requests.
|
|
// Do not race this with cancellation because we want a
|
|
// chain of events to wait for all the previous diagnostics to complete
|
|
previous_handle.await;
|
|
}
|
|
|
|
// 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);
|
|
tokio::select! {
|
|
_ = token.cancelled() => { return; }
|
|
_ = tokio::time::sleep(DELAY) => {}
|
|
};
|
|
|
|
let mark =
|
|
performance.mark("update_diagnostics_ts", None::<()>);
|
|
let diagnostics = generate_ts_diagnostics(
|
|
snapshot.clone(),
|
|
&config,
|
|
&ts_server,
|
|
token.clone(),
|
|
)
|
|
.await
|
|
.map_err(|err| {
|
|
error!("Error generating TypeScript diagnostics: {}", err);
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let mut messages_len = 0;
|
|
if !token.is_cancelled() {
|
|
ts_diagnostics_store.update(&diagnostics);
|
|
messages_len = diagnostics_publisher
|
|
.publish(
|
|
DiagnosticSource::Ts,
|
|
diagnostics,
|
|
&url_map,
|
|
&token,
|
|
)
|
|
.await;
|
|
|
|
if !token.is_cancelled() {
|
|
performance.measure(mark);
|
|
}
|
|
}
|
|
|
|
if let Some(batch_index) = batch_index {
|
|
diagnostics_publisher
|
|
.client
|
|
.send_diagnostic_batch_notification(
|
|
DiagnosticBatchNotificationParams {
|
|
batch_index,
|
|
messages_len,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}));
|
|
|
|
let previous_deps_handle = deps_handle.take();
|
|
deps_handle = Some(spawn({
|
|
let performance = performance.clone();
|
|
let diagnostics_publisher = diagnostics_publisher.clone();
|
|
let token = token.clone();
|
|
let snapshot = snapshot.clone();
|
|
let config = config.clone();
|
|
let url_map = url_map.clone();
|
|
async move {
|
|
if let Some(previous_handle) = previous_deps_handle {
|
|
previous_handle.await;
|
|
}
|
|
let mark =
|
|
performance.mark("update_diagnostics_deps", None::<()>);
|
|
let diagnostics = spawn_blocking({
|
|
let token = token.clone();
|
|
move || generate_deno_diagnostics(&snapshot, &config, token)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
let mut messages_len = 0;
|
|
if !token.is_cancelled() {
|
|
messages_len = diagnostics_publisher
|
|
.publish(
|
|
DiagnosticSource::Deno,
|
|
diagnostics,
|
|
&url_map,
|
|
&token,
|
|
)
|
|
.await;
|
|
|
|
if !token.is_cancelled() {
|
|
performance.measure(mark);
|
|
}
|
|
}
|
|
|
|
if let Some(batch_index) = batch_index {
|
|
diagnostics_publisher
|
|
.client
|
|
.send_diagnostic_batch_notification(
|
|
DiagnosticBatchNotificationParams {
|
|
batch_index,
|
|
messages_len,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}));
|
|
|
|
let previous_lint_handle = lint_handle.take();
|
|
lint_handle = Some(spawn({
|
|
let performance = performance.clone();
|
|
let diagnostics_publisher = diagnostics_publisher.clone();
|
|
let token = token.clone();
|
|
let snapshot = snapshot.clone();
|
|
let config = config.clone();
|
|
let url_map = url_map.clone();
|
|
async move {
|
|
if let Some(previous_handle) = previous_lint_handle {
|
|
previous_handle.await;
|
|
}
|
|
let mark =
|
|
performance.mark("update_diagnostics_lint", None::<()>);
|
|
let diagnostics = spawn_blocking({
|
|
let token = token.clone();
|
|
move || {
|
|
generate_lint_diagnostics(
|
|
&snapshot,
|
|
&config,
|
|
&lint_options,
|
|
token,
|
|
)
|
|
}
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
let mut messages_len = 0;
|
|
if !token.is_cancelled() {
|
|
messages_len = diagnostics_publisher
|
|
.publish(
|
|
DiagnosticSource::Lint,
|
|
diagnostics,
|
|
&url_map,
|
|
&token,
|
|
)
|
|
.await;
|
|
|
|
if !token.is_cancelled() {
|
|
performance.measure(mark);
|
|
}
|
|
}
|
|
|
|
if let Some(batch_index) = batch_index {
|
|
diagnostics_publisher
|
|
.client
|
|
.send_diagnostic_batch_notification(
|
|
DiagnosticBatchNotificationParams {
|
|
batch_index,
|
|
messages_len,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
})
|
|
});
|
|
}
|
|
|
|
pub fn latest_batch_index(&self) -> Option<usize> {
|
|
self.batch_counter.get()
|
|
}
|
|
|
|
pub fn update(
|
|
&self,
|
|
message: DiagnosticServerUpdateMessage,
|
|
) -> Result<(), AnyError> {
|
|
// todo(dsherret): instead of queuing up messages, it would be better to
|
|
// instead only store the latest message (ex. maybe using a
|
|
// tokio::sync::watch::channel)
|
|
if let Some(tx) = &self.channel {
|
|
tx.send(ChannelMessage::Update(ChannelUpdateMessage {
|
|
message,
|
|
batch_index: self.batch_counter.inc(),
|
|
}))
|
|
.map_err(|err| err.into())
|
|
} else {
|
|
Err(anyhow!("diagnostics server not started"))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> From<&'a crate::tsc::DiagnosticCategory> for lsp::DiagnosticSeverity {
|
|
fn from(category: &'a crate::tsc::DiagnosticCategory) -> Self {
|
|
match category {
|
|
crate::tsc::DiagnosticCategory::Error => lsp::DiagnosticSeverity::ERROR,
|
|
crate::tsc::DiagnosticCategory::Warning => {
|
|
lsp::DiagnosticSeverity::WARNING
|
|
}
|
|
crate::tsc::DiagnosticCategory::Suggestion => {
|
|
lsp::DiagnosticSeverity::HINT
|
|
}
|
|
crate::tsc::DiagnosticCategory::Message => {
|
|
lsp::DiagnosticSeverity::INFORMATION
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> From<&'a crate::tsc::Position> for lsp::Position {
|
|
fn from(pos: &'a crate::tsc::Position) -> Self {
|
|
Self {
|
|
line: pos.line as u32,
|
|
character: pos.character as u32,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn get_diagnostic_message(diagnostic: &crate::tsc::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_range(
|
|
start: &crate::tsc::Position,
|
|
end: &crate::tsc::Position,
|
|
) -> lsp::Range {
|
|
lsp::Range {
|
|
start: start.into(),
|
|
end: end.into(),
|
|
}
|
|
}
|
|
|
|
fn to_lsp_related_information(
|
|
related_information: &Option<Vec<crate::tsc::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: Vec<crate::tsc::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(DiagnosticSource::Ts.as_lsp_source().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 | 6205 | 7027
|
|
| 7028 => Some(vec![lsp::DiagnosticTag::UNNECESSARY]),
|
|
// These are codes that indicated the variable is deprecated.
|
|
2789 | 6385 | 6387 => Some(vec![lsp::DiagnosticTag::DEPRECATED]),
|
|
_ => None,
|
|
},
|
|
data: None,
|
|
})
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn generate_lint_diagnostics(
|
|
snapshot: &language_server::StateSnapshot,
|
|
config: &ConfigSnapshot,
|
|
lint_options: &LintOptions,
|
|
token: CancellationToken,
|
|
) -> DiagnosticVec {
|
|
let documents = snapshot
|
|
.documents
|
|
.documents(DocumentsFilter::OpenDiagnosable);
|
|
let workspace_settings = config.settings.workspace.clone();
|
|
let lint_rules = get_configured_rules(lint_options.rules.clone());
|
|
let mut diagnostics_vec = Vec::new();
|
|
if workspace_settings.lint {
|
|
for document in documents {
|
|
// exit early if cancelled
|
|
if token.is_cancelled() {
|
|
break;
|
|
}
|
|
|
|
// ignore any npm package files
|
|
if let Some(node_resolver) = &snapshot.maybe_node_resolver {
|
|
if node_resolver.in_npm_package(document.specifier()) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
let version = document.maybe_lsp_version();
|
|
diagnostics_vec.push(DiagnosticRecord {
|
|
specifier: document.specifier().clone(),
|
|
versioned: VersionedDiagnostics {
|
|
version,
|
|
diagnostics: generate_document_lint_diagnostics(
|
|
config,
|
|
lint_options,
|
|
lint_rules.clone(),
|
|
&document,
|
|
),
|
|
},
|
|
});
|
|
}
|
|
}
|
|
diagnostics_vec
|
|
}
|
|
|
|
fn generate_document_lint_diagnostics(
|
|
config: &ConfigSnapshot,
|
|
lint_options: &LintOptions,
|
|
lint_rules: Vec<&'static dyn LintRule>,
|
|
document: &Document,
|
|
) -> Vec<lsp::Diagnostic> {
|
|
if !config.specifier_enabled(document.specifier()) {
|
|
return Vec::new();
|
|
}
|
|
if !lint_options.files.matches_specifier(document.specifier()) {
|
|
return Vec::new();
|
|
}
|
|
match document.maybe_parsed_source() {
|
|
Some(Ok(parsed_source)) => {
|
|
if let Ok(references) =
|
|
analysis::get_lint_references(&parsed_source, lint_rules)
|
|
{
|
|
references
|
|
.into_iter()
|
|
.map(|r| r.to_diagnostic())
|
|
.collect::<Vec<_>>()
|
|
} else {
|
|
Vec::new()
|
|
}
|
|
}
|
|
Some(Err(_)) => Vec::new(),
|
|
None => {
|
|
error!("Missing file contents for: {}", document.specifier());
|
|
Vec::new()
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn generate_ts_diagnostics(
|
|
snapshot: Arc<language_server::StateSnapshot>,
|
|
config: &ConfigSnapshot,
|
|
ts_server: &tsc::TsServer,
|
|
token: CancellationToken,
|
|
) -> Result<DiagnosticVec, AnyError> {
|
|
let mut diagnostics_vec = Vec::new();
|
|
let specifiers = snapshot
|
|
.documents
|
|
.documents(DocumentsFilter::OpenDiagnosable)
|
|
.into_iter()
|
|
.map(|d| d.specifier().clone());
|
|
let (enabled_specifiers, disabled_specifiers) = specifiers
|
|
.into_iter()
|
|
.partition::<Vec<_>, _>(|s| config.specifier_enabled(s));
|
|
let ts_diagnostics_map = if !enabled_specifiers.is_empty() {
|
|
ts_server
|
|
.get_diagnostics(snapshot.clone(), enabled_specifiers, token)
|
|
.await?
|
|
} else {
|
|
Default::default()
|
|
};
|
|
for (specifier_str, ts_json_diagnostics) in ts_diagnostics_map {
|
|
let specifier = resolve_url(&specifier_str)?;
|
|
let version = snapshot
|
|
.documents
|
|
.get(&specifier)
|
|
.and_then(|d| d.maybe_lsp_version());
|
|
// check if the specifier is enabled again just in case TS returns us
|
|
// diagnostics for a disabled specifier
|
|
let ts_diagnostics = if config.specifier_enabled(&specifier) {
|
|
ts_json_to_diagnostics(ts_json_diagnostics)
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
diagnostics_vec.push(DiagnosticRecord {
|
|
specifier,
|
|
versioned: VersionedDiagnostics {
|
|
version,
|
|
diagnostics: ts_diagnostics,
|
|
},
|
|
});
|
|
}
|
|
// add an empty diagnostic publish for disabled specifiers in order
|
|
// to clear those diagnostics if they exist
|
|
for specifier in disabled_specifiers {
|
|
let version = snapshot
|
|
.documents
|
|
.get(&specifier)
|
|
.and_then(|d| d.maybe_lsp_version());
|
|
diagnostics_vec.push(DiagnosticRecord {
|
|
specifier,
|
|
versioned: VersionedDiagnostics {
|
|
version,
|
|
diagnostics: Vec::new(),
|
|
},
|
|
});
|
|
}
|
|
Ok(diagnostics_vec)
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct DiagnosticDataSpecifier {
|
|
pub specifier: ModuleSpecifier,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct DiagnosticDataStrSpecifier {
|
|
pub specifier: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct DiagnosticDataRedirect {
|
|
pub redirect: ModuleSpecifier,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct DiagnosticDataImportMapRemap {
|
|
pub from: String,
|
|
pub to: String,
|
|
}
|
|
|
|
/// An enum which represents diagnostic errors which originate from Deno itself.
|
|
pub enum DenoDiagnostic {
|
|
/// A `x-deno-warning` is associated with the specifier and should be displayed
|
|
/// as a warning to the user.
|
|
DenoWarn(String),
|
|
/// An informational diagnostic that indicates an existing specifier can be
|
|
/// remapped to an import map import specifier.
|
|
ImportMapRemap { from: String, to: String },
|
|
/// The import assertion type is incorrect.
|
|
InvalidAssertType(String),
|
|
/// A module requires an assertion type to be a valid import.
|
|
NoAssertType,
|
|
/// A remote module was not found in the cache.
|
|
NoCache(ModuleSpecifier),
|
|
/// A remote npm package reference was not found in the cache.
|
|
NoCacheNpm(PackageReq, ModuleSpecifier),
|
|
/// A local module was not found on the local file system.
|
|
NoLocal(ModuleSpecifier),
|
|
/// The specifier resolved to a remote specifier that was redirected to
|
|
/// another specifier.
|
|
Redirect {
|
|
from: ModuleSpecifier,
|
|
to: ModuleSpecifier,
|
|
},
|
|
/// An error occurred when resolving the specifier string.
|
|
ResolutionError(deno_graph::ResolutionError),
|
|
/// Invalid `node:` specifier.
|
|
InvalidNodeSpecifier(ModuleSpecifier),
|
|
}
|
|
|
|
impl DenoDiagnostic {
|
|
fn code(&self) -> &str {
|
|
match self {
|
|
Self::DenoWarn(_) => "deno-warn",
|
|
Self::ImportMapRemap { .. } => "import-map-remap",
|
|
Self::InvalidAssertType(_) => "invalid-assert-type",
|
|
Self::NoAssertType => "no-assert-type",
|
|
Self::NoCache(_) => "no-cache",
|
|
Self::NoCacheNpm(_, _) => "no-cache-npm",
|
|
Self::NoLocal(_) => "no-local",
|
|
Self::Redirect { .. } => "redirect",
|
|
Self::ResolutionError(err) => {
|
|
if graph_util::get_resolution_error_bare_node_specifier(err).is_some() {
|
|
"import-node-prefix-missing"
|
|
} else {
|
|
match err {
|
|
ResolutionError::InvalidDowngrade { .. } => "invalid-downgrade",
|
|
ResolutionError::InvalidLocalImport { .. } => {
|
|
"invalid-local-import"
|
|
}
|
|
ResolutionError::InvalidSpecifier { error, .. } => match error {
|
|
SpecifierError::ImportPrefixMissing(_, _) => {
|
|
"import-prefix-missing"
|
|
}
|
|
SpecifierError::InvalidUrl(_) => "invalid-url",
|
|
},
|
|
ResolutionError::ResolverError { .. } => "resolver-error",
|
|
}
|
|
}
|
|
}
|
|
Self::InvalidNodeSpecifier(_) => "resolver-error",
|
|
}
|
|
}
|
|
|
|
/// A "static" method which for a diagnostic that originated from the
|
|
/// structure returns a code action which can resolve the diagnostic.
|
|
pub fn get_code_action(
|
|
specifier: &ModuleSpecifier,
|
|
diagnostic: &lsp::Diagnostic,
|
|
) -> Result<lsp::CodeAction, AnyError> {
|
|
if let Some(lsp::NumberOrString::String(code)) = &diagnostic.code {
|
|
let code_action = match code.as_str() {
|
|
"import-map-remap" => {
|
|
let data = diagnostic
|
|
.data
|
|
.clone()
|
|
.ok_or_else(|| anyhow!("Diagnostic is missing data"))?;
|
|
let DiagnosticDataImportMapRemap { from, to } =
|
|
serde_json::from_value(data)?;
|
|
lsp::CodeAction {
|
|
title: format!("Update \"{from}\" to \"{to}\" to use import map."),
|
|
kind: Some(lsp::CodeActionKind::QUICKFIX),
|
|
diagnostics: Some(vec![diagnostic.clone()]),
|
|
edit: Some(lsp::WorkspaceEdit {
|
|
changes: Some(HashMap::from([(
|
|
specifier.clone(),
|
|
vec![lsp::TextEdit {
|
|
new_text: format!("\"{to}\""),
|
|
range: diagnostic.range,
|
|
}],
|
|
)])),
|
|
..Default::default()
|
|
}),
|
|
..Default::default()
|
|
}
|
|
}
|
|
"no-assert-type" => lsp::CodeAction {
|
|
title: "Insert import assertion.".to_string(),
|
|
kind: Some(lsp::CodeActionKind::QUICKFIX),
|
|
diagnostics: Some(vec![diagnostic.clone()]),
|
|
edit: Some(lsp::WorkspaceEdit {
|
|
changes: Some(HashMap::from([(
|
|
specifier.clone(),
|
|
vec![lsp::TextEdit {
|
|
new_text: " assert { type: \"json\" }".to_string(),
|
|
range: lsp::Range {
|
|
start: diagnostic.range.end,
|
|
end: diagnostic.range.end,
|
|
},
|
|
}],
|
|
)])),
|
|
..Default::default()
|
|
}),
|
|
..Default::default()
|
|
},
|
|
"no-cache" | "no-cache-npm" => {
|
|
let data = diagnostic
|
|
.data
|
|
.clone()
|
|
.ok_or_else(|| anyhow!("Diagnostic is missing data"))?;
|
|
let data: DiagnosticDataSpecifier = serde_json::from_value(data)?;
|
|
let title = match code.as_str() {
|
|
"no-cache" | "no-cache-npm" => {
|
|
format!("Cache \"{}\" and its dependencies.", data.specifier)
|
|
}
|
|
_ => "Cache the data URL and its dependencies.".to_string(),
|
|
};
|
|
lsp::CodeAction {
|
|
title,
|
|
kind: Some(lsp::CodeActionKind::QUICKFIX),
|
|
diagnostics: Some(vec![diagnostic.clone()]),
|
|
command: Some(lsp::Command {
|
|
title: "".to_string(),
|
|
command: "deno.cache".to_string(),
|
|
arguments: Some(vec![json!([data.specifier])]),
|
|
}),
|
|
..Default::default()
|
|
}
|
|
}
|
|
"redirect" => {
|
|
let data = diagnostic
|
|
.data
|
|
.clone()
|
|
.ok_or_else(|| anyhow!("Diagnostic is missing data"))?;
|
|
let data: DiagnosticDataRedirect = serde_json::from_value(data)?;
|
|
lsp::CodeAction {
|
|
title: "Update specifier to its redirected specifier.".to_string(),
|
|
kind: Some(lsp::CodeActionKind::QUICKFIX),
|
|
diagnostics: Some(vec![diagnostic.clone()]),
|
|
edit: Some(lsp::WorkspaceEdit {
|
|
changes: Some(HashMap::from([(
|
|
specifier.clone(),
|
|
vec![lsp::TextEdit {
|
|
new_text: format!("\"{}\"", data.redirect),
|
|
range: diagnostic.range,
|
|
}],
|
|
)])),
|
|
..Default::default()
|
|
}),
|
|
..Default::default()
|
|
}
|
|
}
|
|
"import-node-prefix-missing" => {
|
|
let data = diagnostic
|
|
.data
|
|
.clone()
|
|
.ok_or_else(|| anyhow!("Diagnostic is missing data"))?;
|
|
let data: DiagnosticDataStrSpecifier = serde_json::from_value(data)?;
|
|
lsp::CodeAction {
|
|
title: format!("Update specifier to node:{}", data.specifier),
|
|
kind: Some(lsp::CodeActionKind::QUICKFIX),
|
|
diagnostics: Some(vec![diagnostic.clone()]),
|
|
edit: Some(lsp::WorkspaceEdit {
|
|
changes: Some(HashMap::from([(
|
|
specifier.clone(),
|
|
vec![lsp::TextEdit {
|
|
new_text: format!("\"node:{}\"", data.specifier),
|
|
range: diagnostic.range,
|
|
}],
|
|
)])),
|
|
..Default::default()
|
|
}),
|
|
..Default::default()
|
|
}
|
|
}
|
|
_ => {
|
|
return Err(anyhow!(
|
|
"Unsupported diagnostic code (\"{}\") provided.",
|
|
code
|
|
))
|
|
}
|
|
};
|
|
Ok(code_action)
|
|
} else {
|
|
Err(anyhow!("Unsupported diagnostic code provided."))
|
|
}
|
|
}
|
|
|
|
/// Given a reference to the code from an LSP diagnostic, determine if the
|
|
/// diagnostic is fixable or not
|
|
pub fn is_fixable(diagnostic: &lsp_types::Diagnostic) -> bool {
|
|
if let Some(lsp::NumberOrString::String(code)) = &diagnostic.code {
|
|
matches!(
|
|
code.as_str(),
|
|
"import-map-remap"
|
|
| "no-cache"
|
|
| "no-cache-npm"
|
|
| "no-assert-type"
|
|
| "redirect"
|
|
| "import-node-prefix-missing"
|
|
)
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Convert to an lsp Diagnostic when the range the diagnostic applies to is
|
|
/// provided.
|
|
pub fn to_lsp_diagnostic(&self, range: &lsp::Range) -> lsp::Diagnostic {
|
|
let (severity, message, data) = match self {
|
|
Self::DenoWarn(message) => (lsp::DiagnosticSeverity::WARNING, message.to_string(), None),
|
|
Self::ImportMapRemap { from, to } => (lsp::DiagnosticSeverity::HINT, format!("The import specifier can be remapped to \"{to}\" which will resolve it via the active import map."), Some(json!({ "from": from, "to": to }))),
|
|
Self::InvalidAssertType(assert_type) => (lsp::DiagnosticSeverity::ERROR, format!("The module is a JSON module and expected an assertion type of \"json\". Instead got \"{assert_type}\"."), None),
|
|
Self::NoAssertType => (lsp::DiagnosticSeverity::ERROR, "The module is a JSON module and not being imported with an import assertion. Consider adding `assert { type: \"json\" }` to the import statement.".to_string(), None),
|
|
Self::NoCache(specifier) => (lsp::DiagnosticSeverity::ERROR, format!("Uncached or missing remote URL: {specifier}"), Some(json!({ "specifier": specifier }))),
|
|
Self::NoCacheNpm(pkg_req, specifier) => (lsp::DiagnosticSeverity::ERROR, format!("Uncached or missing npm package: {}", pkg_req), Some(json!({ "specifier": specifier }))),
|
|
Self::NoLocal(specifier) => (lsp::DiagnosticSeverity::ERROR, format!("Unable to load a local module: {specifier}\n Please check the file path."), None),
|
|
Self::Redirect { from, to} => (lsp::DiagnosticSeverity::INFORMATION, format!("The import of \"{from}\" was redirected to \"{to}\"."), Some(json!({ "specifier": from, "redirect": to }))),
|
|
Self::ResolutionError(err) => (
|
|
lsp::DiagnosticSeverity::ERROR,
|
|
enhanced_resolution_error_message(err),
|
|
graph_util::get_resolution_error_bare_node_specifier(err)
|
|
.map(|specifier| json!({ "specifier": specifier }))
|
|
),
|
|
Self::InvalidNodeSpecifier(specifier) => (lsp::DiagnosticSeverity::ERROR, format!("Unknown Node built-in module: {}", specifier.path()), None),
|
|
};
|
|
lsp::Diagnostic {
|
|
range: *range,
|
|
severity: Some(severity),
|
|
code: Some(lsp::NumberOrString::String(self.code().to_string())),
|
|
source: Some(DiagnosticSource::Deno.as_lsp_source().to_string()),
|
|
message,
|
|
data,
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
fn diagnose_resolution(
|
|
snapshot: &language_server::StateSnapshot,
|
|
resolution: &Resolution,
|
|
is_dynamic: bool,
|
|
maybe_assert_type: Option<&str>,
|
|
) -> Vec<DenoDiagnostic> {
|
|
let mut diagnostics = vec![];
|
|
match resolution {
|
|
Resolution::Ok(resolved) => {
|
|
let specifier = &resolved.specifier;
|
|
// If the module is a remote module and has a `X-Deno-Warning` header, we
|
|
// want a warning diagnostic with that message.
|
|
if let Some(metadata) = snapshot.cache_metadata.get(specifier) {
|
|
if let Some(message) =
|
|
metadata.get(&cache::MetadataKey::Warning).cloned()
|
|
{
|
|
diagnostics.push(DenoDiagnostic::DenoWarn(message));
|
|
}
|
|
}
|
|
if let Some(doc) = snapshot.documents.get(specifier) {
|
|
let doc_specifier = doc.specifier();
|
|
// If the module was redirected, we want to issue an informational
|
|
// diagnostic that indicates this. This then allows us to issue a code
|
|
// action to replace the specifier with the final redirected one.
|
|
if doc_specifier != specifier {
|
|
diagnostics.push(DenoDiagnostic::Redirect {
|
|
from: specifier.clone(),
|
|
to: doc_specifier.clone(),
|
|
});
|
|
}
|
|
if doc.media_type() == MediaType::Json {
|
|
match maybe_assert_type {
|
|
// The module has the correct assertion type, no diagnostic
|
|
Some("json") => (),
|
|
// The dynamic import statement is missing an assertion type, which
|
|
// we might not be able to statically detect, therefore we will
|
|
// not provide a potentially incorrect diagnostic.
|
|
None if is_dynamic => (),
|
|
// The module has an incorrect assertion type, diagnostic
|
|
Some(assert_type) => diagnostics
|
|
.push(DenoDiagnostic::InvalidAssertType(assert_type.to_string())),
|
|
// The module is missing an assertion type, diagnostic
|
|
None => diagnostics.push(DenoDiagnostic::NoAssertType),
|
|
}
|
|
}
|
|
} else if let Ok(pkg_ref) =
|
|
NpmPackageReqReference::from_specifier(specifier)
|
|
{
|
|
if let Some(npm_resolver) = &snapshot.maybe_npm_resolver {
|
|
// show diagnostics for npm package references that aren't cached
|
|
let req = pkg_ref.into_inner().req;
|
|
if !npm_resolver.is_pkg_req_folder_cached(&req) {
|
|
diagnostics
|
|
.push(DenoDiagnostic::NoCacheNpm(req, specifier.clone()));
|
|
}
|
|
}
|
|
} else if let Some(module_name) = specifier.as_str().strip_prefix("node:")
|
|
{
|
|
if !deno_node::is_builtin_node_module(module_name) {
|
|
diagnostics
|
|
.push(DenoDiagnostic::InvalidNodeSpecifier(specifier.clone()));
|
|
} else if let Some(npm_resolver) = &snapshot.maybe_npm_resolver {
|
|
// check that a @types/node package exists in the resolver
|
|
let types_node_req = PackageReq::from_str("@types/node").unwrap();
|
|
if !npm_resolver.is_pkg_req_folder_cached(&types_node_req) {
|
|
diagnostics.push(DenoDiagnostic::NoCacheNpm(
|
|
types_node_req,
|
|
ModuleSpecifier::parse("npm:@types/node").unwrap(),
|
|
));
|
|
}
|
|
}
|
|
} else {
|
|
// When the document is not available, it means that it cannot be found
|
|
// in the cache or locally on the disk, so we want to issue a diagnostic
|
|
// about that.
|
|
let deno_diagnostic = match specifier.scheme() {
|
|
"file" => DenoDiagnostic::NoLocal(specifier.clone()),
|
|
_ => DenoDiagnostic::NoCache(specifier.clone()),
|
|
};
|
|
diagnostics.push(deno_diagnostic);
|
|
}
|
|
}
|
|
// The specifier resolution resulted in an error, so we want to issue a
|
|
// diagnostic for that.
|
|
Resolution::Err(err) => {
|
|
diagnostics.push(DenoDiagnostic::ResolutionError(*err.clone()))
|
|
}
|
|
_ => (),
|
|
}
|
|
diagnostics
|
|
}
|
|
|
|
/// Generate diagnostics related to a dependency. The dependency is analyzed to
|
|
/// determine if it can be remapped to the active import map as well as surface
|
|
/// any diagnostics related to the resolved code or type dependency.
|
|
fn diagnose_dependency(
|
|
diagnostics: &mut Vec<lsp::Diagnostic>,
|
|
snapshot: &language_server::StateSnapshot,
|
|
referrer: &ModuleSpecifier,
|
|
dependency_key: &str,
|
|
dependency: &deno_graph::Dependency,
|
|
) {
|
|
if let Some(npm_resolver) = &snapshot.maybe_npm_resolver {
|
|
if npm_resolver.in_npm_package(referrer) {
|
|
return; // ignore, surface typescript errors instead
|
|
}
|
|
}
|
|
|
|
if let Some(import_map) = &snapshot.maybe_import_map {
|
|
if let Resolution::Ok(resolved) = &dependency.maybe_code {
|
|
if let Some(to) = import_map.lookup(&resolved.specifier, referrer) {
|
|
if dependency_key != to {
|
|
diagnostics.push(
|
|
DenoDiagnostic::ImportMapRemap {
|
|
from: dependency_key.to_string(),
|
|
to,
|
|
}
|
|
.to_lsp_diagnostic(&documents::to_lsp_range(&resolved.range)),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let import_ranges: Vec<_> = dependency
|
|
.imports
|
|
.iter()
|
|
.map(|i| documents::to_lsp_range(&i.range))
|
|
.collect();
|
|
|
|
diagnostics.extend(
|
|
diagnose_resolution(
|
|
snapshot,
|
|
if dependency.maybe_code.is_none() {
|
|
&dependency.maybe_type
|
|
} else {
|
|
&dependency.maybe_code
|
|
},
|
|
dependency.is_dynamic,
|
|
dependency.maybe_assert_type.as_deref(),
|
|
)
|
|
.iter()
|
|
.flat_map(|diag| {
|
|
import_ranges
|
|
.iter()
|
|
.map(|range| diag.to_lsp_diagnostic(range))
|
|
}),
|
|
);
|
|
// TODO(nayeemrmn): This is a crude way of detecting `@deno-types` which has
|
|
// a different specifier and therefore needs a separate call to
|
|
// `diagnose_resolution()`. It would be much cleaner if that were modelled as
|
|
// a separate dependency: https://github.com/denoland/deno_graph/issues/247.
|
|
if !dependency.maybe_type.is_none()
|
|
&& !dependency
|
|
.imports
|
|
.iter()
|
|
.any(|i| dependency.maybe_type.includes(&i.range.start).is_some())
|
|
{
|
|
let range = match &dependency.maybe_type {
|
|
Resolution::Ok(resolved) => documents::to_lsp_range(&resolved.range),
|
|
Resolution::Err(error) => documents::to_lsp_range(error.range()),
|
|
Resolution::None => unreachable!(),
|
|
};
|
|
diagnostics.extend(
|
|
diagnose_resolution(
|
|
snapshot,
|
|
&dependency.maybe_type,
|
|
dependency.is_dynamic,
|
|
dependency.maybe_assert_type.as_deref(),
|
|
)
|
|
.iter()
|
|
.map(|diag| diag.to_lsp_diagnostic(&range)),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Generate diagnostics that come from Deno module resolution logic (like
|
|
/// dependencies) or other Deno specific diagnostics, like the ability to use
|
|
/// an import map to shorten an URL.
|
|
fn generate_deno_diagnostics(
|
|
snapshot: &language_server::StateSnapshot,
|
|
config: &ConfigSnapshot,
|
|
token: CancellationToken,
|
|
) -> DiagnosticVec {
|
|
let mut diagnostics_vec = Vec::new();
|
|
|
|
for document in snapshot
|
|
.documents
|
|
.documents(DocumentsFilter::OpenDiagnosable)
|
|
{
|
|
if token.is_cancelled() {
|
|
break;
|
|
}
|
|
let mut diagnostics = Vec::new();
|
|
let specifier = document.specifier();
|
|
if config.specifier_enabled(specifier) {
|
|
for (dependency_key, dependency) in document.dependencies() {
|
|
diagnose_dependency(
|
|
&mut diagnostics,
|
|
snapshot,
|
|
specifier,
|
|
dependency_key,
|
|
dependency,
|
|
);
|
|
}
|
|
}
|
|
diagnostics_vec.push(DiagnosticRecord {
|
|
specifier: specifier.clone(),
|
|
versioned: VersionedDiagnostics {
|
|
version: document.maybe_lsp_version(),
|
|
diagnostics,
|
|
},
|
|
});
|
|
}
|
|
|
|
diagnostics_vec
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::cache::GlobalHttpCache;
|
|
use crate::cache::RealDenoCacheEnv;
|
|
use crate::lsp::config::ConfigSnapshot;
|
|
use crate::lsp::config::Settings;
|
|
use crate::lsp::config::SpecifierSettings;
|
|
use crate::lsp::config::WorkspaceSettings;
|
|
use crate::lsp::documents::Documents;
|
|
use crate::lsp::documents::LanguageId;
|
|
use crate::lsp::language_server::StateSnapshot;
|
|
use pretty_assertions::assert_eq;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use test_util::TempDir;
|
|
|
|
fn mock_state_snapshot(
|
|
fixtures: &[(&str, &str, i32, LanguageId)],
|
|
location: &Path,
|
|
maybe_import_map: Option<(&str, &str)>,
|
|
) -> StateSnapshot {
|
|
let cache = Arc::new(GlobalHttpCache::new(
|
|
location.to_path_buf(),
|
|
RealDenoCacheEnv,
|
|
));
|
|
let mut documents = Documents::new(cache);
|
|
for (specifier, source, version, language_id) in fixtures {
|
|
let specifier =
|
|
resolve_url(specifier).expect("failed to create specifier");
|
|
documents.open(
|
|
specifier.clone(),
|
|
*version,
|
|
*language_id,
|
|
(*source).into(),
|
|
);
|
|
}
|
|
let maybe_import_map = maybe_import_map.map(|(base, json_string)| {
|
|
let base_url = ModuleSpecifier::parse(base).unwrap();
|
|
let result = import_map::parse_from_json(&base_url, json_string).unwrap();
|
|
if !result.diagnostics.is_empty() {
|
|
panic!("unexpected import map diagnostics");
|
|
}
|
|
Arc::new(result.import_map)
|
|
});
|
|
StateSnapshot {
|
|
documents,
|
|
maybe_import_map,
|
|
assets: Default::default(),
|
|
cache_metadata: cache::CacheMetadata::new(Arc::new(
|
|
GlobalHttpCache::new(location.to_path_buf(), RealDenoCacheEnv),
|
|
)),
|
|
maybe_node_resolver: None,
|
|
maybe_npm_resolver: None,
|
|
}
|
|
}
|
|
|
|
fn mock_config() -> ConfigSnapshot {
|
|
ConfigSnapshot {
|
|
settings: Settings {
|
|
workspace: WorkspaceSettings {
|
|
enable: true,
|
|
lint: true,
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn setup(
|
|
temp_dir: &TempDir,
|
|
sources: &[(&str, &str, i32, LanguageId)],
|
|
maybe_import_map: Option<(&str, &str)>,
|
|
) -> (StateSnapshot, PathBuf) {
|
|
let location = temp_dir.path().join("deps").to_path_buf();
|
|
let state_snapshot =
|
|
mock_state_snapshot(sources, &location, maybe_import_map);
|
|
(state_snapshot, location)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_enabled_then_disabled_specifier() {
|
|
let temp_dir = TempDir::new();
|
|
let specifier = ModuleSpecifier::parse("file:///a.ts").unwrap();
|
|
let (snapshot, cache_location) = setup(
|
|
&temp_dir,
|
|
&[(
|
|
"file:///a.ts",
|
|
r#"import * as b from "./b.ts";
|
|
let a: any = "a";
|
|
let c: number = "a";
|
|
"#,
|
|
1,
|
|
LanguageId::TypeScript,
|
|
)],
|
|
None,
|
|
);
|
|
let snapshot = Arc::new(snapshot);
|
|
let cache =
|
|
Arc::new(GlobalHttpCache::new(cache_location, RealDenoCacheEnv));
|
|
let ts_server = TsServer::new(Default::default(), cache);
|
|
|
|
// test enabled
|
|
{
|
|
let enabled_config = mock_config();
|
|
let diagnostics = generate_lint_diagnostics(
|
|
&snapshot,
|
|
&enabled_config,
|
|
&Default::default(),
|
|
Default::default(),
|
|
);
|
|
assert_eq!(get_diagnostics_for_single(diagnostics).len(), 6);
|
|
let diagnostics = generate_ts_diagnostics(
|
|
snapshot.clone(),
|
|
&enabled_config,
|
|
&ts_server,
|
|
Default::default(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(get_diagnostics_for_single(diagnostics).len(), 5);
|
|
let diagnostics = generate_deno_diagnostics(
|
|
&snapshot,
|
|
&enabled_config,
|
|
Default::default(),
|
|
);
|
|
assert_eq!(get_diagnostics_for_single(diagnostics).len(), 1);
|
|
}
|
|
|
|
// now test disabled specifier
|
|
{
|
|
let mut disabled_config = mock_config();
|
|
disabled_config.settings.specifiers.insert(
|
|
specifier.clone(),
|
|
SpecifierSettings {
|
|
enable: false,
|
|
enable_paths: Vec::new(),
|
|
code_lens: Default::default(),
|
|
},
|
|
);
|
|
|
|
let diagnostics = generate_lint_diagnostics(
|
|
&snapshot,
|
|
&disabled_config,
|
|
&Default::default(),
|
|
Default::default(),
|
|
);
|
|
assert_eq!(get_diagnostics_for_single(diagnostics).len(), 0);
|
|
let diagnostics = generate_ts_diagnostics(
|
|
snapshot.clone(),
|
|
&disabled_config,
|
|
&ts_server,
|
|
Default::default(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(get_diagnostics_for_single(diagnostics).len(), 0);
|
|
let diagnostics = generate_deno_diagnostics(
|
|
&snapshot,
|
|
&disabled_config,
|
|
Default::default(),
|
|
);
|
|
assert_eq!(get_diagnostics_for_single(diagnostics).len(), 0);
|
|
}
|
|
}
|
|
|
|
fn get_diagnostics_for_single(
|
|
diagnostic_vec: DiagnosticVec,
|
|
) -> Vec<lsp::Diagnostic> {
|
|
assert_eq!(diagnostic_vec.len(), 1);
|
|
diagnostic_vec
|
|
.into_iter()
|
|
.next()
|
|
.unwrap()
|
|
.versioned
|
|
.diagnostics
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_cancelled_ts_diagnostics_request() {
|
|
let temp_dir = TempDir::new();
|
|
let (snapshot, cache_location) = setup(
|
|
&temp_dir,
|
|
&[(
|
|
"file:///a.ts",
|
|
r#"export let a: string = 5;"#,
|
|
1,
|
|
LanguageId::TypeScript,
|
|
)],
|
|
None,
|
|
);
|
|
let snapshot = Arc::new(snapshot);
|
|
let cache =
|
|
Arc::new(GlobalHttpCache::new(cache_location, RealDenoCacheEnv));
|
|
let ts_server = TsServer::new(Default::default(), cache);
|
|
|
|
let config = mock_config();
|
|
let token = CancellationToken::new();
|
|
token.cancel();
|
|
let diagnostics =
|
|
generate_ts_diagnostics(snapshot.clone(), &config, &ts_server, token)
|
|
.await
|
|
.unwrap();
|
|
// should be none because it's cancelled
|
|
assert_eq!(diagnostics.len(), 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_deno_diagnostics_with_import_map() {
|
|
let temp_dir = TempDir::new();
|
|
let (snapshot, _) = setup(
|
|
&temp_dir,
|
|
&[
|
|
("file:///std/testing/asserts.ts", "export function assert() {}", 1, LanguageId::TypeScript),
|
|
("file:///a/file.ts", "import { assert } from \"../std/testing/asserts.ts\";\n\nassert();\n", 1, LanguageId::TypeScript),
|
|
],
|
|
Some(("file:///a/import-map.json", r#"{
|
|
"imports": {
|
|
"/~/std/": "../std/"
|
|
}
|
|
}"#)),
|
|
);
|
|
let config = mock_config();
|
|
let token = CancellationToken::new();
|
|
let actual = generate_deno_diagnostics(&snapshot, &config, token);
|
|
assert_eq!(actual.len(), 2);
|
|
for record in actual {
|
|
match record.specifier.as_str() {
|
|
"file:///std/testing/asserts.ts" => {
|
|
assert_eq!(json!(record.versioned.diagnostics), json!([]))
|
|
}
|
|
"file:///a/file.ts" => assert_eq!(
|
|
json!(record.versioned.diagnostics),
|
|
json!([
|
|
{
|
|
"range": {
|
|
"start": {
|
|
"line": 0,
|
|
"character": 23
|
|
},
|
|
"end": {
|
|
"line": 0,
|
|
"character": 50
|
|
}
|
|
},
|
|
"severity": 4,
|
|
"code": "import-map-remap",
|
|
"source": "deno",
|
|
"message": "The import specifier can be remapped to \"/~/std/testing/asserts.ts\" which will resolve it via the active import map.",
|
|
"data": {
|
|
"from": "../std/testing/asserts.ts",
|
|
"to": "/~/std/testing/asserts.ts"
|
|
}
|
|
}
|
|
])
|
|
),
|
|
_ => unreachable!("unexpected specifier {}", record.specifier),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_get_code_action_import_map_remap() {
|
|
let specifier = ModuleSpecifier::parse("file:///a/file.ts").unwrap();
|
|
let result = DenoDiagnostic::get_code_action(&specifier, &lsp::Diagnostic {
|
|
range: lsp::Range {
|
|
start: lsp::Position { line: 0, character: 23 },
|
|
end: lsp::Position { line: 0, character: 50 },
|
|
},
|
|
severity: Some(lsp::DiagnosticSeverity::HINT),
|
|
code: Some(lsp::NumberOrString::String("import-map-remap".to_string())),
|
|
source: Some("deno".to_string()),
|
|
message: "The import specifier can be remapped to \"/~/std/testing/asserts.ts\" which will resolve it via the active import map.".to_string(),
|
|
data: Some(json!({
|
|
"from": "../std/testing/asserts.ts",
|
|
"to": "/~/std/testing/asserts.ts"
|
|
})),
|
|
..Default::default()
|
|
});
|
|
assert!(result.is_ok());
|
|
let actual = result.unwrap();
|
|
assert_eq!(
|
|
json!(actual),
|
|
json!({
|
|
"title": "Update \"../std/testing/asserts.ts\" to \"/~/std/testing/asserts.ts\" to use import map.",
|
|
"kind": "quickfix",
|
|
"diagnostics": [
|
|
{
|
|
"range": {
|
|
"start": {
|
|
"line": 0,
|
|
"character": 23
|
|
},
|
|
"end": {
|
|
"line": 0,
|
|
"character": 50
|
|
}
|
|
},
|
|
"severity": 4,
|
|
"code": "import-map-remap",
|
|
"source": "deno",
|
|
"message": "The import specifier can be remapped to \"/~/std/testing/asserts.ts\" which will resolve it via the active import map.",
|
|
"data": {
|
|
"from": "../std/testing/asserts.ts",
|
|
"to": "/~/std/testing/asserts.ts"
|
|
}
|
|
}
|
|
],
|
|
"edit": {
|
|
"changes": {
|
|
"file:///a/file.ts": [
|
|
{
|
|
"range": {
|
|
"start": {
|
|
"line": 0,
|
|
"character": 23
|
|
},
|
|
"end": {
|
|
"line": 0,
|
|
"character": 50
|
|
}
|
|
},
|
|
"newText": "\"/~/std/testing/asserts.ts\""
|
|
}
|
|
]
|
|
}
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn duplicate_diagnostics_for_duplicate_imports() {
|
|
let temp_dir = TempDir::new();
|
|
let (snapshot, _) = setup(
|
|
&temp_dir,
|
|
&[(
|
|
"file:///a.ts",
|
|
r#"
|
|
// @deno-types="bad.d.ts"
|
|
import "bad.js";
|
|
import "bad.js";
|
|
"#,
|
|
1,
|
|
LanguageId::TypeScript,
|
|
)],
|
|
None,
|
|
);
|
|
let config = mock_config();
|
|
let token = CancellationToken::new();
|
|
let actual = generate_deno_diagnostics(&snapshot, &config, token);
|
|
assert_eq!(actual.len(), 1);
|
|
let record = actual.first().unwrap();
|
|
assert_eq!(
|
|
json!(record.versioned.diagnostics),
|
|
json!([
|
|
{
|
|
"range": {
|
|
"start": {
|
|
"line": 2,
|
|
"character": 15
|
|
},
|
|
"end": {
|
|
"line": 2,
|
|
"character": 23
|
|
}
|
|
},
|
|
"severity": 1,
|
|
"code": "import-prefix-missing",
|
|
"source": "deno",
|
|
"message": "Relative import path \"bad.js\" not prefixed with / or ./ or ../",
|
|
},
|
|
{
|
|
"range": {
|
|
"start": {
|
|
"line": 3,
|
|
"character": 15
|
|
},
|
|
"end": {
|
|
"line": 3,
|
|
"character": 23
|
|
}
|
|
},
|
|
"severity": 1,
|
|
"code": "import-prefix-missing",
|
|
"source": "deno",
|
|
"message": "Relative import path \"bad.js\" not prefixed with / or ./ or ../",
|
|
},
|
|
{
|
|
"range": {
|
|
"start": {
|
|
"line": 1,
|
|
"character": 23
|
|
},
|
|
"end": {
|
|
"line": 1,
|
|
"character": 33
|
|
}
|
|
},
|
|
"severity": 1,
|
|
"code": "import-prefix-missing",
|
|
"source": "deno",
|
|
"message": "Relative import path \"bad.d.ts\" not prefixed with / or ./ or ../",
|
|
},
|
|
])
|
|
);
|
|
}
|
|
}
|