summaryrefslogtreecommitdiff
path: root/cli/lsp/analysis.rs
diff options
context:
space:
mode:
authorDavid Sherret <dsherret@users.noreply.github.com>2023-07-01 21:07:57 -0400
committerGitHub <noreply@github.com>2023-07-02 01:07:57 +0000
commitcfbc9b471f9ae0a00639b69623068021a6cfcbbd (patch)
tree6609ee07363f0e5b415b9a12706dd72fa93d41c7 /cli/lsp/analysis.rs
parente746b6d80654ba4e4e26370fe6e4f784ce841d92 (diff)
feat(lsp): basic support of auto-imports for npm specifiers (#19675)
Closes #19625 Closes https://github.com/denoland/vscode_deno/issues/857
Diffstat (limited to 'cli/lsp/analysis.rs')
-rw-r--r--cli/lsp/analysis.rs248
1 files changed, 230 insertions, 18 deletions
diff --git a/cli/lsp/analysis.rs b/cli/lsp/analysis.rs
index a5ebbbbb8..80c748055 100644
--- a/cli/lsp/analysis.rs
+++ b/cli/lsp/analysis.rs
@@ -5,6 +5,8 @@ use super::documents::Documents;
use super::language_server;
use super::tsc;
+use crate::npm::CliNpmResolver;
+use crate::npm::NpmResolution;
use crate::tools::lint::create_linter;
use deno_ast::SourceRange;
@@ -14,13 +16,17 @@ use deno_core::anyhow::anyhow;
use deno_core::error::custom_error;
use deno_core::error::AnyError;
use deno_core::serde::Deserialize;
+use deno_core::serde_json;
use deno_core::serde_json::json;
use deno_core::ModuleSpecifier;
use deno_lint::rules::LintRule;
+use deno_runtime::deno_node::PackageJson;
+use deno_runtime::deno_node::PathClean;
use once_cell::sync::Lazy;
use regex::Regex;
use std::cmp::Ordering;
use std::collections::HashMap;
+use std::path::Path;
use tower_lsp::lsp_types as lsp;
use tower_lsp::lsp_types::Position;
use tower_lsp::lsp_types::Range;
@@ -148,21 +154,169 @@ fn code_as_string(code: &Option<lsp::NumberOrString>) -> String {
}
}
-/// Iterate over the supported extensions, concatenating the extension on the
-/// specifier, returning the first specifier that is resolve-able, otherwise
-/// None if none match.
-fn check_specifier(
- specifier: &str,
- referrer: &ModuleSpecifier,
- documents: &Documents,
+/// Rewrites imports in quick fixes and code changes to be Deno specific.
+pub struct TsResponseImportMapper<'a> {
+ documents: &'a Documents,
+ npm_resolution: &'a NpmResolution,
+ npm_resolver: &'a CliNpmResolver,
+}
+
+impl<'a> TsResponseImportMapper<'a> {
+ pub fn new(
+ documents: &'a Documents,
+ npm_resolution: &'a NpmResolution,
+ npm_resolver: &'a CliNpmResolver,
+ ) -> Self {
+ Self {
+ documents,
+ npm_resolution,
+ npm_resolver,
+ }
+ }
+
+ pub fn check_specifier(&self, specifier: &ModuleSpecifier) -> Option<String> {
+ if self.npm_resolver.in_npm_package(specifier) {
+ if let Ok(pkg_id) = self
+ .npm_resolver
+ .resolve_package_id_from_specifier(specifier)
+ {
+ // todo(dsherret): once supporting an import map, we should prioritize which
+ // pkg requirement we use, based on what's specified in the import map
+ if let Some(pkg_req) = self
+ .npm_resolution
+ .resolve_pkg_reqs_from_pkg_id(&pkg_id)
+ .first()
+ {
+ let result = format!("npm:{}", pkg_req);
+ return Some(match self.resolve_package_path(specifier) {
+ Some(path) => format!("{}/{}", result, path),
+ None => result,
+ });
+ }
+ }
+ }
+ None
+ }
+
+ fn resolve_package_path(
+ &self,
+ specifier: &ModuleSpecifier,
+ ) -> Option<String> {
+ let specifier_path = specifier.to_file_path().ok()?;
+ let root_folder = self
+ .npm_resolver
+ .resolve_package_folder_from_specifier(specifier)
+ .ok()?;
+ let package_json_path = root_folder.join("package.json");
+ let package_json_text = std::fs::read_to_string(&package_json_path).ok()?;
+ let package_json =
+ PackageJson::load_from_string(package_json_path, package_json_text)
+ .ok()?;
+
+ let mut search_paths = vec![specifier_path.clone()];
+ // TypeScript will provide a .js extension for quick fixes, so do
+ // a search for the .d.ts file instead
+ if specifier_path.extension().and_then(|e| e.to_str()) == Some("js") {
+ search_paths.insert(0, specifier_path.with_extension("d.ts"));
+ }
+
+ for search_path in search_paths {
+ if let Some(exports) = &package_json.exports {
+ if let Some(result) = try_reverse_map_package_json_exports(
+ &root_folder,
+ &search_path,
+ exports,
+ ) {
+ return Some(result);
+ }
+ }
+ }
+
+ None
+ }
+
+ /// Iterate over the supported extensions, concatenating the extension on the
+ /// specifier, returning the first specifier that is resolve-able, otherwise
+ /// None if none match.
+ pub fn check_specifier_with_referrer(
+ &self,
+ specifier: &str,
+ referrer: &ModuleSpecifier,
+ ) -> Option<String> {
+ if let Ok(specifier) = referrer.join(specifier) {
+ if let Some(specifier) = self.check_specifier(&specifier) {
+ return Some(specifier);
+ }
+ }
+ for ext in SUPPORTED_EXTENSIONS {
+ let specifier_with_ext = format!("{specifier}{ext}");
+ if self
+ .documents
+ .contains_import(&specifier_with_ext, referrer)
+ {
+ return Some(specifier_with_ext);
+ }
+ }
+ None
+ }
+}
+
+fn try_reverse_map_package_json_exports(
+ root_path: &Path,
+ target_path: &Path,
+ exports: &serde_json::Map<String, serde_json::Value>,
) -> Option<String> {
- for ext in SUPPORTED_EXTENSIONS {
- let specifier_with_ext = format!("{specifier}{ext}");
- if documents.contains_import(&specifier_with_ext, referrer) {
- return Some(specifier_with_ext);
+ use deno_core::serde_json::Value;
+
+ fn try_reverse_map_package_json_exports_inner(
+ root_path: &Path,
+ target_path: &Path,
+ exports: &serde_json::Map<String, Value>,
+ ) -> Option<String> {
+ for (key, value) in exports {
+ match value {
+ Value::String(str) => {
+ if root_path.join(str).clean() == target_path {
+ return Some(if let Some(suffix) = key.strip_prefix("./") {
+ suffix.to_string()
+ } else {
+ String::new() // condition (ex. "types"), ignore
+ });
+ }
+ }
+ Value::Object(obj) => {
+ if let Some(result) = try_reverse_map_package_json_exports_inner(
+ root_path,
+ target_path,
+ obj,
+ ) {
+ return Some(if let Some(suffix) = key.strip_prefix("./") {
+ if result.is_empty() {
+ suffix.to_string()
+ } else {
+ format!("{}/{}", suffix, result)
+ }
+ } else {
+ result // condition (ex. "types"), ignore
+ });
+ }
+ }
+ _ => {}
+ }
}
+ None
+ }
+
+ let result = try_reverse_map_package_json_exports_inner(
+ root_path,
+ target_path,
+ exports,
+ )?;
+ if result.is_empty() {
+ None
+ } else {
+ Some(result)
}
- None
}
/// For a set of tsc changes, can them for any that contain something that looks
@@ -170,7 +324,7 @@ fn check_specifier(
pub fn fix_ts_import_changes(
referrer: &ModuleSpecifier,
changes: &[tsc::FileTextChanges],
- documents: &Documents,
+ import_mapper: &TsResponseImportMapper,
) -> Result<Vec<tsc::FileTextChanges>, AnyError> {
let mut r = Vec::new();
for change in changes {
@@ -184,7 +338,7 @@ pub fn fix_ts_import_changes(
if let Some(captures) = IMPORT_SPECIFIER_RE.captures(line) {
let specifier = captures.get(1).unwrap().as_str();
if let Some(new_specifier) =
- check_specifier(specifier, referrer, documents)
+ import_mapper.check_specifier_with_referrer(specifier, referrer)
{
line.replace(specifier, &new_specifier)
} else {
@@ -215,7 +369,7 @@ pub fn fix_ts_import_changes(
fn fix_ts_import_action(
referrer: &ModuleSpecifier,
action: &tsc::CodeFixAction,
- documents: &Documents,
+ import_mapper: &TsResponseImportMapper,
) -> Result<tsc::CodeFixAction, AnyError> {
if action.fix_name == "import" {
let change = action
@@ -233,7 +387,7 @@ fn fix_ts_import_action(
.ok_or_else(|| anyhow!("Missing capture."))?
.as_str();
if let Some(new_specifier) =
- check_specifier(specifier, referrer, documents)
+ import_mapper.check_specifier_with_referrer(specifier, referrer)
{
let description = action.description.replace(specifier, &new_specifier);
let changes = action
@@ -554,8 +708,11 @@ impl CodeActionCollection {
"The action returned from TypeScript is unsupported.",
));
}
- let action =
- fix_ts_import_action(specifier, action, &language_server.documents)?;
+ let action = fix_ts_import_action(
+ specifier,
+ action,
+ &language_server.get_ts_response_import_mapper(),
+ )?;
let edit = ts_changes_to_edit(&action.changes, language_server)?;
let code_action = lsp::CodeAction {
title: action.description.clone(),
@@ -735,6 +892,8 @@ pub fn source_range_to_lsp_range(
#[cfg(test)]
mod tests {
+ use std::path::PathBuf;
+
use super::*;
#[test]
@@ -824,4 +983,57 @@ mod tests {
}
);
}
+
+ #[test]
+ fn test_try_reverse_map_package_json_exports() {
+ let exports = json!({
+ ".": {
+ "types": "./src/index.d.ts",
+ "browser": "./dist/module.js",
+ },
+ "./hooks": {
+ "types": "./hooks/index.d.ts",
+ "browser": "./dist/devtools.module.js",
+ },
+ "./utils": {
+ "types": {
+ "./sub_utils": "./utils_sub_utils.d.ts"
+ }
+ }
+ });
+ let exports = exports.as_object().unwrap();
+ assert_eq!(
+ try_reverse_map_package_json_exports(
+ &PathBuf::from("/project/"),
+ &PathBuf::from("/project/hooks/index.d.ts"),
+ exports,
+ )
+ .unwrap(),
+ "hooks"
+ );
+ assert_eq!(
+ try_reverse_map_package_json_exports(
+ &PathBuf::from("/project/"),
+ &PathBuf::from("/project/dist/devtools.module.js"),
+ exports,
+ )
+ .unwrap(),
+ "hooks"
+ );
+ assert!(try_reverse_map_package_json_exports(
+ &PathBuf::from("/project/"),
+ &PathBuf::from("/project/src/index.d.ts"),
+ exports,
+ )
+ .is_none());
+ assert_eq!(
+ try_reverse_map_package_json_exports(
+ &PathBuf::from("/project/"),
+ &PathBuf::from("/project/utils_sub_utils.d.ts"),
+ exports,
+ )
+ .unwrap(),
+ "utils/sub_utils"
+ );
+ }
}