Repository: denoland/deno
Pull request: fix(lsp): typescript-go basic resolution

- Coupled with https://github.com/denoland/typescript-go/pull/23

Changed files:
filename: cli/lsp/documents.rs
patch: @@ -1296,6 +1296,49 @@ impl DocumentModules {
     module
   }
 
+  pub fn module_for_tsgo_referrer(
+    &self,
+    uri: &Uri,
+    compiler_options_key: &CompilerOptionsKey,
+  ) -> Option<Arc<DocumentModule>> {
+    let document = self.documents.get(uri)?;
+    let scope = self.primary_scope(document.uri()).unwrap_or_else(|| {
+      self
+        .compiler_options_resolver
+        .for_key(compiler_options_key)
+        .unwrap()
+        .workspace_dir_or_source_url
+        .as_ref()
+        .and_then(|s| self.config.tree.scope_for_specifier(s))
+    });
+    self.module(&document, scope.map(|s| s.as_ref()))
+  }
+
+  pub fn module_for_tsgo_dependency(
+    &self,
+    specifier: &Url,
+    compiler_options_key: &CompilerOptionsKey,
+  ) -> Option<Arc<DocumentModule>> {
+    let scope = if specifier.scheme() == "file"
+      && !self.cache.in_global_cache_directory(specifier)
+    {
+      self.config.tree.scope_for_specifier(specifier)
+    } else {
+      self
+        .compiler_options_resolver
+        .for_key(compiler_options_key)
+        .unwrap()
+        .workspace_dir_or_source_url
+        .as_ref()
+        .and_then(|s| self.config.tree.scope_for_specifier(s))
+    };
+    self.module_for_specifier(
+      specifier,
+      scope.map(|s| s.as_ref()),
+      Some(compiler_options_key),
+    )
+  }
+
   pub fn primary_module(
     &self,
     document: &Document,
@@ -1693,10 +1736,9 @@ impl DocumentModules {
     if let Some(module_name) = specifier.as_str().strip_prefix("node:")
       && deno_node::is_builtin_node_module(module_name)
     {
-      // return itself for node: specifiers because during type checking
-      // we resolve to the ambient modules in the @types/node package
-      // rather than deno_std/node
-      return Some((specifier.clone(), MediaType::Dts, None));
+      // Don't resolve node: specifiers because during type checking we resolve
+      // to the ambient modules in the @types/node package.
+      return None;
     }
     let mut specifier = specifier.clone();
     let mut media_type = None;
@@ -1738,6 +1780,96 @@ impl DocumentModules {
       ))
     }
   }
+
+  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
+  pub fn resolve_dependency_document(
+    &self,
+    raw_specifier: &str,
+    referrer_module: &DocumentModule,
+    resolution_mode: ResolutionMode,
+  ) -> Option<(Arc<Uri>, MediaType)> {
+    if raw_specifier.starts_with("asset:") {
+      let specifier = resolve_url(raw_specifier).ok()?;
+      let uri = remote_or_asset_url_to_uri(&specifier)?;
+      let media_type = MediaType::from_specifier(&specifier);
+      return Some((Arc::new(uri), media_type));
+    }
+    if let Some(dependency) = referrer_module.dependencies.get(raw_specifier) {
+      let specifier = dependency
+        .maybe_type
+        .maybe_specifier()
+        .or_else(|| dependency.maybe_code.maybe_specifier())?;
+      return self.resolve_dependency_document_inner(
+        specifier,
+        referrer_module,
+        resolution_mode,
+        &referrer_module.compiler_options_key,
+      );
+    }
+    let scoped_resolver = self
+      .resolver
+      .get_scoped_resolver(referrer_module.scope.as_deref());
+    let specifier = scoped_resolver
+      .as_cli_resolver()
+      .resolve(
+        raw_specifier,
+        &referrer_module.specifier,
+        deno_graph::Position::zeroed(),
+        resolution_mode,
+        NodeResolutionKind::Types,
+      )
+      .ok()?;
+    self.resolve_dependency_document_inner(
+      &specifier,
+      referrer_module,
+      resolution_mode,
+      &referrer_module.compiler_options_key,
+    )
+  }
+
+  fn resolve_dependency_document_inner(
+    &self,
+    specifier: &Url,
+    referrer_module: &DocumentModule,
+    resolution_mode: ResolutionMode,
+    compiler_options_key: &CompilerOptionsKey,
+  ) -> Option<(Arc<Uri>, MediaType)> {
+    if let Some(module_name) = specifier.as_str().strip_prefix("node:")
+      && deno_node::is_builtin_node_module(module_name)
+    {
+      // Don't resolve node: specifiers because during type checking we resolve
+      // to the ambient modules in the @types/node package.
+      return None;
+    }
+    let mut specifier = Cow::Borrowed(specifier);
+    if let Ok(npm_ref) = NpmPackageReqReference::from_specifier(&specifier) {
+      let scoped_resolver = self
+        .resolver
+        .get_scoped_resolver(referrer_module.scope.as_deref());
+      let (s, _) = scoped_resolver.npm_to_file_url(
+        &npm_ref,
+        &referrer_module.specifier,
+        NodeResolutionKind::Types,
+        resolution_mode,
+      )?;
+      specifier = Cow::Owned(s);
+    }
+    let module =
+      self.module_for_tsgo_dependency(&specifier, compiler_options_key)?;
+    if let Some(types) = module
+      .types_dependency
+      .as_ref()
+      .and_then(|d| d.dependency.maybe_specifier())
+    {
+      return self.resolve_dependency_document_inner(
+        types,
+        &module,
+        module.resolution_mode,
+        compiler_options_key,
+      );
+    }
+    Some((module.uri.clone(), module.media_type))
+  }
 }
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]

