deno 1.28.1

Provides the deno executable
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.

use std::path::Path;

use deno_ast::CjsAnalysis;
use deno_core::error::AnyError;
use deno_core::parking_lot::Mutex;
use deno_core::serde_json;
use deno_runtime::deno_webstorage::rusqlite::params;
use deno_runtime::deno_webstorage::rusqlite::Connection;
use serde::Deserialize;
use serde::Serialize;
use std::path::PathBuf;
use std::sync::Arc;

use super::common::run_sqlite_pragma;
use super::FastInsecureHasher;

// todo(dsherret): use deno_ast::CjsAnalysisData directly when upgrading deno_ast
// See https://github.com/denoland/deno_ast/pull/117
#[derive(Serialize, Deserialize)]
struct CjsAnalysisData {
  pub exports: Vec<String>,
  pub reexports: Vec<String>,
}

pub struct NodeAnalysisCache {
  db_file_path: Option<PathBuf>,
  inner: Arc<Mutex<Option<Option<NodeAnalysisCacheInner>>>>,
}

impl NodeAnalysisCache {
  pub fn new(db_file_path: Option<PathBuf>) -> Self {
    Self {
      db_file_path,
      inner: Default::default(),
    }
  }

  pub fn compute_source_hash(text: &str) -> String {
    FastInsecureHasher::new()
      .write_str(text)
      .finish()
      .to_string()
  }

  pub fn get_cjs_analysis(
    &self,
    specifier: &str,
    expected_source_hash: &str,
  ) -> Option<CjsAnalysis> {
    self
      .with_inner(|inner| {
        inner.get_cjs_analysis(specifier, expected_source_hash)
      })
      .flatten()
  }

  pub fn set_cjs_analysis(
    &self,
    specifier: &str,
    source_hash: &str,
    cjs_analysis: &CjsAnalysis,
  ) {
    self.with_inner(|inner| {
      inner.set_cjs_analysis(specifier, source_hash, cjs_analysis)
    });
  }

  pub fn get_esm_analysis(
    &self,
    specifier: &str,
    expected_source_hash: &str,
  ) -> Option<Vec<String>> {
    self
      .with_inner(|inner| {
        inner.get_esm_analysis(specifier, expected_source_hash)
      })
      .flatten()
  }

  pub fn set_esm_analysis(
    &self,
    specifier: &str,
    source_hash: &str,
    top_level_decls: &Vec<String>,
  ) {
    self.with_inner(|inner| {
      inner.set_esm_analysis(specifier, source_hash, top_level_decls)
    });
  }

  fn with_inner<TResult>(
    &self,
    action: impl FnOnce(&NodeAnalysisCacheInner) -> Result<TResult, AnyError>,
  ) -> Option<TResult> {
    // lazily create the cache in order to not
    let mut maybe_created = self.inner.lock();
    let inner = match maybe_created.as_ref() {
      Some(maybe_inner) => maybe_inner.as_ref(),
      None => {
        let maybe_inner = match NodeAnalysisCacheInner::new(
          self.db_file_path.as_deref(),
          crate::version::deno(),
        ) {
          Ok(cache) => Some(cache),
          Err(err) => {
            // should never error here, but if it ever does don't fail
            if cfg!(debug_assertions) {
              panic!("Error creating node analysis cache: {:#}", err);
            } else {
              log::debug!("Error creating node analysis cache: {:#}", err);
              None
            }
          }
        };
        *maybe_created = Some(maybe_inner);
        maybe_created.as_ref().and_then(|p| p.as_ref())
      }
    }?;
    match action(inner) {
      Ok(result) => Some(result),
      Err(err) => {
        // should never error here, but if it ever does don't fail
        if cfg!(debug_assertions) {
          panic!("Error using esm analysis: {:#}", err);
        } else {
          log::debug!("Error using esm analysis: {:#}", err);
        }
        None
      }
    }
  }
}

struct NodeAnalysisCacheInner {
  conn: Connection,
}

