2023-01-02 16:00:42 -05:00
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
2020-12-07 05:46:39 -05:00
2021-05-11 10:43:00 -04:00
use super ::analysis ;
2022-02-01 21:04:26 -05:00
use super ::cache ;
2021-12-15 13:23:43 -05:00
use super ::client ::Client ;
2022-01-19 17:10:14 -05:00
use super ::config ::ConfigSnapshot ;
2021-10-28 19:56:01 -04:00
use super ::documents ;
2022-01-29 17:50:15 -05:00
use super ::documents ::Document ;
2023-03-29 16:25:48 -04:00
use super ::documents ::DocumentsFilter ;
2021-03-09 21:41:35 -05:00
use super ::language_server ;
2022-02-02 18:02:59 -05:00
use super ::language_server ::StateSnapshot ;
2022-01-17 17:09:43 -05:00
use super ::performance ::Performance ;
2020-12-07 05:46:39 -05:00
use super ::tsc ;
2022-01-17 17:09:43 -05:00
use super ::tsc ::TsServer ;
2020-12-07 05:46:39 -05:00
2023-01-07 15:22:09 -05:00
use crate ::args ::LintOptions ;
2023-01-27 17:36:23 -05:00
use crate ::graph_util ;
2023-01-24 15:14:49 -05:00
use crate ::graph_util ::enhanced_resolution_error_message ;
2023-05-26 02:10:18 -04:00
use crate ::lsp ::lsp_custom ::DiagnosticBatchNotificationParams ;
2023-01-07 15:22:09 -05:00
use crate ::tools ::lint ::get_configured_rules ;
2020-12-07 05:46:39 -05:00
2021-12-15 22:53:17 -05:00
use deno_ast ::MediaType ;
2021-11-16 09:02:28 -05:00
use deno_core ::anyhow ::anyhow ;
2020-12-07 05:46:39 -05:00
use deno_core ::error ::AnyError ;
2021-05-11 10:43:00 -04:00
use deno_core ::resolve_url ;
2022-02-04 02:14:57 -05:00
use deno_core ::serde ::Deserialize ;
use deno_core ::serde_json ;
2021-05-19 08:28:23 -04:00
use deno_core ::serde_json ::json ;
2023-05-14 17:40:01 -04:00
use deno_core ::task ::spawn ;
use deno_core ::task ::JoinHandle ;
2021-01-22 05:03:16 -05:00
use deno_core ::ModuleSpecifier ;
2023-02-09 22:00:23 -05:00
use deno_graph ::Resolution ;
2023-01-24 15:14:49 -05:00
use deno_graph ::ResolutionError ;
use deno_graph ::SpecifierError ;
2023-01-07 15:22:09 -05:00
use deno_lint ::rules ::LintRule ;
2023-04-21 21:02:46 -04:00
use deno_runtime ::deno_node ;
2021-10-21 07:05:43 -04:00
use deno_runtime ::tokio_util ::create_basic_runtime ;
2023-04-06 18:46:44 -04:00
use deno_semver ::npm ::NpmPackageReqReference ;
2021-03-26 12:34:25 -04:00
use log ::error ;
2020-12-07 05:46:39 -05:00
use std ::collections ::HashMap ;
2023-05-26 02:10:18 -04:00
use std ::sync ::atomic ::AtomicUsize ;
2021-03-09 21:41:35 -05:00
use std ::sync ::Arc ;
use std ::thread ;
use tokio ::sync ::mpsc ;
2021-05-11 10:43:00 -04:00
use tokio ::sync ::Mutex ;
2021-03-18 16:26:41 -04:00
use tokio ::time ::Duration ;
2022-01-24 15:30:01 -05:00
use tokio_util ::sync ::CancellationToken ;
2022-04-03 00:17:30 -04:00
use tower_lsp ::lsp_types as lsp ;
2020-12-07 05:46:39 -05:00
2023-05-26 02:10:18 -04:00
#[ derive(Debug) ]
pub struct DiagnosticServerUpdateMessage {
pub snapshot : Arc < StateSnapshot > ,
pub config : Arc < ConfigSnapshot > ,
pub lint_options : LintOptions ,
}
2021-05-11 10:43:00 -04:00
pub type DiagnosticRecord =
( ModuleSpecifier , Option < i32 > , Vec < lsp ::Diagnostic > ) ;
pub type DiagnosticVec = Vec < DiagnosticRecord > ;
2022-01-24 15:30:01 -05:00
type DiagnosticMap =
HashMap < ModuleSpecifier , ( Option < i32 > , Vec < lsp ::Diagnostic > ) > ;
2022-01-24 18:04:24 -05:00
type DiagnosticsByVersionMap = HashMap < Option < i32 > , Vec < lsp ::Diagnostic > > ;
#[ derive(Clone) ]
struct DiagnosticsPublisher {
client : Client ,
all_diagnostics :
Arc < Mutex < HashMap < ModuleSpecifier , DiagnosticsByVersionMap > > > ,
}
impl DiagnosticsPublisher {
pub fn new ( client : Client ) -> Self {
Self {
client ,
all_diagnostics : Default ::default ( ) ,
}
}
pub async fn publish (
& self ,
diagnostics : DiagnosticVec ,
token : & CancellationToken ,
) {
let mut all_diagnostics = self . all_diagnostics . lock ( ) . await ;
for ( specifier , version , diagnostics ) in diagnostics {
if token . is_cancelled ( ) {
return ;
}
// the versions of all the published diagnostics should be the same, but just
// in case they're not keep track of that
let diagnostics_by_version =
all_diagnostics . entry ( specifier . clone ( ) ) . or_default ( ) ;
2022-02-23 16:01:20 -05:00
let version_diagnostics =
2022-01-24 18:04:24 -05:00
diagnostics_by_version . entry ( version ) . or_default ( ) ;
version_diagnostics . extend ( diagnostics ) ;
self
. client
2023-03-15 10:34:23 -04:00
. when_outside_lsp_lock ( )
2022-01-24 18:04:24 -05:00
. publish_diagnostics ( specifier , version_diagnostics . clone ( ) , version )
. await ;
}
}
pub async fn clear ( & self ) {
let mut all_diagnostics = self . all_diagnostics . lock ( ) . await ;
all_diagnostics . clear ( ) ;
}
}
2021-05-11 10:43:00 -04:00
2022-02-02 18:02:59 -05:00
#[ 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 ( ( diagnostics_doc_version , diagnostics ) ) =
ts_diagnostics . get ( specifier )
{
// only get the diagnostics if they're up to date
if document_version = = * diagnostics_doc_version {
return 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 ( | ( specifier , version , diagnostics ) | {
( specifier . clone ( ) , ( * version , diagnostics . clone ( ) ) )
} )
. collect ( ) ;
}
}
2023-05-26 02:10:18 -04:00
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) ]
struct ChannelMessage {
message : DiagnosticServerUpdateMessage ,
batch_index : Option < usize > ,
}
2022-01-17 17:09:43 -05:00
#[ derive(Debug) ]
2022-03-23 09:54:22 -04:00
pub struct DiagnosticsServer {
2023-05-26 02:10:18 -04:00
channel : Option < mpsc ::UnboundedSender < ChannelMessage > > ,
2022-02-02 18:02:59 -05:00
ts_diagnostics : TsDiagnosticsStore ,
2022-01-17 17:09:43 -05:00
client : Client ,
performance : Arc < Performance > ,
ts_server : Arc < TsServer > ,
2023-05-26 02:10:18 -04:00
batch_counter : DiagnosticBatchCounter ,
2021-05-11 10:43:00 -04:00
}
2021-03-09 21:41:35 -05:00
impl DiagnosticsServer {
2022-01-17 17:09:43 -05:00
pub fn new (
client : Client ,
performance : Arc < Performance > ,
ts_server : Arc < TsServer > ,
) -> Self {
DiagnosticsServer {
channel : Default ::default ( ) ,
2022-01-24 15:30:01 -05:00
ts_diagnostics : Default ::default ( ) ,
2022-01-17 17:09:43 -05:00
client ,
performance ,
ts_server ,
2023-05-26 02:10:18 -04:00
batch_counter : Default ::default ( ) ,
2022-01-17 17:09:43 -05:00
}
}
2022-03-23 09:54:22 -04:00
pub fn get_ts_diagnostics (
2021-05-11 10:43:00 -04:00
& self ,
specifier : & ModuleSpecifier ,
2022-01-24 15:30:01 -05:00
document_version : Option < i32 > ,
2021-05-11 10:43:00 -04:00
) -> Vec < lsp ::Diagnostic > {
2022-02-02 18:02:59 -05:00
self . ts_diagnostics . get ( specifier , document_version )
2021-05-11 10:43:00 -04:00
}
2022-03-23 09:54:22 -04:00
pub fn invalidate ( & self , specifiers : & [ ModuleSpecifier ] ) {
2022-02-02 18:02:59 -05:00
self . ts_diagnostics . invalidate ( specifiers ) ;
2021-03-09 21:41:35 -05:00
}
2022-03-23 09:54:22 -04:00
pub fn invalidate_all ( & self ) {
2022-02-02 18:02:59 -05:00
self . ts_diagnostics . invalidate_all ( ) ;
2021-07-25 01:33:42 -04:00
}
2022-02-23 16:01:20 -05:00
#[ allow(unused_must_use) ]
2022-03-23 09:54:22 -04:00
pub fn start ( & mut self ) {
2023-05-26 02:10:18 -04:00
let ( tx , mut rx ) = mpsc ::unbounded_channel ::< ChannelMessage > ( ) ;
2021-05-11 10:43:00 -04:00
self . channel = Some ( tx ) ;
2022-01-17 17:09:43 -05:00
let client = self . client . clone ( ) ;
let performance = self . performance . clone ( ) ;
2022-02-02 18:02:59 -05:00
let ts_diagnostics_store = self . ts_diagnostics . clone ( ) ;
2022-01-17 17:09:43 -05:00
let ts_server = self . ts_server . clone ( ) ;
2021-03-09 21:41:35 -05:00
let _join_handle = thread ::spawn ( move | | {
let runtime = create_basic_runtime ( ) ;
runtime . block_on ( async {
2022-01-24 15:30:01 -05:00
let mut token = CancellationToken ::new ( ) ;
2023-05-14 17:40:01 -04:00
let mut ts_handle : Option < JoinHandle < ( ) > > = None ;
let mut lint_handle : Option < JoinHandle < ( ) > > = None ;
let mut deps_handle : Option < JoinHandle < ( ) > > = None ;
2022-01-24 18:04:24 -05:00
let diagnostics_publisher = DiagnosticsPublisher ::new ( client . clone ( ) ) ;
2021-03-09 21:41:35 -05:00
2021-03-18 16:26:41 -04:00
loop {
2022-01-24 15:30:01 -05:00
match rx . recv ( ) . await {
// channel has closed
None = > break ,
2023-05-26 02:10:18 -04:00
Some ( message ) = > {
let ChannelMessage {
message :
DiagnosticServerUpdateMessage {
snapshot ,
config ,
lint_options ,
} ,
batch_index ,
} = message ;
2022-01-24 15:30:01 -05:00
// cancel the previous run
token . cancel ( ) ;
token = CancellationToken ::new ( ) ;
2022-01-24 18:04:24 -05:00
diagnostics_publisher . clear ( ) . await ;
2021-03-18 16:26:41 -04:00
2022-01-24 15:30:01 -05:00
let previous_ts_handle = ts_handle . take ( ) ;
2023-05-14 17:40:01 -04:00
ts_handle = Some ( spawn ( {
2022-01-24 15:30:01 -05:00
let performance = performance . clone ( ) ;
2022-01-24 18:04:24 -05:00
let diagnostics_publisher = diagnostics_publisher . clone ( ) ;
2022-01-24 15:30:01 -05:00
let ts_server = ts_server . clone ( ) ;
let token = token . clone ( ) ;
2022-02-02 18:02:59 -05:00
let ts_diagnostics_store = ts_diagnostics_store . clone ( ) ;
2022-01-24 15:30:01 -05:00
let snapshot = snapshot . clone ( ) ;
let config = config . 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 ::< ( ) > ) ;
2022-01-29 17:50:15 -05:00
let diagnostics = generate_ts_diagnostics (
snapshot . clone ( ) ,
& config ,
& ts_server ,
2022-02-02 09:25:22 -05:00
token . clone ( ) ,
2022-01-29 17:50:15 -05:00
)
. await
. map_err ( | err | {
error! ( " Error generating TypeScript diagnostics: {} " , err ) ;
} )
. unwrap_or_default ( ) ;
2022-01-24 15:30:01 -05:00
2023-05-26 02:10:18 -04:00
let messages_len = diagnostics . len ( ) ;
2022-01-24 15:30:01 -05:00
if ! token . is_cancelled ( ) {
2022-02-02 18:02:59 -05:00
ts_diagnostics_store . update ( & diagnostics ) ;
2022-01-24 18:04:24 -05:00
diagnostics_publisher . publish ( diagnostics , & token ) . await ;
if ! token . is_cancelled ( ) {
performance . measure ( mark ) ;
2022-01-24 15:30:01 -05:00
}
}
2023-05-26 02:10:18 -04:00
if let Some ( batch_index ) = batch_index {
diagnostics_publisher
. client
. send_diagnostic_batch_notification (
DiagnosticBatchNotificationParams {
batch_index ,
messages_len ,
} ,
) ;
}
2022-01-24 15:30:01 -05:00
}
} ) ) ;
let previous_deps_handle = deps_handle . take ( ) ;
2023-05-14 17:40:01 -04:00
deps_handle = Some ( spawn ( {
2022-01-24 15:30:01 -05:00
let performance = performance . clone ( ) ;
2022-01-24 18:04:24 -05:00
let diagnostics_publisher = diagnostics_publisher . clone ( ) ;
2022-01-24 15:30:01 -05:00
let token = token . clone ( ) ;
let snapshot = snapshot . clone ( ) ;
let config = config . clone ( ) ;
async move {
if let Some ( previous_handle ) = previous_deps_handle {
previous_handle . await ;
}
let mark =
performance . mark ( " update_diagnostics_deps " , None ::< ( ) > ) ;
2022-07-13 21:12:18 -04:00
let diagnostics = generate_deno_diagnostics (
2022-01-29 17:50:15 -05:00
& snapshot ,
& config ,
2022-01-24 15:30:01 -05:00
token . clone ( ) ,
)
. await ;
2023-05-26 02:10:18 -04:00
let messages_len = diagnostics . len ( ) ;
2023-05-26 00:31:54 -04:00
if ! token . is_cancelled ( ) {
2023-05-26 02:10:18 -04:00
diagnostics_publisher . publish ( diagnostics , & 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 ,
} ,
) ;
2022-01-24 15:30:01 -05:00
}
}
} ) ) ;
let previous_lint_handle = lint_handle . take ( ) ;
2023-05-14 17:40:01 -04:00
lint_handle = Some ( spawn ( {
2022-01-24 15:30:01 -05:00
let performance = performance . clone ( ) ;
2022-01-24 18:04:24 -05:00
let diagnostics_publisher = diagnostics_publisher . clone ( ) ;
2022-01-24 15:30:01 -05:00
let token = token . clone ( ) ;
let snapshot = snapshot . clone ( ) ;
let config = config . clone ( ) ;
async move {
if let Some ( previous_handle ) = previous_lint_handle {
previous_handle . await ;
}
let mark =
performance . mark ( " update_diagnostics_lint " , None ::< ( ) > ) ;
let diagnostics = generate_lint_diagnostics (
& snapshot ,
& config ,
2023-01-07 15:22:09 -05:00
& lint_options ,
2022-01-24 15:30:01 -05:00
token . clone ( ) ,
)
. await ;
2023-05-26 02:10:18 -04:00
let messages_len = diagnostics . len ( ) ;
2023-05-26 00:31:54 -04:00
if ! token . is_cancelled ( ) {
2023-05-26 02:10:18 -04:00
diagnostics_publisher . publish ( diagnostics , & 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 ,
} ,
) ;
2022-01-24 15:30:01 -05:00
}
}
} ) ) ;
2021-03-09 21:41:35 -05:00
}
}
}
} )
} ) ;
}
2023-05-26 02:10:18 -04:00
pub fn latest_batch_index ( & self ) -> Option < usize > {
self . batch_counter . get ( )
}
2022-03-23 09:54:22 -04:00
pub fn update (
2022-02-02 18:02:59 -05:00
& self ,
2023-05-26 02:10:18 -04:00
message : DiagnosticServerUpdateMessage ,
2022-02-02 18:02:59 -05:00
) -> 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)
2021-05-11 10:43:00 -04:00
if let Some ( tx ) = & self . channel {
2023-05-26 02:10:18 -04:00
tx . send ( ChannelMessage {
message ,
batch_index : self . batch_counter . inc ( ) ,
} )
. map_err ( | err | err . into ( ) )
2021-05-07 07:05:32 -04:00
} else {
2021-05-11 10:43:00 -04:00
Err ( anyhow! ( " diagnostics server not started " ) )
2021-05-07 07:05:32 -04:00
}
2020-12-07 05:46:39 -05:00
}
2020-12-21 08:44:26 -05:00
}
2022-11-25 18:29:48 -05:00
impl < ' a > From < & ' a crate ::tsc ::DiagnosticCategory > for lsp ::DiagnosticSeverity {
fn from ( category : & ' a crate ::tsc ::DiagnosticCategory ) -> Self {
2020-12-21 08:44:26 -05:00
match category {
2022-11-25 18:29:48 -05:00
crate ::tsc ::DiagnosticCategory ::Error = > lsp ::DiagnosticSeverity ::ERROR ,
crate ::tsc ::DiagnosticCategory ::Warning = > {
2021-11-24 20:10:12 -05:00
lsp ::DiagnosticSeverity ::WARNING
2020-12-21 08:44:26 -05:00
}
2022-11-25 18:29:48 -05:00
crate ::tsc ::DiagnosticCategory ::Suggestion = > {
2021-11-24 20:10:12 -05:00
lsp ::DiagnosticSeverity ::HINT
2020-12-21 08:44:26 -05:00
}
2022-11-25 18:29:48 -05:00
crate ::tsc ::DiagnosticCategory ::Message = > {
2021-11-24 20:10:12 -05:00
lsp ::DiagnosticSeverity ::INFORMATION
2020-12-21 08:44:26 -05:00
}
}
}
}
2022-11-25 18:29:48 -05:00
impl < ' a > From < & ' a crate ::tsc ::Position > for lsp ::Position {
fn from ( pos : & ' a crate ::tsc ::Position ) -> Self {
2020-12-21 08:44:26 -05:00
Self {
line : pos . line as u32 ,
character : pos . character as u32 ,
}
2020-12-07 05:46:39 -05:00
}
2020-12-21 08:44:26 -05:00
}
2020-12-07 05:46:39 -05:00
2022-11-25 18:29:48 -05:00
fn get_diagnostic_message ( diagnostic : & crate ::tsc ::Diagnostic ) -> String {
2020-12-07 05:46:39 -05:00
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 ( )
}
}
2021-05-11 10:43:00 -04:00
fn to_lsp_range (
2022-11-25 18:29:48 -05:00
start : & crate ::tsc ::Position ,
end : & crate ::tsc ::Position ,
2021-05-11 10:43:00 -04:00
) -> lsp ::Range {
lsp ::Range {
start : start . into ( ) ,
end : end . into ( ) ,
}
}
2020-12-07 05:46:39 -05:00
fn to_lsp_related_information (
2022-11-25 18:29:48 -05:00
related_information : & Option < Vec < crate ::tsc ::Diagnostic > > ,
2021-01-29 14:34:33 -05:00
) -> Option < Vec < lsp ::DiagnosticRelatedInformation > > {
2021-03-25 14:17:37 -04:00
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 )
{
2021-07-30 09:03:41 -04:00
let uri = lsp ::Url ::parse ( source ) . unwrap ( ) ;
2021-03-25 14:17:37 -04:00
Some ( lsp ::DiagnosticRelatedInformation {
location : lsp ::Location {
uri ,
range : to_lsp_range ( start , end ) ,
} ,
2021-07-30 09:03:41 -04:00
message : get_diagnostic_message ( ri ) ,
2021-03-25 14:17:37 -04:00
} )
} else {
None
}
} )
. collect ( )
} )
2020-12-07 05:46:39 -05:00
}
fn ts_json_to_diagnostics (
2022-11-25 18:29:48 -05:00
diagnostics : Vec < crate ::tsc ::Diagnostic > ,
2021-01-29 14:34:33 -05:00
) -> Vec < lsp ::Diagnostic > {
2021-01-22 05:03:16 -05:00
diagnostics
. iter ( )
. filter_map ( | d | {
if let ( Some ( start ) , Some ( end ) ) = ( & d . start , & d . end ) {
2021-01-29 14:34:33 -05:00
Some ( lsp ::Diagnostic {
2021-01-22 05:03:16 -05:00
range : to_lsp_range ( start , end ) ,
severity : Some ( ( & d . category ) . into ( ) ) ,
2021-01-29 14:34:33 -05:00
code : Some ( lsp ::NumberOrString ::Number ( d . code as i32 ) ) ,
2021-01-22 05:03:16 -05:00
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.
2021-11-17 21:05:20 -05:00
2695 | 6133 | 6138 | 6192 | 6196 | 6198 | 6199 | 6205 | 7027
2021-11-24 20:10:12 -05:00
| 7028 = > Some ( vec! [ lsp ::DiagnosticTag ::UNNECESSARY ] ) ,
2021-11-17 21:05:20 -05:00
// These are codes that indicated the variable is deprecated.
2021-11-24 20:10:12 -05:00
2789 | 6385 | 6387 = > Some ( vec! [ lsp ::DiagnosticTag ::DEPRECATED ] ) ,
2021-01-22 05:03:16 -05:00
_ = > None ,
} ,
data : None ,
} )
} else {
None
}
} )
. collect ( )
2020-12-07 05:46:39 -05:00
}
2021-05-11 10:43:00 -04:00
async fn generate_lint_diagnostics (
snapshot : & language_server ::StateSnapshot ,
2022-01-19 17:10:14 -05:00
config : & ConfigSnapshot ,
2023-01-07 15:22:09 -05:00
lint_options : & LintOptions ,
2022-01-24 15:30:01 -05:00
token : CancellationToken ,
) -> DiagnosticVec {
2023-03-29 16:25:48 -04:00
let documents = snapshot
. documents
. documents ( DocumentsFilter ::OpenDiagnosable ) ;
2022-01-19 17:10:14 -05:00
let workspace_settings = config . settings . workspace . clone ( ) ;
2023-01-07 15:22:09 -05:00
let lint_rules = get_configured_rules ( lint_options . rules . clone ( ) ) ;
2022-01-24 15:30:01 -05:00
let mut diagnostics_vec = Vec ::new ( ) ;
if workspace_settings . lint {
for document in documents {
// exit early if cancelled
if token . is_cancelled ( ) {
break ;
}
2022-10-21 11:20:18 -04:00
// ignore any npm package files
2023-04-21 21:02:46 -04:00
if let Some ( node_resolver ) = & snapshot . maybe_node_resolver {
if node_resolver . in_npm_package ( document . specifier ( ) ) {
2022-10-21 11:20:18 -04:00
continue ;
}
}
2022-01-24 15:30:01 -05:00
let version = document . maybe_lsp_version ( ) ;
diagnostics_vec . push ( (
document . specifier ( ) . clone ( ) ,
version ,
2022-01-29 17:50:15 -05:00
generate_document_lint_diagnostics (
config ,
2023-01-07 15:22:09 -05:00
lint_options ,
lint_rules . clone ( ) ,
2022-01-29 17:50:15 -05:00
& document ,
) ,
2022-01-24 15:30:01 -05:00
) ) ;
2021-01-22 05:03:16 -05:00
}
2022-01-24 15:30:01 -05:00
}
diagnostics_vec
2021-05-11 10:43:00 -04:00
}
2022-01-29 17:50:15 -05:00
fn generate_document_lint_diagnostics (
config : & ConfigSnapshot ,
2023-01-07 15:22:09 -05:00
lint_options : & LintOptions ,
2023-03-02 16:50:17 -05:00
lint_rules : Vec < & 'static dyn LintRule > ,
2022-01-29 17:50:15 -05:00
document : & Document ,
) -> Vec < lsp ::Diagnostic > {
if ! config . specifier_enabled ( document . specifier ( ) ) {
return Vec ::new ( ) ;
}
2023-01-07 15:22:09 -05:00
if ! lint_options . files . matches_specifier ( document . specifier ( ) ) {
return Vec ::new ( ) ;
2022-01-29 17:50:15 -05:00
}
match document . maybe_parsed_source ( ) {
Some ( Ok ( parsed_source ) ) = > {
2023-01-07 15:22:09 -05:00
if let Ok ( references ) =
analysis ::get_lint_references ( & parsed_source , lint_rules )
{
2022-01-29 17:50:15 -05:00
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 ( )
}
}
}
2021-05-11 10:43:00 -04:00
async fn generate_ts_diagnostics (
2021-11-18 13:50:24 -05:00
snapshot : Arc < language_server ::StateSnapshot > ,
2022-01-29 17:50:15 -05:00
config : & ConfigSnapshot ,
2021-05-11 10:43:00 -04:00
ts_server : & tsc ::TsServer ,
2022-02-02 09:25:22 -05:00
token : CancellationToken ,
2021-05-11 10:43:00 -04:00
) -> Result < DiagnosticVec , AnyError > {
let mut diagnostics_vec = Vec ::new ( ) ;
2022-01-24 15:30:01 -05:00
let specifiers = snapshot
. documents
2023-03-29 16:25:48 -04:00
. documents ( DocumentsFilter ::OpenDiagnosable )
2023-01-16 15:27:41 -05:00
. into_iter ( )
. map ( | d | d . specifier ( ) . clone ( ) ) ;
2022-01-29 17:50:15 -05:00
let ( enabled_specifiers , disabled_specifiers ) = specifiers
2023-01-16 15:27:41 -05:00
. into_iter ( )
2022-01-29 17:50:15 -05:00
. partition ::< Vec < _ > , _ > ( | s | config . specifier_enabled ( s ) ) ;
2023-05-12 19:07:40 -04:00
let ts_diagnostics_map = if ! enabled_specifiers . is_empty ( ) {
2022-02-02 09:25:22 -05:00
ts_server
2023-05-12 19:07:40 -04:00
. get_diagnostics ( snapshot . clone ( ) , enabled_specifiers , token )
2022-02-02 09:25:22 -05:00
. await ?
2022-01-29 17:50:15 -05:00
} 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 )
2022-02-24 20:03:12 -05:00
. and_then ( | d | d . maybe_lsp_version ( ) ) ;
2022-01-29 17:50:15 -05:00
// 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 ( ( specifier , version , 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 )
2022-02-24 20:03:12 -05:00
. and_then ( | d | d . maybe_lsp_version ( ) ) ;
2022-01-29 17:50:15 -05:00
diagnostics_vec . push ( ( specifier , version , Vec ::new ( ) ) ) ;
2020-12-07 05:46:39 -05:00
}
2021-05-11 10:43:00 -04:00
Ok ( diagnostics_vec )
2020-12-07 05:46:39 -05:00
}
2020-12-24 05:53:03 -05:00
2022-02-04 02:14:57 -05:00
#[ derive(Debug, Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
struct DiagnosticDataSpecifier {
pub specifier : ModuleSpecifier ,
}
2023-01-24 15:14:49 -05:00
#[ derive(Debug, Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
struct DiagnosticDataStrSpecifier {
pub specifier : String ,
}
2022-02-04 02:14:57 -05:00
#[ derive(Debug, Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
struct DiagnosticDataRedirect {
pub redirect : ModuleSpecifier ,
}
2021-10-28 19:56:01 -04:00
2022-07-13 21:12:18 -04:00
#[ derive(Debug, Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
struct DiagnosticDataImportMapRemap {
pub from : String ,
pub to : String ,
}
2022-02-04 02:14:57 -05:00
/// An enum which represents diagnostic errors which originate from Deno itself.
2022-03-23 09:54:22 -04:00
pub enum DenoDiagnostic {
2022-06-17 11:41:28 -04:00
/// A `x-deno-warning` is associated with the specifier and should be displayed
2022-02-04 02:14:57 -05:00
/// as a warning to the user.
DenoWarn ( String ) ,
2022-07-13 21:12:18 -04:00
/// An informational diagnostic that indicates an existing specifier can be
/// remapped to an import map import specifier.
ImportMapRemap { from : String , to : String } ,
2022-02-04 02:14:57 -05:00
/// 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 blob module was not found in the cache.
NoCacheBlob ,
/// A data module was not found in the cache.
NoCacheData ( ModuleSpecifier ) ,
2022-10-21 11:20:18 -04:00
/// A remote npm package reference was not found in the cache.
2023-02-21 12:03:48 -05:00
NoCacheNpm ( NpmPackageReqReference , ModuleSpecifier ) ,
2022-02-04 02:14:57 -05:00
/// 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 ) ,
2023-01-24 09:05:54 -05:00
/// Invalid `node:` specifier.
InvalidNodeSpecifier ( ModuleSpecifier ) ,
2022-02-04 02:14:57 -05:00
}
impl DenoDiagnostic {
fn code ( & self ) -> & str {
match self {
Self ::DenoWarn ( _ ) = > " deno-warn " ,
2022-07-13 21:12:18 -04:00
Self ::ImportMapRemap { .. } = > " import-map-remap " ,
2022-02-04 02:14:57 -05:00
Self ::InvalidAssertType ( _ ) = > " invalid-assert-type " ,
Self ::NoAssertType = > " no-assert-type " ,
Self ::NoCache ( _ ) = > " no-cache " ,
Self ::NoCacheBlob = > " no-cache-blob " ,
Self ::NoCacheData ( _ ) = > " no-cache-data " ,
2022-10-21 11:20:18 -04:00
Self ::NoCacheNpm ( _ , _ ) = > " no-cache-npm " ,
2022-02-04 02:14:57 -05:00
Self ::NoLocal ( _ ) = > " no-local " ,
Self ::Redirect { .. } = > " redirect " ,
2023-01-27 17:36:23 -05:00
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 " ,
}
}
}
2023-01-24 09:05:54 -05:00
Self ::InvalidNodeSpecifier ( _ ) = > " resolver-error " ,
2021-10-28 19:56:01 -04:00
}
2022-02-04 02:14:57 -05:00
}
/// A "static" method which for a diagnostic that originated from the
/// structure returns a code action which can resolve the diagnostic.
2022-03-23 09:54:22 -04:00
pub fn get_code_action (
2022-02-04 02:14:57 -05:00
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 ( ) {
2022-07-13 21:12:18 -04:00
" 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 {
2023-01-27 10:43:16 -05:00
title : format ! ( " Update \" {from} \" to \" {to} \" to use import map. " ) ,
2022-07-13 21:12:18 -04:00
kind : Some ( lsp ::CodeActionKind ::QUICKFIX ) ,
diagnostics : Some ( vec! [ diagnostic . clone ( ) ] ) ,
edit : Some ( lsp ::WorkspaceEdit {
changes : Some ( HashMap ::from ( [ (
specifier . clone ( ) ,
vec! [ lsp ::TextEdit {
2023-01-27 10:43:16 -05:00
new_text : format ! ( " \" {to} \" " ) ,
2022-07-13 21:12:18 -04:00
range : diagnostic . range ,
} ] ,
) ] ) ) ,
.. Default ::default ( )
} ) ,
.. Default ::default ( )
}
}
2022-02-04 02:14:57 -05:00
" 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 ( )
} ,
2022-10-21 11:20:18 -04:00
" no-cache " | " no-cache-data " | " no-cache-npm " = > {
2022-02-04 02:14:57 -05:00
let data = diagnostic
. data
. clone ( )
. ok_or_else ( | | anyhow! ( " Diagnostic is missing data " ) ) ? ;
let data : DiagnosticDataSpecifier = serde_json ::from_value ( data ) ? ;
2022-10-21 11:20:18 -04:00
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 ( ) ,
2022-02-04 02:14:57 -05:00
} ;
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 ( )
}
}
2023-01-27 17:36:23 -05:00
" import-node-prefix-missing " = > {
2023-01-24 15:14:49 -05:00
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 ( )
}
}
2022-02-04 02:14:57 -05:00
_ = > {
return Err ( anyhow! (
" Unsupported diagnostic code ( \" {} \" ) provided. " ,
code
) )
}
} ;
Ok ( code_action )
} else {
Err ( anyhow! ( " Unsupported diagnostic code provided. " ) )
2021-10-28 19:56:01 -04:00
}
2022-02-04 02:14:57 -05:00
}
/// Given a reference to the code from an LSP diagnostic, determine if the
/// diagnostic is fixable or not
2023-01-24 15:14:49 -05:00
pub fn is_fixable ( diagnostic : & lsp_types ::Diagnostic ) -> bool {
if let Some ( lsp ::NumberOrString ::String ( code ) ) = & diagnostic . code {
2023-01-27 17:36:23 -05:00
matches! (
code . as_str ( ) ,
" import-map-remap "
| " no-cache "
| " no-cache-npm "
| " no-cache-data "
| " no-assert-type "
| " redirect "
| " import-node-prefix-missing "
)
2022-02-04 02:14:57 -05:00
} else {
false
}
}
/// Convert to an lsp Diagnostic when the range the diagnostic applies to is
/// provided.
2022-03-23 09:54:22 -04:00
pub fn to_lsp_diagnostic ( & self , range : & lsp ::Range ) -> lsp ::Diagnostic {
2022-02-04 02:14:57 -05:00
let ( severity , message , data ) = match self {
Self ::DenoWarn ( message ) = > ( lsp ::DiagnosticSeverity ::WARNING , message . to_string ( ) , None ) ,
2023-01-27 10:43:16 -05:00
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 ) ,
2022-02-04 02:14:57 -05:00
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 ) ,
2023-01-27 10:43:16 -05:00
Self ::NoCache ( specifier ) = > ( lsp ::DiagnosticSeverity ::ERROR , format! ( " Uncached or missing remote URL: \" {specifier} \" . " ) , Some ( json! ( { " specifier " : specifier } ) ) ) ,
2022-02-04 02:14:57 -05:00
Self ::NoCacheBlob = > ( lsp ::DiagnosticSeverity ::ERROR , " Uncached blob URL. " . to_string ( ) , None ) ,
Self ::NoCacheData ( specifier ) = > ( lsp ::DiagnosticSeverity ::ERROR , " Uncached data URL. " . to_string ( ) , Some ( json! ( { " specifier " : specifier } ) ) ) ,
2022-10-21 11:20:18 -04:00
Self ::NoCacheNpm ( pkg_ref , specifier ) = > ( lsp ::DiagnosticSeverity ::ERROR , format! ( " Uncached or missing npm package: \" {} \" . " , pkg_ref . req ) , Some ( json! ( { " specifier " : specifier } ) ) ) ,
2023-01-27 10:43:16 -05:00
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 } ) ) ) ,
2023-01-24 15:14:49 -05:00
Self ::ResolutionError ( err ) = > (
lsp ::DiagnosticSeverity ::ERROR ,
enhanced_resolution_error_message ( err ) ,
2023-01-27 17:36:23 -05:00
graph_util ::get_resolution_error_bare_node_specifier ( err )
. map ( | specifier | json! ( { " specifier " : specifier } ) )
2023-01-24 15:14:49 -05:00
) ,
2023-01-24 09:05:54 -05:00
Self ::InvalidNodeSpecifier ( specifier ) = > ( lsp ::DiagnosticSeverity ::ERROR , format! ( " Unknown Node built-in module: {} " , specifier . path ( ) ) , None ) ,
2022-02-04 02:14:57 -05:00
} ;
lsp ::Diagnostic {
range : * range ,
severity : Some ( severity ) ,
code : Some ( lsp ::NumberOrString ::String ( self . code ( ) . to_string ( ) ) ) ,
source : Some ( " deno " . to_string ( ) ) ,
message ,
data ,
.. Default ::default ( )
2021-10-28 19:56:01 -04:00
}
}
}
2023-02-09 22:00:23 -05:00
fn diagnose_resolution (
2023-04-24 19:52:27 -04:00
lsp_diagnostics : & mut Vec < lsp ::Diagnostic > ,
2022-07-13 21:12:18 -04:00
snapshot : & language_server ::StateSnapshot ,
2023-02-09 22:00:23 -05:00
resolution : & Resolution ,
2021-12-15 22:53:17 -05:00
is_dynamic : bool ,
maybe_assert_type : Option < & str > ,
2023-04-24 19:52:27 -04:00
ranges : Vec < lsp ::Range > ,
2021-05-24 22:34:01 -04:00
) {
2023-04-24 19:52:27 -04:00
let mut diagnostics = vec! [ ] ;
2023-02-09 22:00:23 -05:00
match resolution {
Resolution ::Ok ( resolved ) = > {
let specifier = & resolved . specifier ;
2022-06-17 11:41:28 -04:00
// If the module is a remote module and has a `X-Deno-Warning` header, we
2022-02-04 02:14:57 -05:00
// want a warning diagnostic with that message.
2022-07-13 21:12:18 -04:00
if let Some ( metadata ) = snapshot . cache_metadata . get ( specifier ) {
2022-02-01 21:04:26 -05:00
if let Some ( message ) =
metadata . get ( & cache ::MetadataKey ::Warning ) . cloned ( )
{
2023-04-24 19:52:27 -04:00
diagnostics . push ( DenoDiagnostic ::DenoWarn ( message ) ) ;
2021-11-12 11:42:04 -05:00
}
2022-02-01 21:04:26 -05:00
}
2022-07-13 21:12:18 -04:00
if let Some ( doc ) = snapshot . documents . get ( specifier ) {
2022-02-04 02:14:57 -05:00
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 {
2023-04-24 19:52:27 -04:00
diagnostics . push ( DenoDiagnostic ::Redirect {
from : specifier . clone ( ) ,
to : doc_specifier . clone ( ) ,
} ) ;
2022-02-04 02:14:57 -05:00
}
2021-12-15 22:53:17 -05:00
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
2023-04-24 19:52:27 -04:00
Some ( assert_type ) = > diagnostics
. push ( DenoDiagnostic ::InvalidAssertType ( assert_type . to_string ( ) ) ) ,
2021-12-15 22:53:17 -05:00
// The module is missing an assertion type, diagnostic
2023-04-24 19:52:27 -04:00
None = > diagnostics . push ( DenoDiagnostic ::NoAssertType ) ,
2021-12-15 22:53:17 -05:00
}
}
2023-02-21 12:03:48 -05:00
} else if let Ok ( pkg_ref ) =
NpmPackageReqReference ::from_specifier ( specifier )
2022-10-21 11:20:18 -04:00
{
if let Some ( npm_resolver ) = & snapshot . maybe_npm_resolver {
// show diagnostics for npm package references that aren't cached
2023-05-22 21:28:36 -04:00
if ! npm_resolver . is_pkg_req_folder_cached ( & pkg_ref . req ) {
2023-04-24 19:52:27 -04:00
diagnostics
. push ( DenoDiagnostic ::NoCacheNpm ( pkg_ref , specifier . clone ( ) ) ) ;
2022-10-21 11:20:18 -04:00
}
}
2023-01-24 09:05:54 -05:00
} else if let Some ( module_name ) = specifier . as_str ( ) . strip_prefix ( " node: " )
{
2023-04-21 21:02:46 -04:00
if deno_node ::resolve_builtin_node_module ( module_name ) . is_err ( ) {
2023-04-24 19:52:27 -04:00
diagnostics
. push ( DenoDiagnostic ::InvalidNodeSpecifier ( specifier . clone ( ) ) ) ;
2023-01-24 09:05:54 -05:00
} else if let Some ( npm_resolver ) = & snapshot . maybe_npm_resolver {
// check that a @types/node package exists in the resolver
let types_node_ref =
2023-02-21 12:03:48 -05:00
NpmPackageReqReference ::from_str ( " npm:@types/node " ) . unwrap ( ) ;
2023-05-22 21:28:36 -04:00
if ! npm_resolver . is_pkg_req_folder_cached ( & types_node_ref . req ) {
2023-04-24 19:52:27 -04:00
diagnostics . push ( DenoDiagnostic ::NoCacheNpm (
types_node_ref ,
ModuleSpecifier ::parse ( " npm:@types/node " ) . unwrap ( ) ,
) ) ;
2023-01-24 09:05:54 -05:00
}
}
2021-11-12 11:42:04 -05:00
} else {
2022-02-04 02:14:57 -05:00
// 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 ( ) ) ,
" data " = > DenoDiagnostic ::NoCacheData ( specifier . clone ( ) ) ,
" blob " = > DenoDiagnostic ::NoCacheBlob ,
_ = > DenoDiagnostic ::NoCache ( specifier . clone ( ) ) ,
2021-10-28 19:56:01 -04:00
} ;
2023-04-24 19:52:27 -04:00
diagnostics . push ( deno_diagnostic ) ;
2021-05-24 22:34:01 -04:00
}
}
2022-02-04 02:14:57 -05:00
// The specifier resolution resulted in an error, so we want to issue a
// diagnostic for that.
2023-04-24 19:52:27 -04:00
Resolution ::Err ( err ) = > {
diagnostics . push ( DenoDiagnostic ::ResolutionError ( * err . clone ( ) ) )
}
2021-10-28 19:56:01 -04:00
_ = > ( ) ,
2021-05-24 22:34:01 -04:00
}
2023-04-24 19:52:27 -04:00
for range in ranges {
for diagnostic in & diagnostics {
lsp_diagnostics . push ( diagnostic . to_lsp_diagnostic ( & range ) ) ;
}
}
2021-05-24 22:34:01 -04:00
}
2022-07-13 21:12:18 -04:00
/// 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 ,
) {
2022-10-21 11:20:18 -04:00
if let Some ( npm_resolver ) = & snapshot . maybe_npm_resolver {
if npm_resolver . in_npm_package ( referrer ) {
return ; // ignore, surface typescript errors instead
}
}
2022-07-13 21:12:18 -04:00
if let Some ( import_map ) = & snapshot . maybe_import_map {
2023-02-09 22:00:23 -05:00
if let Resolution ::Ok ( resolved ) = & dependency . maybe_code {
if let Some ( to ) = import_map . lookup ( & resolved . specifier , referrer ) {
2022-07-13 21:12:18 -04:00
if dependency_key ! = to {
diagnostics . push (
DenoDiagnostic ::ImportMapRemap {
from : dependency_key . to_string ( ) ,
to ,
}
2023-02-09 22:00:23 -05:00
. to_lsp_diagnostic ( & documents ::to_lsp_range ( & resolved . range ) ) ,
2022-07-13 21:12:18 -04:00
) ;
}
}
}
}
2023-02-09 22:00:23 -05:00
diagnose_resolution (
2022-07-13 21:12:18 -04:00
diagnostics ,
snapshot ,
2023-04-24 19:52:27 -04:00
if dependency . maybe_code . is_none ( ) {
& dependency . maybe_type
} else {
& dependency . maybe_code
} ,
2022-07-13 21:12:18 -04:00
dependency . is_dynamic ,
dependency . maybe_assert_type . as_deref ( ) ,
2023-04-24 19:52:27 -04:00
dependency
. imports
. iter ( )
. map ( | i | documents ::to_lsp_range ( & i . range ) )
. collect ( ) ,
2022-07-13 21:12:18 -04:00
) ;
2023-04-24 19:52:27 -04:00
// 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! ( ) ,
} ;
diagnose_resolution (
diagnostics ,
snapshot ,
& dependency . maybe_type ,
dependency . is_dynamic ,
dependency . maybe_assert_type . as_deref ( ) ,
vec! [ range ] ,
) ;
}
2022-07-13 21:12:18 -04:00
}
/// 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.
async fn generate_deno_diagnostics (
2022-01-29 17:50:15 -05:00
snapshot : & language_server ::StateSnapshot ,
config : & ConfigSnapshot ,
2022-01-24 15:30:01 -05:00
token : CancellationToken ,
) -> DiagnosticVec {
let mut diagnostics_vec = Vec ::new ( ) ;
2021-11-23 20:04:27 -05:00
2023-03-29 16:25:48 -04:00
for document in snapshot
. documents
. documents ( DocumentsFilter ::OpenDiagnosable )
{
2022-01-24 15:30:01 -05:00
if token . is_cancelled ( ) {
break ;
2021-05-11 10:43:00 -04:00
}
2022-01-24 15:30:01 -05:00
let mut diagnostics = Vec ::new ( ) ;
2022-03-20 21:33:37 -04:00
let specifier = document . specifier ( ) ;
if config . specifier_enabled ( specifier ) {
2022-07-13 21:12:18 -04:00
for ( dependency_key , dependency ) in document . dependencies ( ) {
2022-01-29 17:50:15 -05:00
diagnose_dependency (
& mut diagnostics ,
2022-07-13 21:12:18 -04:00
snapshot ,
specifier ,
2022-10-21 11:20:18 -04:00
dependency_key ,
dependency ,
2022-01-29 17:50:15 -05:00
) ;
}
2021-05-11 10:43:00 -04:00
}
2022-01-24 15:30:01 -05:00
diagnostics_vec . push ( (
2022-03-20 21:33:37 -04:00
specifier . clone ( ) ,
2022-01-24 15:30:01 -05:00
document . maybe_lsp_version ( ) ,
diagnostics ,
) ) ;
}
2021-05-11 10:43:00 -04:00
2022-01-24 15:30:01 -05:00
diagnostics_vec
2021-05-11 10:43:00 -04:00
}
2021-10-28 19:56:01 -04:00
#[ cfg(test) ]
mod tests {
use super ::* ;
use crate ::lsp ::config ::ConfigSnapshot ;
use crate ::lsp ::config ::Settings ;
2022-01-29 17:50:15 -05:00
use crate ::lsp ::config ::SpecifierSettings ;
2021-10-28 19:56:01 -04:00
use crate ::lsp ::config ::WorkspaceSettings ;
2022-07-13 21:12:18 -04:00
use crate ::lsp ::documents ::Documents ;
2021-10-28 19:56:01 -04:00
use crate ::lsp ::documents ::LanguageId ;
use crate ::lsp ::language_server ::StateSnapshot ;
2023-02-09 22:00:23 -05:00
use pretty_assertions ::assert_eq ;
2021-10-28 19:56:01 -04:00
use std ::path ::Path ;
use std ::path ::PathBuf ;
2022-07-13 21:12:18 -04:00
use std ::sync ::Arc ;
2022-04-01 11:15:37 -04:00
use test_util ::TempDir ;
2021-10-28 19:56:01 -04:00
fn mock_state_snapshot (
fixtures : & [ ( & str , & str , i32 , LanguageId ) ] ,
location : & Path ,
2022-07-13 21:12:18 -04:00
maybe_import_map : Option < ( & str , & str ) > ,
2021-10-28 19:56:01 -04:00
) -> StateSnapshot {
2023-05-11 17:17:14 -04:00
let mut documents = Documents ::new ( location ) ;
2021-10-28 19:56:01 -04:00
for ( specifier , source , version , language_id ) in fixtures {
let specifier =
resolve_url ( specifier ) . expect ( " failed to create specifier " ) ;
documents . open (
specifier . clone ( ) ,
* version ,
2022-12-20 15:19:35 -05:00
* language_id ,
2022-05-20 16:40:55 -04:00
( * source ) . into ( ) ,
2021-10-28 19:56:01 -04:00
) ;
}
2022-07-13 21:12:18 -04:00
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 )
} ) ;
2022-01-19 17:10:14 -05:00
StateSnapshot {
documents ,
2022-07-13 21:12:18 -04:00
maybe_import_map ,
2022-01-19 17:10:14 -05:00
.. Default ::default ( )
}
}
fn mock_config ( ) -> ConfigSnapshot {
ConfigSnapshot {
2021-10-28 19:56:01 -04:00
settings : Settings {
workspace : WorkspaceSettings {
enable : true ,
lint : true ,
.. Default ::default ( )
} ,
.. Default ::default ( )
} ,
.. Default ::default ( )
}
}
fn setup (
2022-04-01 11:15:37 -04:00
temp_dir : & TempDir ,
2021-10-28 19:56:01 -04:00
sources : & [ ( & str , & str , i32 , LanguageId ) ] ,
2022-07-13 21:12:18 -04:00
maybe_import_map : Option < ( & str , & str ) > ,
2022-01-29 17:50:15 -05:00
) -> ( StateSnapshot , PathBuf ) {
2021-10-28 19:56:01 -04:00
let location = temp_dir . path ( ) . join ( " deps " ) ;
2022-07-13 21:12:18 -04:00
let state_snapshot =
mock_state_snapshot ( sources , & location , maybe_import_map ) ;
2022-01-29 17:50:15 -05:00
( state_snapshot , location )
2021-10-28 19:56:01 -04:00
}
#[ tokio::test ]
2022-01-29 17:50:15 -05:00
async fn test_enabled_then_disabled_specifier ( ) {
2022-04-01 11:15:37 -04:00
let temp_dir = TempDir ::new ( ) ;
2022-01-29 17:50:15 -05:00
let specifier = ModuleSpecifier ::parse ( " file:///a.ts " ) . unwrap ( ) ;
2022-04-01 11:15:37 -04:00
let ( snapshot , _ ) = setup (
& temp_dir ,
& [ (
" file:///a.ts " ,
r #" import * as b from " . / b . ts " ;
2022-01-29 17:50:15 -05:00
let a : any = " a " ;
let c : number = " a " ;
2021-10-28 19:56:01 -04:00
" #,
2022-04-01 11:15:37 -04:00
1 ,
LanguageId ::TypeScript ,
) ] ,
2022-07-13 21:12:18 -04:00
None ,
2022-04-01 11:15:37 -04:00
) ;
2022-01-29 17:50:15 -05:00
let snapshot = Arc ::new ( snapshot ) ;
let ts_server = TsServer ::new ( Default ::default ( ) ) ;
// test enabled
{
let enabled_config = mock_config ( ) ;
let diagnostics = generate_lint_diagnostics (
& snapshot ,
& enabled_config ,
2023-01-07 15:22:09 -05:00
& Default ::default ( ) ,
2022-01-29 17:50:15 -05:00
Default ::default ( ) ,
)
. await ;
assert_eq! ( get_diagnostics_for_single ( diagnostics ) . len ( ) , 6 ) ;
2022-02-02 09:25:22 -05:00
let diagnostics = generate_ts_diagnostics (
snapshot . clone ( ) ,
& enabled_config ,
& ts_server ,
Default ::default ( ) ,
)
. await
. unwrap ( ) ;
2023-03-21 11:46:40 -04:00
assert_eq! ( get_diagnostics_for_single ( diagnostics ) . len ( ) , 5 ) ;
2022-07-13 21:12:18 -04:00
let diagnostics = generate_deno_diagnostics (
2022-01-29 17:50:15 -05:00
& snapshot ,
& enabled_config ,
Default ::default ( ) ,
)
. await ;
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 ( ) ,
2023-03-15 10:34:23 -04:00
SpecifierSettings {
enable : false ,
enable_paths : Vec ::new ( ) ,
code_lens : Default ::default ( ) ,
} ,
2022-01-29 17:50:15 -05:00
) ;
let diagnostics = generate_lint_diagnostics (
& snapshot ,
& disabled_config ,
2023-01-07 15:22:09 -05:00
& Default ::default ( ) ,
2022-01-29 17:50:15 -05:00
Default ::default ( ) ,
)
. await ;
assert_eq! ( get_diagnostics_for_single ( diagnostics ) . len ( ) , 0 ) ;
2022-02-02 09:25:22 -05:00
let diagnostics = generate_ts_diagnostics (
snapshot . clone ( ) ,
& disabled_config ,
& ts_server ,
Default ::default ( ) ,
)
. await
. unwrap ( ) ;
2022-01-29 17:50:15 -05:00
assert_eq! ( get_diagnostics_for_single ( diagnostics ) . len ( ) , 0 ) ;
2022-07-13 21:12:18 -04:00
let diagnostics = generate_deno_diagnostics (
2022-01-29 17:50:15 -05:00
& snapshot ,
& disabled_config ,
Default ::default ( ) ,
)
. await ;
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 ) ;
let ( _ , _ , diagnostics ) = diagnostic_vec . into_iter ( ) . next ( ) . unwrap ( ) ;
diagnostics
2021-10-28 19:56:01 -04:00
}
2022-02-02 09:25:22 -05:00
#[ tokio::test ]
async fn test_cancelled_ts_diagnostics_request ( ) {
2022-04-01 11:15:37 -04:00
let temp_dir = TempDir ::new ( ) ;
let ( snapshot , _ ) = setup (
& temp_dir ,
& [ (
" file:///a.ts " ,
r # "export let a: string = 5;"# ,
1 ,
LanguageId ::TypeScript ,
) ] ,
2022-07-13 21:12:18 -04:00
None ,
2022-04-01 11:15:37 -04:00
) ;
2022-02-02 09:25:22 -05:00
let snapshot = Arc ::new ( snapshot ) ;
let ts_server = TsServer ::new ( Default ::default ( ) ) ;
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 ) ;
}
2022-07-13 21:12:18 -04:00
#[ 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 \n assert(); \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 ) . await ;
assert_eq! ( actual . len ( ) , 2 ) ;
for ( specifier , _ , diagnostics ) in actual {
match specifier . as_str ( ) {
" file:///std/testing/asserts.ts " = > {
assert_eq! ( json! ( diagnostics ) , json! ( [ ] ) )
}
" file:///a/file.ts " = > assert_eq! (
json! ( 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 {} " , 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 \" "
}
]
}
}
} )
) ;
}
2023-04-24 19:52:27 -04:00
#[ 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 ) . await ;
assert_eq! ( actual . len ( ) , 1 ) ;
let ( _ , _ , diagnostics ) = actual . first ( ) . unwrap ( ) ;
assert_eq! (
json! ( 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 ../ " ,
} ,
] )
) ;
}
2021-10-28 19:56:01 -04:00
}