filename: cli/lsp/ts_server.rs
patch: @@ -49,9 +49,7 @@ impl TsServer {
     deno_dir: &DenoDir,
     http_client_provider: &Arc<HttpClientProvider>,
   ) -> Self {
-    // TODO(nayeemrmn): Expose typescript-go backend when ready.
-    #[allow(clippy::overly_complex_bool_expr)]
-    if false && std::env::var("DENO_UNSTABLE_TSGO_LSP").is_ok() {
+    if std::env::var("DENO_UNSTABLE_TSGO_LSP").is_ok() {
       Self::Go(TsGoServer::new(deno_dir, http_client_provider))
     } else {
       Self::Js(TsJsServer::new(performance))

filename: cli/lsp/tsgo.rs
patch: @@ -38,11 +38,11 @@ use crate::cache::DenoDir;
 use crate::http_util::HttpClientProvider;
 use crate::lsp::completions::CompletionItemData;
 use crate::lsp::documents::Document;
-use crate::lsp::documents::DocumentText;
 use crate::lsp::logging::lsp_log;
 use crate::lsp::logging::lsp_warn;
 use crate::lsp::resolver::SingleReferrerGraphResolver;
 use crate::lsp::urls::uri_to_url;
+use crate::tsc::IGNORED_DIAGNOSTIC_CODES;
 
 #[derive(Debug, Serialize, Deserialize)]
 #[serde(rename_all = "camelCase")]
@@ -158,18 +158,19 @@ impl TsGoCompilerOptions {
   }
 }
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Deserialize)]
 #[serde(rename_all = "camelCase")]
-struct TsGoGetDocumentParams {
-  uri: Uri,
-}
-
-#[derive(Debug, Serialize)]
-#[serde(rename_all = "camelCase")]
-struct TsGoDocumentData {
-  text: DocumentText,
-  line_starts: Vec<i32>,
-  ascii_only: bool,
+enum TsGoCallbackParams {
+  #[serde(rename_all = "camelCase")]
+  GetDocument { uri: Uri },
+  #[serde(rename_all = "camelCase")]
+  ResolveModuleName {
+    module_name: String,
+    referrer_uri: Uri,
+    import_attribute_type: Option<String>,
+    resolution_mode: deno_typescript_go_client_rust::types::ResolutionMode,
+    compiler_options_key: CompilerOptionsKey,
+  },
 }
 
 #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize)]
@@ -612,70 +613,52 @@ impl TsGoServerInner {
             break;
           }
         };