impl NodeAnalysisCacheInner {
  pub fn new(
    db_file_path: Option<&Path>,
    version: String,
  ) -> Result<Self, AnyError> {
    let conn = match db_file_path {
      Some(path) => Connection::open(path)?,
      None => Connection::open_in_memory()?,
    };
    Self::from_connection(conn, version)
  }

  fn from_connection(
    conn: Connection,
    version: String,
  ) -> Result<Self, AnyError> {
    run_sqlite_pragma(&conn)?;
    create_tables(&conn, &version)?;

    Ok(Self { conn })
  }

  pub fn get_cjs_analysis(
    &self,
    specifier: &str,
    expected_source_hash: &str,
  ) -> Result<Option<CjsAnalysis>, AnyError> {
    let query = "
      SELECT
        data
      FROM
        cjsanalysiscache
      WHERE
        specifier=?1
        AND source_hash=?2
      LIMIT 1";
    let mut stmt = self.conn.prepare_cached(query)?;
    let mut rows = stmt.query(params![specifier, &expected_source_hash])?;
    if let Some(row) = rows.next()? {
      let analysis_info: String = row.get(0)?;
      let analysis_info: CjsAnalysisData =
        serde_json::from_str(&analysis_info)?;
      Ok(Some(CjsAnalysis {
        exports: analysis_info.exports,
        reexports: analysis_info.reexports,
      }))
    } else {
      Ok(None)
    }
  }

  pub fn set_cjs_analysis(
    &self,
    specifier: &str,
    source_hash: &str,
    cjs_analysis: &CjsAnalysis,
  ) -> Result<(), AnyError> {
    let sql = "
      INSERT OR REPLACE INTO
        cjsanalysiscache (specifier, source_hash, data)
      VALUES
        (?1, ?2, ?3)";
    let mut stmt = self.conn.prepare_cached(sql)?;
    stmt.execute(params![
      specifier,
      &source_hash.to_string(),
      &serde_json::to_string(&CjsAnalysisData {
        // temporary clones until upgrading deno_ast
        exports: cjs_analysis.exports.clone(),
        reexports: cjs_analysis.reexports.clone(),
      })?,
    ])?;
    Ok(())
  }

  pub fn get_esm_analysis(
    &self,
    specifier: &str,
    expected_source_hash: &str,
  ) -> Result<Option<Vec<String>>, AnyError> {
    let query = "
      SELECT
        data
      FROM
        esmglobalscache
      WHERE
        specifier=?1
        AND source_hash=?2
      LIMIT 1";
    let mut stmt = self.conn.prepare_cached(query)?;
    let mut rows = stmt.query(params![specifier, &expected_source_hash])?;
    if let Some(row) = rows.next()? {
      let top_level_decls: String = row.get(0)?;
      let decls: Vec<String> = serde_json::from_str(&top_level_decls)?;
      Ok(Some(decls))
    } else {
      Ok(None)
    }
  }

  pub fn set_esm_analysis(
    &self,
    specifier: &str,
    source_hash: &str,
    top_level_decls: &Vec<String>,
  ) -> Result<(), AnyError> {
    let sql = "
      INSERT OR REPLACE INTO
        esmglobalscache (specifier, source_hash, data)
      VALUES
        (?1, ?2, ?3)";
    let mut stmt = self.conn.prepare_cached(sql)?;
    stmt.execute(params![
      specifier,
      &source_hash.to_string(),
      &serde_json::to_string(top_level_decls)?,
    ])?;
    Ok(())
  }
}

