Update to vscode-eslint 2.4.4 & support flat config file extensions (#9708)

Thorsten Ball and Kristján Oddsson created

This upgrades to vscode-eslint 2.4.4 to support flat configs, in
multiple configuration files, ending in `.js`, `.cjs`, `.mjs`.

We changed the code to not use the GitHub release because we actually
don't need the artifacts of the release, we just need the source code,
which we compile anyway.

Fixes #7271.

Release Notes:

- Added support for ESLint flat config files.
([#7271](https://github.com/zed-industries/zed/issues/7271)).

Co-authored-by: Kristján Oddsson <koddsson@gmail.com>

Change summary

crates/languages/src/typescript.rs | 28 ++++++------
crates/util/src/github.rs          | 69 +++++--------------------------
2 files changed, 27 insertions(+), 70 deletions(-)

Detailed changes

crates/languages/src/typescript.rs 🔗

@@ -20,7 +20,7 @@ use std::{
 use util::{
     async_maybe,
     fs::remove_matching,
-    github::{github_release_with_tag, GitHubLspBinaryVersion},
+    github::{build_tarball_url, GitHubLspBinaryVersion},
     ResultExt,
 };
 
@@ -230,9 +230,14 @@ pub struct EsLintLspAdapter {
 }
 
 impl EsLintLspAdapter {
+    const CURRENT_VERSION: &'static str = "release/2.4.4";
+
     const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js";
     const SERVER_NAME: &'static str = "eslint";
 
+    const FLAT_CONFIG_FILE_NAMES: &'static [&'static str] =
+        &["eslint.config.js", "eslint.config.mjs", "eslint.config.cjs"];
+
     pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
         EsLintLspAdapter { node }
     }
@@ -269,6 +274,9 @@ impl LspAdapter for EsLintLspAdapter {
         }
 
         let node_path = eslint_user_settings.get("nodePath").unwrap_or(&Value::Null);
+        let use_flat_config = Self::FLAT_CONFIG_FILE_NAMES
+            .iter()
+            .any(|file| workspace_root.join(file).is_file());
 
         json!({
             "": {
@@ -285,7 +293,7 @@ impl LspAdapter for EsLintLspAdapter {
                 "problems": {},
                 "codeActionOnSave": code_action_on_save,
                 "experimental": {
-                    "useFlatConfig": workspace_root.join("eslint.config.js").is_file(),
+                    "useFlatConfig": use_flat_config,
                 },
             }
         })
@@ -297,19 +305,13 @@ impl LspAdapter for EsLintLspAdapter {
 
     async fn fetch_latest_server_version(
         &self,
-        delegate: &dyn LspAdapterDelegate,
+        _delegate: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Send + Any>> {
-        // We're using this hardcoded release tag, because ESLint's API changed with
-        // >= 2.3 and we haven't upgraded yet.
-        let release = github_release_with_tag(
-            "microsoft/vscode-eslint",
-            "release/2.2.20-Insider",
-            delegate.http_client(),
-        )
-        .await?;
+        let url = build_tarball_url("microsoft/vscode-eslint", Self::CURRENT_VERSION)?;
+
         Ok(Box::new(GitHubLspBinaryVersion {
-            name: release.tag_name,
-            url: release.tarball_url,
+            name: Self::CURRENT_VERSION.into(),
+            url,
         }))
     }
 

crates/util/src/github.rs 🔗

@@ -76,78 +76,33 @@ pub async fn latest_github_release(
         .ok_or(anyhow!("Failed to find a release"))
 }
 
-pub async fn github_release_with_tag(
-    repo_name_with_owner: &str,
-    tag: &str,
-    http: Arc<dyn HttpClient>,
-) -> Result<GithubRelease, anyhow::Error> {
-    let url = build_tagged_release_url(repo_name_with_owner, tag)?;
-    let mut response = http
-        .get(&url, Default::default(), true)
-        .await
-        .with_context(|| format!("error fetching release {} of {}", tag, repo_name_with_owner))?;
-
-    let mut body = Vec::new();
-    response
-        .body_mut()
-        .read_to_end(&mut body)
-        .await
-        .with_context(|| {
-            format!(
-                "error reading response body for release {} of {}",
-                tag, repo_name_with_owner
-            )
-        })?;
-
-    if response.status().is_client_error() {
-        let text = String::from_utf8_lossy(body.as_slice());
-        bail!(
-            "status error {}, response: {text:?}",
-            response.status().as_u16()
-        );
-    }
-
-    match serde_json::from_slice::<GithubRelease>(body.as_slice()) {
-        Ok(release) => Ok(release),
-
-        Err(err) => {
-            log::error!("Error deserializing: {:?}", err);
-            log::error!(
-                "GitHub API response text: {:?}",
-                String::from_utf8_lossy(body.as_slice())
-            );
-            Err(anyhow!(
-                "error deserializing release {} of {}",
-                tag,
-                repo_name_with_owner
-            ))
-        }
-    }
-}
-
-fn build_tagged_release_url(repo_name_with_owner: &str, tag: &str) -> Result<String> {
+pub fn build_tarball_url(repo_name_with_owner: &str, tag: &str) -> Result<String> {
     let mut url = Url::parse(&format!(
-        "https://api.github.com/repos/{repo_name_with_owner}/releases/tags"
+        "https://github.com/{repo_name_with_owner}/archive/refs/tags",
     ))?;
     // We're pushing this here, because tags may contain `/` and other characters
     // that need to be escaped.
+    let tarball_filename = format!("{}.tar.gz", tag);
     url.path_segments_mut()
         .map_err(|_| anyhow!("cannot modify url path segments"))?
-        .push(tag);
+        .push(&tarball_filename);
     Ok(url.to_string())
 }
 
 #[cfg(test)]
 mod tests {
-    use super::build_tagged_release_url;
+    use crate::github::build_tarball_url;
 
     #[test]
-    fn test_build_tagged_release_url() {
-        let tag = "release/2.2.20-Insider";
+    fn test_build_tarball_url() {
+        let tag = "release/2.3.5";
         let repo_name_with_owner = "microsoft/vscode-eslint";
 
-        let have = build_tagged_release_url(repo_name_with_owner, tag).unwrap();
+        let have = build_tarball_url(repo_name_with_owner, tag).unwrap();
 
-        assert_eq!(have, "https://api.github.com/repos/microsoft/vscode-eslint/releases/tags/release%2F2.2.20-Insider");
+        assert_eq!(
+            have,
+            "https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.tar.gz"
+        );
     }
 }