-        // Check if it's a request (has method) vs response (has id but no method)
         if let Some(method) = message.get("method").and_then(|m| m.as_str()) {
           let id = message.get("id");
           let params = message.get("params");
           match method {
-            "deno/host/getDocument" => {
-              let params: TsGoGetDocumentParams = match params
+            "deno/callback" => {
+              let Some(id) = id else {
+                lsp_warn!("Missing id in tsgo callback: {:#}", &message,);
+                continue;
+              };
+              let params: TsGoCallbackParams = match params
                 .and_then(|p| serde_json::from_value(p.clone()).ok())
               {
                 Some(p) => p,
                 None => {
-                  if let Some(id) = id {
-                    let response = json!({
-                      "jsonrpc": "2.0",
-                      "id": id,
-                      "error": {
-                        "code": -32602,
-                        "message": "Invalid params"
-                      }
-                    });
-                    let mut stdin = stdin_clone.lock();
-                    let _ = write_lsp_message(&mut stdin, &response);
-                  }
+                  let response = json!({
+                    "jsonrpc": "2.0",
+                    "id": id,
+                    "error": {
+                      "code": -32602,
+                      "message": "Invalid params",
+                    },
+                  });
+                  let mut stdin = stdin_clone.lock();
+                  let _ = write_lsp_message(&mut stdin, &response);
                   continue;
                 }
               };
-
-              let snapshot = snapshot_clone.lock();
               let result =
-                snapshot.document_modules.documents.get(&params.uri).map(
-                  |doc| {
-                    let text = doc.text();
-                    TsGoDocumentData {
-                      ascii_only: text.is_ascii(),
-                      text,
-                      line_starts: doc
-                        .line_index()
-                        .line_starts()
-                        .iter()
-                        .map(|&s| u32::from(s) as i32)
-                        .collect(),
-                    }
+                Self::handle_callback(params, &snapshot_clone.lock());
+              let response = match result {
+                Ok(result) => json!({
+                  "jsonrpc": "2.0",
+                  "id": id,
+                  "result": result,
+                }),
+                Err(err) => json!({
+                  "jsonrpc": "2.0",
+                  "id": id,
+                  "error": {
+                    "code": -32001,
+                    "message": err.to_string(),
                   },
-                );
-
-              if let Some(id) = id {
-                let response = match result {
-                  Some(data) => json!({
-                    "jsonrpc": "2.0",
-                    "id": id,
-                    "result": data,
-                  }),
-                  None => json!({
-                    "jsonrpc": "2.0",
-                    "id": id,
-                    "error": {
-                      "code": -32001,
-                      "message": "Document not found"
-                    },
-                  }),
-                };
-                let mut stdin = stdin_clone.lock();
-                let _ = write_lsp_message(&mut stdin, &response);
-              }
+                }),
+              };
+              let mut stdin = stdin_clone.lock();
+              let _ = write_lsp_message(&mut stdin, &response);
             }
             "client/registerCapability" => {
               let response = json!({
@@ -712,12 +695,11 @@ impl TsGoServerInner {
                 });
                 let mut stdin = stdin_clone.lock();
                 let _ = write_lsp_message(&mut stdin, &response);
-              } else {
-                lsp_warn!(
-                  "Received unknown notification from tsgo: {:#}",
-                  &message
-                );
               }
+              lsp_warn!(
+                "Received unknown notification from tsgo: {:#}",
+                &message,
+              );
             }
           }
         } else if let Some(id) = message.get("id") {
@@ -797,6 +779,68 @@ impl TsGoServerInner {
     }
   }
 
+  fn handle_callback(
+    params: TsGoCallbackParams,
+    snapshot: &StateSnapshot,
+  ) -> Result<serde_json::Value, AnyError> {
+    match params {
+      TsGoCallbackParams::GetDocument { uri } => {
+        let document = snapshot
+          .document_modules
+          .documents
+          .get(&uri)
+          .ok_or_else(|| anyhow!("Document not found"))?;
+        let text = document.text();
+        Ok(json!({
+          "text": &text,
+          "lineStarts": document
+            .line_index()
+            .line_starts()
+            .iter()
+            .map(|&s| u32::from(s) as i32)
+            .collect::<Vec<_>>(),
+          "asciiOnly": text.is_ascii(),
+        }))
+      }
+      TsGoCallbackParams::ResolveModuleName {
+        module_name,
+        referrer_uri,
+        // TODO(nayeemrmn): Use this to redirect bytes/text imports.
+        import_attribute_type: _import_attribute_type,
+        resolution_mode,
+        compiler_options_key,
+      } => {
+        let referrer_module = snapshot
+          .document_modules
+          .module_for_tsgo_referrer(&referrer_uri, &compiler_options_key)
+          .ok_or_else(|| anyhow!("Referrer module not found"))?;
+        let Some((uri, media_type)) = snapshot
+          .document_modules
+          .resolve_dependency_document(
+          &module_name,
+          &referrer_module,
+          match resolution_mode {
+            deno_typescript_go_client_rust::types::ResolutionMode::None => {
+              ResolutionMode::Import
+            }
+            deno_typescript_go_client_rust::types::ResolutionMode::CommonJS => {
+              ResolutionMode::Require
+            }
+            deno_typescript_go_client_rust::types::ResolutionMode::ESM => {
+              ResolutionMode::Import
+            }
+          },
+        ) else {
+          return Ok(json!(null));
+        };
+        Ok(json!({
+          "uri": uri,
+          "extension": media_type.as_ts_extension(),
+        }))
+      }
+    }
+  }
+
   async fn request<R>(
     &self,
     request: TsGoRequest,
@@ -947,8 +991,8 @@ impl TsGoServer {
     snapshot: Arc<StateSnapshot>,
     token: &CancellationToken,
   ) -> Result<lsp::DocumentDiagnosticReport, AnyError> {
-    self
-      .request(
+    let mut report = self
+      .request::<lsp::DocumentDiagnosticReport>(
         TsGoRequest::LanguageServiceMethod {
           name: "ProvideDiagnostics".to_string(),
           args: json!([&module.uri]),
@@ -958,7 +1002,19 @@ impl TsGoServer {
         snapshot,
         token,
       )
-      .await
+      .await?;
+    if let lsp::DocumentDiagnosticReport::Full(report) = &mut report {
+      report
+        .full_document_diagnostic_report
+        .items
+        .retain(|diagnostic| {
+          let Some(lsp::NumberOrString::Number(code)) = &diagnostic.code else {
+            return true;
+          };
+          !IGNORED_DIAGNOSTIC_CODES.contains(&(*code as _))
+        });
+    }
+    Ok(report)
   }
 
   pub async fn provide_references(

filename: cli/tsc/go/tsgo_version.rs
patch: @@ -25,16 +25,16 @@ impl Hashes {
   }
 }
 
-pub const VERSION: &str = "0.1.16";
+pub const VERSION: &str = "0.1.18";
 pub const DOWNLOAD_BASE_URL: &str =
-  "https://github.com/denoland/typescript-go/releases/download/v0.1.16";
+  "https://github.com/denoland/typescript-go/releases/download/v0.1.18";
 pub const HASHES: Hashes = Hashes {
-  windows_x64: "sha256:749491a66c442e38fc61e8b261a08fcd312b7d886ddc80188fd0abb5b054eccd",
-  windows_arm64: "sha256:cd21587880d05ff2ef87d30a9eea959d7a72a520d103a8f41df0f387d5adcc3a",
-  macos_x64: "sha256:804efa966e1f303b8af37a72c1b866b0e8b41a539327bc2b8c5cac20bbc7e666",
-  macos_arm64: "sha256:50600b58d59f046686bc2bb173431b86657e175dbf16b479ef25b450f5768f4f",
-  linux_x64: "sha256:53eadc4030f3ef55a205fc4e819aaee10e84228638345fd225cb64f2539d28e5",
-  linux_arm64: "sha256:aba1d0f24ee121403951e6c6d69688718971617159b357afc827aa94317dbb3d",
+  windows_x64: "sha256:d66796c57a33b17a4eec565771e3554d86bc1961dfac76ce497313f7143a6824",
+  windows_arm64: "sha256:4be12a26e9854733d56811c1c42e9f27f82f8d010231bd73a790975a30735659",
+  macos_x64: "sha256:db341d3175d69c0d8f9c03b9ddef894969cd4024289ad5431f9ae47c6f18015f",
+  macos_arm64: "sha256:bc9a2d6a7786b2f39c1d444ad58894c96caa70c7fbc1684ad10068d7563d706b",
+  linux_x64: "sha256:9fc51e74189cdbc19b308e3dffd23ec0fa06fb427389049970d78d220916f9e6",
+  linux_arm64: "sha256:06b1129a5e853157c1cae35eb3722287dce6580dc70198041b129eff39c73f89",
 };
 
 const _: () = {

filename: libs/resolver/deno_json.rs
patch: @@ -34,6 +34,7 @@ use node_resolver::ResolutionMode;
 use once_cell::sync::OnceCell;
 #[cfg(not(feature = "sync"))]
 use once_cell::unsync::OnceCell;
+use serde::Deserialize;
 use serde::Serialize;
 use serde::Serializer;
 use serde_json::json;
@@ -1330,6 +1331,35 @@ impl Serialize for CompilerOptionsKey {
   }
 }
 
+impl<'de> Deserialize<'de> for CompilerOptionsKey {
+  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+  where
+    D: serde::Deserializer<'de>,
+  {
+    let s = String::deserialize(deserializer)?;
+    if s == "workspace-root" {
+      return Ok(Self::WorkspaceConfig(None));
+    }
+    if let Some(inner) = s
+      .strip_prefix("workspace(")
+      .and_then(|s| s.strip_suffix(')'))
+    {
+      let url = Url::parse(inner).map_err(serde::de::Error::custom)?;
+      return Ok(Self::WorkspaceConfig(Some(new_rc(url))));
+    }
+    if let Some(inner) = s
+      .strip_prefix("ts-config(")
+      .and_then(|s| s.strip_suffix(')'))
+    {
+      let i = inner.parse::<usize>().map_err(serde::de::Error::custom)?;
+      return Ok(Self::TsConfig(i));
+    }
+    Err(serde::de::Error::custom(format!(
+      "invalid CompilerOptionsKey: {s}"
+    )))
+  }
+}
+
 #[derive(Debug)]
 pub struct CompilerOptionsResolver {
   workspace_configs: FolderScopedWithUnscopedMap<CompilerOptionsData>,