fn create_tables(conn: &Connection, cli_version: &str) -> Result<(), AnyError> {
  // INT doesn't store up to u64, so use TEXT for source_hash
  conn.execute(
    "CREATE TABLE IF NOT EXISTS cjsanalysiscache (
        specifier TEXT PRIMARY KEY,
        source_hash TEXT NOT NULL,
        data TEXT NOT NULL
      )",
    [],
  )?;
  conn.execute(
    "CREATE UNIQUE INDEX IF NOT EXISTS cjsanalysiscacheidx
    ON cjsanalysiscache(specifier)",
    [],
  )?;
  conn.execute(
    "CREATE TABLE IF NOT EXISTS esmglobalscache (
        specifier TEXT PRIMARY KEY,
        source_hash TEXT NOT NULL,
        data TEXT NOT NULL
      )",
    [],
  )?;
  conn.execute(
    "CREATE UNIQUE INDEX IF NOT EXISTS esmglobalscacheidx
      ON esmglobalscache(specifier)",
    [],
  )?;
  conn.execute(
    "CREATE TABLE IF NOT EXISTS info (
        key TEXT PRIMARY KEY,
        value TEXT NOT NULL
      )",
    [],
  )?;

  // delete the cache when the CLI version changes
  let data_cli_version: Option<String> = conn
    .query_row(
      "SELECT value FROM info WHERE key='CLI_VERSION' LIMIT 1",
      [],
      |row| row.get(0),
    )
    .ok();
  if data_cli_version != Some(cli_version.to_string()) {
    conn.execute("DELETE FROM cjsanalysiscache", params![])?;
    conn.execute("DELETE FROM esmglobalscache", params![])?;
    let mut stmt = conn
      .prepare("INSERT OR REPLACE INTO info (key, value) VALUES (?1, ?2)")?;
    stmt.execute(params!["CLI_VERSION", &cli_version])?;
  }

  Ok(())
}

#[cfg(test)]
mod test {
  use super::*;

  #[test]
  pub fn node_analysis_cache_general_use() {
    let conn = Connection::open_in_memory().unwrap();
    let cache =
      NodeAnalysisCacheInner::from_connection(conn, "1.0.0".to_string())
        .unwrap();

    assert!(cache.get_cjs_analysis("file.js", "2").unwrap().is_none());
    let cjs_analysis = CjsAnalysis {
      exports: vec!["export1".to_string()],
      reexports: vec!["re-export1".to_string()],
    };
    cache
      .set_cjs_analysis("file.js", "2", &cjs_analysis)
      .unwrap();
    assert!(cache.get_cjs_analysis("file.js", "3").unwrap().is_none()); // different hash
    let actual_cjs_analysis =
      cache.get_cjs_analysis("file.js", "2").unwrap().unwrap();
    assert_eq!(actual_cjs_analysis.exports, cjs_analysis.exports);
    assert_eq!(actual_cjs_analysis.reexports, cjs_analysis.reexports);

    assert!(cache.get_esm_analysis("file.js", "2").unwrap().is_none());
    let esm_analysis = vec!["esm1".to_string()];
    cache
      .set_esm_analysis("file.js", "2", &esm_analysis)
      .unwrap();
    assert!(cache.get_esm_analysis("file.js", "3").unwrap().is_none()); // different hash
    let actual_esm_analysis =
      cache.get_esm_analysis("file.js", "2").unwrap().unwrap();
    assert_eq!(actual_esm_analysis, esm_analysis);

    // adding when already exists should not cause issue
    cache
      .set_cjs_analysis("file.js", "2", &cjs_analysis)
      .unwrap();
    cache
      .set_esm_analysis("file.js", "2", &esm_analysis)
      .unwrap();

    // recreating with same cli version should still have it
    let conn = cache.conn;
    let cache =
      NodeAnalysisCacheInner::from_connection(conn, "1.0.0".to_string())
        .unwrap();
    let actual_analysis =
      cache.get_cjs_analysis("file.js", "2").unwrap().unwrap();
    assert_eq!(actual_analysis.exports, cjs_analysis.exports);
    assert_eq!(actual_analysis.reexports, cjs_analysis.reexports);
    let actual_esm_analysis =
      cache.get_esm_analysis("file.js", "2").unwrap().unwrap();
    assert_eq!(actual_esm_analysis, esm_analysis);

    // now changing the cli version should clear it
    let conn = cache.conn;
    let cache =
      NodeAnalysisCacheInner::from_connection(conn, "2.0.0".to_string())
        .unwrap();
    assert!(cache.get_cjs_analysis("file.js", "2").unwrap().is_none());
    assert!(cache.get_esm_analysis("file.js", "2").unwrap().is_none());
  }
}